From 5ef798d7fce7d42db7a26e8b9c4a821674774cb4 Mon Sep 17 00:00:00 2001 From: Ruben Taelman Date: Fri, 4 Sep 2026 19:16:04 +0000 Subject: [PATCH 1/2] Show the crafting machine in crafting option tooltips Hovering a craftable in the storage terminal now says which machine it is crafted in, both by name and by icon, above the crafting requirements. The machines are resolved server-side once per channel, from the crafting interfaces that expose each recipe, and travel to the client alongside the recipe. Interfaces that target the same machine type are only shown once, and the label names the machine only when there is exactly one of them. To let the machines and the requirements each get their own labelled icon grid, tooltips are now built as a list of elements that mixes text and visual components, rather than a list of lines with a single trailing component. Closes #181 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01RM86uiqD4soLkMyjC9Eeid --- ...edientComponentTerminalStorageHandler.java | 7 +- .../crafting/ITerminalCraftingOption.java | 10 + ...ComponentTerminalStorageHandlerEnergy.java | 6 +- ...onentTerminalStorageHandlerFluidStack.java | 6 +- ...ponentTerminalStorageHandlerItemStack.java | 6 +- ...lientCraftingOptionIngredientsTooltip.java | 76 +------- .../ClientCraftingOptionMachinesTooltip.java | 30 +++ .../ClientCraftingOptionSlotsTooltip.java | 100 ++++++++++ .../CraftingOptionMachinesTooltip.java | 15 ++ .../gui/tooltip/TooltipRenderHelpers.java | 13 +- ...alStorageSlotIngredientCraftingOption.java | 51 ++++- ...ameTestTerminalCraftingOptionMachines.java | 179 ++++++++++++++++++ ...erminalCraftingOptionRecipeDefinition.java | 16 ++ ...redientCraftingHandlerCraftingNetwork.java | 48 ++++- .../proxy/ClientProxy.java | 3 + .../integratedterminals/lang/en_us.json | 2 + 16 files changed, 473 insertions(+), 95 deletions(-) create mode 100644 src/main/java/org/cyclops/integratedterminals/client/gui/tooltip/ClientCraftingOptionMachinesTooltip.java create mode 100644 src/main/java/org/cyclops/integratedterminals/client/gui/tooltip/ClientCraftingOptionSlotsTooltip.java create mode 100644 src/main/java/org/cyclops/integratedterminals/client/gui/tooltip/CraftingOptionMachinesTooltip.java create mode 100644 src/main/java/org/cyclops/integratedterminals/gametest/GameTestTerminalCraftingOptionMachines.java diff --git a/src/main/java/org/cyclops/integratedterminals/api/ingredient/IIngredientComponentTerminalStorageHandler.java b/src/main/java/org/cyclops/integratedterminals/api/ingredient/IIngredientComponentTerminalStorageHandler.java index b984b6913..31a585c44 100644 --- a/src/main/java/org/cyclops/integratedterminals/api/ingredient/IIngredientComponentTerminalStorageHandler.java +++ b/src/main/java/org/cyclops/integratedterminals/api/ingredient/IIngredientComponentTerminalStorageHandler.java @@ -5,6 +5,8 @@ import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.inventory.AbstractContainerMenu; +import com.mojang.datafixers.util.Either; +import net.minecraft.network.chat.FormattedText; import net.minecraft.world.inventory.tooltip.TooltipComponent; import net.minecraft.world.item.ItemStack; import net.minecraft.network.chat.Component; @@ -76,12 +78,13 @@ public default void drawInstance(GuiGraphics guiGraphics, T instance, long maxQu * @param mouseX The mouse X position. * @param mouseY The mouse Y position. * @param additionalTooltipLines The additional tooltip lines to add. - * @param additionalTooltipComponent An optional visual tooltip component to render below the tooltip lines. + * @param additionalTooltipElements Optional elements to render below the tooltip lines, + * which can mix text and visual components. */ @OnlyIn(Dist.CLIENT) public void drawInstance(GuiGraphics guiGraphics, T instance, long maxQuantity, @Nullable String label, AbstractContainerScreen gui, ContainerScreenTerminalStorage.DrawLayer layer, float partialTick, int x, int y, int mouseX, int mouseY, @Nullable List additionalTooltipLines, - @Nullable TooltipComponent additionalTooltipComponent); + @Nullable List> additionalTooltipElements); /** * Show the quantity of the given instance on the second tooltip line. diff --git a/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/ITerminalCraftingOption.java b/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/ITerminalCraftingOption.java index 6d581b64f..64b626d2e 100644 --- a/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/ITerminalCraftingOption.java +++ b/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/ITerminalCraftingOption.java @@ -1,5 +1,6 @@ package org.cyclops.integratedterminals.api.terminalstorage.crafting; +import net.minecraft.world.item.ItemStack; import org.cyclops.commoncapabilities.api.ingredient.IIngredientMatcher; import org.cyclops.commoncapabilities.api.ingredient.IPrototypedIngredient; import org.cyclops.commoncapabilities.api.ingredient.IngredientComponent; @@ -59,6 +60,15 @@ public interface ITerminalCraftingOption extends Comparable Collection getInputs(IngredientComponent ingredientComponent); + /** + * Item representations of the machines that this crafting job option is crafted in. + * + * @return The crafting machines, can be empty if they are unknown. + */ + public default List getCraftingMachines() { + return Collections.emptyList(); + } + /** * The inputs of this crafting job option for the given ingredient component, * where each input can be fulfilled by any of its alternatives. diff --git a/src/main/java/org/cyclops/integratedterminals/capability/ingredient/IngredientComponentTerminalStorageHandlerEnergy.java b/src/main/java/org/cyclops/integratedterminals/capability/ingredient/IngredientComponentTerminalStorageHandlerEnergy.java index 5cf5c96b0..4fe020c06 100644 --- a/src/main/java/org/cyclops/integratedterminals/capability/ingredient/IngredientComponentTerminalStorageHandlerEnergy.java +++ b/src/main/java/org/cyclops/integratedterminals/capability/ingredient/IngredientComponentTerminalStorageHandlerEnergy.java @@ -10,6 +10,8 @@ import net.minecraft.world.entity.player.Player; import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.world.inventory.Slot; +import com.mojang.datafixers.util.Either; +import net.minecraft.network.chat.FormattedText; import net.minecraft.world.inventory.tooltip.TooltipComponent; import net.minecraft.world.item.ItemStack; import net.neoforged.api.distmarker.Dist; @@ -67,7 +69,7 @@ public ItemStack getIcon() { public void drawInstance(GuiGraphics guiGraphics, Long instance, long maxQuantity, @Nullable String label, AbstractContainerScreen gui, ContainerScreenTerminalStorage.DrawLayer layer, float partialTick, int x, int y, int mouseX, int mouseY, @Nullable List additionalTooltipLines, - @Nullable TooltipComponent additionalTooltipComponent) { + @Nullable List> additionalTooltipElements) { if (instance > 0) { if (layer == ContainerScreenTerminalStorage.DrawLayer.BACKGROUND){ @@ -103,7 +105,7 @@ public void drawInstance(GuiGraphics guiGraphics, Long instance, long maxQuantit lines.addAll(additionalTooltipLines); } return lines; - }, additionalTooltipComponent); + }, additionalTooltipElements); } } } diff --git a/src/main/java/org/cyclops/integratedterminals/capability/ingredient/IngredientComponentTerminalStorageHandlerFluidStack.java b/src/main/java/org/cyclops/integratedterminals/capability/ingredient/IngredientComponentTerminalStorageHandlerFluidStack.java index 8844232f8..8e3cc0212 100644 --- a/src/main/java/org/cyclops/integratedterminals/capability/ingredient/IngredientComponentTerminalStorageHandlerFluidStack.java +++ b/src/main/java/org/cyclops/integratedterminals/capability/ingredient/IngredientComponentTerminalStorageHandlerFluidStack.java @@ -12,6 +12,8 @@ import net.minecraft.world.entity.player.Player; import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.world.inventory.Slot; +import com.mojang.datafixers.util.Either; +import net.minecraft.network.chat.FormattedText; import net.minecraft.world.inventory.tooltip.TooltipComponent; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; @@ -74,7 +76,7 @@ public void drawInstance(GuiGraphics guiGraphics, FluidStack instance, long maxQ ContainerScreenTerminalStorage.DrawLayer layer, float partialTick, int x, int y, int mouseX, int mouseY, @Nullable List additionalTooltipLines, - @Nullable TooltipComponent additionalTooltipComponent) { + @Nullable List> additionalTooltipElements) { if (instance != null) { if (layer == ContainerScreenTerminalStorage.DrawLayer.BACKGROUND) { // Draw fluid @@ -92,7 +94,7 @@ public void drawInstance(GuiGraphics guiGraphics, FluidStack instance, long maxQ lines.addAll(additionalTooltipLines); } return lines; - }, additionalTooltipComponent); + }, additionalTooltipElements); } } } diff --git a/src/main/java/org/cyclops/integratedterminals/capability/ingredient/IngredientComponentTerminalStorageHandlerItemStack.java b/src/main/java/org/cyclops/integratedterminals/capability/ingredient/IngredientComponentTerminalStorageHandlerItemStack.java index 2d3c4d228..083db7e52 100644 --- a/src/main/java/org/cyclops/integratedterminals/capability/ingredient/IngredientComponentTerminalStorageHandlerItemStack.java +++ b/src/main/java/org/cyclops/integratedterminals/capability/ingredient/IngredientComponentTerminalStorageHandlerItemStack.java @@ -12,6 +12,8 @@ import net.minecraft.world.entity.player.Player; import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.world.inventory.Slot; +import com.mojang.datafixers.util.Either; +import net.minecraft.network.chat.FormattedText; import net.minecraft.world.inventory.tooltip.TooltipComponent; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; @@ -70,7 +72,7 @@ public ItemStack getIcon() { public void drawInstance(GuiGraphics guiGraphics, ItemStack instance, long maxQuantity, @Nullable String label, AbstractContainerScreen gui, ContainerScreenTerminalStorage.DrawLayer layer, float partialTick, int x, int y, int mouseX, int mouseY, @Nullable List additionalTooltipLines, - @Nullable TooltipComponent additionalTooltipComponent) { + @Nullable List> additionalTooltipElements) { // Make a copy of the item to make sure that any changes in the NBT tag that the mod may make during rendering // does not propagate into our client-side index. Otherwise, the client may think it has different items than // the server, which will cause these items not to be extractable by the client from the terminal. @@ -98,7 +100,7 @@ public void drawInstance(GuiGraphics guiGraphics, ItemStack instance, long maxQu } addQuantityTooltip(lines, instanceCopy); return lines; - }, additionalTooltipComponent); + }, additionalTooltipElements); } Lighting.setupForFlatItems(); } diff --git a/src/main/java/org/cyclops/integratedterminals/client/gui/tooltip/ClientCraftingOptionIngredientsTooltip.java b/src/main/java/org/cyclops/integratedterminals/client/gui/tooltip/ClientCraftingOptionIngredientsTooltip.java index 4563defc7..62e4adb5e 100644 --- a/src/main/java/org/cyclops/integratedterminals/client/gui/tooltip/ClientCraftingOptionIngredientsTooltip.java +++ b/src/main/java/org/cyclops/integratedterminals/client/gui/tooltip/ClientCraftingOptionIngredientsTooltip.java @@ -1,96 +1,34 @@ package org.cyclops.integratedterminals.client.gui.tooltip; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.Font; import net.minecraft.client.gui.GuiGraphics; -import net.minecraft.client.gui.screens.Screen; -import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; -import net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipComponent; -import net.minecraft.resources.ResourceLocation; import net.neoforged.api.distmarker.Dist; import net.neoforged.api.distmarker.OnlyIn; import org.cyclops.commoncapabilities.api.ingredient.IPrototypedIngredient; -import org.cyclops.commoncapabilities.api.ingredient.IngredientComponent; -import org.cyclops.integratedterminals.Capabilities; -import org.cyclops.integratedterminals.client.gui.container.ContainerScreenTerminalStorage; -import javax.annotation.Nullable; import java.util.List; /** * Renders the ingredients of a {@link CraftingOptionIngredientsTooltip} as a grid of icons. * - * The grid wraps after {@link #MAX_COLUMNS} icons, - * and inputs that accept multiple alternatives are cycled through within their own slot. + * Inputs that accept multiple alternatives are cycled through within their own slot. * * @author rubensworks */ @OnlyIn(Dist.CLIENT) -public class ClientCraftingOptionIngredientsTooltip implements ClientTooltipComponent { - - private static final ResourceLocation SLOT_SPRITE = ResourceLocation.withDefaultNamespace("container/bundle/slot"); - private static final int SLOT_WIDTH = 18; - private static final int SLOT_HEIGHT = 20; - private static final int MAX_COLUMNS = 9; - private static final int MARGIN_Y = 2; - - /** - * The number of ticks an alternative is shown before cycling to the next one. - */ - private static final int TICK_DELAY = 30; +public class ClientCraftingOptionIngredientsTooltip extends ClientCraftingOptionSlotsTooltip { private final List>> ingredients; - private final int columns; - private final int rows; public ClientCraftingOptionIngredientsTooltip(CraftingOptionIngredientsTooltip tooltip) { + super(tooltip.ingredients().size()); this.ingredients = tooltip.ingredients(); - // Spread the ingredients evenly over as few rows as possible - this.rows = Math.max(1, (int) Math.ceil((double) this.ingredients.size() / MAX_COLUMNS)); - this.columns = Math.max(1, (int) Math.ceil((double) this.ingredients.size() / this.rows)); - } - - @Override - public int getHeight() { - return this.rows * SLOT_HEIGHT + MARGIN_Y; - } - - @Override - public int getWidth(Font font) { - return this.columns * SLOT_WIDTH; } @Override - public void renderImage(Font font, int x, int y, GuiGraphics guiGraphics) { - int tick = getTick(); - for (int i = 0; i < this.ingredients.size(); i++) { - List> alternatives = this.ingredients.get(i); - int slotX = x + (i % this.columns) * SLOT_WIDTH; - int slotY = y + (i / this.columns) * SLOT_HEIGHT; - guiGraphics.blitSprite(SLOT_SPRITE, slotX, slotY, SLOT_WIDTH, SLOT_HEIGHT); - // Cycle over the alternatives of this input - drawIngredient(guiGraphics, alternatives.get(tick % alternatives.size()), slotX + 1, slotY + 1); - } - } - - protected static void drawIngredient(GuiGraphics guiGraphics, IPrototypedIngredient ingredient, int x, int y) { - IngredientComponent ingredientComponent = ingredient.getComponent(); - long quantity = ingredientComponent.getMatcher().getQuantity(ingredient.getPrototype()); - ingredientComponent.getCapability(Capabilities.IngredientComponentTerminalStorageHandler.INGREDIENT) - .ifPresent(handler -> handler.drawInstance(guiGraphics, ingredient.getPrototype(), quantity, null, - getContainerScreen(), ContainerScreenTerminalStorage.DrawLayer.BACKGROUND, 0, - x, y, 0, 0, null)); - } - - @Nullable - protected static AbstractContainerScreen getContainerScreen() { - Screen screen = Minecraft.getInstance().screen; - return screen instanceof AbstractContainerScreen containerScreen ? containerScreen : null; - } - - protected static int getTick() { - Minecraft minecraft = Minecraft.getInstance(); - return minecraft.level == null ? 0 : (int) (minecraft.level.getGameTime() / TICK_DELAY); + protected void drawSlot(GuiGraphics guiGraphics, int slot, int x, int y) { + // Cycle over the alternatives of this input + List> alternatives = this.ingredients.get(slot); + drawIngredient(guiGraphics, alternatives.get(getTick() % alternatives.size()), x, y); } } diff --git a/src/main/java/org/cyclops/integratedterminals/client/gui/tooltip/ClientCraftingOptionMachinesTooltip.java b/src/main/java/org/cyclops/integratedterminals/client/gui/tooltip/ClientCraftingOptionMachinesTooltip.java new file mode 100644 index 000000000..2f43f696e --- /dev/null +++ b/src/main/java/org/cyclops/integratedterminals/client/gui/tooltip/ClientCraftingOptionMachinesTooltip.java @@ -0,0 +1,30 @@ +package org.cyclops.integratedterminals.client.gui.tooltip; + +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.world.item.ItemStack; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; + +import java.util.List; + +/** + * Renders the machines of a {@link CraftingOptionMachinesTooltip} as a grid of icons. + * + * @author rubensworks + */ +@OnlyIn(Dist.CLIENT) +public class ClientCraftingOptionMachinesTooltip extends ClientCraftingOptionSlotsTooltip { + + private final List machines; + + public ClientCraftingOptionMachinesTooltip(CraftingOptionMachinesTooltip tooltip) { + super(tooltip.machines().size()); + this.machines = tooltip.machines(); + } + + @Override + protected void drawSlot(GuiGraphics guiGraphics, int slot, int x, int y) { + guiGraphics.renderItem(this.machines.get(slot), x, y); + } + +} diff --git a/src/main/java/org/cyclops/integratedterminals/client/gui/tooltip/ClientCraftingOptionSlotsTooltip.java b/src/main/java/org/cyclops/integratedterminals/client/gui/tooltip/ClientCraftingOptionSlotsTooltip.java new file mode 100644 index 000000000..4ac80353c --- /dev/null +++ b/src/main/java/org/cyclops/integratedterminals/client/gui/tooltip/ClientCraftingOptionSlotsTooltip.java @@ -0,0 +1,100 @@ +package org.cyclops.integratedterminals.client.gui.tooltip; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipComponent; +import net.minecraft.resources.ResourceLocation; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +import org.cyclops.commoncapabilities.api.ingredient.IPrototypedIngredient; +import org.cyclops.commoncapabilities.api.ingredient.IngredientComponent; +import org.cyclops.integratedterminals.Capabilities; +import org.cyclops.integratedterminals.client.gui.container.ContainerScreenTerminalStorage; + +import javax.annotation.Nullable; + +/** + * Base class for tooltip components that render a grid of slots. + * + * The grid wraps after {@link #MAX_COLUMNS} slots, + * and is spread evenly over as few rows as possible. + * + * @author rubensworks + */ +@OnlyIn(Dist.CLIENT) +public abstract class ClientCraftingOptionSlotsTooltip implements ClientTooltipComponent { + + protected static final ResourceLocation SLOT_SPRITE = ResourceLocation.withDefaultNamespace("container/bundle/slot"); + protected static final int SLOT_WIDTH = 18; + protected static final int SLOT_HEIGHT = 20; + protected static final int MAX_COLUMNS = 9; + protected static final int MARGIN_Y = 2; + + /** + * The number of ticks an alternative is shown before cycling to the next one. + */ + protected static final int TICK_DELAY = 30; + + private final int slots; + private final int columns; + private final int rows; + + public ClientCraftingOptionSlotsTooltip(int slots) { + this.slots = slots; + this.rows = Math.max(1, (int) Math.ceil((double) slots / MAX_COLUMNS)); + this.columns = Math.max(1, (int) Math.ceil((double) slots / this.rows)); + } + + /** + * Draw the contents of the slot at the given index. + * @param guiGraphics The gui graphics. + * @param slot The slot index. + * @param x The X position to draw at. + * @param y The Y position to draw at. + */ + protected abstract void drawSlot(GuiGraphics guiGraphics, int slot, int x, int y); + + @Override + public int getHeight() { + return this.rows * SLOT_HEIGHT + MARGIN_Y; + } + + @Override + public int getWidth(Font font) { + return this.columns * SLOT_WIDTH; + } + + @Override + public void renderImage(Font font, int x, int y, GuiGraphics guiGraphics) { + for (int i = 0; i < this.slots; i++) { + int slotX = x + (i % this.columns) * SLOT_WIDTH; + int slotY = y + (i / this.columns) * SLOT_HEIGHT; + guiGraphics.blitSprite(SLOT_SPRITE, slotX, slotY, SLOT_WIDTH, SLOT_HEIGHT); + drawSlot(guiGraphics, i, slotX + 1, slotY + 1); + } + } + + protected static void drawIngredient(GuiGraphics guiGraphics, IPrototypedIngredient ingredient, int x, int y) { + IngredientComponent ingredientComponent = ingredient.getComponent(); + long quantity = ingredientComponent.getMatcher().getQuantity(ingredient.getPrototype()); + ingredientComponent.getCapability(Capabilities.IngredientComponentTerminalStorageHandler.INGREDIENT) + .ifPresent(handler -> handler.drawInstance(guiGraphics, ingredient.getPrototype(), quantity, null, + getContainerScreen(), ContainerScreenTerminalStorage.DrawLayer.BACKGROUND, 0, + x, y, 0, 0, null)); + } + + @Nullable + protected static AbstractContainerScreen getContainerScreen() { + Screen screen = Minecraft.getInstance().screen; + return screen instanceof AbstractContainerScreen containerScreen ? containerScreen : null; + } + + protected static int getTick() { + Minecraft minecraft = Minecraft.getInstance(); + return minecraft.level == null ? 0 : (int) (minecraft.level.getGameTime() / TICK_DELAY); + } + +} diff --git a/src/main/java/org/cyclops/integratedterminals/client/gui/tooltip/CraftingOptionMachinesTooltip.java b/src/main/java/org/cyclops/integratedterminals/client/gui/tooltip/CraftingOptionMachinesTooltip.java new file mode 100644 index 000000000..4c2bd8b5c --- /dev/null +++ b/src/main/java/org/cyclops/integratedterminals/client/gui/tooltip/CraftingOptionMachinesTooltip.java @@ -0,0 +1,15 @@ +package org.cyclops.integratedterminals.client.gui.tooltip; + +import net.minecraft.world.inventory.tooltip.TooltipComponent; +import net.minecraft.world.item.ItemStack; + +import java.util.List; + +/** + * A tooltip component holding the machines that a crafting option is crafted in. + * + * @param machines The crafting machines. + * @author rubensworks + */ +public record CraftingOptionMachinesTooltip(List machines) implements TooltipComponent { +} diff --git a/src/main/java/org/cyclops/integratedterminals/client/gui/tooltip/TooltipRenderHelpers.java b/src/main/java/org/cyclops/integratedterminals/client/gui/tooltip/TooltipRenderHelpers.java index 60fdc2207..fd933009e 100644 --- a/src/main/java/org/cyclops/integratedterminals/client/gui/tooltip/TooltipRenderHelpers.java +++ b/src/main/java/org/cyclops/integratedterminals/client/gui/tooltip/TooltipRenderHelpers.java @@ -31,9 +31,9 @@ private TooltipRenderHelpers() {} /** * Render a tooltip if the mouse hovers over the given region. * - * If no visual component is given, this is equivalent to {@link GuiHelpers}'s tooltip rendering. + * If no additional elements are given, this is equivalent to {@link GuiHelpers}'s tooltip rendering. * Otherwise, the tooltip is rendered by vanilla, - * so that the visual component can be rendered below the tooltip lines. + * so that visual components can be rendered below the tooltip lines. * * This must be called while rendering the foreground layer of the given gui, * as the given position is expected to be relative to the gui. @@ -47,19 +47,20 @@ private TooltipRenderHelpers() {} * @param mouseX The mouse X position. * @param mouseY The mouse Y position. * @param linesSupplier A supplier of the tooltip lines. - * @param visualComponent An optional visual tooltip component. + * @param additionalElements Optional elements to append below the tooltip lines, + * which can mix text and visual components. */ public static void renderTooltip(AbstractContainerScreen gui, GuiGraphics guiGraphics, int x, int y, int width, int height, int mouseX, int mouseY, Supplier> linesSupplier, - @Nullable TooltipComponent visualComponent) { - if (visualComponent == null) { + @Nullable List> additionalElements) { + if (additionalElements == null || additionalElements.isEmpty()) { GuiHelpers.renderTooltip(gui, guiGraphics.pose(), x, y, width, height, mouseX, mouseY, linesSupplier); } else if (isHovering(gui, x, y, width, height, mouseX, mouseY)) { List> elements = Lists.newArrayList(); for (Component line : linesSupplier.get()) { elements.add(Either.left(line)); } - elements.add(Either.right(visualComponent)); + elements.addAll(additionalElements); // Just like GuiHelpers#renderTooltip, don't write to the depth buffer, // so that anything that is drawn after this tooltip is not occluded by it. diff --git a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/slot/TerminalStorageSlotIngredientCraftingOption.java b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/slot/TerminalStorageSlotIngredientCraftingOption.java index 544bf8624..f4d92dbcb 100644 --- a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/slot/TerminalStorageSlotIngredientCraftingOption.java +++ b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/slot/TerminalStorageSlotIngredientCraftingOption.java @@ -1,11 +1,15 @@ package org.cyclops.integratedterminals.core.terminalstorage.slot; import com.google.common.collect.Lists; +import com.mojang.datafixers.util.Either; import net.minecraft.ChatFormatting; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.FormattedText; +import net.minecraft.world.inventory.tooltip.TooltipComponent; +import net.minecraft.world.item.ItemStack; import net.neoforged.api.distmarker.Dist; import net.neoforged.api.distmarker.OnlyIn; import org.cyclops.commoncapabilities.api.ingredient.IPrototypedIngredient; @@ -17,6 +21,7 @@ import org.cyclops.integratedterminals.client.gui.container.ContainerScreenTerminalStorage; import org.cyclops.integratedterminals.client.gui.container.component.GuiCraftingPlan; import org.cyclops.integratedterminals.client.gui.tooltip.CraftingOptionIngredientsTooltip; +import org.cyclops.integratedterminals.client.gui.tooltip.CraftingOptionMachinesTooltip; import org.cyclops.integratedterminals.client.gui.tooltip.TooltipRenderHelpers; import org.cyclops.integratedterminals.core.terminalstorage.TerminalStorageTabIngredientComponentClient; import org.cyclops.integratedterminals.core.terminalstorage.crafting.HandlerWrappedTerminalCraftingOption; @@ -55,19 +60,19 @@ public void drawGuiContainerLayer(AbstractContainerScreen gui, GuiGraphics guiGr } else { // This is called for all visible slots on every frame, // so only determine the requirements when they are actually going to be shown. - List>> inputs = TooltipRenderHelpers.isHovering(gui, x, y, - GuiHelpers.SLOT_SIZE_INNER, GuiHelpers.SLOT_SIZE_INNER, mouseX, mouseY) - ? getInputs() : List.of(); + boolean hovering = TooltipRenderHelpers.isHovering(gui, x, y, + GuiHelpers.SLOT_SIZE_INNER, GuiHelpers.SLOT_SIZE_INNER, mouseX, mouseY); + List>> inputs = hovering ? getInputs() : List.of(); + List machines = hovering ? getCraftingOption().getCraftingOption().getCraftingMachines() : List.of(); viewHandler.drawInstance(guiGraphics, getInstance(), maxQuantity, label, gui, layer, partialTick, x, y, mouseX, mouseY, - getTooltipLines(pendingCraftingJobOutput, inputs), - inputs.isEmpty() ? null : new CraftingOptionIngredientsTooltip(inputs)); + getTooltipLines(pendingCraftingJobOutput), + getTooltipElements(machines, inputs)); } drawCraftingJobOverlay(guiGraphics, layer, x, y, pendingCraftingJobOutput); } @OnlyIn(Dist.CLIENT) - protected List getTooltipLines(@Nullable PendingCraftingJobOutput pendingCraftingJobOutput, - List>> inputs) { + protected List getTooltipLines(@Nullable PendingCraftingJobOutput pendingCraftingJobOutput) { List tooltipLines = Lists.newArrayList(); if (pendingCraftingJobOutput != null) { addCraftingJobTooltipLines(tooltipLines, pendingCraftingJobOutput); @@ -78,11 +83,37 @@ protected List getTooltipLines(@Nullable PendingCraftingJobOutput tooltipLines.add(Component.translatable("gui.integratedterminals.terminal_storage.tooltip.duration", GuiCraftingPlan.getDurationValue(estimatedTickDuration))); } + return tooltipLines; + } + + /** + * Show the machines that this crafting option is crafted in, and the inputs that it requires, + * each as a labelled grid of icons. + * + * @param machines The crafting machines. + * @param inputs The required inputs, with their alternatives. + * @return The tooltip elements. + */ + @OnlyIn(Dist.CLIENT) + protected List> getTooltipElements(List machines, + List>> inputs) { + List> tooltipElements = Lists.newArrayList(); + if (!machines.isEmpty()) { + // Only a single machine can be named on the label, otherwise the icons have to speak for themselves + tooltipElements.add(Either.left(machines.size() == 1 + ? Component.translatable("gui.integratedterminals.terminal_storage.tooltip.crafting_machine", + machines.get(0).getHoverName()).withStyle(ChatFormatting.YELLOW) + : Component.translatable("gui.integratedterminals.terminal_storage.tooltip.crafting_machines") + .withStyle(ChatFormatting.YELLOW))); + tooltipElements.add(Either.right(new CraftingOptionMachinesTooltip(machines))); + } if (!inputs.isEmpty()) { - tooltipLines.add(Component.translatable("gui.integratedterminals.terminal_storage.tooltip.requirements") - .withStyle(ChatFormatting.YELLOW)); + tooltipElements.add(Either.left(Component + .translatable("gui.integratedterminals.terminal_storage.tooltip.requirements") + .withStyle(ChatFormatting.YELLOW))); + tooltipElements.add(Either.right(new CraftingOptionIngredientsTooltip(inputs))); } - return tooltipLines; + return tooltipElements; } /** diff --git a/src/main/java/org/cyclops/integratedterminals/gametest/GameTestTerminalCraftingOptionMachines.java b/src/main/java/org/cyclops/integratedterminals/gametest/GameTestTerminalCraftingOptionMachines.java new file mode 100644 index 000000000..be50b14a2 --- /dev/null +++ b/src/main/java/org/cyclops/integratedterminals/gametest/GameTestTerminalCraftingOptionMachines.java @@ -0,0 +1,179 @@ +package org.cyclops.integratedterminals.gametest; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; +import net.minecraft.core.BlockPos; +import net.minecraft.gametest.framework.GameTest; +import net.minecraft.gametest.framework.GameTestHelper; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +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.Block; +import net.minecraft.world.level.block.Blocks; +import net.neoforged.neoforge.gametest.GameTestHolder; +import net.neoforged.neoforge.gametest.PrefixGameTestTemplate; +import org.apache.commons.lang3.tuple.Triple; +import org.cyclops.commoncapabilities.api.capability.recipehandler.IRecipeDefinition; +import org.cyclops.commoncapabilities.api.capability.recipehandler.RecipeDefinition; +import org.cyclops.commoncapabilities.api.ingredient.IPrototypedIngredient; +import org.cyclops.commoncapabilities.api.ingredient.IngredientComponent; +import org.cyclops.commoncapabilities.api.ingredient.MixedIngredients; +import org.cyclops.commoncapabilities.api.ingredient.PrototypedIngredient; +import org.cyclops.integratedcrafting.api.crafting.ICraftingInterface; +import org.cyclops.integratedcrafting.core.CraftingHelpers; +import org.cyclops.integratedcrafting.core.part.PartTypeInterfaceCraftingBase; +import org.cyclops.integratedcrafting.gametest.GameTestHelpersIntegratedCrafting; +import org.cyclops.integrateddynamics.api.network.INetwork; +import org.cyclops.integrateddynamics.api.network.IPositionedAddonsNetworkIngredients; +import org.cyclops.integrateddynamics.core.helper.NetworkHelpers; +import org.cyclops.integratedterminals.Reference; +import org.cyclops.integratedterminals.modcompat.integratedcrafting.TerminalCraftingOptionRecipeDefinition; +import org.cyclops.integratedterminals.modcompat.integratedcrafting.TerminalStorageTabIngredientCraftingHandlerCraftingNetwork; + +import java.util.Collection; +import java.util.List; + +/** + * Game tests for the crafting machines that are exposed by crafting options. + * @author rubensworks + */ +@GameTestHolder(Reference.MOD_ID) +@PrefixGameTestTemplate(false) +public class GameTestTerminalCraftingOptionMachines { + + public static final BlockPos POS = BlockPos.ZERO.offset(2, 0, 2); + public static final int TIMEOUT = 2000; + + private static IRecipeDefinition createRecipe() { + List>> inputs = Lists.newArrayList(); + inputs.add(Lists.newArrayList(new PrototypedIngredient<>(IngredientComponent.ITEMSTACK, + new ItemStack(Items.OAK_PLANKS), + IngredientComponent.ITEMSTACK.getMatcher().getExactMatchNoQuantityCondition()))); + return RecipeDefinition.ofIngredients(IngredientComponent.ITEMSTACK, inputs, + MixedIngredients.ofInstance(IngredientComponent.ITEMSTACK, new ItemStack(Items.STICK, 4))); + } + + /** + * The machines of a crafting option must survive being sent to the client. + */ + @GameTest(template = "empty", templateNamespace = "cyclopscore") + public void testCraftingMachinesSerialization(GameTestHelper helper) { + TerminalStorageTabIngredientCraftingHandlerCraftingNetwork handler = + new TerminalStorageTabIngredientCraftingHandlerCraftingNetwork(); + TerminalCraftingOptionRecipeDefinition craftingOption = + new TerminalCraftingOptionRecipeDefinition<>(IngredientComponent.ITEMSTACK, createRecipe(), -1, + Lists.newArrayList(new ItemStack(Items.FURNACE), new ItemStack(Items.CRAFTING_TABLE))); + + CompoundTag tag = handler.serializeCraftingOption(helper.getLevel().registryAccess(), craftingOption); + TerminalCraftingOptionRecipeDefinition deserialized = handler.deserializeCraftingOption( + helper.getLevel().registryAccess(), IngredientComponent.ITEMSTACK, tag); + + List machines = deserialized.getCraftingMachines(); + helper.assertValueEqual(machines.size(), 2, "Crafting machine count is incorrect"); + helper.assertValueEqual(machines.get(0).getItem(), Items.FURNACE, "First crafting machine is incorrect"); + helper.assertValueEqual(machines.get(1).getItem(), Items.CRAFTING_TABLE, "Second crafting machine is incorrect"); + + helper.succeed(); + } + + /** + * A crafting option without machines must not write them, + * so that nothing is sent for handlers that can not determine them. + */ + @GameTest(template = "empty", templateNamespace = "cyclopscore") + public void testCraftingMachinesSerializationEmpty(GameTestHelper helper) { + TerminalStorageTabIngredientCraftingHandlerCraftingNetwork handler = + new TerminalStorageTabIngredientCraftingHandlerCraftingNetwork(); + TerminalCraftingOptionRecipeDefinition craftingOption = + new TerminalCraftingOptionRecipeDefinition<>(IngredientComponent.ITEMSTACK, createRecipe()); + + CompoundTag tag = handler.serializeCraftingOption(helper.getLevel().registryAccess(), craftingOption); + helper.assertFalse(tag.contains("craftingMachines"), "Expected no crafting machines to be serialized"); + + TerminalCraftingOptionRecipeDefinition deserialized = handler.deserializeCraftingOption( + helper.getLevel().registryAccess(), IngredientComponent.ITEMSTACK, tag); + helper.assertTrue(deserialized.getCraftingMachines().isEmpty(), "Expected no crafting machines"); + + helper.succeed(); + } + + /** + * Check the machines that are determined for the first recipe that the network exposes. + * + * @param helper The game test helper. + * @param attuned If an attuned crafting interface should be used. + * @param crafters The machines to place before the crafting interfaces. + * @param expectedInterfaces The expected number of interfaces that expose the recipe. + * @param expectedMachine The expected machine item. + */ + protected void testCraftingMachinesInNetwork(GameTestHelper helper, boolean attuned, Block[] crafters, + int expectedInterfaces, Item expectedMachine, + RecipeType recipeType, ResourceLocation recipeName) { + GameTestHelpersIntegratedCrafting.INetworkPositions> positions = + GameTestHelpersIntegratedCrafting.createBasicNetwork(helper, POS, attuned, crafters); + + // Add the recipe to every crafting interface. + // Attuned interfaces derive their recipes from their machine, so this is a no-op for those. + for (int i = 0; i < crafters.length; i++) { + positions.interfaceRecipeAdders().get(i).accept(Triple.of(0, recipeType, recipeName)); + } + + helper.succeedWhen(() -> { + INetwork network = NetworkHelpers.getNetwork(helper.getLevel(), helper.absolutePos(POS), null) + .orElseThrow(() -> new IllegalStateException("Could not find a network")); + Multimap recipeCraftingInterfaces = CraftingHelpers + .getCraftingNetworkChecked(network) + .getRecipeCraftingInterfaces(IPositionedAddonsNetworkIngredients.DEFAULT_CHANNEL); + helper.assertFalse(recipeCraftingInterfaces.isEmpty(), "The network exposes no recipes yet"); + + IRecipeDefinition recipe = recipeCraftingInterfaces.keySet().iterator().next(); + Collection craftingInterfaces = recipeCraftingInterfaces.get(recipe); + helper.assertValueEqual(craftingInterfaces.size(), expectedInterfaces, + "Crafting interface count is incorrect"); + + List machines = TerminalStorageTabIngredientCraftingHandlerCraftingNetwork + .getCraftingMachines(craftingInterfaces, Maps.newIdentityHashMap()); + helper.assertValueEqual(machines.size(), 1, "Crafting machine count is incorrect"); + helper.assertValueEqual(machines.get(0).getItem(), expectedMachine, "Crafting machine is incorrect"); + }); + } + + @GameTest(template = "empty10", templateNamespace = Reference.MOD_ID, timeoutTicks = TIMEOUT) + public void testCraftingMachinesInNetworkCraftingTable(GameTestHelper helper) { + testCraftingMachinesInNetwork(helper, false, new Block[]{Blocks.CRAFTING_TABLE}, 1, Items.CRAFTING_TABLE, + RecipeType.CRAFTING, ResourceLocation.fromNamespaceAndPath("minecraft", "chest")); + } + + @GameTest(template = "empty10", templateNamespace = Reference.MOD_ID, timeoutTicks = TIMEOUT) + public void testCraftingMachinesInNetworkFurnace(GameTestHelper helper) { + testCraftingMachinesInNetwork(helper, false, new Block[]{Blocks.FURNACE}, 1, Items.FURNACE, + RecipeType.SMELTING, ResourceLocation.fromNamespaceAndPath("minecraft", "iron_ingot_from_smelting_raw_iron")); + } + + @GameTest(template = "empty10", templateNamespace = Reference.MOD_ID, timeoutTicks = TIMEOUT) + public void testCraftingMachinesInNetworkAttunedCraftingTable(GameTestHelper helper) { + testCraftingMachinesInNetwork(helper, true, new Block[]{Blocks.CRAFTING_TABLE}, 1, Items.CRAFTING_TABLE, + RecipeType.CRAFTING, ResourceLocation.fromNamespaceAndPath("minecraft", "chest")); + } + + @GameTest(template = "empty10", templateNamespace = Reference.MOD_ID, timeoutTicks = TIMEOUT) + public void testCraftingMachinesInNetworkAttunedFurnace(GameTestHelper helper) { + testCraftingMachinesInNetwork(helper, true, new Block[]{Blocks.FURNACE}, 1, Items.FURNACE, + RecipeType.SMELTING, ResourceLocation.fromNamespaceAndPath("minecraft", "iron_ingot_from_smelting_raw_iron")); + } + + /** + * Two interfaces that target the same machine type must only show that machine once. + */ + @GameTest(template = "empty10", templateNamespace = Reference.MOD_ID, timeoutTicks = TIMEOUT) + public void testCraftingMachinesInNetworkDeduplicated(GameTestHelper helper) { + testCraftingMachinesInNetwork(helper, false, new Block[]{Blocks.CRAFTING_TABLE, Blocks.CRAFTING_TABLE}, + 2, Items.CRAFTING_TABLE, + RecipeType.CRAFTING, ResourceLocation.fromNamespaceAndPath("minecraft", "chest")); + } + +} diff --git a/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalCraftingOptionRecipeDefinition.java b/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalCraftingOptionRecipeDefinition.java index 1358ffc2b..1eeb2639c 100644 --- a/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalCraftingOptionRecipeDefinition.java +++ b/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalCraftingOptionRecipeDefinition.java @@ -2,12 +2,14 @@ import com.google.common.collect.Iterables; import com.google.common.collect.Lists; +import net.minecraft.world.item.ItemStack; import org.cyclops.commoncapabilities.api.capability.recipehandler.IRecipeDefinition; import org.cyclops.commoncapabilities.api.ingredient.IPrototypedIngredient; import org.cyclops.commoncapabilities.api.ingredient.IngredientComponent; import org.cyclops.integratedterminals.api.terminalstorage.crafting.ITerminalCraftingOption; import java.util.Collection; +import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.stream.Collectors; @@ -23,6 +25,7 @@ public class TerminalCraftingOptionRecipeDefinition implements ITerminalCr private final IngredientComponent ingredientComponent; private final IRecipeDefinition prioritizedRecipe; private final long estimatedTickDuration; + private final List craftingMachines; @Deprecated // TODO: rm in next major public TerminalCraftingOptionRecipeDefinition(IngredientComponent ingredientComponent, @@ -33,9 +36,17 @@ public TerminalCraftingOptionRecipeDefinition(IngredientComponent ingredie public TerminalCraftingOptionRecipeDefinition(IngredientComponent ingredientComponent, IRecipeDefinition prioritizedRecipe, long estimatedTickDuration) { + this(ingredientComponent, prioritizedRecipe, estimatedTickDuration, Collections.emptyList()); + } + + public TerminalCraftingOptionRecipeDefinition(IngredientComponent ingredientComponent, + IRecipeDefinition prioritizedRecipe, + long estimatedTickDuration, + List craftingMachines) { this.ingredientComponent = ingredientComponent; this.prioritizedRecipe = prioritizedRecipe; this.estimatedTickDuration = estimatedTickDuration; + this.craftingMachines = craftingMachines; } @Override @@ -43,6 +54,11 @@ public long getEstimatedTickDuration() { return estimatedTickDuration; } + @Override + public List getCraftingMachines() { + return craftingMachines; + } + @Override public Iterator getOutputs() { return prioritizedRecipe.getOutput().getInstances(ingredientComponent).iterator(); diff --git a/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java b/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java index fc9e53b90..d2df624c8 100644 --- a/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java +++ b/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java @@ -1,13 +1,18 @@ package org.cyclops.integratedterminals.modcompat.integratedcrafting; +import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; import com.mojang.authlib.GameProfile; import net.minecraft.core.HolderLookup; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.IntTag; +import net.minecraft.nbt.ListTag; import net.minecraft.nbt.Tag; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.ItemStack; import net.neoforged.neoforge.server.ServerLifecycleHooks; import org.apache.logging.log4j.Level; import org.cyclops.commoncapabilities.api.capability.recipehandler.IRecipeDefinition; @@ -71,25 +76,64 @@ public int[] getChannels(TerminalStorageTabIngredientComponentServer ingredientComponent = tab.getIngredientNetwork().getComponent(); IRecipeIndex recipeIndex = getRecipeIndex(tab.getNetwork(), channel); ICraftingNetwork craftingNetwork = CraftingHelpers.getCraftingNetwork(tab.getNetwork()).orElse(null); + Multimap recipeCraftingInterfaces = craftingNetwork == null + ? ImmutableMultimap.of() : craftingNetwork.getRecipeCraftingInterfaces(channel); + // Multiple recipes are commonly exposed by the same crafting interface, + // so only resolve the machine of each interface once. + Map machineCache = Maps.newIdentityHashMap(); Iterable recipes = () -> recipeIndex.getRecipes(ingredientComponent, instance, matchCondition); return StreamSupport.stream(recipes.spliterator(), false) .map((recipe) -> new TerminalCraftingOptionRecipeDefinition<>(ingredientComponent, recipe, - craftingNetwork == null ? -1 : craftingNetwork.getEstimatedRecipeDuration(channel, recipe))) + craftingNetwork == null ? -1 : craftingNetwork.getEstimatedRecipeDuration(channel, recipe), + getCraftingMachines(recipeCraftingInterfaces.get(recipe), machineCache))) .collect(Collectors.toList()); } + /** + * Determine the distinct machines that are targeted by the given crafting interfaces. + * @param craftingInterfaces The crafting interfaces that expose a recipe. + * @param machineCache A cache of machines by crafting interface. + * @return The machines, without duplicates. + */ + public static List getCraftingMachines(Collection craftingInterfaces, + Map machineCache) { + List craftingMachines = Lists.newArrayList(); + for (ICraftingInterface craftingInterface : craftingInterfaces) { + ItemStack machine = machineCache.computeIfAbsent(craftingInterface, ICraftingInterface::getTargetMachineItem); + // Different interfaces can target the same machine type, which we only want to show once + if (!machine.isEmpty() && craftingMachines.stream() + .noneMatch(existing -> ItemStack.isSameItemSameComponents(existing, machine))) { + craftingMachines.add(machine); + } + } + return craftingMachines; + } + @Override public CompoundTag serializeCraftingOption(HolderLookup.Provider lookupProvider, TerminalCraftingOptionRecipeDefinition craftingOption) { CompoundTag tag = IRecipeDefinition.serialize(lookupProvider, craftingOption.getRecipe()); tag.putLong("estimatedTickDuration", craftingOption.getEstimatedTickDuration()); + List craftingMachines = craftingOption.getCraftingMachines(); + if (!craftingMachines.isEmpty()) { + ListTag craftingMachinesTag = new ListTag(); + for (ItemStack craftingMachine : craftingMachines) { + craftingMachinesTag.add(craftingMachine.save(lookupProvider)); + } + tag.put("craftingMachines", craftingMachinesTag); + } return tag; } @Override public TerminalCraftingOptionRecipeDefinition deserializeCraftingOption(HolderLookup.Provider lookupProvider, IngredientComponent ingredientComponent, CompoundTag tag) throws IllegalArgumentException { + List craftingMachines = Lists.newArrayList(); + for (Tag craftingMachineTag : tag.getList("craftingMachines", Tag.TAG_COMPOUND)) { + ItemStack.parse(lookupProvider, craftingMachineTag).ifPresent(craftingMachines::add); + } return new TerminalCraftingOptionRecipeDefinition<>(ingredientComponent, IRecipeDefinition.deserialize(lookupProvider, tag), - tag.contains("estimatedTickDuration", Tag.TAG_LONG) ? tag.getLong("estimatedTickDuration") : -1); + tag.contains("estimatedTickDuration", Tag.TAG_LONG) ? tag.getLong("estimatedTickDuration") : -1, + craftingMachines); } @Override diff --git a/src/main/java/org/cyclops/integratedterminals/proxy/ClientProxy.java b/src/main/java/org/cyclops/integratedterminals/proxy/ClientProxy.java index 7c68e9286..4a1e3e959 100644 --- a/src/main/java/org/cyclops/integratedterminals/proxy/ClientProxy.java +++ b/src/main/java/org/cyclops/integratedterminals/proxy/ClientProxy.java @@ -16,7 +16,9 @@ import org.cyclops.integratedterminals.IntegratedTerminals; import org.cyclops.integratedterminals.Reference; import org.cyclops.integratedterminals.client.gui.tooltip.ClientCraftingOptionIngredientsTooltip; +import org.cyclops.integratedterminals.client.gui.tooltip.ClientCraftingOptionMachinesTooltip; import org.cyclops.integratedterminals.client.gui.tooltip.CraftingOptionIngredientsTooltip; +import org.cyclops.integratedterminals.client.gui.tooltip.CraftingOptionMachinesTooltip; import org.cyclops.integratedterminals.item.ItemTerminalStoragePortable; import org.cyclops.integratedterminals.network.packet.TerminalStorageIngredientItemOpenGenericPacket; import org.lwjgl.glfw.GLFW; @@ -69,6 +71,7 @@ public ModBase getMod() { public void registerClientTooltipComponentFactories(RegisterClientTooltipComponentFactoriesEvent event) { event.register(CraftingOptionIngredientsTooltip.class, ClientCraftingOptionIngredientsTooltip::new); + event.register(CraftingOptionMachinesTooltip.class, ClientCraftingOptionMachinesTooltip::new); } @Override diff --git a/src/main/resources/assets/integratedterminals/lang/en_us.json b/src/main/resources/assets/integratedterminals/lang/en_us.json index 9b3193616..dd266ce58 100644 --- a/src/main/resources/assets/integratedterminals/lang/en_us.json +++ b/src/main/resources/assets/integratedterminals/lang/en_us.json @@ -18,6 +18,8 @@ "gui.integratedterminals.terminal_storage.no_tabs": "No tabs available. Connect storage to this network with an interface.", "gui.integratedterminals.terminal_storage.ender_chest": "Ender Storage", "gui.integratedterminals.terminal_storage.craft": "craft", + "gui.integratedterminals.terminal_storage.tooltip.crafting_machine": "Crafted In: %s", + "gui.integratedterminals.terminal_storage.tooltip.crafting_machines": "Crafted In:", "gui.integratedterminals.terminal_storage.tooltip.requirements": "Crafting Requirements:", "gui.integratedterminals.terminal_storage.tooltip.crafting": "Being crafted: %s", "gui.integratedterminals.terminal_storage.tooltip.duration": "Estimated: %s per craft", From 95ca62bd0ce6dc0f186d43023500aa519a96b082 Mon Sep 17 00:00:00 2001 From: Ruben Taelman Date: Fri, 4 Sep 2026 19:20:43 +0000 Subject: [PATCH 2/2] Show the crafting requirements before the crafting machine Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01RM86uiqD4soLkMyjC9Eeid --- ...erminalStorageSlotIngredientCraftingOption.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/slot/TerminalStorageSlotIngredientCraftingOption.java b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/slot/TerminalStorageSlotIngredientCraftingOption.java index f4d92dbcb..8cf01376f 100644 --- a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/slot/TerminalStorageSlotIngredientCraftingOption.java +++ b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/slot/TerminalStorageSlotIngredientCraftingOption.java @@ -87,7 +87,7 @@ protected List getTooltipLines(@Nullable PendingCraftingJobOutput } /** - * Show the machines that this crafting option is crafted in, and the inputs that it requires, + * Show the inputs that this crafting option requires, and the machines that it is crafted in, * each as a labelled grid of icons. * * @param machines The crafting machines. @@ -98,6 +98,12 @@ protected List getTooltipLines(@Nullable PendingCraftingJobOutput protected List> getTooltipElements(List machines, List>> inputs) { List> tooltipElements = Lists.newArrayList(); + if (!inputs.isEmpty()) { + tooltipElements.add(Either.left(Component + .translatable("gui.integratedterminals.terminal_storage.tooltip.requirements") + .withStyle(ChatFormatting.YELLOW))); + tooltipElements.add(Either.right(new CraftingOptionIngredientsTooltip(inputs))); + } if (!machines.isEmpty()) { // Only a single machine can be named on the label, otherwise the icons have to speak for themselves tooltipElements.add(Either.left(machines.size() == 1 @@ -107,12 +113,6 @@ protected List> getTooltipElements(List< .withStyle(ChatFormatting.YELLOW))); tooltipElements.add(Either.right(new CraftingOptionMachinesTooltip(machines))); } - if (!inputs.isEmpty()) { - tooltipElements.add(Either.left(Component - .translatable("gui.integratedterminals.terminal_storage.tooltip.requirements") - .withStyle(ChatFormatting.YELLOW))); - tooltipElements.add(Either.right(new CraftingOptionIngredientsTooltip(inputs))); - } return tooltipElements; }