diff --git a/src/main/java/com/gregtechceu/gtceu/api/recipe/GTRecipe.java b/src/main/java/com/gregtechceu/gtceu/api/recipe/GTRecipe.java index 203ed6d841..c2ea9e487e 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/recipe/GTRecipe.java +++ b/src/main/java/com/gregtechceu/gtceu/api/recipe/GTRecipe.java @@ -1,8 +1,6 @@ package com.gregtechceu.gtceu.api.recipe; -import com.google.common.collect.Table; import com.gregtechceu.gtceu.GTCEu; -import com.gregtechceu.gtceu.api.GTValues; import com.gregtechceu.gtceu.api.capability.recipe.*; import com.gregtechceu.gtceu.api.machine.trait.RecipeLogic; import com.gregtechceu.gtceu.api.recipe.content.Content; @@ -14,7 +12,6 @@ import net.minecraft.nbt.CompoundTag; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; -import net.minecraft.util.Tuple; import net.minecraft.world.Container; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.crafting.RecipeSerializer; @@ -31,7 +28,6 @@ * @date 2023/2/20 * @implNote GTRecipe */ -@SuppressWarnings({"ConstantValue", "rawtypes", "unchecked"}) @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault public class GTRecipe implements net.minecraft.world.item.crafting.Recipe { @@ -142,6 +138,7 @@ public ItemStack getResultItem(RegistryAccess registryManager) { // **********************internal logic********************* // /////////////////////////////////////////////////////////////// + public List getInputContents(RecipeCapability capability) { return inputs.getOrDefault(capability, Collections.emptyList()); } @@ -159,72 +156,37 @@ public List getTickOutputContents(RecipeCapability capability) { } public ActionResult matchRecipe(IRecipeCapabilityHolder holder) { + return matchRecipe(holder, false); + } + + public ActionResult matchTickRecipe(IRecipeCapabilityHolder holder) { + return hasTick() ? matchRecipe(holder, true) : ActionResult.SUCCESS; + } + + private ActionResult matchRecipe(IRecipeCapabilityHolder holder, boolean tick) { if (!holder.hasProxies()) return ActionResult.FAIL_NO_REASON; - var result = matchRecipe(IO.IN, holder, inputs, false); + + var result = matchRecipeContents(IO.IN, holder, tick ? tickInputs : inputs); if (!result.isSuccess()) return result; - result = matchRecipe(IO.OUT, holder, outputs, false); + + result = matchRecipeContents(IO.OUT, holder, tick ? tickOutputs : outputs); if (!result.isSuccess()) return result; - return ActionResult.SUCCESS; - } - public ActionResult matchTickRecipe(IRecipeCapabilityHolder holder) { - if (hasTick()) { - if (!holder.hasProxies()) return ActionResult.FAIL_NO_REASON; - var result = matchRecipe(IO.IN, holder, tickInputs, false); - if (!result.isSuccess()) return result; - result = matchRecipe(IO.OUT, holder, tickOutputs, false); - if (!result.isSuccess()) return result; - } return ActionResult.SUCCESS; } - public ActionResult matchRecipe(IO io, IRecipeCapabilityHolder holder, Map, List> contents, boolean calculateExpectingRate) { - Table, List>> capabilityProxies = holder.getCapabilitiesProxy(); + public ActionResult matchRecipeContents(IO io, IRecipeCapabilityHolder holder, Map, List> contents) { + RecipeRunner runner = new RecipeRunner(this, io, holder, true); for (Map.Entry, List> entry : contents.entrySet()) { - Set> used = new HashSet<>(); - List content = new ArrayList<>(); - Map contentSlot = new HashMap<>(); - for (Content cont : entry.getValue()) { - if (cont.slotName == null) { - content.add(cont.content); - } else { - contentSlot.computeIfAbsent(cont.slotName, s -> new ArrayList<>()).add(cont.content); - } - } - RecipeCapability capability = entry.getKey(); - if (!capability.doMatchInRecipe()) { + var result = runner.handle(entry); + if (result == null) continue; - } - List newContent = new ArrayList(); - for (Object cont : content) { - newContent.add(capability.copyContent(cont)); - } - content = newContent; - if (content.isEmpty() && contentSlot.isEmpty()) continue; - if (content.isEmpty()) content = null; - - var result = handlerContentsInternal(io, io, capabilityProxies, capability, used, content, contentSlot, content, contentSlot, true); - if (result.getA() == null && result.getB().isEmpty()) continue; - result = handlerContentsInternal(IO.BOTH, io, capabilityProxies, capability, used, result.getA(), result.getB(), content, contentSlot, true); - - if (result.getA() != null || !result.getB().isEmpty()) { - var expectingRate = 0f; - // TODO calculateExpectingRate -// if (calculateExpectingRate) { -// if (result.getA() != null) { -// expectingRate = Math.max(capability.calculateAmount(result.getA()), expectingRate); -// } -// if (!result.getB().isEmpty()) { -// for (var c : result.getB().values()) { -// expectingRate = Math.max(capability.calculateAmount(c), expectingRate); -// } -// } -// } + if (result.result().content != null || !result.result().slots.isEmpty()) { if (io == IO.IN) { - return ActionResult.fail(() -> Component.translatable("gtceu.recipe_logic.insufficient_in").append(": ").append(capability.getName()), expectingRate); + return ActionResult.fail(() -> Component.translatable("gtceu.recipe_logic.insufficient_in").append(": ").append(result.capability().getName()), 0f); } else if (io == IO.OUT) { - return ActionResult.fail(() -> Component.translatable("gtceu.recipe_logic.insufficient_out").append(": ").append(capability.getName()), expectingRate); + return ActionResult.fail(() -> Component.translatable("gtceu.recipe_logic.insufficient_out").append(": ").append(result.capability().getName()), 0f); } else { return ActionResult.FAIL_NO_REASON; } @@ -244,41 +206,13 @@ public boolean handleRecipeIO(IO io, IRecipeCapabilityHolder holder) { } public boolean handleRecipe(IO io, IRecipeCapabilityHolder holder, Map, List> contents) { - Table, List>> capabilityProxies = holder.getCapabilitiesProxy(); + RecipeRunner runner = new RecipeRunner(this, io, holder, false); for (Map.Entry, List> entry : contents.entrySet()) { - Set> used = new HashSet<>(); - List content = new ArrayList<>(); - Map contentSlot = new HashMap<>(); - List contentSearch = new ArrayList<>(); - Map contentSlotSearch = new HashMap<>(); - for (Content cont : entry.getValue()) { - if (cont.slotName == null) { - contentSearch.add(cont.content); - } else { - contentSlotSearch.computeIfAbsent(cont.slotName, s -> new ArrayList<>()).add(cont.content); - } - if (cont.chance >= 1 || GTValues.RNG.nextFloat() < (cont.chance + holder.getChanceTier() * cont.tierChanceBoost)) { // chance input - if (cont.slotName == null) { - content.add(cont.content); - } else { - contentSlot.computeIfAbsent(cont.slotName, s -> new ArrayList<>()).add(cont.content); - } - } - } - RecipeCapability capability = entry.getKey(); - if (!capability.doMatchInRecipe()) { + var handled = runner.handle(entry); + if (handled == null) continue; - } - - content = content.stream().map(capability::copyContent).toList(); - if (content.isEmpty() && contentSlot.isEmpty()) continue; - if (content.isEmpty()) content = null; - var result = handlerContentsInternal(io, io, capabilityProxies, capability, used, content, contentSlot, contentSearch, contentSlotSearch, false); - if (result.getA() == null && result.getB().isEmpty()) continue; - result = handlerContentsInternal(IO.BOTH, io, capabilityProxies, capability, used, result.getA(), result.getB(), contentSearch, contentSlotSearch, false); - - if (result.getA() != null || !result.getB().isEmpty()) { + if (handled.result().content != null || !handled.result().slots.isEmpty()) { GTCEu.LOGGER.warn("io error while handling a recipe {} outputs. holder: {}", id, holder); return false; } @@ -286,78 +220,7 @@ public boolean handleRecipe(IO io, IRecipeCapabilityHolder holder, Map> handlerContentsInternal( - IO capIO, IO io, Table, List>> capabilityProxies, - RecipeCapability capability, Set> used, - @Nullable List content, Map contentSlot, - List contentSearch, Map contentSlotSearch, - boolean simulate - ) { - if (!capabilityProxies.contains(capIO, capability)) - return new Tuple<>(content, contentSlot); - - //noinspection DataFlowIssue checked above. - var handlers = new ArrayList<>(capabilityProxies.get(capIO, capability)); - handlers.sort(IRecipeHandler.ENTRY_COMPARATOR); - - // handle distinct first - for (IRecipeHandler handler : handlers) { - if (!handler.isDistinct()) continue; - var result = handler.handleRecipe(io, this, contentSearch, null, true); - if (result == null) { - // check distint slot handler - if (handler.getSlotNames() != null && handler.getSlotNames().containsAll(contentSlotSearch.keySet())) { - boolean success = true; - for (var entry : contentSlotSearch.entrySet()) { - List left = handler.handleRecipe(io, this, entry.getValue(), entry.getKey(), true); - if (left != null) { - success = false; - break; - } - } - if (success) { - if (!simulate) { - for (var entry : contentSlot.entrySet()) { - handler.handleRecipe(io, this, entry.getValue(), entry.getKey(), false); - } - } - contentSlot.clear(); - } - } - if (contentSlot.isEmpty()) { - if (!simulate) { - handler.handleRecipe(io, this, content, null, false); - } - content = null; - } - } - if (content == null && contentSlot.isEmpty()) { - break; - } - } - if (content != null || !contentSlot.isEmpty()) { - // handle undistinct later - for (IRecipeHandler proxy : handlers) { - if (used.contains(proxy) || proxy.isDistinct()) continue; - used.add(proxy); - if (content != null) { - content = proxy.handleRecipe(io, this, content, null, simulate); - } - if (proxy.getSlotNames() != null) { - Iterator iterator = contentSlot.keySet().iterator(); - while (iterator.hasNext()) { - String key = iterator.next(); - if (proxy.getSlotNames().contains(key)) { - List left = proxy.handleRecipe(io, this, contentSlot.get(key), key, simulate); - if (left == null) iterator.remove(); - } - } - } - if (content == null && contentSlot.isEmpty()) break; - } - } - return new Tuple<>(content, contentSlot); - } + public boolean hasTick() { return !tickInputs.isEmpty() || !tickOutputs.isEmpty(); diff --git a/src/main/java/com/gregtechceu/gtceu/api/recipe/RecipeRunner.java b/src/main/java/com/gregtechceu/gtceu/api/recipe/RecipeRunner.java new file mode 100644 index 0000000000..e00e46095f --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/recipe/RecipeRunner.java @@ -0,0 +1,186 @@ +package com.gregtechceu.gtceu.api.recipe; + +import com.google.common.collect.Table; +import com.gregtechceu.gtceu.api.GTValues; +import com.gregtechceu.gtceu.api.capability.recipe.IO; +import com.gregtechceu.gtceu.api.capability.recipe.IRecipeCapabilityHolder; +import com.gregtechceu.gtceu.api.capability.recipe.IRecipeHandler; +import com.gregtechceu.gtceu.api.capability.recipe.RecipeCapability; +import com.gregtechceu.gtceu.api.recipe.content.Content; +import lombok.experimental.Accessors; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.UnknownNullability; + +import java.util.*; + +/** + * Used to handle recipes, only valid for a single RecipeCapability's entries + */ +@SuppressWarnings({"rawtypes", "unchecked"}) +class RecipeRunner { + static class ContentSlots { + public @UnknownNullability List content = new ArrayList<>(); + public @NotNull Map slots = new HashMap<>(); + } + + record RecipeHandlingResult(RecipeCapability capability, ContentSlots result) { + } + +// -------------------------------------------------------------------------------------------------------- + + private final GTRecipe recipe; + private final IO io; + private final IRecipeCapabilityHolder holder; + private final Table, List>> capabilityProxies; + private final boolean simulated; + + // These are only used to store mutable state during each invocation of handle() + private RecipeCapability capability; + private Set> used; + private ContentSlots content; + private ContentSlots search; + + + public RecipeRunner(GTRecipe recipe, IO io, IRecipeCapabilityHolder holder, boolean simulated) { + this.recipe = recipe; + this.io = io; + this.holder = holder; + this.capabilityProxies = holder.getCapabilitiesProxy(); + this.simulated = simulated; + } + + @Nullable + public RecipeHandlingResult handle(Map.Entry, List> entry) { + initState(); + + this.fillContent(holder, entry); + this.capability = this.resolveCapability(entry); + + if (capability == null) + return null; + + var result = this.handleContents(); + if (result == null) + return null; + + return new RecipeHandlingResult(capability, result); + } + + private void initState() { + used = new HashSet<>(); + content = new ContentSlots(); + search = simulated ? content : new ContentSlots(); + } + + private void fillContent(IRecipeCapabilityHolder holder, Map.Entry, List> entry) { + for (Content cont : entry.getValue()) { + // For simulated handling, search/content are the same instance, so there's no need to switch between them + if (cont.slotName == null) { + this.search.content.add(cont.content); + } else { + this.search.slots.computeIfAbsent(cont.slotName, s -> new ArrayList<>()).add(cont.content); + } + + // When simulating the recipe handling (used for recipe matching), chanced contents are ignored. + if (simulated) continue; + + if (cont.chance >= 1 || GTValues.RNG.nextFloat() < (cont.chance + holder.getChanceTier() * cont.tierChanceBoost)) { // chance input + if (cont.slotName == null) { + this.content.content.add(cont.content); + } else { + this.content.slots.computeIfAbsent(cont.slotName, s -> new ArrayList<>()).add(cont.content); + } + } + } + } + + private RecipeCapability resolveCapability(Map.Entry, List> entry) { + RecipeCapability capability = entry.getKey(); + if (!capability.doMatchInRecipe()) { + return null; + } + + content.content = this.content.content.stream().map(capability::copyContent).toList(); + if (this.content.content.isEmpty() && this.content.slots.isEmpty()) return null; + if (this.content.content.isEmpty()) content.content = null; + + return capability; + } + + @Nullable + private ContentSlots handleContents() { + handleContentsInternal(io); + if (content.content == null && content.slots.isEmpty()) return null; + handleContentsInternal(IO.BOTH); + + return content; + } + + + private void handleContentsInternal(IO capIO) { + if (!capabilityProxies.contains(capIO, capability)) + return; + + //noinspection DataFlowIssue checked above. + var handlers = new ArrayList<>(capabilityProxies.get(capIO, capability)); + handlers.sort(IRecipeHandler.ENTRY_COMPARATOR); + + // handle distinct first + for (IRecipeHandler handler : handlers) { + if (!handler.isDistinct()) continue; + var result = handler.handleRecipe(io, recipe, search.content, null, true); + if (result == null) { + // check distint slot handler + if (handler.getSlotNames() != null && handler.getSlotNames().containsAll(search.slots.keySet())) { + boolean success = true; + for (var entry : search.slots.entrySet()) { + List left = handler.handleRecipe(io, recipe, entry.getValue(), entry.getKey(), true); + if (left != null) { + success = false; + break; + } + } + if (success) { + if (!simulated) { + for (var entry : content.slots.entrySet()) { + handler.handleRecipe(io, recipe, entry.getValue(), entry.getKey(), false); + } + } + content.slots.clear(); + } + } + if (content.slots.isEmpty()) { + if (!simulated) { + handler.handleRecipe(io, recipe, content.content, null, false); + } + content.content = null; + } + } + if (content.content == null && content.slots.isEmpty()) { + break; + } + } + if (content.content != null || !content.slots.isEmpty()) { + // handle undistinct later + for (IRecipeHandler proxy : handlers) { + if (used.contains(proxy) || proxy.isDistinct()) continue; + used.add(proxy); + if (content.content != null) { + content.content = proxy.handleRecipe(io, recipe, content.content, null, simulated); + } + if (proxy.getSlotNames() != null) { + Iterator iterator = content.slots.keySet().iterator(); + while (iterator.hasNext()) { + String key = iterator.next(); + if (proxy.getSlotNames().contains(key)) { + List left = proxy.handleRecipe(io, recipe, content.slots.get(key), key, simulated); + if (left == null) iterator.remove(); + } + } + } + if (content.content == null && content.slots.isEmpty()) break; + } + } + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/common/machine/multiblock/electric/research/ResearchStationMachine.java b/src/main/java/com/gregtechceu/gtceu/common/machine/multiblock/electric/research/ResearchStationMachine.java index d4b6c472ec..aefa0c3a4f 100644 --- a/src/main/java/com/gregtechceu/gtceu/common/machine/multiblock/electric/research/ResearchStationMachine.java +++ b/src/main/java/com/gregtechceu/gtceu/common/machine/multiblock/electric/research/ResearchStationMachine.java @@ -116,10 +116,10 @@ protected Iterator searchRecipe() { var iterator = machine.getRecipeType().getLookup().getRecipeIterator(holder, recipe -> { if (recipe.isFuel) return false; if (!holder.hasProxies()) return false; - var result = recipe.matchRecipe(IO.IN, holder, recipe.inputs, false); + var result = recipe.matchRecipeContents(IO.IN, holder, recipe.inputs); if (!result.isSuccess()) return false; if (recipe.hasTick()) { - result = recipe.matchRecipe(IO.IN, holder, recipe.tickInputs, false); + result = recipe.matchRecipeContents(IO.IN, holder, recipe.tickInputs); return result.isSuccess(); } return true; @@ -167,7 +167,7 @@ protected boolean checkMatchedRecipeAvailable(GTRecipe match) { public GTRecipe.ActionResult matchRecipeNoOutput(GTRecipe recipe, IRecipeCapabilityHolder holder) { if (!holder.hasProxies()) return GTRecipe.ActionResult.FAIL_NO_REASON; - var result = recipe.matchRecipe(IO.IN, holder, recipe.inputs, false); + var result = recipe.matchRecipeContents(IO.IN, holder, recipe.inputs); if (!result.isSuccess()) return result; return GTRecipe.ActionResult.SUCCESS; } @@ -175,7 +175,7 @@ public GTRecipe.ActionResult matchRecipeNoOutput(GTRecipe recipe, IRecipeCapabil public GTRecipe.ActionResult matchTickRecipeNoOutput(GTRecipe recipe, IRecipeCapabilityHolder holder) { if (recipe.hasTick()) { if (!holder.hasProxies()) return GTRecipe.ActionResult.FAIL_NO_REASON; - var result = recipe.matchRecipe(IO.IN, holder, recipe.tickInputs, false); + var result = recipe.matchRecipeContents(IO.IN, holder, recipe.tickInputs); if (!result.isSuccess()) return result; } return GTRecipe.ActionResult.SUCCESS;