diff --git a/src/main/java/org/cyclops/integratedcrafting/GeneralConfig.java b/src/main/java/org/cyclops/integratedcrafting/GeneralConfig.java index dee3a0713..e5d5cee14 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. 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; @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..6906805de 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 @@ -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/core/part/PartTypeInterfaceCraftingVariableBase.java b/src/main/java/org/cyclops/integratedcrafting/core/part/PartTypeInterfaceCraftingVariableBase.java new file mode 100644 index 000000000..974981647 --- /dev/null +++ b/src/main/java/org/cyclops/integratedcrafting/core/part/PartTypeInterfaceCraftingVariableBase.java @@ -0,0 +1,570 @@ +package org.cyclops.integratedcrafting.core.part; + +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; +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.Objects; +import java.util.Optional; +import java.util.Set; + +/** + * 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) { + 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 + 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 (!newRecipesLookup.contains(oldRecipe)) { + craftingNetwork.removeCraftingInterfaceRecipe(channel, state, oldRecipe); + } + } + for (IRecipeDefinition newRecipe : newRecipes) { + if (!oldRecipesLookup.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; + // 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); + 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(); + this.validationCache = Maps.newIdentityHashMap(); + } + + /** + * @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 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.validationCache.clear(); + 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 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) { + 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")); + } + } + + 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 + } + } + } + + // A slot whose recipes did not change needs no client sync and no network re-indexing + if (changed) { + sendUpdate(); + } + return changed; + } + + /** + * 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 (this.validationCache.computeIfAbsent(recipe, this::isValid)) { + 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..98d9d731a --- /dev/null +++ b/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestsItemsCraftList.java @@ -0,0 +1,212 @@ +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.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; + +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 in full, + * 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)); + // 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); + 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..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,16 +8,16 @@ 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; -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,21 +31,24 @@ * Container for the crafting interface. * @author rubensworks */ -public class ContainerPartInterfaceCrafting extends ContainerMultipart { +public class ContainerPartInterfaceCrafting

, S extends PartTypeInterfaceCraftingVariableBase.State> + extends ContainerMultipart { + + public static final int GUI_WIDTH = 176; 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()); + 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)); } @@ -93,7 +103,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..6527e5e58 --- /dev/null +++ b/src/main/java/org/cyclops/integratedcrafting/part/PartTypeInterfaceCraftingList.java @@ -0,0 +1,123 @@ +package org.cyclops.integratedcrafting.part; + +import com.google.common.collect.Lists; +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(); + public State() { + super(INVENTORY_SIZE); + } + + @Override + protected PartTypeInterfaceCraftingList getPartTypeInstance() { + return PartTypes.INTERFACE_CRAFTING_LIST; + } + + @Override + 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 + 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..d693a6754 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 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 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 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.", 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 000000000..4d7c7d452 Binary files /dev/null and b/src/main/resources/assets/integratedcrafting/textures/gui/part_interface_crafting_list.png differ 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 000000000..96285c6ff Binary files /dev/null and b/src/main/resources/assets/integratedcrafting/textures/part/interface_crafting_list.png differ 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