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 5607b47ab..b984b6913 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,7 @@ import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.tooltip.TooltipComponent; import net.minecraft.world.item.ItemStack; import net.minecraft.network.chat.Component; import net.minecraft.ChatFormatting; @@ -54,9 +55,33 @@ public interface IIngredientComponentTerminalStorageHandler { * @param mouseY The mouse Y position. * @param additionalTooltipLines The additional tooltip lines to add. */ + @Deprecated // TODO: remove in next major + @OnlyIn(Dist.CLIENT) + public default 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) { + drawInstance(guiGraphics, instance, maxQuantity, label, gui, layer, partialTick, x, y, mouseX, mouseY, additionalTooltipLines, null); + } + + /** + * Draw the given instance in the given gui. + * @param guiGraphics The matrix stack. + * @param instance An instance. + * @param maxQuantity The maximum allowed quantity of the given instance. + * @param label An optional label that should be rendered instead of the quantity. + * @param gui A gui to render in. + * @param layer The layer to render in. + * @param partialTick The partial tick. + * @param x The slot X position. + * @param y The slot Y position. + * @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. + */ @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); + int mouseX, int mouseY, @Nullable List additionalTooltipLines, + @Nullable TooltipComponent additionalTooltipComponent); /** * 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 ff6460c3a..14d5f1f9b 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,9 +1,15 @@ package org.cyclops.integratedterminals.api.terminalstorage.crafting; +import org.cyclops.commoncapabilities.api.ingredient.IIngredientMatcher; +import org.cyclops.commoncapabilities.api.ingredient.IPrototypedIngredient; import org.cyclops.commoncapabilities.api.ingredient.IngredientComponent; +import org.cyclops.commoncapabilities.api.ingredient.PrototypedIngredient; import java.util.Collection; +import java.util.Collections; import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; /** * Identifies a crafting job possibility. @@ -45,4 +51,24 @@ public interface ITerminalCraftingOption extends Comparable Collection getInputs(IngredientComponent ingredientComponent); + /** + * The inputs of this crafting job option for the given ingredient component, + * where each input can be fulfilled by any of its alternatives. + * + * By default, this only exposes the single input instances from {@link #getInputs(IngredientComponent)}. + * + * @param ingredientComponent An ingredient component, + * @param The instance type. + * @param The matching condition parameter, may be Void. + * @return The inputs, where each entry holds at least one alternative. + */ + public default List>> getInputAlternatives(IngredientComponent ingredientComponent) { + IIngredientMatcher matcher = ingredientComponent.getMatcher(); + return getInputs(ingredientComponent) + .stream() + .>>map(input -> Collections.singletonList( + new PrototypedIngredient<>(ingredientComponent, input, matcher.getExactMatchNoQuantityCondition()))) + .collect(Collectors.toList()); + } + } 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 94a69dd52..5cf5c96b0 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,7 @@ import net.minecraft.world.entity.player.Player; import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.world.inventory.Slot; +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; @@ -28,6 +29,7 @@ import org.cyclops.integratedterminals.api.ingredient.IIngredientInstanceSorter; import org.cyclops.integratedterminals.client.gui.container.ContainerScreenTerminalStorage; import org.cyclops.integratedterminals.client.gui.image.Images; +import org.cyclops.integratedterminals.client.gui.tooltip.TooltipRenderHelpers; import org.cyclops.integratedterminals.core.terminalstorage.query.SearchMode; import javax.annotation.Nullable; @@ -64,7 +66,8 @@ public ItemStack getIcon() { @OnlyIn(Dist.CLIENT) 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) { + int mouseX, int mouseY, @Nullable List additionalTooltipLines, + @Nullable TooltipComponent additionalTooltipComponent) { if (instance > 0) { if (layer == ContainerScreenTerminalStorage.DrawLayer.BACKGROUND){ @@ -91,7 +94,7 @@ public void drawInstance(GuiGraphics guiGraphics, Long instance, long maxQuantit Lighting.setupFor3DItems(); } else { - GuiHelpers.renderTooltip(gui, guiGraphics.pose(), x, y, GuiHelpers.SLOT_SIZE_INNER, GuiHelpers.SLOT_SIZE_INNER, + TooltipRenderHelpers.renderTooltip(gui, guiGraphics, x, y, GuiHelpers.SLOT_SIZE_INNER, GuiHelpers.SLOT_SIZE_INNER, mouseX, mouseY, () -> { List lines = Lists.newArrayList(); lines.add(Component.translatable("gui.integratedterminals.terminal_storage.tooltip.energy")); @@ -100,7 +103,7 @@ public void drawInstance(GuiGraphics guiGraphics, Long instance, long maxQuantit lines.addAll(additionalTooltipLines); } return lines; - }); + }, additionalTooltipComponent); } } } 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 297237a6d..8844232f8 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,7 @@ import net.minecraft.world.entity.player.Player; import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.world.inventory.Slot; +import net.minecraft.world.inventory.tooltip.TooltipComponent; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; import net.neoforged.api.distmarker.Dist; @@ -35,6 +36,7 @@ import org.cyclops.integratedterminals.capability.ingredient.sorter.FluidStackNameSorter; import org.cyclops.integratedterminals.capability.ingredient.sorter.FluidStackQuantitySorter; import org.cyclops.integratedterminals.client.gui.container.ContainerScreenTerminalStorage; +import org.cyclops.integratedterminals.client.gui.tooltip.TooltipRenderHelpers; import org.cyclops.integratedterminals.core.terminalstorage.query.SearchMode; import javax.annotation.Nullable; @@ -71,7 +73,8 @@ public ItemStack getIcon() { public void drawInstance(GuiGraphics guiGraphics, FluidStack 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 List additionalTooltipLines, + @Nullable TooltipComponent additionalTooltipComponent) { if (instance != null) { if (layer == ContainerScreenTerminalStorage.DrawLayer.BACKGROUND) { // Draw fluid @@ -81,7 +84,7 @@ public void drawInstance(GuiGraphics guiGraphics, FluidStack instance, long maxQ GuiGraphicsExtended renderItem = new GuiGraphicsExtended(guiGraphics); renderItem.drawSlotText(Minecraft.getInstance().font, label != null ? label : GuiHelpers.quantityToScaledString(instance.getAmount()), x, y); } else { - GuiHelpers.renderTooltip(gui, guiGraphics.pose(), x, y, GuiHelpers.SLOT_SIZE_INNER, GuiHelpers.SLOT_SIZE_INNER, mouseX, mouseY, () -> { + TooltipRenderHelpers.renderTooltip(gui, guiGraphics, x, y, GuiHelpers.SLOT_SIZE_INNER, GuiHelpers.SLOT_SIZE_INNER, mouseX, mouseY, () -> { List lines = Lists.newArrayList(); lines.add(((MutableComponent) instance.getHoverName()).withStyle(instance.getFluid().getFluidType().getRarity().getStyleModifier().apply(Style.EMPTY))); addQuantityTooltip(lines, instance); @@ -89,7 +92,7 @@ public void drawInstance(GuiGraphics guiGraphics, FluidStack instance, long maxQ lines.addAll(additionalTooltipLines); } return lines; - }); + }, additionalTooltipComponent); } } } 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 3f6e53d5a..2d3c4d228 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,7 @@ import net.minecraft.world.entity.player.Player; import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.world.inventory.Slot; +import net.minecraft.world.inventory.tooltip.TooltipComponent; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.TooltipFlag; @@ -31,6 +32,7 @@ import org.cyclops.integratedterminals.capability.ingredient.sorter.ItemStackNameSorter; import org.cyclops.integratedterminals.capability.ingredient.sorter.ItemStackQuantitySorter; import org.cyclops.integratedterminals.client.gui.container.ContainerScreenTerminalStorage; +import org.cyclops.integratedterminals.client.gui.tooltip.TooltipRenderHelpers; import org.cyclops.integratedterminals.core.terminalstorage.query.SearchMode; import org.lwjgl.opengl.GL11; @@ -67,7 +69,8 @@ public ItemStack getIcon() { @OnlyIn(Dist.CLIENT) 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) { + int mouseX, int mouseY, @Nullable List additionalTooltipLines, + @Nullable TooltipComponent additionalTooltipComponent) { // 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. @@ -85,7 +88,7 @@ public void drawInstance(GuiGraphics guiGraphics, ItemStack instance, long maxQu guiGraphics.renderItem(instanceCopy, x, y); renderItem.renderItemDecorations(Minecraft.getInstance().font, instanceCopy, x, y, label); } else { - GuiHelpers.renderTooltip(gui, guiGraphics.pose(), x, y, GuiHelpers.SLOT_SIZE_INNER, GuiHelpers.SLOT_SIZE_INNER, mouseX, mouseY, () -> { + TooltipRenderHelpers.renderTooltip(gui, guiGraphics, x, y, GuiHelpers.SLOT_SIZE_INNER, GuiHelpers.SLOT_SIZE_INNER, mouseX, mouseY, () -> { List lines = instanceCopy.getTooltipLines( Item.TooltipContext.of(Minecraft.getInstance().player.registryAccess()), Minecraft.getInstance().player, Minecraft.getInstance().options.advancedItemTooltips @@ -95,7 +98,7 @@ public void drawInstance(GuiGraphics guiGraphics, ItemStack instance, long maxQu } addQuantityTooltip(lines, instanceCopy); return lines; - }); + }, additionalTooltipComponent); } 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 new file mode 100644 index 000000000..4563defc7 --- /dev/null +++ b/src/main/java/org/cyclops/integratedterminals/client/gui/tooltip/ClientCraftingOptionIngredientsTooltip.java @@ -0,0 +1,96 @@ +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. + * + * @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; + + private final List>> ingredients; + private final int columns; + private final int rows; + + public ClientCraftingOptionIngredientsTooltip(CraftingOptionIngredientsTooltip tooltip) { + 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); + } + +} diff --git a/src/main/java/org/cyclops/integratedterminals/client/gui/tooltip/CraftingOptionIngredientsTooltip.java b/src/main/java/org/cyclops/integratedterminals/client/gui/tooltip/CraftingOptionIngredientsTooltip.java new file mode 100644 index 000000000..dc6be1f77 --- /dev/null +++ b/src/main/java/org/cyclops/integratedterminals/client/gui/tooltip/CraftingOptionIngredientsTooltip.java @@ -0,0 +1,19 @@ +package org.cyclops.integratedterminals.client.gui.tooltip; + +import net.minecraft.world.inventory.tooltip.TooltipComponent; +import org.cyclops.commoncapabilities.api.ingredient.IPrototypedIngredient; + +import java.util.List; + +/** + * A tooltip component holding the ingredients that are required by a crafting option. + * + * Every entry in {@link #ingredients()} represents a single required input, + * which holds all ingredients that are valid alternatives for that input. + * + * @param ingredients The required inputs, with their alternatives. + * @author rubensworks + */ +public record CraftingOptionIngredientsTooltip( + List>> ingredients) 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 new file mode 100644 index 000000000..60fdc2207 --- /dev/null +++ b/src/main/java/org/cyclops/integratedterminals/client/gui/tooltip/TooltipRenderHelpers.java @@ -0,0 +1,97 @@ +package org.cyclops.integratedterminals.client.gui.tooltip; + +import com.google.common.collect.Lists; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.datafixers.util.Either; +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.cyclopscore.helper.GuiHelpers; +import org.cyclops.cyclopscore.helper.RenderHelpers; + +import javax.annotation.Nullable; +import java.util.List; +import java.util.function.Supplier; + +/** + * Helpers for rendering tooltips that can contain visual components next to text lines. + * @author rubensworks + */ +@OnlyIn(Dist.CLIENT) +public final class TooltipRenderHelpers { + + 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. + * Otherwise, the tooltip is rendered by vanilla, + * so that the visual component 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. + * + * @param gui The gui to render in. + * @param guiGraphics The gui graphics. + * @param x The region X position, relative to the gui. + * @param y The region Y position, relative to the gui. + * @param width The region width. + * @param height The region height. + * @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. + */ + 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) { + 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)); + + // 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. + RenderSystem.disableDepthTest(); + + // Tooltips are positioned in screen space, + // while the foreground layer is translated to the position of the gui. + guiGraphics.pose().pushPose(); + guiGraphics.pose().translate(-gui.getGuiLeft(), -gui.getGuiTop(), 0); + guiGraphics.renderComponentTooltipFromElements(Minecraft.getInstance().font, elements, mouseX, mouseY, ItemStack.EMPTY); + guiGraphics.pose().popPose(); + + RenderSystem.enableDepthTest(); + } + } + + /** + * Check if the mouse hovers over the given region, + * which is when {@link #renderTooltip} would show a tooltip for it. + * + * @param gui The gui that is being rendered in. + * @param x The region X position, relative to the gui. + * @param y The region Y position, relative to the gui. + * @param width The region width. + * @param height The region height. + * @param mouseX The mouse X position. + * @param mouseY The mouse Y position. + * @return If the mouse hovers over the region. + */ + public static boolean isHovering(AbstractContainerScreen gui, int x, int y, int width, int height, + int mouseX, int mouseY) { + return RenderHelpers.isPointInRegion(x, y, width, height, mouseX - gui.getGuiLeft(), mouseY - gui.getGuiTop()); + } + +} diff --git a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/crafting/TerminalCraftingOptionInputs.java b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/crafting/TerminalCraftingOptionInputs.java new file mode 100644 index 000000000..5d8fd5a07 --- /dev/null +++ b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/crafting/TerminalCraftingOptionInputs.java @@ -0,0 +1,116 @@ +package org.cyclops.integratedterminals.core.terminalstorage.crafting; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import org.cyclops.commoncapabilities.api.ingredient.IIngredientMatcher; +import org.cyclops.commoncapabilities.api.ingredient.IPrototypedIngredient; +import org.cyclops.commoncapabilities.api.ingredient.IngredientComponent; +import org.cyclops.commoncapabilities.api.ingredient.PrototypedIngredient; +import org.cyclops.integratedterminals.api.terminalstorage.crafting.ITerminalCraftingOption; + +import java.util.List; +import java.util.Map; + +/** + * Helpers for listing the inputs that are required by a {@link ITerminalCraftingOption}. + * @author rubensworks + */ +public final class TerminalCraftingOptionInputs { + + private TerminalCraftingOptionInputs() {} + + /** + * Get all inputs that are required by the given crafting option, over all its ingredient components. + * + * Every entry holds all alternatives that can fulfill that input, + * and inputs that can be fulfilled by the same alternatives are grouped into a single entry + * by summing their quantities. + * + * @param craftingOption A crafting option. + * @return The required inputs, with at least one alternative each. + */ + public static List>> getGroupedInputs(ITerminalCraftingOption craftingOption) { + List>> inputs = Lists.newArrayList(); + for (IngredientComponent inputComponent : craftingOption.getInputComponents()) { + addInputs(craftingOption, inputComponent, inputs); + } + return group(inputs); + } + + protected static void addInputs(ITerminalCraftingOption craftingOption, IngredientComponent inputComponent, + List>> inputs) { + IIngredientMatcher matcher = inputComponent.getMatcher(); + for (List> alternatives : craftingOption.getInputAlternatives(inputComponent)) { + List> nonEmptyAlternatives = Lists.newArrayList(); + for (IPrototypedIngredient alternative : alternatives) { + if (!matcher.isEmpty(alternative.getPrototype())) { + nonEmptyAlternatives.add(alternative); + } + } + if (!nonEmptyAlternatives.isEmpty()) { + inputs.add(nonEmptyAlternatives); + } + } + } + + /** + * Group the inputs that can be fulfilled by the same alternatives, by summing their quantities. + * + * Recipes commonly require the same ingredient in multiple slots, + * which we want to show as a single input with a higher quantity. + * + * @param inputs The inputs to group, in the order they should be shown in. + * @return The grouped inputs, where every group is placed at the position of its first input. + */ + protected static List>> group(List>> inputs) { + List>> groupedInputs = Lists.newArrayList(); + // Inputs are keyed on their alternatives without quantities, so that only their quantities may differ. + Map>, Integer> groupIndexes = Maps.newHashMap(); + for (List> alternatives : inputs) { + List> groupKey = withoutQuantities(alternatives); + Integer groupIndex = groupIndexes.get(groupKey); + if (groupIndex == null) { + groupIndexes.put(groupKey, groupedInputs.size()); + groupedInputs.add(alternatives); + } else { + groupedInputs.set(groupIndex, addQuantities(groupedInputs.get(groupIndex), alternatives)); + } + } + return groupedInputs; + } + + protected static List> withoutQuantities(List> alternatives) { + List> withoutQuantities = Lists.newArrayListWithExpectedSize(alternatives.size()); + for (IPrototypedIngredient alternative : alternatives) { + withoutQuantities.add(withQuantity(alternative, 1)); + } + return withoutQuantities; + } + + /** + * Sum the quantities of two equal lists of alternatives. + * These are always of equal length, as they were grouped on their alternatives without quantities. + */ + protected static List> addQuantities(List> alternatives, + List> addedAlternatives) { + List> summedAlternatives = Lists.newArrayListWithExpectedSize(alternatives.size()); + for (int i = 0; i < alternatives.size(); i++) { + IPrototypedIngredient alternative = alternatives.get(i); + summedAlternatives.add(withQuantity(alternative, + getQuantity(alternative) + getQuantity(addedAlternatives.get(i)))); + } + return summedAlternatives; + } + + protected static IPrototypedIngredient withQuantity(IPrototypedIngredient ingredient, long quantity) { + IngredientComponent ingredientComponent = ingredient.getComponent(); + return new PrototypedIngredient<>(ingredientComponent, + ingredientComponent.getMatcher().withQuantity(ingredient.getPrototype(), quantity), + ingredient.getCondition()); + } + + protected static long getQuantity(IPrototypedIngredient ingredient) { + return ingredient.getComponent().getMatcher().getQuantity(ingredient.getPrototype()); + } + +} 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 5f65d622d..5bdc085be 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 @@ -8,17 +8,19 @@ import net.minecraft.network.chat.Component; import net.neoforged.api.distmarker.Dist; import net.neoforged.api.distmarker.OnlyIn; -import org.cyclops.commoncapabilities.api.ingredient.IIngredientMatcher; -import org.cyclops.commoncapabilities.api.ingredient.IngredientComponent; +import org.cyclops.commoncapabilities.api.ingredient.IPrototypedIngredient; import org.cyclops.cyclopscore.client.gui.GuiGraphicsExtended; +import org.cyclops.cyclopscore.helper.GuiHelpers; import org.cyclops.cyclopscore.helper.L10NHelpers; import org.cyclops.integratedterminals.api.ingredient.IIngredientComponentTerminalStorageHandler; import org.cyclops.integratedterminals.api.terminalstorage.ITerminalStorageTabClient; -import org.cyclops.integratedterminals.api.terminalstorage.crafting.ITerminalCraftingOption; import org.cyclops.integratedterminals.client.gui.container.ContainerScreenTerminalStorage; +import org.cyclops.integratedterminals.client.gui.tooltip.CraftingOptionIngredientsTooltip; +import org.cyclops.integratedterminals.client.gui.tooltip.TooltipRenderHelpers; import org.cyclops.integratedterminals.core.terminalstorage.TerminalStorageTabIngredientComponentClient; import org.cyclops.integratedterminals.core.terminalstorage.crafting.HandlerWrappedTerminalCraftingOption; import org.cyclops.integratedterminals.core.terminalstorage.crafting.PendingCraftingJobOutput; +import org.cyclops.integratedterminals.core.terminalstorage.crafting.TerminalCraftingOptionInputs; import javax.annotation.Nullable; import java.util.List; @@ -50,32 +52,38 @@ public void drawGuiContainerLayer(AbstractContainerScreen gui, GuiGraphics guiGr viewHandler.drawInstance(guiGraphics, getInstance(), maxQuantity, null, gui, layer, partialTick, x, y, mouseX, mouseY, null); drawCraftLabel(guiGraphics, x, y); } 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(); viewHandler.drawInstance(guiGraphics, getInstance(), maxQuantity, label, gui, layer, partialTick, x, y, mouseX, mouseY, - getTooltipLines(pendingCraftingJobOutput)); + getTooltipLines(pendingCraftingJobOutput, inputs), + inputs.isEmpty() ? null : new CraftingOptionIngredientsTooltip(inputs)); } drawCraftingJobOverlay(guiGraphics, layer, x, y, pendingCraftingJobOutput); } - protected List getTooltipLines(@Nullable PendingCraftingJobOutput pendingCraftingJobOutput) { + protected List getTooltipLines(@Nullable PendingCraftingJobOutput pendingCraftingJobOutput, + List>> inputs) { List tooltipLines = Lists.newArrayList(); if (pendingCraftingJobOutput != null) { addCraftingJobTooltipLines(tooltipLines, pendingCraftingJobOutput); } - tooltipLines.add(Component.translatable("gui.integratedterminals.terminal_storage.tooltip.requirements") - .withStyle(ChatFormatting.YELLOW)); - ITerminalCraftingOption option = getCraftingOption().getCraftingOption(); - for (IngredientComponent inputComponent : option.getInputComponents()) { - IIngredientMatcher matcher = inputComponent.getMatcher(); - for (Object inputInstance : option.getInputs(inputComponent)) { - if (!matcher.isEmpty(inputInstance)) { - tooltipLines.add(Component.literal(String.format("%s- %s (%s)", - ChatFormatting.GRAY, matcher.localize(inputInstance), matcher.getQuantity(inputInstance)))); - } - } + if (!inputs.isEmpty()) { + tooltipLines.add(Component.translatable("gui.integratedterminals.terminal_storage.tooltip.requirements") + .withStyle(ChatFormatting.YELLOW)); } return tooltipLines; } + /** + * @return The inputs that are required by this crafting option, with all their alternatives. + */ + protected List>> getInputs() { + return TerminalCraftingOptionInputs.getGroupedInputs(getCraftingOption().getCraftingOption()); + } + @Nullable @Override @OnlyIn(Dist.CLIENT) diff --git a/src/main/java/org/cyclops/integratedterminals/gametest/GameTestTerminalCraftingOptionIntegratedTerminals.java b/src/main/java/org/cyclops/integratedterminals/gametest/GameTestTerminalCraftingOptionIntegratedTerminals.java new file mode 100644 index 000000000..bfc38de61 --- /dev/null +++ b/src/main/java/org/cyclops/integratedterminals/gametest/GameTestTerminalCraftingOptionIntegratedTerminals.java @@ -0,0 +1,134 @@ +package org.cyclops.integratedterminals.gametest; + +import com.google.common.collect.Lists; +import net.minecraft.gametest.framework.GameTest; +import net.minecraft.gametest.framework.GameTestHelper; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.neoforged.neoforge.gametest.GameTestHolder; +import net.neoforged.neoforge.gametest.PrefixGameTestTemplate; +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.integratedterminals.Reference; +import org.cyclops.integratedterminals.api.terminalstorage.crafting.ITerminalCraftingOption; +import org.cyclops.integratedterminals.core.terminalstorage.crafting.TerminalCraftingOptionInputs; +import org.cyclops.integratedterminals.modcompat.integratedcrafting.TerminalCraftingOptionRecipeDefinition; + +import java.util.List; + +/** + * Game tests for the inputs that are exposed by crafting options. + * @author rubensworks + */ +@GameTestHolder(Reference.MOD_ID) +@PrefixGameTestTemplate(false) +public class GameTestTerminalCraftingOptionIntegratedTerminals { + + private static List> alternatives(ItemStack... itemStacks) { + List> alternatives = Lists.newArrayList(); + for (ItemStack itemStack : itemStacks) { + alternatives.add(new PrototypedIngredient<>(IngredientComponent.ITEMSTACK, itemStack, + IngredientComponent.ITEMSTACK.getMatcher().getExactMatchNoQuantityCondition())); + } + return alternatives; + } + + private static ITerminalCraftingOption createCraftingOption(List>> inputs) { + IRecipeDefinition recipe = RecipeDefinition.ofIngredients(IngredientComponent.ITEMSTACK, inputs, + MixedIngredients.ofInstance(IngredientComponent.ITEMSTACK, new ItemStack(Items.STICK, 4))); + return new TerminalCraftingOptionRecipeDefinition<>(IngredientComponent.ITEMSTACK, recipe); + } + + @GameTest(template = "empty", templateNamespace = "cyclopscore") + public void testInputAlternativesSingle(GameTestHelper helper) { + List>> inputs = Lists.newArrayList(); + inputs.add(alternatives(new ItemStack(Items.OAK_PLANKS, 2))); + + List>> alternatives = createCraftingOption(inputs) + .getInputAlternatives(IngredientComponent.ITEMSTACK); + helper.assertTrue(alternatives.size() == 1, "Expected a single input"); + helper.assertTrue(alternatives.get(0).size() == 1, "Expected a single alternative"); + helper.assertTrue(ItemStack.isSameItemSameComponents(alternatives.get(0).get(0).getPrototype(), new ItemStack(Items.OAK_PLANKS)), + "Expected oak planks as input"); + helper.assertTrue(alternatives.get(0).get(0).getPrototype().getCount() == 2, "Expected an input quantity of 2"); + + helper.succeed(); + } + + @GameTest(template = "empty", templateNamespace = "cyclopscore") + public void testInputAlternativesMultiple(GameTestHelper helper) { + List>> inputs = Lists.newArrayList(); + inputs.add(alternatives(new ItemStack(Items.OAK_PLANKS), new ItemStack(Items.BIRCH_PLANKS))); + inputs.add(alternatives(new ItemStack(Items.STONE))); + ITerminalCraftingOption craftingOption = createCraftingOption(inputs); + + List>> alternatives = craftingOption + .getInputAlternatives(IngredientComponent.ITEMSTACK); + helper.assertTrue(alternatives.size() == 2, "Expected two inputs"); + helper.assertTrue(alternatives.get(0).size() == 2, "Expected two alternatives for the first input"); + helper.assertTrue(ItemStack.isSameItemSameComponents(alternatives.get(0).get(0).getPrototype(), new ItemStack(Items.OAK_PLANKS)), + "Expected oak planks as first alternative"); + helper.assertTrue(ItemStack.isSameItemSameComponents(alternatives.get(0).get(1).getPrototype(), new ItemStack(Items.BIRCH_PLANKS)), + "Expected birch planks as second alternative"); + helper.assertTrue(alternatives.get(1).size() == 1, "Expected one alternative for the second input"); + + // The plain inputs only expose the first alternative + List plainInputs = Lists.newArrayList(craftingOption.getInputs(IngredientComponent.ITEMSTACK)); + helper.assertTrue(plainInputs.size() == 2, "Expected two plain inputs"); + helper.assertTrue(ItemStack.isSameItemSameComponents(plainInputs.get(0), new ItemStack(Items.OAK_PLANKS)), + "Expected oak planks as first plain input"); + + helper.succeed(); + } + + @GameTest(template = "empty", templateNamespace = "cyclopscore") + public void testGroupedInputsEqual(GameTestHelper helper) { + // A recipe that requires the same ingredient in multiple slots, like a redstone-based recipe + List>> inputs = Lists.newArrayList(); + inputs.add(alternatives(new ItemStack(Items.REDSTONE))); + inputs.add(alternatives(new ItemStack(Items.REDSTONE))); + inputs.add(alternatives(new ItemStack(Items.HAY_BLOCK))); + inputs.add(alternatives(new ItemStack(Items.REDSTONE, 2))); + + List>> groupedInputs = TerminalCraftingOptionInputs + .getGroupedInputs(createCraftingOption(inputs)); + helper.assertTrue(groupedInputs.size() == 2, "Expected the equal inputs to be grouped"); + helper.assertTrue(ItemStack.isSameItemSameComponents((ItemStack) groupedInputs.get(0).get(0).getPrototype(), new ItemStack(Items.REDSTONE)), + "Expected redstone as first grouped input"); + helper.assertTrue(((ItemStack) groupedInputs.get(0).get(0).getPrototype()).getCount() == 4, + "Expected the redstone quantities to be summed"); + helper.assertTrue(ItemStack.isSameItemSameComponents((ItemStack) groupedInputs.get(1).get(0).getPrototype(), new ItemStack(Items.HAY_BLOCK)), + "Expected hay as second grouped input"); + helper.assertTrue(((ItemStack) groupedInputs.get(1).get(0).getPrototype()).getCount() == 1, + "Expected the hay quantity to be unchanged"); + + helper.succeed(); + } + + @GameTest(template = "empty", templateNamespace = "cyclopscore") + public void testGroupedInputsEqualAlternatives(GameTestHelper helper) { + // Inputs are only grouped if all their alternatives are equal + List>> inputs = Lists.newArrayList(); + inputs.add(alternatives(new ItemStack(Items.OAK_PLANKS), new ItemStack(Items.BIRCH_PLANKS))); + inputs.add(alternatives(new ItemStack(Items.OAK_PLANKS), new ItemStack(Items.BIRCH_PLANKS))); + inputs.add(alternatives(new ItemStack(Items.OAK_PLANKS))); + + List>> groupedInputs = TerminalCraftingOptionInputs + .getGroupedInputs(createCraftingOption(inputs)); + helper.assertTrue(groupedInputs.size() == 2, "Expected only the inputs with equal alternatives to be grouped"); + helper.assertTrue(groupedInputs.get(0).size() == 2, "Expected the grouped input to keep its alternatives"); + for (IPrototypedIngredient alternative : groupedInputs.get(0)) { + helper.assertTrue(((ItemStack) alternative.getPrototype()).getCount() == 2, + "Expected the quantity of every alternative to be summed"); + } + helper.assertTrue(((ItemStack) groupedInputs.get(1).get(0).getPrototype()).getCount() == 1, + "Expected the ungrouped input to be unchanged"); + + helper.succeed(); + } + +} 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 95aedcf30..c4261f811 100644 --- a/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalCraftingOptionRecipeDefinition.java +++ b/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalCraftingOptionRecipeDefinition.java @@ -1,12 +1,15 @@ package org.cyclops.integratedterminals.modcompat.integratedcrafting; import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; 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.Iterator; +import java.util.List; import java.util.stream.Collectors; /** @@ -55,6 +58,15 @@ public Collection getInputs(IngredientComponent ingredientCom .collect(Collectors.toList()); } + @Override + public List>> getInputAlternatives(IngredientComponent ingredientComponent) { + return prioritizedRecipe.getInputs(ingredientComponent) + .stream() + .>>map(input -> Lists.newArrayList(input.getAlternatives())) + .filter(alternatives -> !alternatives.isEmpty()) + .collect(Collectors.toList()); + } + public IngredientComponent getIngredientComponent() { return ingredientComponent; } diff --git a/src/main/java/org/cyclops/integratedterminals/proxy/ClientProxy.java b/src/main/java/org/cyclops/integratedterminals/proxy/ClientProxy.java index 4dfb6960a..7c68e9286 100644 --- a/src/main/java/org/cyclops/integratedterminals/proxy/ClientProxy.java +++ b/src/main/java/org/cyclops/integratedterminals/proxy/ClientProxy.java @@ -4,6 +4,7 @@ import net.minecraft.client.KeyMapping; import net.minecraft.client.Minecraft; import net.minecraft.client.player.LocalPlayer; +import net.neoforged.neoforge.client.event.RegisterClientTooltipComponentFactoriesEvent; import net.neoforged.neoforge.client.event.RegisterKeyMappingsEvent; import net.neoforged.neoforge.client.settings.KeyConflictContext; import net.neoforged.neoforge.client.settings.KeyModifier; @@ -14,6 +15,8 @@ import org.cyclops.cyclopscore.proxy.ClientProxyComponent; 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.CraftingOptionIngredientsTooltip; import org.cyclops.integratedterminals.item.ItemTerminalStoragePortable; import org.cyclops.integratedterminals.network.packet.TerminalStorageIngredientItemOpenGenericPacket; import org.lwjgl.glfw.GLFW; @@ -55,6 +58,8 @@ public class ClientProxy extends ClientProxyComponent { public ClientProxy() { super(new CommonProxy()); + + getMod().getModEventBus().addListener(this::registerClientTooltipComponentFactories); } @Override @@ -62,6 +67,10 @@ public ModBase getMod() { return IntegratedTerminals._instance; } + public void registerClientTooltipComponentFactories(RegisterClientTooltipComponentFactoriesEvent event) { + event.register(CraftingOptionIngredientsTooltip.class, ClientCraftingOptionIngredientsTooltip::new); + } + @Override public void registerKeyBindings(IKeyRegistry keyRegistry, RegisterKeyMappingsEvent event) { event.register(TERMINAL_TAB_NEXT);