From 2c9f98309722761a5fa8dd78c656ed8475a91559 Mon Sep 17 00:00:00 2001 From: screret <68943070+screret@users.noreply.github.com> Date: Sun, 5 May 2024 17:10:24 +0300 Subject: [PATCH 1/3] remake parallel logic in hopes of it being faster this way --- .../recipe/FluidRecipeCapability.java | 144 +++++++++++++- .../recipe/ItemRecipeCapability.java | 143 +++++++++++++- .../capability/recipe/RecipeCapability.java | 62 ++++-- .../api/machine/SimpleGeneratorMachine.java | 2 +- .../api/recipe/ResearchRecipeBuilder.java | 1 + .../api/recipe/modifier/ParallelLogic.java | 178 ++++++++++++++++++ .../api/recipe/modifier/RecipeModifier.java | 4 +- .../recipe/modifier/RecipeModifierList.java | 4 +- .../gtceu/common/data/GTRecipeModifiers.java | 63 +++---- .../electric/ProcessingArrayMachine.java | 4 +- .../LargeCombustionEngineMachine.java | 6 +- .../generator/LargeTurbineMachine.java | 4 +- .../steam/SteamParallelMultiblockMachine.java | 2 +- .../api/machine/trait/ParallelLogicTest.java | 6 +- 14 files changed, 545 insertions(+), 78 deletions(-) create mode 100644 src/main/java/com/gregtechceu/gtceu/api/recipe/modifier/ParallelLogic.java diff --git a/src/main/java/com/gregtechceu/gtceu/api/capability/recipe/FluidRecipeCapability.java b/src/main/java/com/gregtechceu/gtceu/api/capability/recipe/FluidRecipeCapability.java index fd766e7b4e..72e94db4fe 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/capability/recipe/FluidRecipeCapability.java +++ b/src/main/java/com/gregtechceu/gtceu/api/capability/recipe/FluidRecipeCapability.java @@ -9,18 +9,24 @@ import com.gregtechceu.gtceu.api.recipe.lookup.AbstractMapIngredient; import com.gregtechceu.gtceu.api.recipe.lookup.MapFluidIngredient; import com.gregtechceu.gtceu.api.recipe.lookup.MapFluidTagIngredient; +import com.gregtechceu.gtceu.api.recipe.modifier.ParallelLogic; import com.gregtechceu.gtceu.api.recipe.ui.GTRecipeTypeUI; import com.gregtechceu.gtceu.integration.GTRecipeWidget; +import com.gregtechceu.gtceu.utils.FluidKey; +import com.gregtechceu.gtceu.utils.GTHashMaps; +import com.gregtechceu.gtceu.utils.OverlayedFluidHandler; import com.gregtechceu.gtceu.utils.OverlayingFluidStorage; import com.lowdragmc.lowdraglib.gui.texture.ProgressTexture; import com.lowdragmc.lowdraglib.gui.widget.TankWidget; import com.lowdragmc.lowdraglib.gui.widget.Widget; import com.lowdragmc.lowdraglib.jei.IngredientIO; +import com.lowdragmc.lowdraglib.misc.FluidTransferList; import com.lowdragmc.lowdraglib.side.fluid.FluidStack; import com.lowdragmc.lowdraglib.side.fluid.IFluidTransfer; import com.lowdragmc.lowdraglib.utils.TagOrCycleFluidTransfer; import com.mojang.datafixers.util.Either; import com.mojang.datafixers.util.Pair; +import it.unimi.dsi.fastutil.objects.Object2LongLinkedOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectArrayList; import net.minecraft.nbt.CompoundTag; import net.minecraft.network.chat.Component; @@ -30,9 +36,7 @@ import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.UnknownNullability; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; +import java.util.*; import java.util.stream.Collectors; /** @@ -77,6 +81,7 @@ public List convertToMapIngredient(Object obj) { } } else if (obj instanceof FluidStack stack) { ingredients.add(new MapFluidIngredient(stack)); + //noinspection deprecation stack.getFluid().builtInRegistryHolder().tags().forEach(tag -> ingredients.add(new MapFluidTagIngredient(tag))); } @@ -131,6 +136,139 @@ public boolean isRecipeSearchFilter() { return true; } + @Override + public int limitParallel(GTRecipe recipe, IRecipeCapabilityHolder holder, int multiplier) { + int minMultiplier = 0; + int maxMultiplier = multiplier; + + OverlayedFluidHandler overlayedFluidHandler = new OverlayedFluidHandler(new FluidTransferList( + Objects.requireNonNullElseGet(holder.getCapabilitiesProxy().get(IO.OUT, FluidRecipeCapability.CAP), Collections::emptyList) + .stream() + .filter(IFluidTransfer.class::isInstance) + .map(IFluidTransfer.class::cast) + .toList() + )); + + while (minMultiplier != maxMultiplier) { + overlayedFluidHandler.reset(); + + long amountLeft = 0; + + for (FluidStack fluidStack : recipe.getOutputContents(FluidRecipeCapability.CAP).stream().map(FluidRecipeCapability.CAP::of).map(ingredient -> ingredient.getStacks()[0]).toList()) { + if (fluidStack.getAmount() <= 0) continue; + // Since multiplier starts at Int.MAX, check here for integer overflow + if (multiplier > Integer.MAX_VALUE / fluidStack.getAmount()) { + amountLeft = Integer.MAX_VALUE; + } else { + amountLeft = fluidStack.getAmount() * multiplier; + } + long inserted = overlayedFluidHandler.insertFluid(fluidStack, amountLeft); + if (inserted > 0) { + amountLeft -= inserted; + } + if (amountLeft > 0) { + break; + } + } + + int[] bin = ParallelLogic.adjustMultiplier(amountLeft == 0, minMultiplier, multiplier, maxMultiplier); + minMultiplier = bin[0]; + multiplier = bin[1]; + maxMultiplier = bin[2]; + + } + return multiplier; + } + + @Override + public int getMaxParallelRatio(IRecipeCapabilityHolder holder, GTRecipe recipe, int parallelAmount) { + // Find all the fluids in the combined Fluid Input inventories and create oversized FluidStacks + Map fluidStacks = Objects.requireNonNullElseGet(holder.getCapabilitiesProxy().get(IO.IN, ItemRecipeCapability.CAP), Collections::emptyList) + .stream() + .flatMap(container -> GTHashMaps.fromFluidHandler((IFluidTransfer) container).entrySet().stream()) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, Long::sum, Object2LongLinkedOpenHashMap::new)); + + int minMultiplier = Integer.MAX_VALUE; + // map the recipe input fluids to account for duplicated fluids, + // so their sum is counted against the total of fluids available in the input + Map fluidCountMap = new HashMap<>(); + Map notConsumableMap = new HashMap<>(); + for (Content content : recipe.getInputContents(FluidRecipeCapability.CAP)) { + FluidIngredient fluidInput = FluidRecipeCapability.CAP.of(content.content); + long fluidAmount = fluidInput.getAmount(); + if (content.chance == 0.0f) { + notConsumableMap.computeIfPresent(new FluidKey(fluidInput.getStacks()[0]), + (k, v) -> v + fluidAmount); + notConsumableMap.putIfAbsent(new FluidKey(fluidInput.getStacks()[0]), fluidAmount); + } else { + fluidCountMap.computeIfPresent(new FluidKey(fluidInput.getStacks()[0]), + (k, v) -> v + fluidAmount); + fluidCountMap.putIfAbsent(new FluidKey(fluidInput.getStacks()[0]), fluidAmount); + } + } + + // Iterate through the recipe inputs, excluding the not consumable fluids from the fluid inventory map + for (Map.Entry notConsumableFluid : notConsumableMap.entrySet()) { + long needed = notConsumableFluid.getValue(); + long available = 0; + // For every fluid gathered from the fluid inputs. + for (Map.Entry inputFluid : fluidStacks.entrySet()) { + // Strip the Non-consumable tags here, as FluidKey compares the tags, which causes finding matching + // fluids + // in the input tanks to fail, because there is nothing in those hatches with a non-consumable tag + if (notConsumableFluid.getKey().equals(inputFluid.getKey())) { + available = inputFluid.getValue(); + if (available > needed) { + inputFluid.setValue(available - needed); + needed -= available; + break; + } else { + inputFluid.setValue(0L); + notConsumableFluid.setValue(needed - available); + needed -= available; + } + } + } + // We need to check >= available here because of Non-Consumable inputs with stack size. If there is a NC + // input + // with size 1000, and only 500 in the input, needed will be equal to available, but this situation should + // still fail + // as not all inputs are present + if (needed >= available) { + return 0; + } + } + + // Return the maximum parallel limit here if there are only non-consumed inputs, which are all found in the + // input bus + // At this point, we would have already returned 0 if we were missing any non-consumable inputs, so we can omit + // that check + if (fluidCountMap.isEmpty() && !notConsumableMap.isEmpty()) { + return parallelAmount; + } + + // Iterate through the fluid inputs in the recipe + for (Map.Entry fs : fluidCountMap.entrySet()) { + long needed = fs.getValue(); + long available = 0; + // For every fluid gathered from the fluid inputs. + for (Map.Entry inputFluid : fluidStacks.entrySet()) { + if (fs.getKey().equals(inputFluid.getKey())) { + available += inputFluid.getValue(); + } + } + if (available >= needed) { + int ratio = (int) Math.min(parallelAmount, available / needed); + if (ratio < minMultiplier) { + minMultiplier = ratio; + } + } else { + return 0; + } + } + return minMultiplier; + } + @Override public @NotNull List createXEIContainerContents(List contents, GTRecipe recipe, IO io) { return contents.stream().map(content -> content.content) diff --git a/src/main/java/com/gregtechceu/gtceu/api/capability/recipe/ItemRecipeCapability.java b/src/main/java/com/gregtechceu/gtceu/api/capability/recipe/ItemRecipeCapability.java index b6e7635539..8663e7fff0 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/capability/recipe/ItemRecipeCapability.java +++ b/src/main/java/com/gregtechceu/gtceu/api/capability/recipe/ItemRecipeCapability.java @@ -12,6 +12,7 @@ import com.gregtechceu.gtceu.api.recipe.ingredient.IntCircuitIngredient; import com.gregtechceu.gtceu.api.recipe.ingredient.SizedIngredient; import com.gregtechceu.gtceu.api.recipe.lookup.*; +import com.gregtechceu.gtceu.api.recipe.modifier.ParallelLogic; import com.gregtechceu.gtceu.api.recipe.ui.GTRecipeTypeUI; import com.gregtechceu.gtceu.common.recipe.ResearchCondition; import com.gregtechceu.gtceu.config.ConfigHolder; @@ -19,16 +20,19 @@ import com.gregtechceu.gtceu.core.mixins.IntersectionIngredientAccessor; import com.gregtechceu.gtceu.core.mixins.TagValueAccessor; import com.gregtechceu.gtceu.integration.GTRecipeWidget; -import com.gregtechceu.gtceu.utils.IngredientEquality; -import com.gregtechceu.gtceu.utils.ResearchManager; +import com.gregtechceu.gtceu.utils.*; import com.lowdragmc.lowdraglib.gui.widget.SlotWidget; import com.lowdragmc.lowdraglib.gui.widget.Widget; import com.lowdragmc.lowdraglib.jei.IngredientIO; +import com.lowdragmc.lowdraglib.misc.ItemTransferList; import com.lowdragmc.lowdraglib.side.item.IItemTransfer; import com.lowdragmc.lowdraglib.utils.CycleItemStackHandler; import com.lowdragmc.lowdraglib.utils.TagOrCycleItemStackTransfer; import com.mojang.datafixers.util.Either; import com.mojang.datafixers.util.Pair; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenCustomHashMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectArrayList; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.network.chat.Component; @@ -186,6 +190,141 @@ public boolean isRecipeSearchFilter() { return true; } + @Override + public int limitParallel(GTRecipe recipe, IRecipeCapabilityHolder holder, int multiplier) { + int minMultiplier = 0; + int maxMultiplier = multiplier; + + OverlayedItemHandler itemHandler = new OverlayedItemHandler(new ItemTransferList( + Objects.requireNonNullElseGet(holder.getCapabilitiesProxy().get(IO.OUT, ItemRecipeCapability.CAP), Collections::emptyList) + .stream() + .filter(IItemTransfer.class::isInstance) + .map(IItemTransfer.class::cast) + .toList() + )); + + Object2IntMap recipeOutputs = GTHashMaps.fromItemStackCollection(recipe.getOutputContents(ItemRecipeCapability.CAP) + .stream() + .map(ItemRecipeCapability.CAP::of) + .filter(ingredient -> !ingredient.isEmpty()) + .map(ingredient -> ingredient.getItems()[0]) + .toList() + ); + + while (minMultiplier != maxMultiplier) { + itemHandler.reset(); + + int returnedAmount = 0; + int amountToInsert; + + for (Object2IntMap.Entry entry : recipeOutputs.object2IntEntrySet()) { + // Since multiplier starts at Int.MAX, check here for integer overflow + if (entry.getIntValue() != 0 && multiplier > Integer.MAX_VALUE / entry.getIntValue()) { + amountToInsert = Integer.MAX_VALUE; + } else { + amountToInsert = entry.getIntValue() * multiplier; + } + returnedAmount = itemHandler.insertStackedItemStack(entry.getKey(), amountToInsert); + if (returnedAmount > 0) { + break; + } + } + + int[] bin = ParallelLogic.adjustMultiplier(returnedAmount == 0, minMultiplier, multiplier, maxMultiplier); + minMultiplier = bin[0]; + multiplier = bin[1]; + maxMultiplier = bin[2]; + + } + return multiplier; + } + + @Override + public int getMaxParallelRatio(IRecipeCapabilityHolder holder, GTRecipe recipe, int parallelAmount) { + // Find all the items in the combined Item Input inventories and create oversized ItemStacks + Object2IntMap ingredientStacks = Objects.requireNonNullElseGet(holder.getCapabilitiesProxy().get(IO.IN, ItemRecipeCapability.CAP), Collections::emptyList) + .stream() + .filter(IItemTransfer.class::isInstance) + .map(IItemTransfer.class::cast) + .flatMap(container -> GTHashMaps.fromItemHandler(container).object2IntEntrySet().stream()) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, Integer::sum, () -> new Object2IntOpenCustomHashMap<>(ItemStackHashStrategy.comparingAllButCount()))); + + int minMultiplier = Integer.MAX_VALUE; + // map the recipe ingredients to account for duplicated and notConsumable ingredients. + // notConsumable ingredients are not counted towards the max ratio + Object2IntOpenHashMap notConsumableMap = new Object2IntOpenHashMap<>(); + Object2IntOpenHashMap countableMap = new Object2IntOpenHashMap<>(); + for (Content content : recipe.getInputContents(ItemRecipeCapability.CAP)) { + Ingredient recipeIngredient = ItemRecipeCapability.CAP.of(content.content); + int ingredientCount = recipeIngredient instanceof SizedIngredient sizedIngredient ? sizedIngredient.getAmount() : 1; + if (content.chance == 0.0f) { + notConsumableMap.computeIfPresent(recipeIngredient, (k, v) -> v + ingredientCount); + notConsumableMap.putIfAbsent(recipeIngredient, ingredientCount); + } else { + countableMap.computeIfPresent(recipeIngredient, (k, v) -> v + ingredientCount); + countableMap.putIfAbsent(recipeIngredient, ingredientCount); + } + } + + // Iterate through the recipe inputs, excluding the not consumable ingredients from the inventory map + for (Object2IntMap.Entry recipeInputEntry : notConsumableMap.object2IntEntrySet()) { + int needed = recipeInputEntry.getIntValue(); + int available = 0; + // For every stack in the ingredients gathered from the input bus. + for (Object2IntMap.Entry inventoryEntry : ingredientStacks.object2IntEntrySet()) { + if (recipeInputEntry.getKey().test(inventoryEntry.getKey())) { + available = inventoryEntry.getIntValue(); + if (available > needed) { + inventoryEntry.setValue(available - needed); + needed -= available; + break; + } else { + inventoryEntry.setValue(0); + recipeInputEntry.setValue(needed - available); + needed -= available; + } + } + } + // We need to check >= available here because of Non-Consumable inputs with stack size. If there is a NC + // input + // with size 2, and only 1 in the input, needed will be equal to available, but this situation should still + // fail + // as not all inputs are present + if (needed >= available) { + return 0; + } + } + + // Return the maximum parallel limit here if there are only non-consumed inputs, which are all found in the + // input bus + // At this point, we would have already returned 0 if we were missing any non-consumable inputs, so we can omit + // that check + if (countableMap.isEmpty() && !notConsumableMap.isEmpty()) { + return parallelAmount; + } + + // Iterate through the recipe inputs + for (Object2IntMap.Entry recipeInputEntry : countableMap.object2IntEntrySet()) { + int needed = recipeInputEntry.getIntValue(); + int available = 0; + // For every stack in the ingredients gathered from the input bus. + for (Object2IntMap.Entry inventoryEntry : ingredientStacks.object2IntEntrySet()) { + if (recipeInputEntry.getKey().test(inventoryEntry.getKey())) { + available += inventoryEntry.getIntValue(); + } + } + if (available >= needed) { + int ratio = Math.min(parallelAmount, available / needed); + if (ratio < minMultiplier) { + minMultiplier = ratio; + } + } else { + return 0; + } + } + return minMultiplier; + } + @Override public @NotNull List createXEIContainerContents(List contents, GTRecipe recipe, IO io) { var outputStacks = contents.stream().map(content -> content.content) diff --git a/src/main/java/com/gregtechceu/gtceu/api/capability/recipe/RecipeCapability.java b/src/main/java/com/gregtechceu/gtceu/api/capability/recipe/RecipeCapability.java index b4232f5cd0..e697a0820e 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/capability/recipe/RecipeCapability.java +++ b/src/main/java/com/gregtechceu/gtceu/api/capability/recipe/RecipeCapability.java @@ -6,6 +6,7 @@ import com.gregtechceu.gtceu.api.recipe.content.ContentModifier; import com.gregtechceu.gtceu.api.recipe.content.IContentSerializer; import com.gregtechceu.gtceu.api.recipe.lookup.AbstractMapIngredient; +import com.gregtechceu.gtceu.api.recipe.modifier.ParallelLogic; import com.gregtechceu.gtceu.api.recipe.ui.GTRecipeTypeUI; import com.lowdragmc.lowdraglib.gui.widget.Widget; import com.lowdragmc.lowdraglib.gui.widget.WidgetGroup; @@ -15,7 +16,6 @@ import org.apache.commons.lang3.mutable.MutableInt; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.jetbrains.annotations.UnknownNullability; import java.util.*; @@ -65,19 +65,6 @@ public final T copyContent(Object content, ContentModifier modifier) { return copyWithModifier((T) content, modifier); } - /** - * Convert the passed object to a list of recipe lookup filters. - * @param ingredient ingredient. e.g. for ITEM, this can be Ingredient or ItemStack - * @return a list of recipe lookup filters. - */ - public List convertToMapIngredient(Object ingredient) { - return List.of(); - } - - public List compressIngredients(Collection ingredients) { - return new ArrayList<>(ingredients); - } - /** * used for recipe builder via KubeJs. */ @@ -101,13 +88,54 @@ public boolean isRecipeSearchFilter() { return false; } + /** + * Convert the passed object to a list of recipe lookup filters. + * @param ingredient ingredient. e.g. for ITEM, this can be Ingredient or ItemStack + * @return a list of recipe lookup filters. + */ + public List convertToMapIngredient(Object ingredient) { + return List.of(); + } + + public List compressIngredients(Collection ingredients) { + return new ArrayList<>(ingredients); + } + /** * Does the recipe test if this capability is workable? if not, you should test validity somewhere else. */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") public boolean doMatchInRecipe() { return true; } + /** + * maximum parallel amount based on the inputs (and possibly outputs) provided. + * + * @param recipe the recipe from which we get the input to product ratio + * @param holder the {@link IRecipeCapabilityHolder} that contains all the inputs and outputs of the machine. + * @param multiplier the maximum possible multiplied we can get from the input inventory + * see {@link ParallelLogic#getMaxRecipeMultiplier(GTRecipe, IRecipeCapabilityHolder, int)} + * @return the amount of times a {@link GTRecipe} outputs can be merged into an inventory without voiding products. + */ + // returns Integer.MAX_VALUE by default, to skip processing. + public int limitParallel(GTRecipe recipe, IRecipeCapabilityHolder holder, int multiplier) { + return Integer.MAX_VALUE; + } + + /** + * Finds the maximum number of GTRecipes that can be performed at the same time based on the contents of input inventories + * + * @param holder The {@link IRecipeCapabilityHolder} that contains all the inputs and outputs of the machine. + * @param recipe The {@link GTRecipe} for which to find the maximum that can be run simultaneously + * @param parallelAmount The limit on the amount of recipes that can be performed at one time + * @return The Maximum number of GTRecipes that can be performed at a single time based on the available Items + */ + // returns Integer.MAX_VALUE by default, to skip processing. + public int getMaxParallelRatio(IRecipeCapabilityHolder holder, GTRecipe recipe, int parallelAmount) { + return Integer.MAX_VALUE; + } + public boolean doAddGuiSlots() { return isRecipeSearchFilter(); } @@ -126,7 +154,7 @@ public Object createXEIContainer(List contents) { return null; } - @UnknownNullability("null when getWidgetClass() == null") + @Nullable("null when getWidgetClass() == null") public Widget createWidget() { return null; } @@ -140,9 +168,9 @@ public void applyWidgetInfo(@NotNull Widget widget, int index, boolean isXEI, IO io, - GTRecipeTypeUI.@UnknownNullability("null when storage == null") RecipeHolder recipeHolder, + @Nullable("null when storage == null") GTRecipeTypeUI.RecipeHolder recipeHolder, @NotNull GTRecipeType recipeType, - @UnknownNullability("null when content == null") GTRecipe recipe, + @Nullable("null when content == null") GTRecipe recipe, @Nullable Content content, @Nullable Object storage) { diff --git a/src/main/java/com/gregtechceu/gtceu/api/machine/SimpleGeneratorMachine.java b/src/main/java/com/gregtechceu/gtceu/api/machine/SimpleGeneratorMachine.java index 667261227a..57d4af1fbb 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/machine/SimpleGeneratorMachine.java +++ b/src/main/java/com/gregtechceu/gtceu/api/machine/SimpleGeneratorMachine.java @@ -80,7 +80,7 @@ public static GTRecipe recipeModifier(MetaMachine machine, @NotNull GTRecipe rec var EUt = RecipeHelper.getOutputEUt(recipe); if (EUt > 0) { var maxParallel = (int)(Math.min(generator.getOverclockVoltage(), GTValues.V[generator.getOverclockTier()]) / EUt); - return GTRecipeModifiers.fastParallel(generator, recipe, maxParallel, false).getA(); + return GTRecipeModifiers.fastParallel(generator, recipe, maxParallel, false).getFirst(); } } return null; diff --git a/src/main/java/com/gregtechceu/gtceu/api/recipe/ResearchRecipeBuilder.java b/src/main/java/com/gregtechceu/gtceu/api/recipe/ResearchRecipeBuilder.java index bcab66aeaa..8a425efe2d 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/recipe/ResearchRecipeBuilder.java +++ b/src/main/java/com/gregtechceu/gtceu/api/recipe/ResearchRecipeBuilder.java @@ -7,6 +7,7 @@ import com.gregtechceu.gtceu.data.recipe.builder.GTRecipeBuilder; import com.gregtechceu.gtceu.utils.ResearchManager; import com.gregtechceu.gtceu.utils.GTStringUtils; +import dev.latvian.mods.kubejs.recipe.RecipeJS; import lombok.NoArgsConstructor; import net.minecraft.world.item.ItemStack; diff --git a/src/main/java/com/gregtechceu/gtceu/api/recipe/modifier/ParallelLogic.java b/src/main/java/com/gregtechceu/gtceu/api/recipe/modifier/ParallelLogic.java new file mode 100644 index 0000000000..88b9bbafe7 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/recipe/modifier/ParallelLogic.java @@ -0,0 +1,178 @@ +package com.gregtechceu.gtceu.api.recipe.modifier; + +import com.gregtechceu.gtceu.api.GTValues; +import com.gregtechceu.gtceu.api.capability.recipe.*; +import com.gregtechceu.gtceu.api.machine.MetaMachine; +import com.gregtechceu.gtceu.api.machine.feature.IOverclockMachine; +import com.gregtechceu.gtceu.api.machine.feature.IRecipeLogicMachine; +import com.gregtechceu.gtceu.api.machine.feature.ITieredMachine; +import com.gregtechceu.gtceu.api.recipe.GTRecipe; +import com.gregtechceu.gtceu.api.recipe.RecipeHelper; +import com.gregtechceu.gtceu.api.recipe.content.ContentModifier; +import com.mojang.datafixers.util.Pair; +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import it.unimi.dsi.fastutil.ints.IntSet; +import it.unimi.dsi.fastutil.objects.*; +import lombok.AllArgsConstructor; +import net.minecraft.MethodsReturnNonnullByDefault; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.annotation.ParametersAreNonnullByDefault; +import java.util.function.Predicate; + +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +@AllArgsConstructor +public class ParallelLogic { + + @Nullable + public static Pair applyParallel(MetaMachine machine, @Nullable GTRecipe recipe, int parallelLimit, boolean modifyDuration) { + if (recipe == null) { + return null; + } + if (machine instanceof IRecipeLogicMachine rlm) { + return doParallelRecipes(recipe, rlm, parallelLimit, modifyDuration); + } + return null; + } + + /** + * @param recipe The recipe + * @param holder The inventories + * @param parallelAmount hard cap on the amount returned + * @return returns the amount of possible time a recipe can be made from a given input inventory + */ + public static int getMaxRecipeMultiplier(@NotNull GTRecipe recipe, @NotNull IRecipeCapabilityHolder holder, int parallelAmount) { + IntSet multipliers = new IntOpenHashSet(); + + for (RecipeCapability cap : recipe.inputs.keySet()) { + // Find the maximum number of recipes that can be performed from the contents of the input inventories + multipliers.add(cap.getMaxParallelRatio(holder, recipe, parallelAmount)); + } + if (multipliers.intStream().allMatch(value -> value == Integer.MAX_VALUE)) { + return 0; + } + // Find the maximum number of recipes that can be performed from all available inputs + return multipliers.intStream().min().orElse(0); + } + + /** + * @param recipe The recipe + * @param holder the inventories + * @param parallelAmount the maximum expected amount + * @param canVoid predicate for what parallel limits should be ignored + * @return returns the amount of recipes that can be merged successfully into a given output inventory + */ + public static int limitByOutputMerging(@NotNull GTRecipe recipe, @NotNull IRecipeCapabilityHolder holder, + int parallelAmount, Predicate> canVoid) { + Object2IntMap> modifiedParallelAmounts = new Object2IntOpenHashMap<>(); + boolean canVoidAll = true; + for (RecipeCapability cap : recipe.inputs.keySet()) { + modifiedParallelAmounts.put(cap, Integer.MAX_VALUE); + if (!canVoid.test(cap)) { + canVoidAll = false; + } + } + // If we are voiding everything, return the maximum number of parallels that can be performed from + // the inputs + if (canVoidAll) { + return parallelAmount; + } + + for (RecipeCapability cap : recipe.inputs.keySet()) { + // Check both normal item outputs and chanced item outputs + if (!recipe.getOutputContents(cap).isEmpty()) { + boolean voiding = canVoid.test(cap); + // If we are voiding items, reset the item limit to the maximum number of parallels + if (voiding) { + modifiedParallelAmounts.put(cap, parallelAmount); + } else { + modifiedParallelAmounts.put(cap, cap.limitParallel(recipe, holder, parallelAmount)); + } + + // If we are not voiding, and cannot fit any items, return 0 + if (modifiedParallelAmounts.getInt(cap) == 0 && !voiding) { + return 0; + } + } + } + + return modifiedParallelAmounts.values().intStream().min().orElse(0); + } + + /** + * Binary-search-like approach to find the maximum amount that can be inserted + * + * @param mergedAll if the merge was successful. + * If true sets {@code minMultiplier} to the as the current multiplier + * then sets {@code multiplier} to the sum of the mean difference between + * {@code multiplier} and {@code maxMultiplier} plus the remainder of the division, if any, + * and itself + * If false, sets {@code maxMultiplier} as the current multiplier, then sets {@code multiplier} + * to half of its value limited it to no less or than the value of {@code minMultiplier} + * @param minMultiplier the last known multiplier what was fully merged + * @param multiplier the current multiplier + * @param maxMultiplier the last know multiplier that resulted in simulation failure + * @return an array consisting of the last known multiplier, new multiplier to be attempted and + * the last know multiplier that resulted in failure + */ + public static int @NotNull [] adjustMultiplier(boolean mergedAll, int minMultiplier, int multiplier, + int maxMultiplier) { + if (mergedAll) { + minMultiplier = multiplier; + int remainder = (maxMultiplier - multiplier) % 2; + multiplier = multiplier + remainder + (maxMultiplier - multiplier) / 2; + } else { + maxMultiplier = multiplier; + multiplier = (multiplier + minMultiplier) / 2; + } + if (maxMultiplier - minMultiplier <= 1) { + multiplier = maxMultiplier = minMultiplier; + } + return new int[] { minMultiplier, multiplier, maxMultiplier }; + } + + // At this point, the recipe is already trimmed according to the item and fluid output limit, so we just need to + // take care of voiding + @Nullable + public static Pair doParallelRecipes(@NotNull GTRecipe currentRecipe, + @NotNull IRecipeLogicMachine machine, + int parallelAmount, boolean modifyDuration) { + long maxVoltage = Long.MAX_VALUE; + if (machine instanceof IOverclockMachine overclockMachine) { + maxVoltage = overclockMachine.getOverclockVoltage(); + } else if (machine instanceof ITieredMachine tieredMachine) { + maxVoltage = GTValues.V[tieredMachine.getTier()]; + } + // First check if we are limited by recipe inputs. This can short circuit a lot of consecutive checking + int multiplierByInputs = getMaxRecipeMultiplier(currentRecipe, machine, parallelAmount); + if (multiplierByInputs == 0) { + return null; + } + + // Simulate the merging of the maximum amount of recipes that can be run with these items + // and limit by the amount we can successfully merge + int limitByOutput = ParallelLogic.limitByOutputMerging(currentRecipe, machine, multiplierByInputs, machine::canVoidRecipeOutputs); + int parallelLimit = limitByOutput; + + long recipeEUt = RecipeHelper.getInputEUt(currentRecipe); + if (recipeEUt == 0) { + recipeEUt = RecipeHelper.getOutputEUt(currentRecipe); + } + if (recipeEUt != 0) { + int limitByVoltage = Math.abs((int) (maxVoltage / recipeEUt)); + int maxParallel = Math.min(limitByVoltage, limitByOutput); + if (maxParallel != 0) { + // Use the minimum between the amount of recipes we can run with available inputs and amount of recipe + // outputs that can fit + parallelLimit = Math.min(maxParallel, multiplierByInputs); + currentRecipe = currentRecipe.copy(ContentModifier.multiplier(parallelLimit), modifyDuration); + } + } else if (limitByOutput > 0) { + currentRecipe = currentRecipe.copy(ContentModifier.multiplier(limitByOutput), modifyDuration); + } + + return Pair.of(currentRecipe, parallelLimit); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/recipe/modifier/RecipeModifier.java b/src/main/java/com/gregtechceu/gtceu/api/recipe/modifier/RecipeModifier.java index ef27a120f0..8b7731c5e3 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/recipe/modifier/RecipeModifier.java +++ b/src/main/java/com/gregtechceu/gtceu/api/recipe/modifier/RecipeModifier.java @@ -3,6 +3,7 @@ import com.gregtechceu.gtceu.api.machine.MetaMachine; import com.gregtechceu.gtceu.api.recipe.GTRecipe; import net.minecraft.MethodsReturnNonnullByDefault; +import org.jetbrains.annotations.Nullable; import javax.annotation.ParametersAreNonnullByDefault; @@ -10,5 +11,6 @@ @ParametersAreNonnullByDefault @FunctionalInterface public interface RecipeModifier { - GTRecipe apply(MetaMachine machine, GTRecipe recipe); + @Nullable + GTRecipe apply(MetaMachine machine, @Nullable GTRecipe recipe); } diff --git a/src/main/java/com/gregtechceu/gtceu/api/recipe/modifier/RecipeModifierList.java b/src/main/java/com/gregtechceu/gtceu/api/recipe/modifier/RecipeModifierList.java index 3f1cb35d7f..c448398178 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/recipe/modifier/RecipeModifierList.java +++ b/src/main/java/com/gregtechceu/gtceu/api/recipe/modifier/RecipeModifierList.java @@ -4,6 +4,7 @@ import com.gregtechceu.gtceu.api.recipe.GTRecipe; import net.minecraft.MethodsReturnNonnullByDefault; +import javax.annotation.Nullable; import javax.annotation.ParametersAreNonnullByDefault; import java.util.List; @@ -16,8 +17,9 @@ public RecipeModifierList(RecipeModifier... modifiers) { this.modifiers = modifiers; } + @Nullable @Override - public GTRecipe apply(MetaMachine machine, GTRecipe recipe) { + public GTRecipe apply(MetaMachine machine, @Nullable GTRecipe recipe) { GTRecipe modifiedRecipe = recipe; for (RecipeModifier modifier : modifiers) { diff --git a/src/main/java/com/gregtechceu/gtceu/common/data/GTRecipeModifiers.java b/src/main/java/com/gregtechceu/gtceu/common/data/GTRecipeModifiers.java index 6fea7fd65d..169b6e7357 100644 --- a/src/main/java/com/gregtechceu/gtceu/common/data/GTRecipeModifiers.java +++ b/src/main/java/com/gregtechceu/gtceu/common/data/GTRecipeModifiers.java @@ -14,18 +14,18 @@ import com.gregtechceu.gtceu.api.recipe.RecipeHelper; import com.gregtechceu.gtceu.api.recipe.content.Content; import com.gregtechceu.gtceu.api.recipe.content.ContentModifier; +import com.gregtechceu.gtceu.api.recipe.modifier.ParallelLogic; import com.gregtechceu.gtceu.api.recipe.modifier.RecipeModifier; -import lombok.val; +import com.mojang.datafixers.util.Pair; import net.minecraft.MethodsReturnNonnullByDefault; import net.minecraft.Util; -import net.minecraft.util.Tuple; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import javax.annotation.ParametersAreNonnullByDefault; import java.util.List; import java.util.Optional; -import java.util.function.BiFunction; import java.util.function.Function; /** @@ -39,19 +39,23 @@ public class GTRecipeModifiers { * Use it if machines are {@link IOverclockMachine}. */ public static final Function ELECTRIC_OVERCLOCK = Util.memoize(ElectricOverclockModifier::new); - public static final RecipeModifier PARALLEL_HATCH = (machine, recipe) -> GTRecipeModifiers.hatchParallel(machine, recipe, false).getA(); + public static final RecipeModifier PARALLEL_HATCH = (machine, recipe) -> GTRecipeModifiers.hatchParallel(machine, recipe, false).getFirst(); @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault public static class ElectricOverclockModifier implements RecipeModifier { private final OverclockingLogic overclockingLogic; - public ElectricOverclockModifier(OverclockingLogic overclockingLogic) { this.overclockingLogic = overclockingLogic; } + + @Nullable @Override - public GTRecipe apply(MetaMachine machine, GTRecipe recipe) { + public GTRecipe apply(MetaMachine machine, @Nullable GTRecipe recipe) { + if (recipe == null) { + return null; + } if (machine instanceof ITieredMachine tieredMachine && RecipeHelper.getRecipeEUtTier(recipe) > tieredMachine.getTier()) { return null; } @@ -70,18 +74,18 @@ public GTRecipe apply(MetaMachine machine, GTRecipe recipe) { * @param modifyDuration should multiply the duration * @return modified recipe and parallel amount */ - public static Tuple fastParallel(MetaMachine machine, @NotNull GTRecipe recipe, int maxParallel, boolean modifyDuration) { + public static Pair fastParallel(MetaMachine machine, @NotNull GTRecipe recipe, int maxParallel, boolean modifyDuration) { if (machine instanceof IRecipeCapabilityHolder holder) { while (maxParallel > 0) { var copied = recipe.copy(ContentModifier.multiplier(maxParallel), modifyDuration); if (copied.matchRecipe(holder).isSuccess() && copied.matchTickRecipe(holder).isSuccess()) { - return new Tuple<>(copied, maxParallel); + return Pair.of(copied, maxParallel); } maxParallel /= 2; } } - return new Tuple<>(recipe, 1); - }; + return Pair.of(recipe, 1); + } /** * Accurate parallel, always look for the maximum parallel value within maxParallel. @@ -91,47 +95,22 @@ public static Tuple fastParallel(MetaMachine machine, @NotNul * @param modifyDuration should multiply the duration * @return modified recipe and parallel amount */ - public static Tuple accurateParallel(MetaMachine machine, @NotNull GTRecipe recipe, int maxParallel, boolean modifyDuration) { + public static Pair accurateParallel(MetaMachine machine, @NotNull GTRecipe recipe, int maxParallel, boolean modifyDuration) { if (maxParallel == 1) { - return new Tuple<>(recipe, 1); - } -// if(!(machine instanceof ITieredMachine))return new Tuple<>(recipe, 1); - if (machine instanceof IRecipeCapabilityHolder holder) { - var parallel = tryParallel(holder, recipe, 1, maxParallel, modifyDuration); - return parallel == null ? new Tuple<>(recipe, 1) : parallel; - } - return null; - } - - private static Tuple tryParallel(IRecipeCapabilityHolder holder, GTRecipe original, int min, int max, boolean modifyDuration) { - if (min > max) return null; - - int mid = (min + max) / 2; - GTRecipe copied = original.copy(ContentModifier.multiplier(mid), modifyDuration); - if (!copied.matchRecipe(holder).isSuccess() || !copied.matchTickRecipe(holder).isSuccess() || - !(holder instanceof ITieredMachine && RecipeHelper.getRecipeEUtTier(copied) <= ((ITieredMachine)holder).getTier())) { - // tried too many - return tryParallel(holder, original, min, mid - 1, modifyDuration); - } else { - // at max parallels - if (mid == max) { - return new Tuple<>(copied, mid); - } - // matches, but try to do more - var tryMore = tryParallel(holder, original, mid + 1, max, modifyDuration); - return tryMore != null ? tryMore : new Tuple<>(copied, mid); + return Pair.of(recipe, 1); } + return ParallelLogic.applyParallel(machine, recipe, maxParallel, modifyDuration); } - public static Tuple hatchParallel(MetaMachine machine, @NotNull GTRecipe recipe, boolean modifyDuration) { + public static Pair hatchParallel(MetaMachine machine, @Nullable GTRecipe recipe, boolean modifyDuration) { if (machine instanceof IMultiController controller && controller.isFormed()) { Optional optional = controller.getParts().stream().filter(IParallelHatch.class::isInstance).map(IParallelHatch.class::cast).findAny(); if (optional.isPresent()) { IParallelHatch hatch = optional.get(); - return accurateParallel(machine, recipe, hatch.getCurrentParallel(), modifyDuration); + return ParallelLogic.applyParallel(machine, recipe, hatch.getCurrentParallel(), modifyDuration); } } - return new Tuple<>(recipe, 1); + return Pair.of(recipe, 1); } public static GTRecipe crackerOverclock(MetaMachine machine, @NotNull GTRecipe recipe) { @@ -153,7 +132,7 @@ public static GTRecipe crackerOverclock(MetaMachine machine, @NotNull GTRecipe r public static GTRecipe ebfOverclock(MetaMachine machine, @NotNull GTRecipe recipe) { if (machine instanceof CoilWorkableElectricMultiblockMachine coilMachine) { - val blastFurnaceTemperature = coilMachine.getCoilType().getCoilTemperature() + 100 * Math.max(0, coilMachine.getTier() - GTValues.MV); + final var blastFurnaceTemperature = coilMachine.getCoilType().getCoilTemperature() + 100 * Math.max(0, coilMachine.getTier() - GTValues.MV); if (!recipe.data.contains("ebf_temp") || recipe.data.getInt("ebf_temp") > blastFurnaceTemperature) { return null; } diff --git a/src/main/java/com/gregtechceu/gtceu/common/machine/multiblock/electric/ProcessingArrayMachine.java b/src/main/java/com/gregtechceu/gtceu/common/machine/multiblock/electric/ProcessingArrayMachine.java index d9330d3bb1..4a0a9c5c14 100644 --- a/src/main/java/com/gregtechceu/gtceu/common/machine/multiblock/electric/ProcessingArrayMachine.java +++ b/src/main/java/com/gregtechceu/gtceu/common/machine/multiblock/electric/ProcessingArrayMachine.java @@ -209,8 +209,8 @@ public static GTRecipe recipeModifier(MetaMachine machine, @NotNull GTRecipe rec var parallel = Objects.requireNonNull(GTRecipeModifiers.accurateParallel( machine, recipe, Math.min(parallelLimit, getMachineLimit(machine.getDefinition().getTier())), false )); - int parallelCount = parallel.getB(); - recipe = parallel.getA(); + int parallelCount = parallel.getSecond(); + recipe = parallel.getFirst(); // apply overclock afterward long maxVoltage = Math.min(processingArray.getOverclockVoltage() * parallelCount, processingArray.getMaxVoltage()); diff --git a/src/main/java/com/gregtechceu/gtceu/common/machine/multiblock/generator/LargeCombustionEngineMachine.java b/src/main/java/com/gregtechceu/gtceu/common/machine/multiblock/generator/LargeCombustionEngineMachine.java index 16108261d0..aaec491731 100644 --- a/src/main/java/com/gregtechceu/gtceu/common/machine/multiblock/generator/LargeCombustionEngineMachine.java +++ b/src/main/java/com/gregtechceu/gtceu/common/machine/multiblock/generator/LargeCombustionEngineMachine.java @@ -109,11 +109,11 @@ public static GTRecipe recipeModifier(MetaMachine machine, @NotNull GTRecipe rec var maxParallel = (int) (engineMachine.getOverclockVoltage() / EUt); // get maximum parallel var parallelResult = GTRecipeModifiers.fastParallel(engineMachine, recipe, maxParallel, false); if (engineMachine.isOxygenBoosted) { // boost production - recipe = parallelResult.getA() == recipe ? recipe.copy() : parallelResult.getA(); - long eut = (long) (EUt * parallelResult.getB() * (engineMachine.isExtreme() ? 2 : 1.5)); + recipe = parallelResult.getFirst() == recipe ? recipe.copy() : parallelResult.getFirst(); + long eut = (long) (EUt * parallelResult.getSecond() * (engineMachine.isExtreme() ? 2 : 1.5)); recipe.tickOutputs.put(EURecipeCapability.CAP, List.of(new Content(eut, 1.0f, 0.0f, null, null))); } else { - recipe = parallelResult.getA(); + recipe = parallelResult.getFirst(); } return recipe; } diff --git a/src/main/java/com/gregtechceu/gtceu/common/machine/multiblock/generator/LargeTurbineMachine.java b/src/main/java/com/gregtechceu/gtceu/common/machine/multiblock/generator/LargeTurbineMachine.java index 4df133d450..7ff2322bd5 100644 --- a/src/main/java/com/gregtechceu/gtceu/common/machine/multiblock/generator/LargeTurbineMachine.java +++ b/src/main/java/com/gregtechceu/gtceu/common/machine/multiblock/generator/LargeTurbineMachine.java @@ -96,8 +96,8 @@ public static GTRecipe recipeModifier(MetaMachine machine, @NotNull GTRecipe rec //this is necessary to prevent over-consumption of fuel turbineMachine.excessVoltage += (int) (maxParallel * EUt * holderEfficiency - turbineMaxVoltage); var parallelResult = GTRecipeModifiers.fastParallel(turbineMachine, recipe, Math.max(1, maxParallel), false); - recipe = parallelResult.getA() == recipe ? recipe.copy() : parallelResult.getA(); - long eut = turbineMachine.boostProduction((long) (EUt * holderEfficiency * parallelResult.getB())); + recipe = parallelResult.getFirst() == recipe ? recipe.copy() : parallelResult.getFirst(); + long eut = turbineMachine.boostProduction((long) (EUt * holderEfficiency * parallelResult.getSecond())); recipe.tickOutputs.put(EURecipeCapability.CAP, List.of(new Content(eut, 1.0f, 0.0f, null, null))); return recipe; } diff --git a/src/main/java/com/gregtechceu/gtceu/common/machine/multiblock/steam/SteamParallelMultiblockMachine.java b/src/main/java/com/gregtechceu/gtceu/common/machine/multiblock/steam/SteamParallelMultiblockMachine.java index 763276e09b..5a7549e43e 100644 --- a/src/main/java/com/gregtechceu/gtceu/common/machine/multiblock/steam/SteamParallelMultiblockMachine.java +++ b/src/main/java/com/gregtechceu/gtceu/common/machine/multiblock/steam/SteamParallelMultiblockMachine.java @@ -71,7 +71,7 @@ public void onStructureFormed() { public static GTRecipe recipeModifier(MetaMachine machine, @NotNull GTRecipe recipe) { int duration = recipe.duration; var eut = RecipeHelper.getInputEUt(recipe); - var result = GTRecipeModifiers.accurateParallel(machine, recipe, MAX_PARALLELS, false).getA(); + var result = GTRecipeModifiers.accurateParallel(machine, recipe, MAX_PARALLELS, false).getFirst(); recipe = result == recipe ? result.copy() : result; // we remove tick inputs, as our "cost" is just steam now, just stored as EU/t diff --git a/src/main/java/com/gregtechceu/gtceu/test/api/machine/trait/ParallelLogicTest.java b/src/main/java/com/gregtechceu/gtceu/test/api/machine/trait/ParallelLogicTest.java index 500a3e95ea..a074f36eac 100644 --- a/src/main/java/com/gregtechceu/gtceu/test/api/machine/trait/ParallelLogicTest.java +++ b/src/main/java/com/gregtechceu/gtceu/test/api/machine/trait/ParallelLogicTest.java @@ -50,7 +50,7 @@ public void getMaxRecipeMultiplier_FluidLimitTest(GameTestHelper helper) { var paralleled = GTRecipeModifiers.accurateParallel(machine, recipe, parallelLimit, false); - helper.assertTrue(paralleled.getB() == 2,"Expected Parallel amount to be 2, is %s.".formatted(paralleled.getB())); + helper.assertTrue(paralleled.getSecond() == 2,"Expected Parallel amount to be 2, is %s.".formatted(paralleled.getSecond())); helper.succeed(); } @@ -84,7 +84,7 @@ public void getMaxRecipeMultiplier_LimitFailureTest(GameTestHelper helper) { var paralleled = GTRecipeModifiers.accurateParallel(machine, recipe, parallelLimit, false); - helper.assertTrue(paralleled == null || paralleled.getB() == 0, "Parallel is too high, should be 0, is %s.".formatted(paralleled.getB())); + helper.assertTrue(paralleled == null || paralleled.getSecond() == 0, "Parallel is too high, should be 0, is %s.".formatted(paralleled.getSecond())); helper.succeed(); } @@ -118,7 +118,7 @@ public void getMaxRecipeMultiplier_ItemFailureTest(GameTestHelper helper) { var paralleled = GTRecipeModifiers.accurateParallel(machine, recipe, parallelLimit, false); - helper.assertTrue(paralleled == null || paralleled.getB() == 0, "Parallel is too high, should be 0, is %s.".formatted(paralleled.getB())); + helper.assertTrue(paralleled == null || paralleled.getSecond() == 0, "Parallel is too high, should be 0, is %s.".formatted(paralleled.getSecond())); helper.succeed(); } From 94aa2cf90862e09e233e547a1a679bf88e595de3 Mon Sep 17 00:00:00 2001 From: screret <68943070+screret@users.noreply.github.com> Date: Sun, 5 May 2024 17:21:05 +0300 Subject: [PATCH 2/3] fix multi smelter not overclocking (fixes #1191) --- .../gtceu/common/data/GTMachines.java | 3 +-- .../gtceu/common/data/GTRecipeModifiers.java | 26 ++++++++++++------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/gregtechceu/gtceu/common/data/GTMachines.java b/src/main/java/com/gregtechceu/gtceu/common/data/GTMachines.java index 763578fe7a..cf09bd5aaf 100644 --- a/src/main/java/com/gregtechceu/gtceu/common/data/GTMachines.java +++ b/src/main/java/com/gregtechceu/gtceu/common/data/GTMachines.java @@ -93,7 +93,6 @@ import java.util.Locale; import java.util.function.BiConsumer; import java.util.function.BiFunction; -import java.util.function.Consumer; import java.util.function.Supplier; import java.util.stream.Stream; @@ -1144,7 +1143,7 @@ public static BiConsumer> createTankTooltips(String n public static final MultiblockMachineDefinition MULTI_SMELTER = REGISTRATE.multiblock("multi_smelter", CoilWorkableElectricMultiblockMachine::new) .rotationState(RotationState.NON_Y_AXIS) .recipeTypes(GTRecipeTypes.FURNACE_RECIPES, GTRecipeTypes.ALLOY_SMELTER_RECIPES) - .recipeModifier(GTRecipeModifiers::multiSmelterOverclock) + .recipeModifiers(GTRecipeModifiers::multiSmelterParallel, GTRecipeModifiers.ELECTRIC_OVERCLOCK.apply(OverclockingLogic.NON_PERFECT_OVERCLOCK)) .appearanceBlock(CASING_INVAR_HEATPROOF) .pattern(definition -> FactoryBlockPattern.start() .aisle("XXX", "CCC", "XXX") diff --git a/src/main/java/com/gregtechceu/gtceu/common/data/GTRecipeModifiers.java b/src/main/java/com/gregtechceu/gtceu/common/data/GTRecipeModifiers.java index 169b6e7357..29aff41768 100644 --- a/src/main/java/com/gregtechceu/gtceu/common/data/GTRecipeModifiers.java +++ b/src/main/java/com/gregtechceu/gtceu/common/data/GTRecipeModifiers.java @@ -130,7 +130,10 @@ public static GTRecipe crackerOverclock(MetaMachine machine, @NotNull GTRecipe r return null; } - public static GTRecipe ebfOverclock(MetaMachine machine, @NotNull GTRecipe recipe) { + public static GTRecipe ebfOverclock(MetaMachine machine, @Nullable GTRecipe recipe) { + if (recipe == null) { + return null; + } if (machine instanceof CoilWorkableElectricMultiblockMachine coilMachine) { final var blastFurnaceTemperature = coilMachine.getCoilType().getCoilTemperature() + 100 * Math.max(0, coilMachine.getTier() - GTValues.MV); if (!recipe.data.contains("ebf_temp") || recipe.data.getInt("ebf_temp") > blastFurnaceTemperature) { @@ -151,7 +154,10 @@ public static GTRecipe ebfOverclock(MetaMachine machine, @NotNull GTRecipe recip return null; } - public static GTRecipe pyrolyseOvenOverclock(MetaMachine machine, @NotNull GTRecipe recipe) { + public static GTRecipe pyrolyseOvenOverclock(MetaMachine machine, @Nullable GTRecipe recipe) { + if (recipe == null) { + return null; + } if (machine instanceof CoilWorkableElectricMultiblockMachine coilMachine) { if (RecipeHelper.getRecipeEUtTier(recipe) > coilMachine.getTier()) { return null; @@ -170,18 +176,20 @@ public static GTRecipe pyrolyseOvenOverclock(MetaMachine machine, @NotNull GTRec return null; } - public static GTRecipe multiSmelterOverclock(MetaMachine machine, @NotNull GTRecipe recipe) { + public static GTRecipe multiSmelterParallel(MetaMachine machine, @Nullable GTRecipe recipe) { + if (recipe == null) { + return null; + } if (machine instanceof CoilWorkableElectricMultiblockMachine coilMachine) { - var energyCost = Math.max(1L, 16 / coilMachine.getCoilType().getEnergyDiscount()); + var maxParallel = 32 * coilMachine.getCoilType().getLevel(); - var parallelLimit = Math.min(maxParallel, (int) (coilMachine.getOverclockVoltage() / energyCost)); - var result = GTRecipeModifiers.accurateParallel(machine, recipe, parallelLimit, false); - recipe = result.getA() == recipe ? result.getA().copy() : result.getA(); + var result = GTRecipeModifiers.accurateParallel(machine, recipe, maxParallel, false); + recipe = result.getFirst() == recipe ? result.getFirst().copy() : result.getFirst(); - int parallelValue = result.getB(); + int parallelValue = result.getSecond(); recipe.duration = Math.max(1, 256 * parallelValue / maxParallel); - long eut = parallelValue * energyCost; + long eut = 4 * (parallelValue / 8) / coilMachine.getCoilType().getEnergyDiscount(); recipe.tickInputs.put(EURecipeCapability.CAP, List.of(new Content(eut, 1.0f, 0.0f, null, null))); return recipe; } From c832023336c9b9edd7faaf7f5ae61b49ca363903 Mon Sep 17 00:00:00 2001 From: screret <68943070+screret@users.noreply.github.com> Date: Sun, 5 May 2024 17:41:03 +0300 Subject: [PATCH 3/3] move EU/t logic into EURecipeCapability, also check for tick inputs in ParallelLogic --- .../capability/recipe/EURecipeCapability.java | 36 +++++++++ .../api/recipe/modifier/ParallelLogic.java | 79 +++++++++++++------ 2 files changed, 90 insertions(+), 25 deletions(-) diff --git a/src/main/java/com/gregtechceu/gtceu/api/capability/recipe/EURecipeCapability.java b/src/main/java/com/gregtechceu/gtceu/api/capability/recipe/EURecipeCapability.java index 7a10f95e0b..3771f5c4e7 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/capability/recipe/EURecipeCapability.java +++ b/src/main/java/com/gregtechceu/gtceu/api/capability/recipe/EURecipeCapability.java @@ -1,10 +1,18 @@ package com.gregtechceu.gtceu.api.capability.recipe; +import com.gregtechceu.gtceu.api.GTValues; +import com.gregtechceu.gtceu.api.capability.IEnergyContainer; +import com.gregtechceu.gtceu.api.machine.feature.IOverclockMachine; +import com.gregtechceu.gtceu.api.machine.feature.ITieredMachine; +import com.gregtechceu.gtceu.api.recipe.GTRecipe; +import com.gregtechceu.gtceu.api.recipe.RecipeHelper; import com.gregtechceu.gtceu.api.recipe.content.ContentModifier; import com.gregtechceu.gtceu.api.recipe.content.SerializerLong; import java.util.Collection; +import java.util.Collections; import java.util.List; +import java.util.Objects; /** * @author KilaBash @@ -33,4 +41,32 @@ public Long copyWithModifier(Long content, ContentModifier modifier) { public List compressIngredients(Collection ingredients) { return List.of(ingredients.stream().map(Long.class::cast).reduce(0L, Long::sum)); } + + @Override + public int limitParallel(GTRecipe recipe, IRecipeCapabilityHolder holder, int multiplier) { + long maxVoltage = Long.MAX_VALUE; + if (holder instanceof IOverclockMachine overclockMachine) { + maxVoltage = overclockMachine.getOverclockVoltage(); + } else if (holder instanceof ITieredMachine tieredMachine) { + maxVoltage = GTValues.V[tieredMachine.getTier()]; + } + + long recipeEUt = RecipeHelper.getInputEUt(recipe); + if (recipeEUt == 0) { + recipeEUt = RecipeHelper.getOutputEUt(recipe); + } + return Math.abs((int) (maxVoltage / recipeEUt)); + } + + @Override + public int getMaxParallelRatio(IRecipeCapabilityHolder holder, GTRecipe recipe, int parallelAmount) { + long needed = RecipeHelper.getInputEUt(recipe); + long available = Objects.requireNonNullElseGet(holder.getCapabilitiesProxy().get(IO.IN, EURecipeCapability.CAP), Collections::emptyList) + .stream() + .filter(IEnergyContainer.class::isInstance) + .map(IEnergyContainer.class::cast) + .map(IEnergyContainer::getEnergyStored) + .reduce(0L, Long::sum); + return (int) Math.min(parallelAmount, available / needed); + } } diff --git a/src/main/java/com/gregtechceu/gtceu/api/recipe/modifier/ParallelLogic.java b/src/main/java/com/gregtechceu/gtceu/api/recipe/modifier/ParallelLogic.java index 88b9bbafe7..731615764c 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/recipe/modifier/ParallelLogic.java +++ b/src/main/java/com/gregtechceu/gtceu/api/recipe/modifier/ParallelLogic.java @@ -46,9 +46,22 @@ public static Pair applyParallel(MetaMachine machine, @Nullab public static int getMaxRecipeMultiplier(@NotNull GTRecipe recipe, @NotNull IRecipeCapabilityHolder holder, int parallelAmount) { IntSet multipliers = new IntOpenHashSet(); + // non-tick inputs. for (RecipeCapability cap : recipe.inputs.keySet()) { - // Find the maximum number of recipes that can be performed from the contents of the input inventories - multipliers.add(cap.getMaxParallelRatio(holder, recipe, parallelAmount)); + if (cap.doMatchInRecipe()) { + // Find the maximum number of recipes that can be performed from the contents of the input inventories + multipliers.add(cap.getMaxParallelRatio(holder, recipe, parallelAmount)); + } + } + if (multipliers.intStream().allMatch(value -> value == Integer.MAX_VALUE)) { + return 0; + } + // tick inputs. + for (RecipeCapability cap : recipe.tickInputs.keySet()) { + if (cap.doMatchInRecipe()) { + // Find the maximum number of recipes that can be performed from the contents of the input inventories + multipliers.add(cap.getMaxParallelRatio(holder, recipe, parallelAmount)); + } } if (multipliers.intStream().allMatch(value -> value == Integer.MAX_VALUE)) { return 0; @@ -74,6 +87,12 @@ public static int limitByOutputMerging(@NotNull GTRecipe recipe, @NotNull IRecip canVoidAll = false; } } + for (RecipeCapability cap : recipe.tickInputs.keySet()) { + modifiedParallelAmounts.put(cap, Integer.MAX_VALUE); + if (!canVoid.test(cap)) { + canVoidAll = false; + } + } // If we are voiding everything, return the maximum number of parallels that can be performed from // the inputs if (canVoidAll) { @@ -81,6 +100,9 @@ public static int limitByOutputMerging(@NotNull GTRecipe recipe, @NotNull IRecip } for (RecipeCapability cap : recipe.inputs.keySet()) { + if (!cap.doMatchInRecipe()) { + continue; + } // Check both normal item outputs and chanced item outputs if (!recipe.getOutputContents(cap).isEmpty()) { boolean voiding = canVoid.test(cap); @@ -97,6 +119,34 @@ public static int limitByOutputMerging(@NotNull GTRecipe recipe, @NotNull IRecip } } } + for (RecipeCapability cap : recipe.tickInputs.keySet()) { + if (!cap.doMatchInRecipe()) { + continue; + } + // Check both normal item outputs and chanced item outputs + if (!recipe.getTickOutputContents(cap).isEmpty()) { + boolean voiding = canVoid.test(cap); + // If we are voiding items, reset the item limit to the maximum number of parallels + if (voiding) { + if (modifiedParallelAmounts.containsKey(cap)) { + modifiedParallelAmounts.put(cap, modifiedParallelAmounts.getInt(cap) + parallelAmount); + } else { + modifiedParallelAmounts.put(cap, parallelAmount); + } + } else { + if (modifiedParallelAmounts.containsKey(cap)) { + modifiedParallelAmounts.put(cap, modifiedParallelAmounts.getInt(cap) + cap.limitParallel(recipe, holder, parallelAmount)); + } else { + modifiedParallelAmounts.put(cap, cap.limitParallel(recipe, holder, parallelAmount)); + } + } + + // If we are not voiding, and cannot fit any items, return 0 + if (modifiedParallelAmounts.getInt(cap) == 0 && !voiding) { + return 0; + } + } + } return modifiedParallelAmounts.values().intStream().min().orElse(0); } @@ -139,12 +189,6 @@ public static int limitByOutputMerging(@NotNull GTRecipe recipe, @NotNull IRecip public static Pair doParallelRecipes(@NotNull GTRecipe currentRecipe, @NotNull IRecipeLogicMachine machine, int parallelAmount, boolean modifyDuration) { - long maxVoltage = Long.MAX_VALUE; - if (machine instanceof IOverclockMachine overclockMachine) { - maxVoltage = overclockMachine.getOverclockVoltage(); - } else if (machine instanceof ITieredMachine tieredMachine) { - maxVoltage = GTValues.V[tieredMachine.getTier()]; - } // First check if we are limited by recipe inputs. This can short circuit a lot of consecutive checking int multiplierByInputs = getMaxRecipeMultiplier(currentRecipe, machine, parallelAmount); if (multiplierByInputs == 0) { @@ -154,25 +198,10 @@ public static Pair doParallelRecipes(@NotNull GTRecipe curren // Simulate the merging of the maximum amount of recipes that can be run with these items // and limit by the amount we can successfully merge int limitByOutput = ParallelLogic.limitByOutputMerging(currentRecipe, machine, multiplierByInputs, machine::canVoidRecipeOutputs); - int parallelLimit = limitByOutput; - - long recipeEUt = RecipeHelper.getInputEUt(currentRecipe); - if (recipeEUt == 0) { - recipeEUt = RecipeHelper.getOutputEUt(currentRecipe); - } - if (recipeEUt != 0) { - int limitByVoltage = Math.abs((int) (maxVoltage / recipeEUt)); - int maxParallel = Math.min(limitByVoltage, limitByOutput); - if (maxParallel != 0) { - // Use the minimum between the amount of recipes we can run with available inputs and amount of recipe - // outputs that can fit - parallelLimit = Math.min(maxParallel, multiplierByInputs); - currentRecipe = currentRecipe.copy(ContentModifier.multiplier(parallelLimit), modifyDuration); - } - } else if (limitByOutput > 0) { + if (limitByOutput > 0) { currentRecipe = currentRecipe.copy(ContentModifier.multiplier(limitByOutput), modifyDuration); } - return Pair.of(currentRecipe, parallelLimit); + return Pair.of(currentRecipe, limitByOutput); } }