From 53f3f2434d928f808c7dfc4743e8b7446625db59 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 4 Sep 2026 20:24:28 +0000 Subject: [PATCH 1/4] Add a list-based crafting interface A new crafting interface variant with a single variable slot that accepts a list of recipes, so that a whole recipe set can be derived with logic instead of placed card by card. Closes #10. The variable machinery of the existing crafting interface is extracted into PartTypeInterfaceCraftingVariableBase, with a per-slot recipe list instead of a single recipe, so both variants share the evaluator, invalidation, validation and slot messages. The container and screen are reused for both, since the inventory size and the accepted value type are derived from the part type. List reads are guarded: infinite lists are rejected, the number of recipes read from a list is capped, duplicates are removed, and the network index is patched with only what changed. Because reader-backed list variables are invalidated on every reader tick, reloads of a list slot are throttled to a configurable minimum interval. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Bn8brBLvFiJsfnBHpwf8ZN --- .../integratedcrafting/GeneralConfig.java | 8 + .../ContainerScreenPartInterfaceCrafting.java | 4 +- ...PartTypeInterfaceCraftingVariableBase.java | 561 ++++++++++++++++++ .../GameTestHelpersIntegratedCrafting.java | 25 +- .../gametest/GameTestsAdvancements.java | 46 ++ .../gametest/GameTestsItemsCraftList.java | 207 +++++++ .../ContainerPartInterfaceCrafting.java | 12 +- ...ontainerPartInterfaceCraftingSettings.java | 6 +- .../part/PartTypeInterfaceCrafting.java | 418 +------------ .../part/PartTypeInterfaceCraftingList.java | 138 +++++ .../integratedcrafting/part/PartTypes.java | 1 + .../part_interface_crafting_list.json | 10 + .../assets/integratedcrafting/lang/en_us.json | 10 + .../block/part_interface_crafting_list.json | 6 + .../item/part_interface_crafting_list.json | 3 + .../gui/part_interface_crafting_list.png | Bin 0 -> 404 bytes .../textures/part/interface_crafting_list.png | Bin 0 -> 530 bytes .../assets/minecraft/atlases/blocks.json | 1 + .../craft_crafting_interface_list.json | 24 + .../integratedcrafting/advancement/root.json | 1 + .../integratedcrafting/info/crafting_info.xml | 3 + .../part_interface_crafting_list.json | 22 + .../integrateddynamics/tags/item/parts.json | 1 + 23 files changed, 1092 insertions(+), 415 deletions(-) create mode 100644 src/main/java/org/cyclops/integratedcrafting/core/part/PartTypeInterfaceCraftingVariableBase.java create mode 100644 src/main/java/org/cyclops/integratedcrafting/gametest/GameTestsItemsCraftList.java create mode 100644 src/main/java/org/cyclops/integratedcrafting/part/PartTypeInterfaceCraftingList.java create mode 100644 src/main/resources/assets/integratedcrafting/blockstates/part_interface_crafting_list.json create mode 100644 src/main/resources/assets/integratedcrafting/models/block/part_interface_crafting_list.json create mode 100644 src/main/resources/assets/integratedcrafting/models/item/part_interface_crafting_list.json create mode 100644 src/main/resources/assets/integratedcrafting/textures/gui/part_interface_crafting_list.png create mode 100644 src/main/resources/assets/integratedcrafting/textures/part/interface_crafting_list.png create mode 100644 src/main/resources/data/integratedcrafting/advancement/autocrafting_setup/craft_crafting_interface_list.json create mode 100644 src/main/resources/data/integratedcrafting/recipe/crafting/part_interface_crafting_list.json diff --git a/src/main/java/org/cyclops/integratedcrafting/GeneralConfig.java b/src/main/java/org/cyclops/integratedcrafting/GeneralConfig.java index dee3a0713..9b02285f2 100644 --- a/src/main/java/org/cyclops/integratedcrafting/GeneralConfig.java +++ b/src/main/java/org/cyclops/integratedcrafting/GeneralConfig.java @@ -34,6 +34,14 @@ public class GeneralConfig extends DummyConfig { public static int interfaceCraftingBaseConsumption = 5; @ConfigurableProperty(category = "general", comment = "The base energy usage for the attuned crafting interface per crafting job being processed.", minimalValue = 0, configLocation = ModConfig.Type.SERVER) public static int interfaceCraftingAttunedBaseConsumption = 10; + @ConfigurableProperty(category = "general", comment = "The base energy usage for the list-based crafting interface per crafting job being processed.", minimalValue = 0, configLocation = ModConfig.Type.SERVER) + public static int interfaceCraftingListBaseConsumption = 10; + + @ConfigurableProperty(category = "machine", comment = "The maximum number of recipes that will be read from a list inside a list-based crafting interface. Set to 0 for no limit.", minimalValue = 0, isCommandable = true, configLocation = ModConfig.Type.SERVER) + public static int maxCraftingInterfaceListRecipes = 256; + + @ConfigurableProperty(category = "machine", comment = "The minimum number of ticks between two reloads of a list inside a list-based crafting interface. Set to 0 to reload on every variable invalidation.", minimalValue = 0, isCommandable = true, configLocation = ModConfig.Type.SERVER) + public static int craftingInterfaceListMinReloadInterval = 20; @ConfigurableProperty(category = "machine", comment = "The maximum number of recipes that a crafting interface remembers crafting durations for, which are used to estimate the duration of crafting jobs. Set to 0 to disable recipe-specific estimations.", minimalValue = 0, isCommandable = true, configLocation = ModConfig.Type.SERVER) public static int craftingInterfaceRecipeDurationEntries = 32; diff --git a/src/main/java/org/cyclops/integratedcrafting/client/gui/ContainerScreenPartInterfaceCrafting.java b/src/main/java/org/cyclops/integratedcrafting/client/gui/ContainerScreenPartInterfaceCrafting.java index 57ff0a58a..dde5cc7e8 100644 --- a/src/main/java/org/cyclops/integratedcrafting/client/gui/ContainerScreenPartInterfaceCrafting.java +++ b/src/main/java/org/cyclops/integratedcrafting/client/gui/ContainerScreenPartInterfaceCrafting.java @@ -11,7 +11,7 @@ import org.cyclops.cyclopscore.client.gui.image.IImage; import org.cyclops.cyclopscore.client.gui.image.Images; import org.cyclops.cyclopscore.helper.GuiHelpers; -import org.cyclops.integratedcrafting.Reference; +import org.cyclops.integratedcrafting.core.part.PartTypeInterfaceCraftingVariableBase; import org.cyclops.integratedcrafting.inventory.container.ContainerPartInterfaceCrafting; import org.cyclops.integrateddynamics.core.inventory.container.ContainerMultipartAspects; @@ -39,7 +39,7 @@ public void init() { @Override protected ResourceLocation constructGuiTexture() { - return ResourceLocation.fromNamespaceAndPath(Reference.MOD_ID, "textures/gui/part_interface_crafting.png"); + return ((PartTypeInterfaceCraftingVariableBase) getMenu().getPartType()).getGuiTexture(); } @Override diff --git a/src/main/java/org/cyclops/integratedcrafting/core/part/PartTypeInterfaceCraftingVariableBase.java b/src/main/java/org/cyclops/integratedcrafting/core/part/PartTypeInterfaceCraftingVariableBase.java new file mode 100644 index 000000000..4ab2bc121 --- /dev/null +++ b/src/main/java/org/cyclops/integratedcrafting/core/part/PartTypeInterfaceCraftingVariableBase.java @@ -0,0 +1,561 @@ +package org.cyclops.integratedcrafting.core.part; + +import com.google.common.collect.Lists; +import com.google.common.collect.MapMaker; +import it.unimi.dsi.fastutil.ints.Int2BooleanArrayMap; +import it.unimi.dsi.fastutil.ints.Int2BooleanMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.IntArraySet; +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import it.unimi.dsi.fastutil.ints.IntSet; +import net.minecraft.core.Direction; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.item.ItemStack; +import net.neoforged.neoforge.common.NeoForge; +import org.apache.commons.lang3.tuple.Triple; +import org.apache.logging.log4j.Level; +import org.cyclops.commoncapabilities.api.capability.recipehandler.IRecipeDefinition; +import org.cyclops.commoncapabilities.api.capability.recipehandler.IRecipeHandler; +import org.cyclops.commoncapabilities.api.ingredient.IMixedIngredients; +import org.cyclops.cyclopscore.datastructure.DimPos; +import org.cyclops.cyclopscore.helper.BlockEntityHelpers; +import org.cyclops.cyclopscore.inventory.SimpleInventory; +import org.cyclops.cyclopscore.persist.nbt.NBTClassType; +import org.cyclops.integratedcrafting.GeneralConfig; +import org.cyclops.integratedcrafting.IntegratedCrafting; +import org.cyclops.integratedcrafting.Reference; +import org.cyclops.integratedcrafting.api.network.ICraftingNetwork; +import org.cyclops.integratedcrafting.inventory.container.ContainerPartInterfaceCrafting; +import org.cyclops.integratedcrafting.inventory.container.ContainerPartInterfaceCraftingSettings; +import org.cyclops.integrateddynamics.api.evaluate.EvaluationException; +import org.cyclops.integrateddynamics.api.evaluate.variable.IValue; +import org.cyclops.integrateddynamics.api.evaluate.variable.IValueType; +import org.cyclops.integrateddynamics.api.evaluate.variable.IVariable; +import org.cyclops.integrateddynamics.api.evaluate.variable.ValueDeseralizationContext; +import org.cyclops.integrateddynamics.api.network.INetwork; +import org.cyclops.integrateddynamics.api.network.IPartNetwork; +import org.cyclops.integrateddynamics.api.part.IPartContainer; +import org.cyclops.integrateddynamics.api.part.PartPos; +import org.cyclops.integrateddynamics.api.part.PartTarget; +import org.cyclops.integrateddynamics.core.evaluate.InventoryVariableEvaluator; +import org.cyclops.integrateddynamics.core.helper.NetworkHelpers; +import org.cyclops.integrateddynamics.core.helper.PartHelpers; +import org.cyclops.integrateddynamics.core.part.PartTypeBase; +import org.cyclops.integrateddynamics.core.part.event.PartVariableDrivenVariableContentsUpdatedEvent; + +import javax.annotation.Nullable; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * Base part for crafting interfaces that derive their recipes from variables in an inventory. + * + * Each slot can contribute zero or more recipes, which allows both one-recipe-per-slot variants + * and variants where a single slot holds a whole list of recipes. + * + * @author rubensworks + */ +public abstract class PartTypeInterfaceCraftingVariableBase

, S extends PartTypeInterfaceCraftingVariableBase.State> + extends PartTypeInterfaceCraftingBase { + + public PartTypeInterfaceCraftingVariableBase(String name) { + super(name); + } + + /** + * @return The value type that the variable slots of this part accept. + */ + public abstract IValueType getSlotValueType(); + + /** + * @return The gui background texture, derived from the part name. + */ + public ResourceLocation getGuiTexture() { + return ResourceLocation.fromNamespaceAndPath(Reference.MOD_ID, "textures/gui/part_" + getUniqueName().getPath() + ".png"); + } + + @Override + public Optional getContainerProvider(PartPos pos) { + return Optional.of(new MenuProvider() { + + @Override + public MutableComponent getDisplayName() { + return Component.translatable(getTranslationKey()); + } + + @Override + public AbstractContainerMenu createMenu(int id, Inventory playerInventory, Player playerEntity) { + Triple data = PartHelpers.getContainerPartConstructionData(pos); + S partState = (S) data.getLeft().getPartState(data.getRight().getCenter().getSide()); + return new ContainerPartInterfaceCrafting<>(id, playerInventory, partState.getInventoryVariables(), + Optional.of(data.getRight()), Optional.of(data.getLeft()), (P) data.getMiddle()); + } + + @Override + public boolean shouldTriggerClientSideContainerClosingOnOpen() { + return false; + } + }); + } + + @Override + public void writeExtraGuiData(RegistryFriendlyByteBuf packetBuffer, PartPos pos, ServerPlayer player) { + // Write inventory size + IPartContainer partContainer = PartHelpers.getPartContainerChecked(pos); + S partState = (S) partContainer.getPartState(pos.getSide()); + packetBuffer.writeInt(partState.getInventoryVariables().getContainerSize()); + + super.writeExtraGuiData(packetBuffer, pos, player); + } + + @Override + public Optional getContainerProviderSettings(PartPos pos) { + return Optional.of(new MenuProvider() { + + @Override + public MutableComponent getDisplayName() { + return Component.translatable(getTranslationKey()); + } + + @Override + public AbstractContainerMenu createMenu(int id, Inventory playerInventory, Player playerEntity) { + Triple data = PartHelpers.getContainerPartConstructionData(pos); + return new ContainerPartInterfaceCraftingSettings(id, playerInventory, new SimpleContainer(0), + data.getRight(), Optional.of(data.getLeft()), data.getMiddle()); + } + + @Override + public boolean shouldTriggerClientSideContainerClosingOnOpen() { + return false; + } + }); + } + + @Override + public void update(INetwork network, IPartNetwork partNetwork, PartTarget target, S state) { + super.update(network, partNetwork, target, state); + + // Reload recipes if needed + IntSet slots = state.getDelayedRecipeReloads(); + if (!slots.isEmpty()) { + ICraftingNetwork craftingNetwork = network.getCapability(getNetworkCapability()).orElse(null); + if (craftingNetwork != null) { + IntSet slotsCopy = new IntOpenHashSet(slots); // Create a copy, to allow insertion into slots during this loop + slots.clear(); + int channel = state.getChannelCrafting(); + for (Integer slot : slotsCopy) { + // Slots that may not be reloaded yet are retried in a later tick + if (!state.mayReloadSlot(slot)) { + slots.add(slot); + continue; + } + + Int2ObjectMap> recipes = state.getRecipesIndexed(); + List oldRecipes = recipes.get(slot); + oldRecipes = oldRecipes == null ? Collections.emptyList() : Lists.newArrayList(oldRecipes); + + // Reload the recipes in the slot + // We simulate initialization for the first two ticks, as dependency variables may still be loading, + // and errored may only go away after these dependencies are fully loaded. + // Related to CyclopsMC/IntegratedCrafting#110 + state.reloadRecipe(slot, state.ticksAfterReload <= 1); + + List newRecipes = recipes.get(slot); + newRecipes = newRecipes == null ? Collections.emptyList() : newRecipes; + + // Only patch what actually changed, as slots can hold many recipes + // that are re-evaluated whenever their variable is invalidated. + for (IRecipeDefinition oldRecipe : oldRecipes) { + if (!newRecipes.contains(oldRecipe)) { + craftingNetwork.removeCraftingInterfaceRecipe(channel, state, oldRecipe); + } + } + for (IRecipeDefinition newRecipe : newRecipes) { + if (!oldRecipes.contains(newRecipe)) { + craftingNetwork.addCraftingInterfaceRecipe(channel, state, newRecipe); + } + } + } + } + } + + // Internal tick counter + state.ticksAfterReload++; + } + + @Override + public void addDrops(PartTarget target, S state, List itemStacks, boolean dropMainElement, boolean saveState) { + // Drop the stored variables + for (int i = 0; i < state.getInventoryVariables().getContainerSize(); i++) { + ItemStack itemStack = state.getInventoryVariables().getItem(i); + if (!itemStack.isEmpty()) { + itemStacks.add(itemStack); + } + } + state.getInventoryVariables().clearContent(); + + super.addDrops(target, state, itemStacks, dropMainElement, saveState); + } + + public static abstract class State

, S extends PartTypeInterfaceCraftingVariableBase.State> + extends PartTypeInterfaceCraftingBase.State { + + protected int ticksAfterReload = 0; + + private final SimpleInventory inventoryVariables; + private final List> variableEvaluators; + private final Int2ObjectMap recipeSlotMessages; + private final Int2BooleanMap recipeSlotValidated; + private final IntSet delayedRecipeReloads; + private final Map variableListeners; + private boolean disableCraftingCheck = false; + + private final Int2ObjectMap> currentRecipes; + private List currentRecipesFlattened; + + public State(int inventorySize) { + this.inventoryVariables = new SimpleInventory(inventorySize, 1); + this.inventoryVariables.addDirtyMarkListener(this); + this.variableEvaluators = Lists.newArrayList(); + this.recipeSlotMessages = new Int2ObjectArrayMap<>(); + this.recipeSlotValidated = new Int2BooleanArrayMap(); + this.delayedRecipeReloads = new IntArraySet(); + this.variableListeners = new MapMaker().weakKeys().makeMap(); + this.currentRecipes = new Int2ObjectArrayMap<>(); + this.currentRecipesFlattened = Collections.emptyList(); + } + + /** + * @return The part type that this state belongs to. + */ + protected abstract P getPartTypeInstance(); + + /** + * Derive the recipes that the given evaluated variable value contributes to this interface. + * @param slot The slot that the value was evaluated for. + * @param value The value that was evaluated. + * @return The recipes in the value, which may be empty. + * @throws EvaluationException If the value could not be converted into recipes. + */ + protected abstract List extractRecipes(int slot, IValue value) throws EvaluationException; + + /** + * @return If the given slot may be reloaded in this tick. + */ + protected boolean mayReloadSlot(int slot) { + return true; + } + + /** + * @return The internal tick counter, which is monotonically increasing. + */ + public int getTicks() { + return this.ticksAfterReload; + } + + /** + * @return The message to show for a slot for which all recipes were accepted by the target. + */ + protected MutableComponent getRecipesValidMessage(int slot, int count) { + return count > 1 + ? Component.translatable("gui.integratedcrafting.partinterface.slot.message.valid.multiple", count) + : Component.translatable("gui.integratedcrafting.partinterface.slot.message.valid"); + } + + /** + * @return The inner variables inventory + */ + public SimpleInventory getInventoryVariables() { + return this.inventoryVariables; + } + + @Override + public void writeToNBT(ValueDeseralizationContext valueDeseralizationContext, CompoundTag tag) { + super.writeToNBT(valueDeseralizationContext, tag); + inventoryVariables.writeToNBT(valueDeseralizationContext.holderLookupProvider(), tag, "variables"); + + CompoundTag recipeSlotErrorsTag = new CompoundTag(); + for (Int2ObjectMap.Entry entry : this.recipeSlotMessages.int2ObjectEntrySet()) { + NBTClassType.writeNbt(MutableComponent.class, String.valueOf(entry.getIntKey()), entry.getValue(), recipeSlotErrorsTag, valueDeseralizationContext.holderLookupProvider()); + } + tag.put("recipeSlotMessages", recipeSlotErrorsTag); + + CompoundTag recipeSlotValidatedTag = new CompoundTag(); + for (Int2BooleanMap.Entry entry : this.recipeSlotValidated.int2BooleanEntrySet()) { + recipeSlotValidatedTag.putBoolean(String.valueOf(entry.getIntKey()), entry.getBooleanValue()); + } + tag.put("recipeSlotValidated", recipeSlotValidatedTag); + + tag.putBoolean("disableCraftingCheck", disableCraftingCheck); + } + + @Override + public void readFromNBT(ValueDeseralizationContext valueDeseralizationContext, CompoundTag tag) { + super.readFromNBT(valueDeseralizationContext, tag); + inventoryVariables.readFromNBT(valueDeseralizationContext.holderLookupProvider(), tag, "variables"); + + this.recipeSlotMessages.clear(); + CompoundTag recipeSlotErrorsTag = tag.getCompound("recipeSlotMessages"); + for (String slot : recipeSlotErrorsTag.getAllKeys()) { + MutableComponent unlocalizedString = NBTClassType.readNbt(MutableComponent.class, slot, recipeSlotErrorsTag, valueDeseralizationContext.holderLookupProvider()); + this.recipeSlotMessages.put(Integer.parseInt(slot), unlocalizedString); + } + + this.recipeSlotValidated.clear(); + CompoundTag recipeSlotValidatedTag = tag.getCompound("recipeSlotValidated"); + for (String slot : recipeSlotValidatedTag.getAllKeys()) { + this.recipeSlotValidated.put(Integer.parseInt(slot), recipeSlotValidatedTag.getBoolean(slot)); + } + + this.disableCraftingCheck = tag.getBoolean("disableCraftingCheck"); + } + + @Override + public void reloadRecipes(boolean initialize) { + this.currentRecipes.clear(); + this.invalidateRecipesFlattened(); + this.recipeSlotMessages.clear(); + this.recipeSlotValidated.clear(); + variableEvaluators.clear(); + for (int i = 0; i < getInventoryVariables().getContainerSize(); i++) { + int slot = i; + variableEvaluators.add(new InventoryVariableEvaluator( + getInventoryVariables(), slot, valueDeseralizationContext, (IValueType) getPartTypeInstance().getSlotValueType()) { + @Override + public void onErrorsChanged() { + super.onErrorsChanged(); + setLocalErrors(slot, getErrors()); + } + }); + } + if (this.partNetwork != null) { + for (int i = 0; i < getInventoryVariables().getContainerSize(); i++) { + reloadRecipe(i, initialize); + } + } + } + + private void setLocalErrors(int slot, List errors) { + if (errors.isEmpty()) { + if (this.recipeSlotMessages.size() > slot) { + this.recipeSlotMessages.remove(slot); + } + } else { + this.recipeSlotMessages.put(slot, errors.get(0)); + } + } + + protected void reloadRecipe(int slot, boolean initialize) { + this.currentRecipes.remove(slot); + this.invalidateRecipesFlattened(); + if (this.recipeSlotMessages.size() > slot) { + this.recipeSlotMessages.remove(slot); + } + if (this.recipeSlotValidated.size() > slot) { + this.recipeSlotValidated.remove(slot); + } + if (this.partNetwork != null) { + InventoryVariableEvaluator evaluator = variableEvaluators.get(slot); + evaluator.refreshVariable(network, false); + IVariable variable = evaluator.getVariable(network); + if (variable != null) { + try { + // Refresh the recipe if variable is changed + // The map is needed because we only want to register the listener once for each variable + if (!this.variableListeners.containsKey(variable)) { + variable.addInvalidationListener(() -> { + this.variableListeners.remove(variable); + delayedReloadRecipe(slot); + }); + this.variableListeners.put(variable, true); + } + + IValue value = variable.getValue(); + if (value.getType() == getPartTypeInstance().getSlotValueType()) { + setSlotRecipes(slot, extractRecipes(slot, value)); + } else { + this.recipeSlotMessages.put(slot, Component.translatable("gui.integratedcrafting.partinterface.slot.message.norecipe")); + } + } catch (EvaluationException e) { + this.recipeSlotMessages.put(slot, e.getErrorMessage()); + } + } else { + // If we're initializing, the variable might be referencing other variables that are not yet loaded. + // So let's retry once in the next tick. + if (initialize && evaluator.hasVariable()) { + this.delayedReloadRecipe(slot); + } else { + this.recipeSlotMessages.put(slot, Component.translatable("gui.integratedcrafting.partinterface.slot.message.norecipe")); + } + } + + try { + IPartNetwork partNetwork = NetworkHelpers.getPartNetworkChecked(network); + NeoForge.EVENT_BUS.post(new PartVariableDrivenVariableContentsUpdatedEvent<>(network, + partNetwork, getTarget(), + getPartTypeInstance(), (S) this, lastPlayer, variable, + variable != null ? variable.getValue() : null)); + } catch (EvaluationException e) { + // Ignore error + } + } + sendUpdate(); + } + + /** + * Validate the given recipes, store the valid ones in the given slot, and set the slot message accordingly. + */ + private void setSlotRecipes(int slot, List recipes) { + if (recipes.isEmpty()) { + this.recipeSlotMessages.put(slot, Component.translatable("gui.integratedcrafting.partinterface.slot.message.empty")); + return; + } + + List validRecipes; + if (!GeneralConfig.validateRecipesCraftingInterface || this.disableCraftingCheck) { + validRecipes = recipes; + } else { + validRecipes = Lists.newArrayListWithExpectedSize(recipes.size()); + for (IRecipeDefinition recipe : recipes) { + if (isValid(recipe)) { + validRecipes.add(recipe); + } + } + } + + if (validRecipes.isEmpty()) { + this.recipeSlotMessages.put(slot, Component.translatable("gui.integratedcrafting.partinterface.slot.message.invalid")); + return; + } + + this.currentRecipes.put(slot, validRecipes); + this.invalidateRecipesFlattened(); + this.recipeSlotValidated.put(slot, true); + if (validRecipes.size() < recipes.size()) { + this.recipeSlotMessages.put(slot, Component.translatable("gui.integratedcrafting.partinterface.slot.message.partial", + validRecipes.size(), recipes.size())); + } else { + this.recipeSlotMessages.put(slot, getRecipesValidMessage(slot, validRecipes.size())); + } + } + + protected void delayedReloadRecipe(int slot) { + this.delayedRecipeReloads.add(slot); + } + + protected boolean isValid(IRecipeDefinition recipe) { + DimPos dimPos = getTarget().getTarget().getPos(); + Direction side = getTarget().getTarget().getSide(); + IRecipeHandler recipeHandler = BlockEntityHelpers.getCapability(dimPos.getLevel(true), dimPos.getBlockPos(), side, org.cyclops.commoncapabilities.api.capability.Capabilities.RecipeHandler.BLOCK).orElse(null); + if (recipeHandler != null) { + IMixedIngredients simulatedOutput = recipeHandler.simulate(recipe); + if (simulatedOutput != null && !simulatedOutput.isEmpty()) { + if (recipe.getOutput().containsAll(simulatedOutput)) { + return true; + } else { + if (GeneralConfig.logRecipeValidationFailures) { + IntegratedCrafting.clog(Level.INFO, "Recipe validation failure: incompatible recipe output and simulated output:\nRecipe output: " + recipe.getOutput() + "\nSimulated output: " + simulatedOutput); + } + return false; + } + } + if (GeneralConfig.logRecipeValidationFailures) { + IntegratedCrafting.clog(Level.INFO, "Recipe validation failure: No output was obtained when simulating a recipe\n" + recipe); + } + return false; + } + return true; // No recipe handler capability is present, so we can't confirm that the recipe will work. + } + + @Override + public void onDirty() { + super.onDirty(); + + // Unregister from the network, when all old recipes are still in place + if (craftingNetwork != null) { + craftingNetwork.removeCraftingInterface(getChannelCrafting(), this); + } + + // Recalculate recipes + if (getTarget() != null && !getTarget().getCenter().getPos().getLevel(true).isClientSide) { + reloadRecipes(false); + } + + // Re-register to the network, to force an update for all new recipes + if (craftingNetwork != null) { + craftingNetwork.addCraftingInterface(getChannelCrafting(), this); + } + } + + @Override + public Collection getRecipes() { + if (this.currentRecipesFlattened == null) { + List flattened = Lists.newArrayList(); + for (List recipes : this.currentRecipes.values()) { + flattened.addAll(recipes); + } + this.currentRecipesFlattened = flattened; + } + return this.currentRecipesFlattened; + } + + private void invalidateRecipesFlattened() { + this.currentRecipesFlattened = null; + } + + public Int2ObjectMap> getRecipesIndexed() { + return currentRecipes; + } + + public boolean isRecipeSlotValid(int slot) { + return this.recipeSlotValidated.containsKey(slot); + } + + @Nullable + public MutableComponent getRecipeSlotUnlocalizedMessage(int slot) { + return this.recipeSlotMessages.get(slot); + } + + public IntSet getDelayedRecipeReloads() { + return delayedRecipeReloads; + } + + public void setDisableCraftingCheck(boolean disableCraftingCheck) { + if (disableCraftingCheck != this.disableCraftingCheck) { + this.disableCraftingCheck = disableCraftingCheck; + + this.sendUpdate(); + } + } + + public boolean isDisableCraftingCheck() { + return disableCraftingCheck; + } + + /** + * Remove duplicates while preserving order. + * + * The crafting network drops a recipe from its index as soon as one removal is requested for it, + * so the same recipe must never be added twice from a single interface. + */ + protected static List deduplicate(List recipes) { + return Lists.newArrayList(new LinkedHashSet<>(recipes)); + } + + } +} diff --git a/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestHelpersIntegratedCrafting.java b/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestHelpersIntegratedCrafting.java index fbd605bcd..30963b134 100644 --- a/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestHelpersIntegratedCrafting.java +++ b/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestHelpersIntegratedCrafting.java @@ -17,10 +17,12 @@ import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.entity.ChestBlockEntity; import net.minecraft.world.level.block.entity.FurnaceBlockEntity; +import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Triple; import org.cyclops.commoncapabilities.IngredientComponents; import org.cyclops.commoncapabilities.api.capability.itemhandler.ItemMatch; import org.cyclops.commoncapabilities.api.capability.recipehandler.IPrototypedIngredientAlternatives; +import org.cyclops.commoncapabilities.api.capability.recipehandler.IRecipeDefinition; import org.cyclops.commoncapabilities.api.capability.recipehandler.PrototypedIngredientAlternativesItemStackTag; import org.cyclops.commoncapabilities.api.capability.recipehandler.PrototypedIngredientAlternativesList; import org.cyclops.commoncapabilities.api.capability.recipehandler.RecipeDefinition; @@ -44,6 +46,7 @@ import org.cyclops.integrateddynamics.api.part.aspect.property.IAspectPropertyTypeInstance; import org.cyclops.integrateddynamics.core.evaluate.variable.ValueObjectTypeItemStack; import org.cyclops.integrateddynamics.core.evaluate.variable.ValueObjectTypeRecipe; +import org.cyclops.integrateddynamics.core.evaluate.variable.ValueTypeList; import org.cyclops.integrateddynamics.core.evaluate.variable.ValueTypes; import org.cyclops.integrateddynamics.core.helper.PartHelpers; import org.cyclops.integratedtunnels.part.aspect.TunnelAspects; @@ -73,7 +76,10 @@ public static INetworkPositions createBasicNetw } public static > INetworkPositions createBasicNetwork(GameTestHelper helper, BlockPos pos, boolean attuned, Block... crafters) { - PartTypeInterfaceCraftingBase, ? extends PartTypeInterfaceCraftingBase.State, ? extends PartTypeInterfaceCraftingBase.State>> partInterface = attuned ? PartTypes.INTERFACE_CRAFTING_ATTUNED : PartTypes.INTERFACE_CRAFTING; + return createBasicNetwork(helper, pos, attuned ? PartTypes.INTERFACE_CRAFTING_ATTUNED : PartTypes.INTERFACE_CRAFTING, crafters); + } + + public static > INetworkPositions createBasicNetwork(GameTestHelper helper, BlockPos pos, PartTypeInterfaceCraftingBase, ? extends PartTypeInterfaceCraftingBase.State, ? extends PartTypeInterfaceCraftingBase.State>> partInterface, Block... crafters) { // Place cable helper.setBlock(pos, RegistryEntries.BLOCK_CABLE.value()); @@ -137,6 +143,19 @@ public static INetworkPositions createBasicNetw } public static ItemStack createVariableForRecipe(Level level, RecipeType recipeType, ResourceLocation recipeName) { + return createVariableForValue(level, ValueTypes.OBJECT_RECIPE, + ValueObjectTypeRecipe.ValueRecipe.of(createRecipeDefinition(level, recipeType, recipeName))); + } + + public static ItemStack createVariableForRecipeList(Level level, List, ResourceLocation>> recipes) { + List values = Lists.newArrayList(); + for (Pair, ResourceLocation> recipe : recipes) { + values.add(ValueObjectTypeRecipe.ValueRecipe.of(createRecipeDefinition(level, recipe.getLeft(), recipe.getRight()))); + } + return createVariableForValue(level, ValueTypes.LIST, ValueTypeList.ValueList.ofList(ValueTypes.OBJECT_RECIPE, values)); + } + + public static IRecipeDefinition createRecipeDefinition(Level level, RecipeType recipeType, ResourceLocation recipeName) { RecipeHolder recipeUnknown = null; try { recipeUnknown = (RecipeHolder) IModHelpers.get().getCraftingHelpers().getServerRecipe((RecipeType) recipeType, recipeName).orElseThrow(() -> new IllegalStateException("Recipe " + recipeName.toString() + " could not be found")); @@ -225,7 +244,7 @@ public static ItemStack createVariableForRecipe(Level level, RecipeType recip } else { throw new IllegalStateException("Unknown recipe type " + recipeType); } - return createVariableForValue(level, ValueTypes.OBJECT_RECIPE, ValueObjectTypeRecipe.ValueRecipe.of(new RecipeDefinition(recipeIn, new MixedIngredients(recipeOut)))); + return new RecipeDefinition(recipeIn, new MixedIngredients(recipeOut)); } public static void enableRecipeInWriter(GameTestHelper helper, PartPos writerPos, ItemStack itemStack) { @@ -241,7 +260,7 @@ public static , V extends IValue> void setWriterAspectPr public static , V extends IValue> void setCraftingInterfaceBlockingMode(PartPos writerPos, boolean blocking) { PartHelpers.PartStateHolder partStateHolder = PartHelpers.getPart(writerPos); - ((PartTypeInterfaceCrafting.State) partStateHolder.getState()).getCraftingJobHandler().setBlockingJobsMode(blocking); + ((PartTypeInterfaceCraftingBase.State) partStateHolder.getState()).getCraftingJobHandler().setBlockingJobsMode(blocking); } /** diff --git a/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestsAdvancements.java b/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestsAdvancements.java index 49b5cad70..1b88b4313 100644 --- a/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestsAdvancements.java +++ b/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestsAdvancements.java @@ -125,6 +125,33 @@ public void testAdvancementCraftCraftingInterfaceAttuned(GameTestHelper helper) }); } + /** + * Test for the craft_crafting_interface_list advancement. + * Trigger: cyclopscore:item_crafted + * Condition: player crafts integratedcrafting:part_interface_crafting_list + */ + @GameTest(template = TEMPLATE_EMPTY, timeoutTicks = TIMEOUT) + public void testAdvancementCraftCraftingInterfaceList(GameTestHelper helper) { + ServerPlayer player = helper.makeMockServerPlayerInLevel(); + + // Fire the PlayerEvent.ItemCraftedEvent via the NeoForge event bus + NeoForge.EVENT_BUS.post(new PlayerEvent.ItemCraftedEvent( + player, + new ItemStack(PartTypes.INTERFACE_CRAFTING_LIST.getItem()), + new SimpleContainer(9) + )); + + helper.succeedWhen(() -> { + AdvancementHolder advancement = helper.getLevel().getServer().getAdvancements() + .get(ResourceLocation.fromNamespaceAndPath(Reference.MOD_ID, "autocrafting_setup/craft_crafting_interface_list")); + helper.assertTrue(advancement != null, "craft_crafting_interface_list advancement not found"); + helper.assertTrue( + player.getAdvancements().getOrStartProgress(advancement).isDone(), + "craft_crafting_interface_list advancement not granted" + ); + }); + } + /** * Test for the craft_crafting_writer advancement. * Trigger: cyclopscore:item_crafted @@ -290,6 +317,25 @@ public void testAdvancementCraftCraftingInterfaceAttunedNegative(GameTestHelper helper.succeedWhen(() -> assertAdvancementNotDone(helper, player, "autocrafting_setup/craft_crafting_interface_attuned")); } + /** + * Negative test for the craft_crafting_interface_list advancement. + * Trigger: cyclopscore:item_crafted + * Condition: player crafts integratedcrafting:part_interface_crafting_list + * Here we craft the non-list interface instead – advancement must NOT be granted. + */ + @GameTest(template = TEMPLATE_EMPTY, timeoutTicks = TIMEOUT) + public void testAdvancementCraftCraftingInterfaceListNegative(GameTestHelper helper) { + ServerPlayer player = helper.makeMockServerPlayerInLevel(); + + NeoForge.EVENT_BUS.post(new PlayerEvent.ItemCraftedEvent( + player, + new ItemStack(PartTypes.INTERFACE_CRAFTING.getItem()), + new SimpleContainer(9) + )); + + helper.succeedWhen(() -> assertAdvancementNotDone(helper, player, "autocrafting_setup/craft_crafting_interface_list")); + } + /** * Negative test for the craft_crafting_writer advancement. * Trigger: cyclopscore:item_crafted diff --git a/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestsItemsCraftList.java b/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestsItemsCraftList.java new file mode 100644 index 000000000..436a5b051 --- /dev/null +++ b/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestsItemsCraftList.java @@ -0,0 +1,207 @@ +package org.cyclops.integratedcrafting.gametest; + +import com.google.common.collect.Lists; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.gametest.framework.GameTest; +import net.minecraft.gametest.framework.GameTestAssertException; +import net.minecraft.gametest.framework.GameTestHelper; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.ChestBlockEntity; +import net.neoforged.neoforge.gametest.GameTestHolder; +import net.neoforged.neoforge.gametest.PrefixGameTestTemplate; +import org.apache.commons.lang3.tuple.Pair; +import org.cyclops.integratedcrafting.GeneralConfig; +import org.cyclops.integratedcrafting.Reference; +import org.cyclops.integratedcrafting.part.PartTypeInterfaceCraftingList; +import org.cyclops.integratedcrafting.part.PartTypes; +import org.cyclops.integrateddynamics.RegistryEntries; +import org.cyclops.integrateddynamics.api.part.PartPos; +import org.cyclops.integrateddynamics.core.evaluate.variable.ValueTypeInteger; +import org.cyclops.integrateddynamics.core.evaluate.variable.ValueTypeList; +import org.cyclops.integrateddynamics.core.evaluate.variable.ValueTypes; +import org.cyclops.integrateddynamics.core.helper.PartHelpers; +import org.cyclops.integrateddynamics.part.aspect.Aspects; + +import java.util.List; + +import static org.cyclops.integratedcrafting.gametest.GameTestHelpersIntegratedCrafting.createBasicNetwork; +import static org.cyclops.integratedcrafting.gametest.GameTestHelpersIntegratedCrafting.createVariableForRecipeList; +import static org.cyclops.integratedcrafting.gametest.GameTestHelpersIntegratedCrafting.enableRecipeInWriter; +import static org.cyclops.integrateddynamics.gametest.GameTestHelpersIntegratedDynamics.createVariableForValue; +import static org.cyclops.integrateddynamics.gametest.GameTestHelpersIntegratedDynamics.createVariableFromReader; + +@GameTestHolder(Reference.MOD_ID) +@PrefixGameTestTemplate(false) +public class GameTestsItemsCraftList { + + public static final String TEMPLATE_EMPTY = "empty10"; + public static final int TIMEOUT = 2000; + public static final BlockPos POS = BlockPos.ZERO.offset(2, 0, 2); + + /** + * A single list variable exposes multiple recipes, of which the first one is crafted. + */ + @GameTest(template = TEMPLATE_EMPTY, timeoutTicks = TIMEOUT) + public void testItemsCraftListChest(GameTestHelper helper) { + GameTestHelpersIntegratedCrafting.INetworkPositions positions = + createBasicNetwork(helper, POS, PartTypes.INTERFACE_CRAFTING_LIST, Blocks.CRAFTING_TABLE); + + ChestBlockEntity chestIn = helper.getBlockEntity(POS.east()); + chestIn.setItem(0, new ItemStack(Items.OAK_PLANKS, 64)); + + positions.interfaceStates().get(0).getInventoryVariables().setItem(0, + createVariableForRecipeList(helper.getLevel(), List.of( + Pair.of(RecipeType.CRAFTING, ResourceLocation.fromNamespaceAndPath("minecraft", "chest")), + Pair.of(RecipeType.CRAFTING, ResourceLocation.fromNamespaceAndPath("minecraft", "crafting_table")) + ))); + + enableRecipeInWriter(helper, positions.writer(), new ItemStack(Items.CHEST)); + + helper.succeedWhen(() -> { + helper.assertTrue(positions.interfaceStates().get(0).isRecipeSlotValid(0), "Recipe list in crafting interface is not valid"); + helper.assertValueEqual(positions.interfaceStates().get(0).getRecipes().size(), 2, "Recipe count is incorrect"); + + helper.assertValueEqual(chestIn.getItem(0).getItem(), Items.OAK_PLANKS, "Slot 0 item is incorrect"); + helper.assertValueEqual(chestIn.getItem(0).getCount(), 56, "Slot 0 amount is incorrect"); + helper.assertValueEqual(chestIn.getItem(1).getItem(), Items.CHEST, "Slot 1 item is incorrect"); + helper.assertValueEqual(chestIn.getItem(1).getCount(), 1, "Slot 1 amount is incorrect"); + }); + } + + /** + * A recipe at the end of the list is craftable as well. + */ + @GameTest(template = TEMPLATE_EMPTY, timeoutTicks = TIMEOUT) + public void testItemsCraftListCraftingTable(GameTestHelper helper) { + GameTestHelpersIntegratedCrafting.INetworkPositions positions = + createBasicNetwork(helper, POS, PartTypes.INTERFACE_CRAFTING_LIST, Blocks.CRAFTING_TABLE); + + ChestBlockEntity chestIn = helper.getBlockEntity(POS.east()); + chestIn.setItem(0, new ItemStack(Items.OAK_PLANKS, 64)); + + positions.interfaceStates().get(0).getInventoryVariables().setItem(0, + createVariableForRecipeList(helper.getLevel(), List.of( + Pair.of(RecipeType.CRAFTING, ResourceLocation.fromNamespaceAndPath("minecraft", "chest")), + Pair.of(RecipeType.CRAFTING, ResourceLocation.fromNamespaceAndPath("minecraft", "crafting_table")) + ))); + + enableRecipeInWriter(helper, positions.writer(), new ItemStack(Items.CRAFTING_TABLE)); + + helper.succeedWhen(() -> { + helper.assertTrue(positions.interfaceStates().get(0).isRecipeSlotValid(0), "Recipe list in crafting interface is not valid"); + helper.assertValueEqual(chestIn.getItem(1).getItem(), Items.CRAFTING_TABLE, "Slot 1 item is incorrect"); + }); + } + + /** + * Replacing the list variable re-indexes the interface with the new recipes. + */ + @GameTest(template = TEMPLATE_EMPTY, timeoutTicks = TIMEOUT) + public void testItemsCraftListReplaceVariable(GameTestHelper helper) { + GameTestHelpersIntegratedCrafting.INetworkPositions positions = + createBasicNetwork(helper, POS, PartTypes.INTERFACE_CRAFTING_LIST, Blocks.CRAFTING_TABLE); + PartTypeInterfaceCraftingList.State state = positions.interfaceStates().get(0); + + state.getInventoryVariables().setItem(0, createVariableForRecipeList(helper.getLevel(), List.of( + Pair.of(RecipeType.CRAFTING, ResourceLocation.fromNamespaceAndPath("minecraft", "chest")), + Pair.of(RecipeType.CRAFTING, ResourceLocation.fromNamespaceAndPath("minecraft", "crafting_table")) + ))); + + boolean[] replaced = {false}; + helper.succeedWhen(() -> { + if (!replaced[0]) { + helper.assertValueEqual(state.getRecipes().size(), 2, "Initial recipe count is incorrect"); + state.getInventoryVariables().setItem(0, createVariableForRecipeList(helper.getLevel(), List.of( + Pair.of(RecipeType.CRAFTING, ResourceLocation.fromNamespaceAndPath("minecraft", "crafting_table")) + ))); + replaced[0] = true; + throw new GameTestAssertException("Waiting for the replaced variable to be picked up"); + } + helper.assertValueEqual(state.getRecipes().size(), 1, "Recipe count after replacement is incorrect"); + }); + } + + /** + * Duplicate recipes inside a list are only exposed once, + * as the crafting network drops a recipe as soon as one removal is requested for it. + */ + @GameTest(template = TEMPLATE_EMPTY, timeoutTicks = TIMEOUT) + public void testItemsCraftListDuplicates(GameTestHelper helper) { + GameTestHelpersIntegratedCrafting.INetworkPositions positions = + createBasicNetwork(helper, POS, PartTypes.INTERFACE_CRAFTING_LIST, Blocks.CRAFTING_TABLE); + + positions.interfaceStates().get(0).getInventoryVariables().setItem(0, + createVariableForRecipeList(helper.getLevel(), List.of( + Pair.of(RecipeType.CRAFTING, ResourceLocation.fromNamespaceAndPath("minecraft", "chest")), + Pair.of(RecipeType.CRAFTING, ResourceLocation.fromNamespaceAndPath("minecraft", "chest")) + ))); + + helper.succeedWhen(() -> helper.assertValueEqual(positions.interfaceStates().get(0).getRecipes().size(), 1, + "Duplicate recipes were not removed")); + } + + /** + * A list that does not hold recipes is rejected. + */ + @GameTest(template = TEMPLATE_EMPTY, timeoutTicks = TIMEOUT) + public void testItemsCraftListWrongElementType(GameTestHelper helper) { + GameTestHelpersIntegratedCrafting.INetworkPositions positions = + createBasicNetwork(helper, POS, PartTypes.INTERFACE_CRAFTING_LIST, Blocks.CRAFTING_TABLE); + + positions.interfaceStates().get(0).getInventoryVariables().setItem(0, createVariableForValue(helper.getLevel(), + ValueTypes.LIST, ValueTypeList.ValueList.ofList(ValueTypes.INTEGER, + Lists.newArrayList(ValueTypeInteger.ValueInteger.of(1))))); + + helper.succeedWhen(() -> { + helper.assertFalse(positions.interfaceStates().get(0).isRecipeSlotValid(0), "Recipe list in crafting interface is valid"); + helper.assertTrue(positions.interfaceStates().get(0).getRecipes().isEmpty(), "Recipes were exposed"); + }); + } + + /** + * A lazy list from a machine reader is read into the interface, + * capped at the configured maximum, and re-read whenever the reader's variable is invalidated. + */ + @GameTest(template = TEMPLATE_EMPTY, timeoutTicks = TIMEOUT) + public void testItemsCraftListMachineReader(GameTestHelper helper) { + GameTestHelpersIntegratedCrafting.INetworkPositions positions = + createBasicNetwork(helper, POS, PartTypes.INTERFACE_CRAFTING_LIST, Blocks.CRAFTING_TABLE); + PartTypeInterfaceCraftingList.State state = positions.interfaceStates().get(0); + + // Validating every recipe of a whole machine against the target is not what is under test here + state.setDisableCraftingCheck(true); + + // Extend the network towards a second crafting table, and read its recipes + helper.setBlock(POS.south(), RegistryEntries.BLOCK_CABLE.value()); + helper.setBlock(POS.south().west(), RegistryEntries.BLOCK_CABLE.value()); + helper.setBlock(POS.south().west().west(), Blocks.CRAFTING_TABLE); + PartPos readerPos = PartPos.of(helper.getLevel(), helper.absolutePos(POS.south().west()), Direction.WEST); + PartHelpers.addPart(helper.getLevel(), helper.absolutePos(POS.south().west()), Direction.WEST, + org.cyclops.integrateddynamics.core.part.PartTypes.MACHINE_READER, + new ItemStack(org.cyclops.integrateddynamics.core.part.PartTypes.MACHINE_READER.getItem())); + + state.getInventoryVariables().setItem(0, createVariableFromReader(helper.getLevel(), readerPos, + Aspects.Read.Machine.LIST_GETRECIPES)); + + boolean[] targetRemoved = {false}; + helper.succeedWhen(() -> { + if (!targetRemoved[0]) { + helper.assertTrue(state.isRecipeSlotValid(0), + "Recipe list from the machine reader is not valid: " + state.getRecipeSlotUnlocalizedMessage(0)); + helper.assertValueEqual(state.getRecipes().size(), GeneralConfig.maxCraftingInterfaceListRecipes, + "Recipe count from the machine reader is not capped"); + + // Remove the reader's target, so that only the list variable changes + helper.setBlock(POS.south().west().west(), Blocks.AIR); + targetRemoved[0] = true; + throw new GameTestAssertException("Waiting for the invalidated variable to be picked up"); + } + helper.assertTrue(state.getRecipes().isEmpty(), "Recipes were not cleared after the reader target was removed"); + }); + } +} diff --git a/src/main/java/org/cyclops/integratedcrafting/inventory/container/ContainerPartInterfaceCrafting.java b/src/main/java/org/cyclops/integratedcrafting/inventory/container/ContainerPartInterfaceCrafting.java index 9aa7e2fa7..ebdf78a19 100644 --- a/src/main/java/org/cyclops/integratedcrafting/inventory/container/ContainerPartInterfaceCrafting.java +++ b/src/main/java/org/cyclops/integratedcrafting/inventory/container/ContainerPartInterfaceCrafting.java @@ -11,13 +11,12 @@ import org.cyclops.cyclopscore.helper.ValueNotifierHelpers; import org.cyclops.cyclopscore.inventory.SimpleInventory; import org.cyclops.integratedcrafting.RegistryEntries; -import org.cyclops.integratedcrafting.part.PartTypeInterfaceCrafting; +import org.cyclops.integratedcrafting.core.part.PartTypeInterfaceCraftingVariableBase; import org.cyclops.integrateddynamics.api.evaluate.variable.ValueDeseralizationContext; import org.cyclops.integrateddynamics.api.item.IVariableFacade; import org.cyclops.integrateddynamics.api.part.IPartContainer; import org.cyclops.integrateddynamics.api.part.PartTarget; import org.cyclops.integrateddynamics.core.evaluate.variable.ValueHelpers; -import org.cyclops.integrateddynamics.core.evaluate.variable.ValueTypes; import org.cyclops.integrateddynamics.core.helper.PartHelpers; import org.cyclops.integrateddynamics.core.inventory.container.ContainerMultipart; import org.cyclops.integrateddynamics.core.inventory.container.ContainerMultipartAspects; @@ -31,18 +30,19 @@ * Container for the crafting interface. * @author rubensworks */ -public class ContainerPartInterfaceCrafting extends ContainerMultipart { +public class ContainerPartInterfaceCrafting

, S extends PartTypeInterfaceCraftingVariableBase.State> + extends ContainerMultipart { private final List readSlotValidIds; private final List readSlotErrorIds; public ContainerPartInterfaceCrafting(int id, Inventory playerInventory, FriendlyByteBuf packetBuffer) { this(id, playerInventory, new SimpleInventory(packetBuffer.readInt(), 1), - Optional.empty(), Optional.empty(), PartHelpers.readPart(packetBuffer)); + Optional.empty(), Optional.empty(), (P) PartHelpers.readPart(packetBuffer)); } public ContainerPartInterfaceCrafting(int id, Inventory playerInventory, Container inventory, - Optional target, Optional partContainer, PartTypeInterfaceCrafting partType) { + Optional target, Optional partContainer, P partType) { super(RegistryEntries.CONTAINER_INTERFACE_CRAFTING.get(), id, playerInventory, inventory, target, partContainer, partType); addInventory(inventory, 0, 8, 22, 1, inventory.getContainerSize()); @@ -93,7 +93,7 @@ protected Slot createNewSlot(Container inventory, int index, int x, int y) { public boolean mayPlace(ItemStack itemStack) { IVariableFacade variableFacade = RegistryEntries.ITEM_VARIABLE.get().getVariableFacade(ValueDeseralizationContext.ofAllEnabled(), itemStack); return variableFacade != null - && ValueHelpers.correspondsTo(variableFacade.getOutputType(), ValueTypes.OBJECT_RECIPE) + && ValueHelpers.correspondsTo(variableFacade.getOutputType(), getPartType().getSlotValueType()) && super.mayPlace(itemStack); } }; diff --git a/src/main/java/org/cyclops/integratedcrafting/inventory/container/ContainerPartInterfaceCraftingSettings.java b/src/main/java/org/cyclops/integratedcrafting/inventory/container/ContainerPartInterfaceCraftingSettings.java index adcb10320..1e8c647ce 100644 --- a/src/main/java/org/cyclops/integratedcrafting/inventory/container/ContainerPartInterfaceCraftingSettings.java +++ b/src/main/java/org/cyclops/integratedcrafting/inventory/container/ContainerPartInterfaceCraftingSettings.java @@ -13,7 +13,7 @@ import org.cyclops.cyclopscore.helper.ValueNotifierHelpers; import org.cyclops.integratedcrafting.RegistryEntries; import org.cyclops.integratedcrafting.core.part.PartTypeInterfaceCraftingBase; -import org.cyclops.integratedcrafting.part.PartTypeInterfaceCrafting; +import org.cyclops.integratedcrafting.core.part.PartTypeInterfaceCraftingVariableBase; import org.cyclops.integrateddynamics.api.part.IPartContainer; import org.cyclops.integrateddynamics.api.part.IPartType; import org.cyclops.integrateddynamics.api.part.PartTarget; @@ -75,7 +75,7 @@ protected void initializeValues() { ValueNotifierHelpers.setValue(this, getTargetSideOverrideValueId(ingredientComponent), partState.getIngredientComponentTargetSideOverride(ingredientComponent).ordinal()); } - if (partState instanceof PartTypeInterfaceCrafting.State stateNormal) { + if (partState instanceof PartTypeInterfaceCraftingVariableBase.State stateNormal) { ValueNotifierHelpers.setValue(this, lastDisableCraftingCheckValueId, stateNormal.isDisableCraftingCheck()); } ValueNotifierHelpers.setValue(this, lastBlockingModeValueId, partState.getCraftingJobHandler().isBlockingJobsMode()); @@ -135,7 +135,7 @@ protected void updatePartSettings() { partState.setIngredientComponentTargetSideOverride(ingredientComponent, getTargetSideOverrideValue(ingredientComponent)); } - if (partState instanceof PartTypeInterfaceCrafting.State stateNormal) { + if (partState instanceof PartTypeInterfaceCraftingVariableBase.State stateNormal) { stateNormal.setDisableCraftingCheck(getLastDisableCraftingCheckValue()); } if (partState.getCraftingJobHandler().setBlockingJobsMode(getLastBlockingModeValue())) { diff --git a/src/main/java/org/cyclops/integratedcrafting/part/PartTypeInterfaceCrafting.java b/src/main/java/org/cyclops/integratedcrafting/part/PartTypeInterfaceCrafting.java index 3e1469d55..ab9479e2c 100644 --- a/src/main/java/org/cyclops/integratedcrafting/part/PartTypeInterfaceCrafting.java +++ b/src/main/java/org/cyclops/integratedcrafting/part/PartTypeInterfaceCrafting.java @@ -1,64 +1,23 @@ package org.cyclops.integratedcrafting.part; -import com.google.common.collect.Lists; -import com.google.common.collect.MapMaker; -import it.unimi.dsi.fastutil.ints.*; -import net.minecraft.core.Direction; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.network.RegistryFriendlyByteBuf; -import net.minecraft.network.chat.Component; -import net.minecraft.network.chat.MutableComponent; -import net.minecraft.server.level.ServerPlayer; -import net.minecraft.world.MenuProvider; -import net.minecraft.world.SimpleContainer; -import net.minecraft.world.entity.player.Inventory; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.inventory.AbstractContainerMenu; -import net.minecraft.world.item.ItemStack; -import net.neoforged.neoforge.common.NeoForge; -import org.apache.commons.lang3.tuple.Triple; -import org.apache.logging.log4j.Level; import org.cyclops.commoncapabilities.api.capability.recipehandler.IRecipeDefinition; -import org.cyclops.commoncapabilities.api.capability.recipehandler.IRecipeHandler; -import org.cyclops.commoncapabilities.api.ingredient.IMixedIngredients; -import org.cyclops.cyclopscore.datastructure.DimPos; -import org.cyclops.cyclopscore.helper.BlockEntityHelpers; -import org.cyclops.cyclopscore.inventory.SimpleInventory; -import org.cyclops.cyclopscore.persist.nbt.NBTClassType; import org.cyclops.integratedcrafting.GeneralConfig; -import org.cyclops.integratedcrafting.IntegratedCrafting; -import org.cyclops.integratedcrafting.api.network.ICraftingNetwork; -import org.cyclops.integratedcrafting.core.part.PartTypeInterfaceCraftingBase; -import org.cyclops.integratedcrafting.inventory.container.ContainerPartInterfaceCrafting; -import org.cyclops.integratedcrafting.inventory.container.ContainerPartInterfaceCraftingSettings; -import org.cyclops.integrateddynamics.api.evaluate.EvaluationException; +import org.cyclops.integratedcrafting.core.part.PartTypeInterfaceCraftingVariableBase; import org.cyclops.integrateddynamics.api.evaluate.variable.IValue; -import org.cyclops.integrateddynamics.api.evaluate.variable.IVariable; -import org.cyclops.integrateddynamics.api.evaluate.variable.ValueDeseralizationContext; -import org.cyclops.integrateddynamics.api.network.INetwork; -import org.cyclops.integrateddynamics.api.network.IPartNetwork; -import org.cyclops.integrateddynamics.api.part.IPartContainer; -import org.cyclops.integrateddynamics.api.part.PartPos; -import org.cyclops.integrateddynamics.api.part.PartTarget; -import org.cyclops.integrateddynamics.core.evaluate.InventoryVariableEvaluator; +import org.cyclops.integrateddynamics.api.evaluate.variable.IValueType; import org.cyclops.integrateddynamics.core.evaluate.variable.ValueObjectTypeRecipe; import org.cyclops.integrateddynamics.core.evaluate.variable.ValueTypes; -import org.cyclops.integrateddynamics.core.helper.NetworkHelpers; -import org.cyclops.integrateddynamics.core.helper.PartHelpers; -import org.cyclops.integrateddynamics.core.part.PartTypeBase; -import org.cyclops.integrateddynamics.core.part.event.PartVariableDrivenVariableContentsUpdatedEvent; -import javax.annotation.Nullable; -import java.util.Collection; +import java.util.Collections; import java.util.List; -import java.util.Map; -import java.util.Optional; /** - * Interface for auto crafting. + * Interface for auto crafting, with one recipe variable per slot. * @author rubensworks */ -public class PartTypeInterfaceCrafting extends PartTypeInterfaceCraftingBase { +public class PartTypeInterfaceCrafting extends PartTypeInterfaceCraftingVariableBase { + + public static final int INVENTORY_SIZE = 9; public PartTypeInterfaceCrafting(String name) { super(name); @@ -70,60 +29,8 @@ public int getConsumptionRate(State state) { } @Override - public Optional getContainerProvider(PartPos pos) { - return Optional.of(new MenuProvider() { - - @Override - public MutableComponent getDisplayName() { - return Component.translatable(getTranslationKey()); - } - - @Override - public AbstractContainerMenu createMenu(int id, Inventory playerInventory, Player playerEntity) { - Triple data = PartHelpers.getContainerPartConstructionData(pos); - PartTypeInterfaceCrafting.State partState = (PartTypeInterfaceCrafting.State) data.getLeft().getPartState(data.getRight().getCenter().getSide()); - return new ContainerPartInterfaceCrafting(id, playerInventory, partState.getInventoryVariables(), - Optional.of(data.getRight()), Optional.of(data.getLeft()), (PartTypeInterfaceCrafting) data.getMiddle()); - } - - @Override - public boolean shouldTriggerClientSideContainerClosingOnOpen() { - return false; - } - }); - } - - @Override - public void writeExtraGuiData(RegistryFriendlyByteBuf packetBuffer, PartPos pos, ServerPlayer player) { - // Write inventory size - IPartContainer partContainer = PartHelpers.getPartContainerChecked(pos); - PartTypeInterfaceCrafting.State partState = (PartTypeInterfaceCrafting.State) partContainer.getPartState(pos.getSide()); - packetBuffer.writeInt(partState.getInventoryVariables().getContainerSize()); - - super.writeExtraGuiData(packetBuffer, pos, player); - } - - @Override - public Optional getContainerProviderSettings(PartPos pos) { - return Optional.of(new MenuProvider() { - - @Override - public MutableComponent getDisplayName() { - return Component.translatable(getTranslationKey()); - } - - @Override - public AbstractContainerMenu createMenu(int id, Inventory playerInventory, Player playerEntity) { - Triple data = PartHelpers.getContainerPartConstructionData(pos); - return new ContainerPartInterfaceCraftingSettings(id, playerInventory, new SimpleContainer(0), - data.getRight(), Optional.of(data.getLeft()), data.getMiddle()); - } - - @Override - public boolean shouldTriggerClientSideContainerClosingOnOpen() { - return false; - } - }); + public IValueType getSlotValueType() { + return ValueTypes.OBJECT_RECIPE; } @Override @@ -131,313 +38,22 @@ protected PartTypeInterfaceCrafting.State constructDefaultState() { return new PartTypeInterfaceCrafting.State(); } - @Override - public void update(INetwork network, IPartNetwork partNetwork, PartTarget target, State state) { - super.update(network, partNetwork, target, state); - - // Reload recipes if needed - IntSet slots = state.getDelayedRecipeReloads(); - if (!slots.isEmpty()) { - ICraftingNetwork craftingNetwork = network.getCapability(getNetworkCapability()).orElse(null); - if (craftingNetwork != null) { - IntSet slotsCopy = new IntOpenHashSet(slots); // Create a copy, to allow insertion into slots during this loop - slots.clear(); - int channel = state.getChannelCrafting(); - for (Integer slot : slotsCopy) { - // Remove the old recipe from the network - Int2ObjectMap recipes = state.getRecipesIndexed(); - IRecipeDefinition oldRecipe = recipes.get(slot); - if (oldRecipe != null) { - craftingNetwork.removeCraftingInterfaceRecipe(channel, state, oldRecipe); - } - - // Reload the recipe in the slot - // We simulate initialization for the first two ticks, as dependency variables may still be loading, - // and errored may only go away after these dependencies are fully loaded. - // Related to CyclopsMC/IntegratedCrafting#110 - state.reloadRecipe(slot, state.ticksAfterReload <= 1); - - // Add the new recipe to the network - IRecipeDefinition newRecipe = recipes.get(slot); - if (newRecipe != null) { - craftingNetwork.addCraftingInterfaceRecipe(channel, state, newRecipe); - } - } - } - } - - // Internal tick counter - state.ticksAfterReload++; - } - - @Override - public void addDrops(PartTarget target, State state, List itemStacks, boolean dropMainElement, boolean saveState) { - // Drop the stored variables - for(int i = 0; i < state.getInventoryVariables().getContainerSize(); i++) { - ItemStack itemStack = state.getInventoryVariables().getItem(i); - if(!itemStack.isEmpty()) { - itemStacks.add(itemStack); - } - } - state.getInventoryVariables().clearContent(); - - super.addDrops(target, state, itemStacks, dropMainElement, saveState); - } - - public static class State extends PartTypeInterfaceCraftingBase.State { - - protected int ticksAfterReload = 0; - - private final SimpleInventory inventoryVariables; - private final List> variableEvaluators; - private final Int2ObjectMap recipeSlotMessages; - private final Int2BooleanMap recipeSlotValidated; - private final IntSet delayedRecipeReloads; - private final Map variableListeners; - private boolean disableCraftingCheck = false; - - private final Int2ObjectMap currentRecipes; + public static class State extends PartTypeInterfaceCraftingVariableBase.State { public State() { - this.inventoryVariables = new SimpleInventory(9, 1); - this.inventoryVariables.addDirtyMarkListener(this); - this.variableEvaluators = Lists.newArrayList(); - this.recipeSlotMessages = new Int2ObjectArrayMap<>(); - this.recipeSlotValidated = new Int2BooleanArrayMap(); - this.delayedRecipeReloads = new IntArraySet(); - this.variableListeners = new MapMaker().weakKeys().makeMap(); - this.currentRecipes = new Int2ObjectArrayMap<>(); - } - - /** - * @return The inner variables inventory - */ - public SimpleInventory getInventoryVariables() { - return this.inventoryVariables; - } - - @Override - public void writeToNBT(ValueDeseralizationContext valueDeseralizationContext, CompoundTag tag) { - super.writeToNBT(valueDeseralizationContext, tag); - inventoryVariables.writeToNBT(valueDeseralizationContext.holderLookupProvider(), tag, "variables"); - - CompoundTag recipeSlotErrorsTag = new CompoundTag(); - for (Int2ObjectMap.Entry entry : this.recipeSlotMessages.int2ObjectEntrySet()) { - NBTClassType.writeNbt(MutableComponent.class, String.valueOf(entry.getIntKey()), entry.getValue(), recipeSlotErrorsTag, valueDeseralizationContext.holderLookupProvider()); - } - tag.put("recipeSlotMessages", recipeSlotErrorsTag); - - CompoundTag recipeSlotValidatedTag = new CompoundTag(); - for (Int2BooleanMap.Entry entry : this.recipeSlotValidated.int2BooleanEntrySet()) { - recipeSlotValidatedTag.putBoolean(String.valueOf(entry.getIntKey()), entry.getBooleanValue()); - } - tag.put("recipeSlotValidated", recipeSlotValidatedTag); - - tag.putBoolean("disableCraftingCheck", disableCraftingCheck); + super(INVENTORY_SIZE); } @Override - public void readFromNBT(ValueDeseralizationContext valueDeseralizationContext, CompoundTag tag) { - super.readFromNBT(valueDeseralizationContext, tag); - inventoryVariables.readFromNBT(valueDeseralizationContext.holderLookupProvider(), tag, "variables"); - - this.recipeSlotMessages.clear(); - CompoundTag recipeSlotErrorsTag = tag.getCompound("recipeSlotMessages"); - for (String slot : recipeSlotErrorsTag.getAllKeys()) { - MutableComponent unlocalizedString = NBTClassType.readNbt(MutableComponent.class, slot, recipeSlotErrorsTag, valueDeseralizationContext.holderLookupProvider()); - this.recipeSlotMessages.put(Integer.parseInt(slot), unlocalizedString); - } - - this.recipeSlotValidated.clear(); - CompoundTag recipeSlotValidatedTag = tag.getCompound("recipeSlotValidated"); - for (String slot : recipeSlotValidatedTag.getAllKeys()) { - this.recipeSlotValidated.put(Integer.parseInt(slot), recipeSlotValidatedTag.getBoolean(slot)); - } - - this.disableCraftingCheck = tag.getBoolean("disableCraftingCheck"); + protected PartTypeInterfaceCrafting getPartTypeInstance() { + return PartTypes.INTERFACE_CRAFTING; } @Override - public void reloadRecipes(boolean initialize) { - this.currentRecipes.clear(); - this.recipeSlotMessages.clear(); - this.recipeSlotValidated.clear(); - variableEvaluators.clear(); - for (int i = 0; i < getInventoryVariables().getContainerSize(); i++) { - int slot = i; - variableEvaluators.add(new InventoryVariableEvaluator( - getInventoryVariables(), slot, valueDeseralizationContext, ValueTypes.OBJECT_RECIPE) { - @Override - public void onErrorsChanged() { - super.onErrorsChanged(); - setLocalErrors(slot, getErrors()); - } - }); - } - if (this.partNetwork != null) { - for (int i = 0; i < getInventoryVariables().getContainerSize(); i++) { - reloadRecipe(i, initialize); - } - } - } - - private void setLocalErrors(int slot, List errors) { - if (errors.isEmpty()) { - if (this.recipeSlotMessages.size() > slot) { - this.recipeSlotMessages.remove(slot); - } - } else { - this.recipeSlotMessages.put(slot, errors.get(0)); - } - } - - protected void reloadRecipe(int slot, boolean initialize) { - this.currentRecipes.remove(slot); - if (this.recipeSlotMessages.size() > slot) { - this.recipeSlotMessages.remove(slot); - } - if (this.recipeSlotValidated.size() > slot) { - this.recipeSlotValidated.remove(slot); - } - if (this.partNetwork != null) { - InventoryVariableEvaluator evaluator = variableEvaluators.get(slot); - evaluator.refreshVariable(network, false); - IVariable variable = evaluator.getVariable(network); - if (variable != null) { - try { - // Refresh the recipe if variable is changed - // The map is needed because we only want to register the listener once for each variable - if (!this.variableListeners.containsKey(variable)) { - variable.addInvalidationListener(() -> { - this.variableListeners.remove(variable); - delayedReloadRecipe(slot); - }); - this.variableListeners.put(variable, true); - } - - IValue value = variable.getValue(); - if (value.getType() == ValueTypes.OBJECT_RECIPE) { - Optional recipeWrapper = ((ValueObjectTypeRecipe.ValueRecipe) value).getRawValue(); - if (recipeWrapper.isPresent()) { - IRecipeDefinition recipe = recipeWrapper.get(); - if (!GeneralConfig.validateRecipesCraftingInterface || this.disableCraftingCheck || isValid(recipe)) { - this.currentRecipes.put(slot, recipe); - this.recipeSlotValidated.put(slot, true); - this.recipeSlotMessages.put(slot, Component.translatable("gui.integratedcrafting.partinterface.slot.message.valid")); - } else { - this.recipeSlotMessages.put(slot, Component.translatable("gui.integratedcrafting.partinterface.slot.message.invalid")); - } - } - } else { - this.recipeSlotMessages.put(slot, Component.translatable("gui.integratedcrafting.partinterface.slot.message.norecipe")); - } - } catch (EvaluationException e) { - this.recipeSlotMessages.put(slot, e.getErrorMessage()); - } - } else { - // If we're initializing, the variable might be referencing other variables that are not yet loaded. - // So let's retry once in the next tick. - if (initialize && evaluator.hasVariable()) { - this.delayedReloadRecipe(slot); - } else { - this.recipeSlotMessages.put(slot, Component.translatable("gui.integratedcrafting.partinterface.slot.message.norecipe")); - } - } - - try { - IPartNetwork partNetwork = NetworkHelpers.getPartNetworkChecked(network); - NeoForge.EVENT_BUS.post(new PartVariableDrivenVariableContentsUpdatedEvent<>(network, - partNetwork, getTarget(), - PartTypes.INTERFACE_CRAFTING, this, lastPlayer, variable, - variable != null ? variable.getValue() : null)); - } catch (EvaluationException e) { - // Ignore error - } - } - sendUpdate(); - } - - private void delayedReloadRecipe(int slot) { - this.delayedRecipeReloads.add(slot); - } - - - private boolean isValid(IRecipeDefinition recipe) { - DimPos dimPos = getTarget().getTarget().getPos(); - Direction side = getTarget().getTarget().getSide(); - IRecipeHandler recipeHandler = BlockEntityHelpers.getCapability(dimPos.getLevel(true), dimPos.getBlockPos(), side, org.cyclops.commoncapabilities.api.capability.Capabilities.RecipeHandler.BLOCK).orElse(null); - if (recipeHandler != null) { - IMixedIngredients simulatedOutput = recipeHandler.simulate(recipe); - if (simulatedOutput != null && !simulatedOutput.isEmpty()) { - if (recipe.getOutput().containsAll(simulatedOutput)) { - return true; - } else { - if (GeneralConfig.logRecipeValidationFailures) { - IntegratedCrafting.clog(Level.INFO, "Recipe validation failure: incompatible recipe output and simulated output:\nRecipe output: " + recipe.getOutput() + "\nSimulated output: " + simulatedOutput); - } - return false; - } - } - if (GeneralConfig.logRecipeValidationFailures) { - IntegratedCrafting.clog(Level.INFO, "Recipe validation failure: No output was obtained when simulating a recipe\n" + recipe); - } - return false; - } - return true; // No recipe handler capability is present, so we can't confirm that the recipe will work. - } - - @Override - public void onDirty() { - super.onDirty(); - - // Unregister from the network, when all old recipes are still in place - if (craftingNetwork != null) { - craftingNetwork.removeCraftingInterface(getChannelCrafting(), this); - } - - // Recalculate recipes - if (getTarget() != null && !getTarget().getCenter().getPos().getLevel(true).isClientSide) { - reloadRecipes(false); - } - - // Re-register to the network, to force an update for all new recipes - if (craftingNetwork != null) { - craftingNetwork.addCraftingInterface(getChannelCrafting(), this); - } - } - - @Override - public Collection getRecipes() { - return this.currentRecipes.values(); - } - - public Int2ObjectMap getRecipesIndexed() { - return currentRecipes; - } - - public boolean isRecipeSlotValid(int slot) { - return this.recipeSlotValidated.containsKey(slot); - } - - @Nullable - public MutableComponent getRecipeSlotUnlocalizedMessage(int slot) { - return this.recipeSlotMessages.get(slot); - } - - public IntSet getDelayedRecipeReloads() { - return delayedRecipeReloads; - } - - public void setDisableCraftingCheck(boolean disableCraftingCheck) { - if (disableCraftingCheck != this.disableCraftingCheck) { - this.disableCraftingCheck = disableCraftingCheck; - - this.sendUpdate(); - } - } - - public boolean isDisableCraftingCheck() { - return disableCraftingCheck; + protected List extractRecipes(int slot, IValue value) { + return ((ValueObjectTypeRecipe.ValueRecipe) value).getRawValue() + .map(Collections::singletonList) + .orElse(Collections.emptyList()); } } diff --git a/src/main/java/org/cyclops/integratedcrafting/part/PartTypeInterfaceCraftingList.java b/src/main/java/org/cyclops/integratedcrafting/part/PartTypeInterfaceCraftingList.java new file mode 100644 index 000000000..b6d850420 --- /dev/null +++ b/src/main/java/org/cyclops/integratedcrafting/part/PartTypeInterfaceCraftingList.java @@ -0,0 +1,138 @@ +package org.cyclops.integratedcrafting.part; + +import com.google.common.collect.Lists; +import it.unimi.dsi.fastutil.ints.Int2IntArrayMap; +import it.unimi.dsi.fastutil.ints.Int2IntMap; +import it.unimi.dsi.fastutil.ints.IntArraySet; +import it.unimi.dsi.fastutil.ints.IntSet; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import org.cyclops.commoncapabilities.api.capability.recipehandler.IRecipeDefinition; +import org.cyclops.integratedcrafting.GeneralConfig; +import org.cyclops.integratedcrafting.core.part.PartTypeInterfaceCraftingVariableBase; +import org.cyclops.integrateddynamics.api.evaluate.EvaluationException; +import org.cyclops.integrateddynamics.api.evaluate.variable.IValue; +import org.cyclops.integrateddynamics.api.evaluate.variable.IValueType; +import org.cyclops.integrateddynamics.api.evaluate.variable.IValueTypeListProxy; +import org.cyclops.integrateddynamics.core.evaluate.variable.ValueHelpers; +import org.cyclops.integrateddynamics.core.evaluate.variable.ValueObjectTypeRecipe; +import org.cyclops.integrateddynamics.core.evaluate.variable.ValueTypeList; +import org.cyclops.integrateddynamics.core.evaluate.variable.ValueTypes; +import org.cyclops.integrateddynamics.core.helper.L10NValues; + +import java.util.List; +import java.util.Optional; + +/** + * Interface for auto crafting that derives all its recipes from a single list variable. + * @author rubensworks + */ +public class PartTypeInterfaceCraftingList extends PartTypeInterfaceCraftingVariableBase { + + public static final int INVENTORY_SIZE = 1; + + public PartTypeInterfaceCraftingList(String name) { + super(name); + } + + @Override + public int getConsumptionRate(State state) { + return state.getCraftingJobHandler().getProcessingCraftingJobs().size() * GeneralConfig.interfaceCraftingListBaseConsumption; + } + + @Override + public IValueType getSlotValueType() { + return ValueTypes.LIST; + } + + @Override + protected PartTypeInterfaceCraftingList.State constructDefaultState() { + return new PartTypeInterfaceCraftingList.State(); + } + + public static class State extends PartTypeInterfaceCraftingVariableBase.State { + + // Slots for which the configured maximum truncated the list + private final IntSet truncatedSlots = new IntArraySet(); + // The tick at which each slot was last reloaded + private final Int2IntMap lastSlotReloadTicks = new Int2IntArrayMap(); + + public State() { + super(INVENTORY_SIZE); + } + + @Override + protected PartTypeInterfaceCraftingList getPartTypeInstance() { + return PartTypes.INTERFACE_CRAFTING_LIST; + } + + @Override + protected boolean mayReloadSlot(int slot) { + // Reader-backed list variables are invalidated on every reader tick, while re-materializing + // a list can be expensive. So throttle how often we act on those invalidations. + int interval = GeneralConfig.craftingInterfaceListMinReloadInterval; + if (interval <= 0 || !this.lastSlotReloadTicks.containsKey(slot)) { + return true; + } + return getTicks() - this.lastSlotReloadTicks.get(slot) >= interval; + } + + @Override + protected void reloadRecipe(int slot, boolean initialize) { + this.lastSlotReloadTicks.put(slot, getTicks()); + super.reloadRecipe(slot, initialize); + } + + @Override + protected MutableComponent getRecipesValidMessage(int slot, int count) { + if (this.truncatedSlots.contains(slot)) { + return Component.translatable("gui.integratedcrafting.partinterface.slot.message.list.toolarge", count); + } + return super.getRecipesValidMessage(slot, count); + } + + @Override + protected List extractRecipes(int slot, IValue value) throws EvaluationException { + this.truncatedSlots.remove(slot); + IValueTypeListProxy, IValue> list = ((ValueTypeList.ValueList) value).getRawValue(); + + // Infinite lists can never be indexed into the crafting network. + if (list.isInfinite()) { + throw new EvaluationException(Component.translatable( + "gui.integratedcrafting.partinterface.slot.message.list.infinite")); + } + + // An ANY-typed list is still allowed here, but then each element is checked separately below. + IValueType elementType = list.getValueType(); + if (!ValueHelpers.correspondsTo(elementType, ValueTypes.OBJECT_RECIPE)) { + throw new EvaluationException(Component.translatable(L10NValues.VALUETYPE_ERROR_INVALIDLISTVALUETYPE, + Component.translatable(ValueTypes.OBJECT_RECIPE.getTranslationKey()), + Component.translatable(elementType.getTranslationKey()))); + } + + int length = list.getLength(); + int max = GeneralConfig.maxCraftingInterfaceListRecipes; + if (max > 0 && length > max) { + length = max; + this.truncatedSlots.add(slot); + } + + // Materialize the list only once: list proxies can be lazy views on remote positions, + // for which each element access is a capability lookup. + List recipes = Lists.newArrayListWithExpectedSize(length); + for (int i = 0; i < length; i++) { + IValue element = list.get(i); + if (element.getType() != ValueTypes.OBJECT_RECIPE) { + throw new EvaluationException(Component.translatable(L10NValues.VALUETYPE_ERROR_INVALIDLISTELEMENT, + Component.translatable(ValueTypes.OBJECT_RECIPE.getTranslationKey()), + Component.translatable(element.getType().getTranslationKey()))); + } + Optional recipe = ((ValueObjectTypeRecipe.ValueRecipe) element).getRawValue(); + recipe.ifPresent(recipes::add); + } + + return deduplicate(recipes); + } + + } +} diff --git a/src/main/java/org/cyclops/integratedcrafting/part/PartTypes.java b/src/main/java/org/cyclops/integratedcrafting/part/PartTypes.java index 83cdf255d..44319e5e1 100644 --- a/src/main/java/org/cyclops/integratedcrafting/part/PartTypes.java +++ b/src/main/java/org/cyclops/integratedcrafting/part/PartTypes.java @@ -16,6 +16,7 @@ public static void load() { public static final PartTypeInterfaceCrafting INTERFACE_CRAFTING = REGISTRY.register(new PartTypeInterfaceCrafting("interface_crafting")); public static final PartTypeInterfaceCraftingAttuned INTERFACE_CRAFTING_ATTUNED = REGISTRY.register(new PartTypeInterfaceCraftingAttuned("interface_crafting_attuned")); + public static final PartTypeInterfaceCraftingList INTERFACE_CRAFTING_LIST = REGISTRY.register(new PartTypeInterfaceCraftingList("interface_crafting_list")); public static final PartTypeCraftingWriter CRAFTING_WRITER = REGISTRY.register(new PartTypeCraftingWriter("crafting_writer")); } diff --git a/src/main/resources/assets/integratedcrafting/blockstates/part_interface_crafting_list.json b/src/main/resources/assets/integratedcrafting/blockstates/part_interface_crafting_list.json new file mode 100644 index 000000000..6f4102f86 --- /dev/null +++ b/src/main/resources/assets/integratedcrafting/blockstates/part_interface_crafting_list.json @@ -0,0 +1,10 @@ +{ + "variants": { + "facing=north": { "model": "integratedcrafting:block/part_interface_crafting_list", "y": 180 }, + "facing=east": { "model": "integratedcrafting:block/part_interface_crafting_list", "y": 270 }, + "facing=south": { "model": "integratedcrafting:block/part_interface_crafting_list" }, + "facing=west": { "model": "integratedcrafting:block/part_interface_crafting_list", "y": 90 }, + "facing=up": { "model": "integratedcrafting:block/part_interface_crafting_list", "x": 90 }, + "facing=down": { "model": "integratedcrafting:block/part_interface_crafting_list", "x": 270 } + } +} diff --git a/src/main/resources/assets/integratedcrafting/lang/en_us.json b/src/main/resources/assets/integratedcrafting/lang/en_us.json index 6a6ca54ef..ab03d16e0 100644 --- a/src/main/resources/assets/integratedcrafting/lang/en_us.json +++ b/src/main/resources/assets/integratedcrafting/lang/en_us.json @@ -12,6 +12,11 @@ "gui.integratedcrafting.partinterface.slot.message.valid": "Recipe is valid for the target.", "gui.integratedcrafting.partinterface.slot.message.invalid": "Recipe is not acceptable by the target.", "gui.integratedcrafting.partinterface.slot.message.norecipe": "The variable does not contain a recipe.", + "gui.integratedcrafting.partinterface.slot.message.empty": "The variable does not contain any recipes.", + "gui.integratedcrafting.partinterface.slot.message.valid.multiple": "%s recipes are valid for the target.", + "gui.integratedcrafting.partinterface.slot.message.partial": "Only %s of %s recipes are acceptable by the target.", + "gui.integratedcrafting.partinterface.slot.message.list.infinite": "Infinite lists of recipes are not supported.", + "gui.integratedcrafting.partinterface.slot.message.list.toolarge": "The list was too long: only the first %s recipes are used.", "_comment": "Advancements", "advancement.integratedcrafting.craft_crafting_interface": "Crafting² Interface", @@ -24,6 +29,8 @@ "advancement.integratedcrafting.craft_planks.desc": "Use a Crafting Writer to start a crafting job for Oak Planks", "advancement.integratedcrafting.craft_crafting_interface_attuned": "Crafting³ Interface", "advancement.integratedcrafting.craft_crafting_interface_attuned.desc": "Craft an Attuned Crafting Interface", + "advancement.integratedcrafting.craft_crafting_interface_list": "Crafting\u2074 Interface", + "advancement.integratedcrafting.craft_crafting_interface_list.desc": "Craft a List-Based Crafting Interface", "_comment": "Part types", "parttype.integratedcrafting.interface_crafting": "Crafting Interface", @@ -31,6 +38,8 @@ "parttype.integratedcrafting.interface_crafting_attuned": "Attuned Crafting Interface", "parttype.integratedcrafting.interface_crafting_attuned.info": "Handles crafting for all recipes exposed by the target machine.", "parttype.integratedcrafting.interface_crafting_attuned.unsupported": "The target machine does not support recipe handling.", + "parttype.integratedcrafting.interface_crafting_list": "List-Based Crafting Interface", + "parttype.integratedcrafting.interface_crafting_list.info": "Handles crafting for all recipes inside a single list variable.", "parttype.integratedcrafting.crafting_writer": "Crafting Writer", "parttype.integratedcrafting.crafting_writer.info": "Starts crafting jobs", @@ -83,6 +92,7 @@ "info_book.integratedcrafting.crafting_interface.basics.text2": "By pointing a &lCrafting Interface&r to a machine &o(like a Crafting table or Furnace)&r, this interface will be bound to this machine to handle recipes.", "info_book.integratedcrafting.crafting_interface.basics.text3": "When opening the GUI of a &lCrafting Interface&r, you can insert &lVariable Cards&r that hold &8Recipes&0. This will tell the interface that the given recipe can be crafted using the bound machine.", "info_book.integratedcrafting.crafting_interface.basics.text4": "When you've progressed later in the game, you will be able to craft the &lAttuned Crafting Interface&r. It does not require you to manually add recipes to it, as it will read and expose all recipes that are available in the target machine. These are the same recipes that are listed in the &lMachine Reader&r. Some modded machines may not be supported, which will be visualized by a red border when placing the &lAttuned Crafting Interface&r.", + "info_book.integratedcrafting.crafting_interface.basics.text5": "The &lList-Based Crafting Interface&r takes a single variable holding a &llist of recipes&r, and exposes every recipe in that list to the network. This allows you to derive the recipes of an interface using logic, for example by pointing the &lMachine Reader&r at a machine, or by filtering a list of recipes. Infinite lists are not supported, and the number of recipes that is read from a list can be limited in the config.", "info_book.integratedcrafting.crafting_interface.crafting": "Crafting Process", "info_book.integratedcrafting.crafting_interface.crafting.text1": "This section will explain how the crafting process is being handled by &lCrafting Interfaces&r. This information is essential to get the most out of this mod, and to debug any issues you may encounter.", diff --git a/src/main/resources/assets/integratedcrafting/models/block/part_interface_crafting_list.json b/src/main/resources/assets/integratedcrafting/models/block/part_interface_crafting_list.json new file mode 100644 index 000000000..1ea83466c --- /dev/null +++ b/src/main/resources/assets/integratedcrafting/models/block/part_interface_crafting_list.json @@ -0,0 +1,6 @@ +{ + "parent": "integratedcrafting:block/interface_crafting", + "textures": { + "texture" : "integratedcrafting:part/interface_crafting_list" + } +} diff --git a/src/main/resources/assets/integratedcrafting/models/item/part_interface_crafting_list.json b/src/main/resources/assets/integratedcrafting/models/item/part_interface_crafting_list.json new file mode 100644 index 000000000..8b5e2c6a8 --- /dev/null +++ b/src/main/resources/assets/integratedcrafting/models/item/part_interface_crafting_list.json @@ -0,0 +1,3 @@ +{ + "parent": "integratedcrafting:block/part_interface_crafting_list" +} diff --git a/src/main/resources/assets/integratedcrafting/textures/gui/part_interface_crafting_list.png b/src/main/resources/assets/integratedcrafting/textures/gui/part_interface_crafting_list.png new file mode 100644 index 0000000000000000000000000000000000000000..7efab63e0bcd7603dd7124508839e5162f31ce34 GIT binary patch literal 404 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K58CaNs)Vi3hp+HJBz$e5tzbF^T{Qv*|v17+V zLqpBY&AYq1Tc^Di1t~8H@(X5gcy=QV$kz3AaSW-L^Y+$3PZmRwhKJK7oTJADU`D(;befq$W?JvI172+_qekEMEccdWM*4^6(b9ju3iLqI{n_Q9J;tK_#!uG#s`_PNFp#W$z!V`T!# zIy5vC?S36De|kx6p0UQ>nErq6SCk%o+2J#%!w4)5w0Ta)oDPUu21fBhrBBM2|MPu# zYEsJ$6ZAX!GWtU}^REx5$|@w{ZJcfQ`d7Yt{o;_&((@b^XDT#+frSKuYnb0(Y3jIt RjYA$J>gnp|vd$@?2>?D=ll=ey literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/integratedcrafting/textures/part/interface_crafting_list.png b/src/main/resources/assets/integratedcrafting/textures/part/interface_crafting_list.png new file mode 100644 index 0000000000000000000000000000000000000000..96285c6ff6e5ce4c035d9abd2b88eca8c14062f9 GIT binary patch literal 530 zcmV+t0`2{YP)!GNE(HJR-%nX zAcg;cmGz-;hMRTxE(9AJztxu+c=MU>drxtog~cV0Hi!t$IYb1Y^v*fR zQa*2gR2-2xG%iUv+g@XgLi-%8AVuN5$Kc6zh0cqo9G6;HT=FhS82s2ni=`UZ01#_I z8{BXJz^LCNFb1hkA=Z{80opKk`#xGMGq=`=uiZhyh{lb3G@5NBjEI(3Isfnpd75&j z^#F7Uju8=Tr9w5XF;lNI>i6gm2Ymn9;q-JJHyq%9{bU!Y#x;~uWj+ALIY;gi0J>W* zOJu+Ef&GJ*#@p{uVFW;yrbVDf3GlwaZnhcqd+fj8qS0)ZMeA3tqivPFx0`rVnIH%N zlvv2pKhYlZKF=}6K$fEOY@!RsIY)}ZB?+C4XXQor^?K2TBw_c>MtPnGdg2R#h*0rG z)o6K@*_9Rm(^qfeFI)y-cBRF9`%#gN3Qh>o7kv(asoE(djIdG6TwJ3YMhwO&(cvuQ z*#yBq4^lbT=Pq9I^LHOZo{^#ON5afd7(AWL=Y09JbMh{TwS0TE{?}y^Yx)258@Mdi Uh5S4sZ2$lO07*qoM6N<$f?PNBDgXcg literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/minecraft/atlases/blocks.json b/src/main/resources/assets/minecraft/atlases/blocks.json index 5d9e4f04a..1819e3d9a 100644 --- a/src/main/resources/assets/minecraft/atlases/blocks.json +++ b/src/main/resources/assets/minecraft/atlases/blocks.json @@ -12,5 +12,6 @@ { "type": "minecraft:single", "resource": "integratedcrafting:part/crafting_writer_front_inactive" }, { "type": "minecraft:single", "resource": "integratedcrafting:part/interface_crafting" }, { "type": "minecraft:single", "resource": "integratedcrafting:part/interface_crafting_attuned" }, + { "type": "minecraft:single", "resource": "integratedcrafting:part/interface_crafting_list" }, { "type": "minecraft:single", "resource": "integratedcrafting:part/crafting_writer_back" } ]} diff --git a/src/main/resources/data/integratedcrafting/advancement/autocrafting_setup/craft_crafting_interface_list.json b/src/main/resources/data/integratedcrafting/advancement/autocrafting_setup/craft_crafting_interface_list.json new file mode 100644 index 000000000..8f49269f3 --- /dev/null +++ b/src/main/resources/data/integratedcrafting/advancement/autocrafting_setup/craft_crafting_interface_list.json @@ -0,0 +1,24 @@ +{ + "display": { + "icon": { + "id": "integratedcrafting:part_interface_crafting_list" + }, + "title": { + "translate": "advancement.integratedcrafting.craft_crafting_interface_list" + }, + "description": { + "translate": "advancement.integratedcrafting.craft_crafting_interface_list.desc" + } + }, + "parent": "integratedcrafting:root", + "criteria": { + "criteria_0": { + "trigger": "cyclopscore:item_crafted", + "conditions": { + "item": { + "items": ["integratedcrafting:part_interface_crafting_list"] + } + } + } + } +} diff --git a/src/main/resources/data/integratedcrafting/advancement/root.json b/src/main/resources/data/integratedcrafting/advancement/root.json index 384b27754..34f6c2f34 100644 --- a/src/main/resources/data/integratedcrafting/advancement/root.json +++ b/src/main/resources/data/integratedcrafting/advancement/root.json @@ -17,6 +17,7 @@ "recipes": [ "integratedcrafting:part_interface_crafting", "integratedcrafting:part_interface_crafting_attuned", + "integratedcrafting:part_interface_crafting_list", "integratedcrafting:part_crafting_writer" ] diff --git a/src/main/resources/data/integratedcrafting/info/crafting_info.xml b/src/main/resources/data/integratedcrafting/info/crafting_info.xml index 1cc8f69a2..9621df186 100644 --- a/src/main/resources/data/integratedcrafting/info/crafting_info.xml +++ b/src/main/resources/data/integratedcrafting/info/crafting_info.xml @@ -15,12 +15,15 @@

integratedcrafting:part_interface_crafting integratedcrafting:part_interface_crafting_attuned + integratedcrafting:part_interface_crafting_list info_book.integratedcrafting.crafting_interface.basics.text1 info_book.integratedcrafting.crafting_interface.basics.text2 info_book.integratedcrafting.crafting_interface.basics.text3 info_book.integratedcrafting.crafting_interface.basics.text4 + info_book.integratedcrafting.crafting_interface.basics.text5 integratedcrafting:crafting/part_interface_crafting integratedcrafting:crafting/part_interface_crafting_attuned + integratedcrafting:crafting/part_interface_crafting_list
diff --git a/src/main/resources/data/integratedcrafting/recipe/crafting/part_interface_crafting_list.json b/src/main/resources/data/integratedcrafting/recipe/crafting/part_interface_crafting_list.json new file mode 100644 index 000000000..fd4a10855 --- /dev/null +++ b/src/main/resources/data/integratedcrafting/recipe/crafting/part_interface_crafting_list.json @@ -0,0 +1,22 @@ +{ + "type": "minecraft:crafting_shaped", + "pattern": [ + " D ", + "ICI", + " D " + ], + "key": { + "I": { + "item": "integratedcrafting:part_interface_crafting" + }, + "C": { + "item": "integrateddynamics:crystalized_chorus_block" + }, + "D": { + "tag": "c:gems/diamond" + } + }, + "result": { + "id": "integratedcrafting:part_interface_crafting_list" + } +} diff --git a/src/main/resources/data/integrateddynamics/tags/item/parts.json b/src/main/resources/data/integrateddynamics/tags/item/parts.json index d2f682aef..5bd60d4a4 100644 --- a/src/main/resources/data/integrateddynamics/tags/item/parts.json +++ b/src/main/resources/data/integrateddynamics/tags/item/parts.json @@ -3,6 +3,7 @@ "values": [ "integratedcrafting:part_interface_crafting", "integratedcrafting:part_interface_crafting_attuned", + "integratedcrafting:part_interface_crafting_list", "integratedcrafting:part_crafting_writer" ] } \ No newline at end of file From bc1dea8c775a6883bd13d9006cc6a7cb8378c425 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 04:50:46 +0000 Subject: [PATCH 2/4] Center the variable slots and shorten the list interface name The single slot of the list interface sat in the top-left corner. The first slot's x position is now derived from the slot count, which centers one slot and leaves the nine-slot row exactly where it was. "List-Based Crafting Interface" ran into the settings button in the gui title, so it becomes "List Crafting Interface". Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Bn8brBLvFiJsfnBHpwf8ZN --- .../ContainerScreenPartInterfaceCrafting.java | 6 ++++-- .../ContainerPartInterfaceCrafting.java | 12 +++++++++++- .../assets/integratedcrafting/lang/en_us.json | 6 +++--- .../gui/part_interface_crafting_list.png | Bin 404 -> 407 bytes 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/cyclops/integratedcrafting/client/gui/ContainerScreenPartInterfaceCrafting.java b/src/main/java/org/cyclops/integratedcrafting/client/gui/ContainerScreenPartInterfaceCrafting.java index dde5cc7e8..6906805de 100644 --- a/src/main/java/org/cyclops/integratedcrafting/client/gui/ContainerScreenPartInterfaceCrafting.java +++ b/src/main/java/org/cyclops/integratedcrafting/client/gui/ContainerScreenPartInterfaceCrafting.java @@ -58,8 +58,9 @@ protected void renderBg(GuiGraphics guiGraphics, float partialTicks, int mouseX, RenderSystem.setShaderColor(1, 1, 1, 1); int y = topPos + 42; + int slotsX = ContainerPartInterfaceCrafting.getVariableSlotsX(getMenu().getContainerInventory().getContainerSize()); for (int i = 0; i < getMenu().getContainerInventory().getContainerSize(); i++) { - int x = leftPos + 10 + i * GuiHelpers.SLOT_SIZE; + int x = leftPos + slotsX + 2 + i * GuiHelpers.SLOT_SIZE; if (!getMenu().getContainerInventory().getItem(i).isEmpty()) { IImage image = container.isRecipeSlotValid(i) ? Images.OK : Images.ERROR; image.draw(guiGraphics, x, y); @@ -74,8 +75,9 @@ protected void renderLabels(GuiGraphics guiGraphics, int mouseX, int mouseY) { guiGraphics.pose().last().pose(), guiGraphics.bufferSource(), Font.DisplayMode.NORMAL, 0, 15728880); int y = 42; + int slotsX = ContainerPartInterfaceCrafting.getVariableSlotsX(getMenu().getContainerInventory().getContainerSize()); for (int i = 0; i < getMenu().getContainerInventory().getContainerSize(); i++) { - int x = 10 + i * GuiHelpers.SLOT_SIZE; + int x = slotsX + 2 + i * GuiHelpers.SLOT_SIZE; int slot = i; GuiHelpers.renderTooltipOptional(this, guiGraphics.pose(), x, y, 14, 13, mouseX, mouseY, () -> { diff --git a/src/main/java/org/cyclops/integratedcrafting/inventory/container/ContainerPartInterfaceCrafting.java b/src/main/java/org/cyclops/integratedcrafting/inventory/container/ContainerPartInterfaceCrafting.java index ebdf78a19..4de44a2e1 100644 --- a/src/main/java/org/cyclops/integratedcrafting/inventory/container/ContainerPartInterfaceCrafting.java +++ b/src/main/java/org/cyclops/integratedcrafting/inventory/container/ContainerPartInterfaceCrafting.java @@ -8,6 +8,7 @@ import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.inventory.Slot; import net.minecraft.world.item.ItemStack; +import org.cyclops.cyclopscore.helper.GuiHelpers; import org.cyclops.cyclopscore.helper.ValueNotifierHelpers; import org.cyclops.cyclopscore.inventory.SimpleInventory; import org.cyclops.integratedcrafting.RegistryEntries; @@ -33,6 +34,8 @@ public class ContainerPartInterfaceCrafting

, S extends PartTypeInterfaceCraftingVariableBase.State> extends ContainerMultipart { + public static final int GUI_WIDTH = 176; + private final List readSlotValidIds; private final List readSlotErrorIds; @@ -45,7 +48,7 @@ public ContainerPartInterfaceCrafting(int id, Inventory playerInventory, Contain Optional target, Optional partContainer, P partType) { super(RegistryEntries.CONTAINER_INTERFACE_CRAFTING.get(), id, playerInventory, inventory, target, partContainer, partType); - addInventory(inventory, 0, 8, 22, 1, inventory.getContainerSize()); + addInventory(inventory, 0, getVariableSlotsX(inventory.getContainerSize()), 22, 1, inventory.getContainerSize()); addPlayerInventory(player.getInventory(), 8, 59); getPartState().ifPresent(p -> p.setLastPlayer(player)); @@ -76,6 +79,13 @@ public void broadcastChanges() { }); } + /** + * @return The x position of the first variable slot, so that the slots are horizontally centered. + */ + public static int getVariableSlotsX(int slotCount) { + return (GUI_WIDTH - slotCount * GuiHelpers.SLOT_SIZE) / 2 + 1; + } + public boolean isRecipeSlotValid(int slot) { return ValueNotifierHelpers.getValueBoolean(this, this.readSlotValidIds.get(slot)); } diff --git a/src/main/resources/assets/integratedcrafting/lang/en_us.json b/src/main/resources/assets/integratedcrafting/lang/en_us.json index ab03d16e0..296298de8 100644 --- a/src/main/resources/assets/integratedcrafting/lang/en_us.json +++ b/src/main/resources/assets/integratedcrafting/lang/en_us.json @@ -30,7 +30,7 @@ "advancement.integratedcrafting.craft_crafting_interface_attuned": "Crafting³ Interface", "advancement.integratedcrafting.craft_crafting_interface_attuned.desc": "Craft an Attuned Crafting Interface", "advancement.integratedcrafting.craft_crafting_interface_list": "Crafting\u2074 Interface", - "advancement.integratedcrafting.craft_crafting_interface_list.desc": "Craft a List-Based Crafting Interface", + "advancement.integratedcrafting.craft_crafting_interface_list.desc": "Craft a List Crafting Interface", "_comment": "Part types", "parttype.integratedcrafting.interface_crafting": "Crafting Interface", @@ -38,7 +38,7 @@ "parttype.integratedcrafting.interface_crafting_attuned": "Attuned Crafting Interface", "parttype.integratedcrafting.interface_crafting_attuned.info": "Handles crafting for all recipes exposed by the target machine.", "parttype.integratedcrafting.interface_crafting_attuned.unsupported": "The target machine does not support recipe handling.", - "parttype.integratedcrafting.interface_crafting_list": "List-Based Crafting Interface", + "parttype.integratedcrafting.interface_crafting_list": "List Crafting Interface", "parttype.integratedcrafting.interface_crafting_list.info": "Handles crafting for all recipes inside a single list variable.", "parttype.integratedcrafting.crafting_writer": "Crafting Writer", @@ -92,7 +92,7 @@ "info_book.integratedcrafting.crafting_interface.basics.text2": "By pointing a &lCrafting Interface&r to a machine &o(like a Crafting table or Furnace)&r, this interface will be bound to this machine to handle recipes.", "info_book.integratedcrafting.crafting_interface.basics.text3": "When opening the GUI of a &lCrafting Interface&r, you can insert &lVariable Cards&r that hold &8Recipes&0. This will tell the interface that the given recipe can be crafted using the bound machine.", "info_book.integratedcrafting.crafting_interface.basics.text4": "When you've progressed later in the game, you will be able to craft the &lAttuned Crafting Interface&r. It does not require you to manually add recipes to it, as it will read and expose all recipes that are available in the target machine. These are the same recipes that are listed in the &lMachine Reader&r. Some modded machines may not be supported, which will be visualized by a red border when placing the &lAttuned Crafting Interface&r.", - "info_book.integratedcrafting.crafting_interface.basics.text5": "The &lList-Based Crafting Interface&r takes a single variable holding a &llist of recipes&r, and exposes every recipe in that list to the network. This allows you to derive the recipes of an interface using logic, for example by pointing the &lMachine Reader&r at a machine, or by filtering a list of recipes. Infinite lists are not supported, and the number of recipes that is read from a list can be limited in the config.", + "info_book.integratedcrafting.crafting_interface.basics.text5": "The &lList Crafting Interface&r takes a single variable holding a &llist of recipes&r, and exposes every recipe in that list to the network. This allows you to derive the recipes of an interface using logic, for example by pointing the &lMachine Reader&r at a machine, or by filtering a list of recipes. Infinite lists are not supported, and the number of recipes that is read from a list can be limited in the config.", "info_book.integratedcrafting.crafting_interface.crafting": "Crafting Process", "info_book.integratedcrafting.crafting_interface.crafting.text1": "This section will explain how the crafting process is being handled by &lCrafting Interfaces&r. This information is essential to get the most out of this mod, and to debug any issues you may encounter.", diff --git a/src/main/resources/assets/integratedcrafting/textures/gui/part_interface_crafting_list.png b/src/main/resources/assets/integratedcrafting/textures/gui/part_interface_crafting_list.png index 7efab63e0bcd7603dd7124508839e5162f31ce34..4d7c7d452543070f847c5859d01bdb86e5494633 100644 GIT binary patch delta 334 zcmbQjJe_$$P`!bti(^Q|oVT|ey^a`&G(4O>Ywe_u3`%hag)8z?`xdx=U^sM!amuch zeWyi!c}#pCo%-P7zX`r4wj5CuI`jPA$tX5RU74 zyk_MPP;h8qV6^5~F}L{g-%UFzrG-CK7gT57X5+cf#K^<~lCO7oU%9~MzYZH~&C{5) zw+Y-}QJ^lSHuft&iW#q-yRCCACgp0!Y1S(rGiM*2)xG9nG}u_6evR$zWnVY`Suy{% zj+gfJz4ryLcFd5UF7M}}47HkRPRE=Mn2EbB1eew>xsv=tjN`-kmmhS&u5$;v!$Oq* ze$#)c3%k@`zg$>vaoA-4a+~j4%Qv`WN-{3*X=h?U1`{4hXTNFc==i?%5=hw7)z4*} HQ$iB}PkxO4 delta 331 zcmbQvJcW5eP`$3Fi(^Q|oVT|Qda@XbG(4O>WA&u141&=cn0|+CW6?dx)N+?;%2y+f z>eB~~Y=7~6t`LW@{SzUh;3RS3Iem+@qdoan1mBb2+P^NxFqQw*-MWI*UZaP9wXfW{ z&Mlze(7?dRq^GdJ)n#wInt8q@qka3P>HmH+zKn)Qv2fI92nNoZ?s+WLv%6!xrFdxa zrR`u%EF1y~3bqg4Oj;$sU2@INH&(ls+_KL#jwrr4bssAeNY!&8%5ZkV8-%h8w7AHtb` zeK=KCArWumY`fRL^4;qfhlG}%=dd_ap}~Lxf@_%HUuo*Ne~m*PB<|_z=d#Wzp$P!x C4uM1f From c3108ae2baea99091b7a570e6f1ab7bb90786a2b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 05:22:52 +0000 Subject: [PATCH 3/4] Use the part update interval instead of a reload throttle, and cut reload cost The list interface had its own config to throttle how often invalidations were acted on. The part's update interval already governs this, so that config is gone. The list interface now has its own minimum update interval config instead, defaulting to 20 ticks, which is what the settings gui exposes. Reload cost is cut so the interval is a preference rather than a necessity, measured against a machine reader on a crafting table (987 recipes): * Validation results are cached per part until it fully reloads, when the target may have changed. The cache is keyed by identity, as recipe handlers hand out the same instances on every read while hashing a recipe by value turned out to be expensive. * A slot whose recipes did not change no longer syncs to the client, posts a contents-updated event, or touches the network recipe index. * The network index diff uses sets rather than repeated list scans. At the default cap of 256 recipes, a repeated reload goes from ~1.4 ms plus network work to ~0.3 ms with none. Uncapped at 987 recipes, from ~9 ms to ~2.7 ms. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Bn8brBLvFiJsfnBHpwf8ZN --- .../integratedcrafting/GeneralConfig.java | 4 +- ...PartTypeInterfaceCraftingVariableBase.java | 77 +++++++++++-------- .../part/PartTypeInterfaceCraftingList.java | 23 +----- 3 files changed, 49 insertions(+), 55 deletions(-) diff --git a/src/main/java/org/cyclops/integratedcrafting/GeneralConfig.java b/src/main/java/org/cyclops/integratedcrafting/GeneralConfig.java index 9b02285f2..007c29caf 100644 --- a/src/main/java/org/cyclops/integratedcrafting/GeneralConfig.java +++ b/src/main/java/org/cyclops/integratedcrafting/GeneralConfig.java @@ -40,8 +40,8 @@ public class GeneralConfig extends DummyConfig { @ConfigurableProperty(category = "machine", comment = "The maximum number of recipes that will be read from a list inside a list-based crafting interface. Set to 0 for no limit.", minimalValue = 0, isCommandable = true, configLocation = ModConfig.Type.SERVER) public static int maxCraftingInterfaceListRecipes = 256; - @ConfigurableProperty(category = "machine", comment = "The minimum number of ticks between two reloads of a list inside a list-based crafting interface. Set to 0 to reload on every variable invalidation.", minimalValue = 0, isCommandable = true, configLocation = ModConfig.Type.SERVER) - public static int craftingInterfaceListMinReloadInterval = 20; + @ConfigurableProperty(category = "machine", comment = "The minimal update frequency in ticks to use for list-based crafting interfaces. Reading a list of recipes is more expensive than reading a single recipe, so this defaults higher than the regular crafting interface.", minimalValue = 1, configLocation = ModConfig.Type.SERVER) + public static int minCraftingInterfaceListUpdateFreq = 20; @ConfigurableProperty(category = "machine", comment = "The maximum number of recipes that a crafting interface remembers crafting durations for, which are used to estimate the duration of crafting jobs. Set to 0 to disable recipe-specific estimations.", minimalValue = 0, isCommandable = true, configLocation = ModConfig.Type.SERVER) public static int craftingInterfaceRecipeDurationEntries = 32; diff --git a/src/main/java/org/cyclops/integratedcrafting/core/part/PartTypeInterfaceCraftingVariableBase.java b/src/main/java/org/cyclops/integratedcrafting/core/part/PartTypeInterfaceCraftingVariableBase.java index 4ab2bc121..974981647 100644 --- a/src/main/java/org/cyclops/integratedcrafting/core/part/PartTypeInterfaceCraftingVariableBase.java +++ b/src/main/java/org/cyclops/integratedcrafting/core/part/PartTypeInterfaceCraftingVariableBase.java @@ -2,6 +2,8 @@ import com.google.common.collect.Lists; import com.google.common.collect.MapMaker; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; import it.unimi.dsi.fastutil.ints.Int2BooleanArrayMap; import it.unimi.dsi.fastutil.ints.Int2BooleanMap; import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap; @@ -60,7 +62,9 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; +import java.util.Set; /** * Base part for crafting interfaces that derive their recipes from variables in an inventory. @@ -159,12 +163,6 @@ public void update(INetwork network, IPartNetwork partNetwork, PartTarget target slots.clear(); int channel = state.getChannelCrafting(); for (Integer slot : slotsCopy) { - // Slots that may not be reloaded yet are retried in a later tick - if (!state.mayReloadSlot(slot)) { - slots.add(slot); - continue; - } - Int2ObjectMap> recipes = state.getRecipesIndexed(); List oldRecipes = recipes.get(slot); oldRecipes = oldRecipes == null ? Collections.emptyList() : Lists.newArrayList(oldRecipes); @@ -173,20 +171,25 @@ public void update(INetwork network, IPartNetwork partNetwork, PartTarget target // We simulate initialization for the first two ticks, as dependency variables may still be loading, // and errored may only go away after these dependencies are fully loaded. // Related to CyclopsMC/IntegratedCrafting#110 - state.reloadRecipe(slot, state.ticksAfterReload <= 1); + if (!state.reloadRecipe(slot, state.ticksAfterReload <= 1)) { + // The recipes of this slot are unchanged, so the network index is still correct + continue; + } List newRecipes = recipes.get(slot); newRecipes = newRecipes == null ? Collections.emptyList() : newRecipes; // Only patch what actually changed, as slots can hold many recipes // that are re-evaluated whenever their variable is invalidated. + Set oldRecipesLookup = Sets.newHashSet(oldRecipes); + Set newRecipesLookup = Sets.newHashSet(newRecipes); for (IRecipeDefinition oldRecipe : oldRecipes) { - if (!newRecipes.contains(oldRecipe)) { + if (!newRecipesLookup.contains(oldRecipe)) { craftingNetwork.removeCraftingInterfaceRecipe(channel, state, oldRecipe); } } for (IRecipeDefinition newRecipe : newRecipes) { - if (!oldRecipes.contains(newRecipe)) { + if (!oldRecipesLookup.contains(newRecipe)) { craftingNetwork.addCraftingInterfaceRecipe(channel, state, newRecipe); } } @@ -227,6 +230,10 @@ public static abstract class State

> currentRecipes; private List currentRecipesFlattened; + // Validation results, reused until the whole part reloads, which is when the target may have changed. + // Keyed by identity: recipe handlers hand out the same instances on every read, + // while hashing a recipe by value is expensive. + private final Map validationCache; public State(int inventorySize) { this.inventoryVariables = new SimpleInventory(inventorySize, 1); @@ -238,6 +245,7 @@ public State(int inventorySize) { this.variableListeners = new MapMaker().weakKeys().makeMap(); this.currentRecipes = new Int2ObjectArrayMap<>(); this.currentRecipesFlattened = Collections.emptyList(); + this.validationCache = Maps.newIdentityHashMap(); } /** @@ -254,20 +262,6 @@ public State(int inventorySize) { */ protected abstract List extractRecipes(int slot, IValue value) throws EvaluationException; - /** - * @return If the given slot may be reloaded in this tick. - */ - protected boolean mayReloadSlot(int slot) { - return true; - } - - /** - * @return The internal tick counter, which is monotonically increasing. - */ - public int getTicks() { - return this.ticksAfterReload; - } - /** * @return The message to show for a slot for which all recipes were accepted by the target. */ @@ -327,6 +321,7 @@ public void readFromNBT(ValueDeseralizationContext valueDeseralizationContext, C @Override public void reloadRecipes(boolean initialize) { + this.validationCache.clear(); this.currentRecipes.clear(); this.invalidateRecipesFlattened(); this.recipeSlotMessages.clear(); @@ -360,7 +355,11 @@ private void setLocalErrors(int slot, List errors) { } } - protected void reloadRecipe(int slot, boolean initialize) { + protected boolean reloadRecipe(int slot, boolean initialize) { + List previousRecipes = this.currentRecipes.get(slot); + MutableComponent previousMessage = this.recipeSlotMessages.get(slot); + boolean changed = true; + this.currentRecipes.remove(slot); this.invalidateRecipesFlattened(); if (this.recipeSlotMessages.size() > slot) { @@ -404,17 +403,27 @@ protected void reloadRecipe(int slot, boolean initialize) { } } - try { - IPartNetwork partNetwork = NetworkHelpers.getPartNetworkChecked(network); - NeoForge.EVENT_BUS.post(new PartVariableDrivenVariableContentsUpdatedEvent<>(network, - partNetwork, getTarget(), - getPartTypeInstance(), (S) this, lastPlayer, variable, - variable != null ? variable.getValue() : null)); - } catch (EvaluationException e) { - // Ignore error + changed = !Objects.equals(previousRecipes, this.currentRecipes.get(slot)) + || !Objects.equals(previousMessage, this.recipeSlotMessages.get(slot)); + + if (changed) { + try { + IPartNetwork partNetwork = NetworkHelpers.getPartNetworkChecked(network); + NeoForge.EVENT_BUS.post(new PartVariableDrivenVariableContentsUpdatedEvent<>(network, + partNetwork, getTarget(), + getPartTypeInstance(), (S) this, lastPlayer, variable, + variable != null ? variable.getValue() : null)); + } catch (EvaluationException e) { + // Ignore error + } } } - sendUpdate(); + + // A slot whose recipes did not change needs no client sync and no network re-indexing + if (changed) { + sendUpdate(); + } + return changed; } /** @@ -432,7 +441,7 @@ private void setSlotRecipes(int slot, List recipes) { } else { validRecipes = Lists.newArrayListWithExpectedSize(recipes.size()); for (IRecipeDefinition recipe : recipes) { - if (isValid(recipe)) { + if (this.validationCache.computeIfAbsent(recipe, this::isValid)) { validRecipes.add(recipe); } } diff --git a/src/main/java/org/cyclops/integratedcrafting/part/PartTypeInterfaceCraftingList.java b/src/main/java/org/cyclops/integratedcrafting/part/PartTypeInterfaceCraftingList.java index b6d850420..6527e5e58 100644 --- a/src/main/java/org/cyclops/integratedcrafting/part/PartTypeInterfaceCraftingList.java +++ b/src/main/java/org/cyclops/integratedcrafting/part/PartTypeInterfaceCraftingList.java @@ -1,8 +1,6 @@ package org.cyclops.integratedcrafting.part; import com.google.common.collect.Lists; -import it.unimi.dsi.fastutil.ints.Int2IntArrayMap; -import it.unimi.dsi.fastutil.ints.Int2IntMap; import it.unimi.dsi.fastutil.ints.IntArraySet; import it.unimi.dsi.fastutil.ints.IntSet; import net.minecraft.network.chat.Component; @@ -54,9 +52,6 @@ public static class State extends PartTypeInterfaceCraftingVariableBase.State= interval; - } - - @Override - protected void reloadRecipe(int slot, boolean initialize) { - this.lastSlotReloadTicks.put(slot, getTicks()); - super.reloadRecipe(slot, initialize); + protected int getDefaultUpdateInterval() { + // Reading a whole list of recipes is more expensive than reading a single recipe, + // and reader-backed list variables are invalidated on every reader tick. + return GeneralConfig.minCraftingInterfaceListUpdateFreq; } @Override From bb4f62f36178fa057278efc73bd26cfcc0fab016 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 06:05:34 +0000 Subject: [PATCH 4/4] Raise the list recipe limit to a runaway guard The attuned crafting interface already exposes every recipe of its target with no limit at all, and indexing those 987 recipes of a crafting table costs it the same as it costs a list interface (~95-100 ms for a network remove+add in both cases). Limiting lists to 256 was therefore inconsistent: it withheld something the mod already allows elsewhere. The limit now defaults to 4096, which no regular machine reaches. It stays as a guard against a computed list that runs away, since a list is not bounded by a real machine the way an attuned interface is. Set it to 0 for no limit. The machine reader game test asserted against the limit, which made it pass for the wrong reason once the limit exceeded the machine's recipe count. It now asserts that the interface exposes exactly the recipes its target holds. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Bn8brBLvFiJsfnBHpwf8ZN --- .../cyclops/integratedcrafting/GeneralConfig.java | 4 ++-- .../gametest/GameTestsItemsCraftList.java | 15 ++++++++++----- .../assets/integratedcrafting/lang/en_us.json | 2 +- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/cyclops/integratedcrafting/GeneralConfig.java b/src/main/java/org/cyclops/integratedcrafting/GeneralConfig.java index 007c29caf..e5d5cee14 100644 --- a/src/main/java/org/cyclops/integratedcrafting/GeneralConfig.java +++ b/src/main/java/org/cyclops/integratedcrafting/GeneralConfig.java @@ -37,8 +37,8 @@ public class GeneralConfig extends DummyConfig { @ConfigurableProperty(category = "general", comment = "The base energy usage for the list-based crafting interface per crafting job being processed.", minimalValue = 0, configLocation = ModConfig.Type.SERVER) public static int interfaceCraftingListBaseConsumption = 10; - @ConfigurableProperty(category = "machine", comment = "The maximum number of recipes that will be read from a list inside a list-based crafting interface. Set to 0 for no limit.", minimalValue = 0, isCommandable = true, configLocation = ModConfig.Type.SERVER) - public static int maxCraftingInterfaceListRecipes = 256; + @ConfigurableProperty(category = "machine", comment = "The maximum number of recipes that will be read from a list inside a list-based crafting interface. This is a guard against runaway lists, not a tuning knob: it is set well above the recipe count of any regular machine. Set to 0 for no limit.", minimalValue = 0, isCommandable = true, configLocation = ModConfig.Type.SERVER) + public static int maxCraftingInterfaceListRecipes = 4096; @ConfigurableProperty(category = "machine", comment = "The minimal update frequency in ticks to use for list-based crafting interfaces. Reading a list of recipes is more expensive than reading a single recipe, so this defaults higher than the regular crafting interface.", minimalValue = 1, configLocation = ModConfig.Type.SERVER) public static int minCraftingInterfaceListUpdateFreq = 20; diff --git a/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestsItemsCraftList.java b/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestsItemsCraftList.java index 436a5b051..98d9d731a 100644 --- a/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestsItemsCraftList.java +++ b/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestsItemsCraftList.java @@ -15,14 +15,15 @@ import net.neoforged.neoforge.gametest.GameTestHolder; import net.neoforged.neoforge.gametest.PrefixGameTestTemplate; import org.apache.commons.lang3.tuple.Pair; -import org.cyclops.integratedcrafting.GeneralConfig; import org.cyclops.integratedcrafting.Reference; import org.cyclops.integratedcrafting.part.PartTypeInterfaceCraftingList; import org.cyclops.integratedcrafting.part.PartTypes; +import org.cyclops.cyclopscore.datastructure.DimPos; import org.cyclops.integrateddynamics.RegistryEntries; import org.cyclops.integrateddynamics.api.part.PartPos; import org.cyclops.integrateddynamics.core.evaluate.variable.ValueTypeInteger; import org.cyclops.integrateddynamics.core.evaluate.variable.ValueTypeList; +import org.cyclops.integrateddynamics.core.evaluate.variable.ValueTypeListProxyPositionedRecipes; import org.cyclops.integrateddynamics.core.evaluate.variable.ValueTypes; import org.cyclops.integrateddynamics.core.helper.PartHelpers; import org.cyclops.integrateddynamics.part.aspect.Aspects; @@ -164,8 +165,8 @@ public void testItemsCraftListWrongElementType(GameTestHelper helper) { } /** - * A lazy list from a machine reader is read into the interface, - * capped at the configured maximum, and re-read whenever the reader's variable is invalidated. + * A lazy list from a machine reader is read into the interface in full, + * and re-read whenever the reader's variable is invalidated. */ @GameTest(template = TEMPLATE_EMPTY, timeoutTicks = TIMEOUT) public void testItemsCraftListMachineReader(GameTestHelper helper) { @@ -193,8 +194,12 @@ public void testItemsCraftListMachineReader(GameTestHelper helper) { if (!targetRemoved[0]) { helper.assertTrue(state.isRecipeSlotValid(0), "Recipe list from the machine reader is not valid: " + state.getRecipeSlotUnlocalizedMessage(0)); - helper.assertValueEqual(state.getRecipes().size(), GeneralConfig.maxCraftingInterfaceListRecipes, - "Recipe count from the machine reader is not capped"); + // The interface exposes exactly the recipes that its reader's target holds + int targetRecipes = new ValueTypeListProxyPositionedRecipes( + DimPos.of(helper.getLevel(), helper.absolutePos(POS.south().west().west())), Direction.UP).getLength(); + helper.assertTrue(targetRecipes > 0, "The reader's target exposes no recipes at all"); + helper.assertValueEqual(state.getRecipes().size(), targetRecipes, + "Recipe count from the machine reader is incorrect"); // Remove the reader's target, so that only the list variable changes helper.setBlock(POS.south().west().west(), Blocks.AIR); diff --git a/src/main/resources/assets/integratedcrafting/lang/en_us.json b/src/main/resources/assets/integratedcrafting/lang/en_us.json index 296298de8..d693a6754 100644 --- a/src/main/resources/assets/integratedcrafting/lang/en_us.json +++ b/src/main/resources/assets/integratedcrafting/lang/en_us.json @@ -92,7 +92,7 @@ "info_book.integratedcrafting.crafting_interface.basics.text2": "By pointing a &lCrafting Interface&r to a machine &o(like a Crafting table or Furnace)&r, this interface will be bound to this machine to handle recipes.", "info_book.integratedcrafting.crafting_interface.basics.text3": "When opening the GUI of a &lCrafting Interface&r, you can insert &lVariable Cards&r that hold &8Recipes&0. This will tell the interface that the given recipe can be crafted using the bound machine.", "info_book.integratedcrafting.crafting_interface.basics.text4": "When you've progressed later in the game, you will be able to craft the &lAttuned Crafting Interface&r. It does not require you to manually add recipes to it, as it will read and expose all recipes that are available in the target machine. These are the same recipes that are listed in the &lMachine Reader&r. Some modded machines may not be supported, which will be visualized by a red border when placing the &lAttuned Crafting Interface&r.", - "info_book.integratedcrafting.crafting_interface.basics.text5": "The &lList Crafting Interface&r takes a single variable holding a &llist of recipes&r, and exposes every recipe in that list to the network. This allows you to derive the recipes of an interface using logic, for example by pointing the &lMachine Reader&r at a machine, or by filtering a list of recipes. Infinite lists are not supported, and the number of recipes that is read from a list can be limited in the config.", + "info_book.integratedcrafting.crafting_interface.basics.text5": "The &lList Crafting Interface&r takes a single variable holding a &llist of recipes&r, and exposes every recipe in that list to the network. This allows you to derive the recipes of an interface using logic, for example by pointing the &lMachine Reader&r at a machine, or by filtering a list of recipes. Infinite lists are not supported, and very long lists are cut off at a limit that can be changed in the config.", "info_book.integratedcrafting.crafting_interface.crafting": "Crafting Process", "info_book.integratedcrafting.crafting_interface.crafting.text1": "This section will explain how the crafting process is being handled by &lCrafting Interfaces&r. This information is essential to get the most out of this mod, and to debug any issues you may encounter.",