diff --git a/src/main/java/codechicken/nei/BookmarkPanel.java b/src/main/java/codechicken/nei/BookmarkPanel.java index ceadf0a1..9ffc7b71 100644 --- a/src/main/java/codechicken/nei/BookmarkPanel.java +++ b/src/main/java/codechicken/nei/BookmarkPanel.java @@ -1048,15 +1048,9 @@ public class BookmarkPanel extends PanelWidget { final BookmarksGridSlot slot = this.grid.getSlotMouseOver(mousex, mousey); if (slot != null) { - int multiplier = 1; - - if (NEIClientUtils.altKey()) { - multiplier = NEIClientConfig.showItemQuantityWidget() ? NEIClientConfig.getItemQuantity() : 0; - if (multiplier == 0) { - multiplier = StackInfo.isFluidDisplayItem(slot.getItemStack()) ? 144 - : slot.getItemStack().getMaxStackSize(); - } - } + final long multiplier = NEIClientUtils.altKey() + ? NEIClientUtils.getScrollMultiplier(slot.getItemStack()) + : 1; this.grid.shiftItemAmount(slot.itemIndex, multiplier * shift); return true; diff --git a/src/main/java/codechicken/nei/ClientHandler.java b/src/main/java/codechicken/nei/ClientHandler.java index 2de5f576..3d229532 100644 --- a/src/main/java/codechicken/nei/ClientHandler.java +++ b/src/main/java/codechicken/nei/ClientHandler.java @@ -48,6 +48,7 @@ import codechicken.nei.api.API; import codechicken.nei.api.GuiInfo; import codechicken.nei.api.IConfigureNEI; import codechicken.nei.api.ItemInfo; +import codechicken.nei.bookmark.tree.GuiCraftingTree; import codechicken.nei.guihook.GuiContainerManager; import codechicken.nei.recipe.GuiRecipeTab; import codechicken.nei.recipe.StackInfo; @@ -279,6 +280,7 @@ public class ClientHandler { PacketCustom.assignHandler(NEICPH.channel, new NEICPH()); FMLCommonHandler.instance().bus().register(instance); MinecraftForge.EVENT_BUS.register(instance); + MinecraftForge.EVENT_BUS.register(new GuiCraftingTree.EventHandler()); API.registerHighlightHandler(new DefaultHighlightHandler(), ItemInfo.Layout.HEADER); HUDRenderer.load(); diff --git a/src/main/java/codechicken/nei/ItemQuantityField.java b/src/main/java/codechicken/nei/ItemQuantityField.java index 1b9d6cec..e0122ba5 100644 --- a/src/main/java/codechicken/nei/ItemQuantityField.java +++ b/src/main/java/codechicken/nei/ItemQuantityField.java @@ -83,11 +83,7 @@ public class ItemQuantityField extends TextField { } if (amount == 0) { - amount = NEIClientConfig.showItemQuantityWidget() ? NEIClientConfig.getItemQuantity() : 0; - - if (amount == 0) { - amount = StackInfo.isFluidDisplayItem(stack) ? 144 : stack.getMaxStackSize(); - } + amount = NEIClientUtils.getScrollMultiplier(stack); } return StackInfo.withAmount(stack, amount); diff --git a/src/main/java/codechicken/nei/ItemsTooltipLineHandler.java b/src/main/java/codechicken/nei/ItemsTooltipLineHandler.java index bc64bdef..2ff46a40 100644 --- a/src/main/java/codechicken/nei/ItemsTooltipLineHandler.java +++ b/src/main/java/codechicken/nei/ItemsTooltipLineHandler.java @@ -79,6 +79,10 @@ public class ItemsTooltipLineHandler implements ITooltipLineHandler { return this.activeStackIndex == -1 ? null : this.items.get(this.activeStackIndex); } + public List getItems() { + return this.items; + } + public boolean isEmpty() { return this.items.isEmpty(); } diff --git a/src/main/java/codechicken/nei/NEIClientConfig.java b/src/main/java/codechicken/nei/NEIClientConfig.java index 2feb7266..7f1a3ed2 100644 --- a/src/main/java/codechicken/nei/NEIClientConfig.java +++ b/src/main/java/codechicken/nei/NEIClientConfig.java @@ -873,6 +873,7 @@ public class NEIClientConfig { API.addKeyBind("bookmark.remove_recipe", Keyboard.KEY_A); API.addKeyBind("bookmark.pull_items", Keyboard.KEY_V); API.addKeyBind("bookmark.chat_link", Keyboard.KEY_L); + API.addKeyBind("bookmark.open_crafting_tree", Keyboard.KEY_T); API.addKeyBind("bookmark.hide", Keyboard.KEY_B); API.addKeyBind("itemzoom.toggle", Keyboard.KEY_Z); diff --git a/src/main/java/codechicken/nei/NEIClientUtils.java b/src/main/java/codechicken/nei/NEIClientUtils.java index a1cd5282..649819d0 100644 --- a/src/main/java/codechicken/nei/NEIClientUtils.java +++ b/src/main/java/codechicken/nei/NEIClientUtils.java @@ -564,6 +564,16 @@ public class NEIClientUtils extends NEIServerUtils { } } + public static long getScrollMultiplier(ItemStack stack) { + final long multiplier = NEIClientConfig.showItemQuantityWidget() ? NEIClientConfig.getItemQuantity() : 0; + + if (multiplier != 0) { + return multiplier; + } + + return StackInfo.itemStackToNBT(stack).hasKey("gtFluidName") ? 144 : stack.getMaxStackSize(); + } + public static void drawRect(double left, double top, double width, double height, Color color) { final boolean is2DTexture = GL11.glGetBoolean(GL11.GL_TEXTURE_2D); final boolean isBlend = GL11.glGetBoolean(GL11.GL_BLEND); @@ -606,6 +616,7 @@ public class NEIClientUtils extends NEIServerUtils { GuiDraw.fontRenderer.drawString(text, 0, 0, color, shadow); GL11.glScaled(1 / smallTextScale, 1 / smallTextScale, 1); GL11.glTranslated(-1 * offsetX, -1 * offsetY, 0); + GL11.glColor4f(1, 1, 1, 1); }); } diff --git a/src/main/java/codechicken/nei/api/INEIGuiAdapter.java b/src/main/java/codechicken/nei/api/INEIGuiAdapter.java index ab12e41b..986c2257 100644 --- a/src/main/java/codechicken/nei/api/INEIGuiAdapter.java +++ b/src/main/java/codechicken/nei/api/INEIGuiAdapter.java @@ -1,15 +1,9 @@ package codechicken.nei.api; -import java.util.Collections; -import java.util.List; import java.util.regex.Pattern; -import net.minecraft.client.gui.inventory.GuiContainer; -import net.minecraft.item.ItemStack; import net.minecraft.util.EnumChatFormatting; -import codechicken.nei.VisiblityData; - /** * Lets you just override those things you want to */ @@ -17,31 +11,6 @@ public class INEIGuiAdapter implements INEIGuiHandler { static final Pattern SPECIAL_REGEX_CHARS = Pattern.compile("[{}()\\[\\].+*?^$\\\\|]"); - @Override - public VisiblityData modifyVisiblity(GuiContainer gui, VisiblityData currentVisibility) { - return currentVisibility; - } - - @Override - public Iterable getItemSpawnSlots(GuiContainer gui, ItemStack item) { - return Collections.emptyList(); - } - - @Override - public List getInventoryAreas(GuiContainer gui) { - return null; - } - - @Override - public boolean handleDragNDrop(GuiContainer gui, int mousex, int mousey, ItemStack draggedStack, int button) { - return false; - } - - @Override - public boolean hideItemPanelSlot(GuiContainer gui, int x, int y, int w, int h) { - return false; - } - protected String formattingText(String displayName) { return SPECIAL_REGEX_CHARS.matcher(EnumChatFormatting.getTextWithoutFormattingCodes(displayName)) .replaceAll("\\\\$0"); diff --git a/src/main/java/codechicken/nei/api/INEIGuiHandler.java b/src/main/java/codechicken/nei/api/INEIGuiHandler.java index b6af67d8..3ffffe96 100644 --- a/src/main/java/codechicken/nei/api/INEIGuiHandler.java +++ b/src/main/java/codechicken/nei/api/INEIGuiHandler.java @@ -1,5 +1,6 @@ package codechicken.nei.api; +import java.util.Collections; import java.util.List; import net.minecraft.client.gui.inventory.GuiContainer; @@ -12,23 +13,29 @@ import codechicken.nei.VisiblityData; */ public interface INEIGuiHandler { - public VisiblityData modifyVisiblity(GuiContainer gui, VisiblityData currentVisibility); + default VisiblityData modifyVisiblity(GuiContainer gui, VisiblityData currentVisibility) { + return currentVisibility; + } /** * NEI will give the specified item to the InventoryRange returned if the player's inventory is full. Should not * return null, just an empty list */ - public Iterable getItemSpawnSlots(GuiContainer gui, ItemStack item); + default Iterable getItemSpawnSlots(GuiContainer gui, ItemStack item) { + return Collections.emptyList(); + } /** * @return A list of TaggedInventoryAreas that will be used with the savestates. */ - public List getInventoryAreas(GuiContainer gui); + default List getInventoryAreas(GuiContainer gui) { + return null; + } /** * Handles clicks while an itemstack has been dragged from the item panel. Use this to set configurable slots and * the like. Changes made to the stackSize of the dragged stack will be kept - * + * * @param gui The current gui instance * @param mousex The x position of the mouse * @param mousey The y position of the mouse @@ -37,18 +44,22 @@ public interface INEIGuiHandler { * @return True if the drag n drop was handled. False to resume processing through other routes. The held stack will * be deleted if draggedStack.stackSize == 0 */ - public boolean handleDragNDrop(GuiContainer gui, int mousex, int mousey, ItemStack draggedStack, int button); + default boolean handleDragNDrop(GuiContainer gui, int mousex, int mousey, ItemStack draggedStack, int button) { + return false; + } /** * Used to prevent the item panel from drawing on top of other gui elements. This function will also be called with * a 1x1 size rectangle on the mouse position for determining if the given coordinate should override item panel * functions such as scrolling - * + * * @param x The x coordinate of the rectangle bounding the slot * @param y The y coordinate of the rectangle bounding the slot * @param w The w coordinate of the rectangle bounding the slot * @param h The h coordinate of the rectangle bounding the slot * @return true if the item panel slot within the specified rectangle should not be rendered. */ - public boolean hideItemPanelSlot(GuiContainer gui, int x, int y, int w, int h); + default boolean hideItemPanelSlot(GuiContainer gui, int x, int y, int w, int h) { + return false; + } } diff --git a/src/main/java/codechicken/nei/api/ShortcutInputHandler.java b/src/main/java/codechicken/nei/api/ShortcutInputHandler.java index 04c81c01..dd8e33af 100644 --- a/src/main/java/codechicken/nei/api/ShortcutInputHandler.java +++ b/src/main/java/codechicken/nei/api/ShortcutInputHandler.java @@ -32,6 +32,7 @@ import codechicken.nei.bookmark.BookmarkItem; import codechicken.nei.bookmark.BookmarkItem.BookmarkItemType; import codechicken.nei.bookmark.BookmarkPayload; import codechicken.nei.bookmark.BookmarksGridSlot; +import codechicken.nei.bookmark.tree.GuiCraftingTree; import codechicken.nei.recipe.AutoCraftingManager; import codechicken.nei.recipe.GuiCraftingRecipe; import codechicken.nei.recipe.GuiFavoriteButton; @@ -101,6 +102,12 @@ public abstract class ShortcutInputHandler { return true; } + if (ItemPanels.bookmarkPanel.getGrid().isCraftingMode(groupId) + && KeyManager.isKeyDown("bookmark.open_crafting_tree")) { + GuiCraftingTree.openCraftingTreeGui(ItemPanels.bookmarkPanel.getGrid(), groupId); + return true; + } + if (NEIClientConfig.autocraftingEnabled() && KeyManager.isKeyDown("gui.craft_items") && NEIClientUtils.shiftKey() && ItemPanels.bookmarkPanel.getGrid().isCraftingMode(groupId)) { @@ -477,6 +484,7 @@ public abstract class ShortcutInputHandler { final Map hotkeys = new HashMap<>(); if (groupId != -1) { + final boolean isCraftingMode = ItemPanels.bookmarkPanel.getGrid().isCraftingMode(groupId); hotkeys.put( KeyManager.getKeyName("bookmark.remove_recipe", NEIClientUtils.SHIFT_HASH), @@ -486,6 +494,12 @@ public abstract class ShortcutInputHandler { KeyManager.getKeyName("bookmark.chat_link", NEIClientUtils.CTRL_HASH), NEIClientUtils.translate("bookmark.group.chat_link")); + if (isCraftingMode) { + hotkeys.put( + KeyManager.getKeyName("bookmark.open_crafting_tree"), + NEIClientUtils.translate("bookmark.group.open_crafting_tree")); + } + if (BookmarkContainerInfo.getBookmarkContainerHandler(gui) != null) { hotkeys.put( KeyManager.getKeyName("bookmark.pull_items"), @@ -495,7 +509,7 @@ public abstract class ShortcutInputHandler { NEIClientUtils.translate("bookmark.group.pull_items_shift")); } - if (NEIClientConfig.autocraftingEnabled() && ItemPanels.bookmarkPanel.getGrid().isCraftingMode(groupId)) { + if (NEIClientConfig.autocraftingEnabled() && isCraftingMode) { hotkeys.put( NEIClientConfig .getKeyName("gui.craft_items", NEIClientUtils.SHIFT_HASH + NEIClientUtils.CTRL_HASH), diff --git a/src/main/java/codechicken/nei/bookmark/BookmarkGrid.java b/src/main/java/codechicken/nei/bookmark/BookmarkGrid.java index cf52df15..942fcfa8 100644 --- a/src/main/java/codechicken/nei/bookmark/BookmarkGrid.java +++ b/src/main/java/codechicken/nei/bookmark/BookmarkGrid.java @@ -179,11 +179,15 @@ public class BookmarkGrid extends ItemsGrid item.groupId).max().orElse(DEFAULT_GROUP_ID) + 1; + final int groupId = getNextGroupId(); this.groups.put(groupId, group); return groupId; } + public int getNextGroupId() { + return this.bookmarkItems.stream().mapToInt(item -> item.groupId).max().orElse(DEFAULT_GROUP_ID) + 1; + } + @Override public int getNumPages() { @@ -950,7 +954,7 @@ public class BookmarkGrid extends ItemsGrid= this.widgetX && y >= this.widgetY + && x < this.widgetX + this.widgetW + && y < this.widgetY + this.widgetH; + } + + public void startDrag(int mouseX, int mouseY) { + this.dragging = true; + this.lastDragX = mouseX; + this.lastDragY = mouseY; + } + + public void drag(int mouseX, int mouseY) { + if (!this.dragging) { + return; + } + + this.scrollX -= (mouseX - this.lastDragX) / this.zoomLevel; + this.scrollY -= (mouseY - this.lastDragY) / this.zoomLevel; + this.lastDragX = mouseX; + this.lastDragY = mouseY; + } + + public void endDrag() { + this.dragging = false; + } + + public boolean isDragging() { + return this.dragging; + } + + public void scroll(int mouseX, int mouseY, int direction, boolean shiftKey, boolean ctrlKey) { + + if (shiftKey) { + this.scrollX -= direction * this.widgetW / (SCROLL_STEP_DIVISOR * this.zoomLevel); + } else if (ctrlKey) { + final int localX = mouseX - this.widgetX; + final int localY = mouseY - this.widgetY; + final float oldZoom = this.zoomLevel; + + this.zoomLevel = clamp((float) (this.zoomLevel * Math.pow(ZOOM_STEP_BASE, direction)), ZOOM_MIN, ZOOM_MAX); + this.scrollX += localX / oldZoom - localX / this.zoomLevel; + this.scrollY += localY / oldZoom - localY / this.zoomLevel; + } else { + this.scrollY -= direction * this.widgetH / (SCROLL_STEP_DIVISOR * this.zoomLevel); + } + + } + + public void centerOn(float worldX, float worldY) { + this.scrollX = worldX - (this.widgetW / this.zoomLevel) / 2f; + this.scrollY = worldY - (this.widgetH / this.zoomLevel) / 2f; + } + + public void clampToBounds(int treeWidth, int treeHeight) { + this.scrollX = clamp(this.scrollX, -this.widgetW / this.zoomLevel + 4, treeWidth - 4); + this.scrollY = clamp(this.scrollY, -this.widgetH / this.zoomLevel + 4, treeHeight - 4); + } + + public int visibleRowMin() { + final float zScrollY = this.scrollY * this.zoomLevel; + return (int) ((zScrollY - CULL_MARGIN) / this.zoomLevel) - CULL_ROW_MARGIN; + } + + public int visibleRowMax() { + final float zScrollY = this.scrollY * this.zoomLevel; + return (int) ((zScrollY + CULL_MARGIN) / this.zoomLevel) + (int) (this.widgetH / this.zoomLevel) + + CULL_ROW_MARGIN; + } + + public Point2D.Double toWorldPoint(Point2D mouse) { + final float zScrollX = this.scrollX * this.zoomLevel; + final float zScrollY = this.scrollY * this.zoomLevel; + + return new Point2D.Double( + (mouse.getX() - this.widgetX + zScrollX) / this.zoomLevel, + (mouse.getY() - this.widgetY + zScrollY) / this.zoomLevel); + } + + static float clamp(float value, float min, float max) { + return value < min ? min : Math.min(value, max); + } + +} diff --git a/src/main/java/codechicken/nei/bookmark/tree/CraftingTreeCanvas.java b/src/main/java/codechicken/nei/bookmark/tree/CraftingTreeCanvas.java new file mode 100644 index 00000000..8cdedc13 --- /dev/null +++ b/src/main/java/codechicken/nei/bookmark/tree/CraftingTreeCanvas.java @@ -0,0 +1,719 @@ +package codechicken.nei.bookmark.tree; + +import java.awt.Color; +import java.awt.Point; +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.function.BiPredicate; + +import net.minecraft.client.renderer.RenderHelper; + +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; + +import codechicken.lib.gui.GuiDraw; +import codechicken.lib.vec.Rectangle4i; +import codechicken.nei.NEIClientUtils; +import codechicken.nei.NEIClientUtils.Alignment; +import codechicken.nei.PositionedStack; +import codechicken.nei.bookmark.BookmarkItem; +import codechicken.nei.drawable.DrawableBuilder; +import codechicken.nei.drawable.DrawableResource; +import codechicken.nei.guihook.GuiContainerManager; +import codechicken.nei.recipe.Recipe.RecipeId; +import codechicken.nei.scroll.GuiHelper; +import codechicken.nei.util.ReadableNumberConverter; + +class CraftingTreeCanvas { + + private static final DrawableResource SCROLLBAR_H = new DrawableBuilder( + "nei:textures/gui/craftingtree.png", + 0, + 88, + 2, + 24).build(); + + private static final DrawableResource SCROLLBAR_W = new DrawableBuilder( + "nei:textures/gui/craftingtree.png", + 0, + 84, + 24, + 2).build(); + + private static final DrawableResource ICON_COLLAPSED = new DrawableBuilder( + "nei:textures/gui/craftingtree.png", + 4, + 88, + 5, + 5).build(); + + private static final DrawableResource ICON_COLLAPSED_HOVER = new DrawableBuilder( + "nei:textures/gui/craftingtree.png", + 11, + 88, + 5, + 5).build(); + + private static final DrawableResource ICON_EXPANDED = new DrawableBuilder( + "nei:textures/gui/craftingtree.png", + 4, + 95, + 5, + 5).build(); + + private static final DrawableResource ICON_EXPANDED_HOVER = new DrawableBuilder( + "nei:textures/gui/craftingtree.png", + 11, + 95, + 5, + 5).build(); + + private static final DrawableResource ICON_REMOVE = new DrawableBuilder( + "nei:textures/gui/craftingtree.png", + 85, + 48, + 16, + 16).build(); + + private static final DrawableResource ICON_CRAFTING = new DrawableBuilder( + "nei:textures/gui/craftingtree.png", + 67, + 48, + 16, + 16).build(); + + private static final DrawableResource SLOT_DEFAULT = new DrawableBuilder( + "nei:textures/gui/craftingtree.png", + 68, + 2, + 18, + 18).build(); + + private static final DrawableResource SLOT_MISSING = new DrawableBuilder( + "nei:textures/gui/craftingtree.png", + 92, + 2, + 18, + 18).build(); + + private static final DrawableResource SLOT_CRAFTING = new DrawableBuilder( + "nei:textures/gui/craftingtree.png", + 116, + 2, + 18, + 18).build(); + + private static final DrawableResource SLOT_DISABLED = new DrawableBuilder( + "nei:textures/gui/craftingtree.png", + 140, + 2, + 18, + 18).build(); + + private static final DrawableResource SLOT_HIGHLIGHTED = new DrawableBuilder( + "nei:textures/gui/craftingtree.png", + 164, + 2, + 18, + 18).build(); + + private static final Color LINE_COLOR = new Color(0xFF888888, true); + private static final int HOVER_COLOR = 0x66888888; + private static final float BADGE_SCALE = 0.6f; + + public static final int SLOT_SIZE = 18; + public static final int X_SPACING = 24; + public static final int Y_SPACING = 14; + public static final int ROOT_SPACING = 36; + private static final float FIT_MARGIN = 8f; + + public final CraftingTreeGraph graph = new CraftingTreeGraph(); + + public final Map pointByNode = new HashMap<>(); + public final TreeMap> rows = new TreeMap<>(); + public final Set collapsedRecipes = Collections.newSetFromMap(new HashMap<>()); + public int treeWidth = 0; + public int treeHeight = 0; + + private final Camera camera = new Camera(); + + public void setViewport(int x, int y, int w, int h) { + this.camera.setViewport(x, y, w, h); + } + + public boolean isInWidget(int x, int y) { + return this.camera.isInWidget(x, y); + } + + public void fitToView() { + final float firstResultCenterX = this.graph.roots.isEmpty() ? this.treeWidth / 2f + : this.pointByNode.get(this.graph.roots.get(0)).x + SLOT_SIZE / 2f; + + this.camera.zoomLevel = 1.0f; + this.camera.scrollX = this.treeWidth <= (this.camera.widgetW - FIT_MARGIN) + ? (this.treeWidth - this.camera.widgetW) / 2f + : firstResultCenterX - this.camera.widgetW / 2f; + this.camera.scrollY = this.treeHeight <= (this.camera.widgetH - FIT_MARGIN) + ? (this.treeHeight - this.camera.widgetH) / 2f + : -FIT_MARGIN; + } + + public void startDrag(int mouseX, int mouseY) { + this.camera.startDrag(mouseX, mouseY); + } + + public void drag(int mouseX, int mouseY) { + this.camera.drag(mouseX, mouseY); + } + + public void endDrag() { + this.camera.endDrag(); + } + + public boolean isDragging() { + return this.camera.isDragging(); + } + + public void scroll(int mouseX, int mouseY, int direction, boolean shiftKey, boolean ctrlKey) { + this.camera.scroll(mouseX, mouseY, direction, shiftKey, ctrlKey); + } + + public void scrollTo(ItemTreeSlot node) { + if (node == null || !this.pointByNode.containsKey(node)) { + return; + } + + final Point point = this.pointByNode.get(node); + final float nodeCenterX = point.x + SLOT_SIZE / 2f; + final float nodeCenterY = point.y + SLOT_SIZE / 2f; + + this.camera.centerOn(nodeCenterX, nodeCenterY); + } + + public ItemTreeSlot getNodeMouseOver(Point2D mouse) { + return findVisibleNode(mouse, (node, local) -> { + final Point point = this.pointByNode.get(node); + + return local.x >= point.x && local.x < point.x + SLOT_SIZE + && local.y >= point.y + && local.y < point.y + SLOT_SIZE; + }); + } + + public ItemTreeSlot getHandlerBadgeMouseOver(Point2D mouse) { + return findVisibleNode(mouse, (node, local) -> { + if (node.prefItem == null || !this.graph.childrenOf.containsKey(node)) { + return false; + } + + final Point point = this.pointByNode.get(node); + final float[] badge = badgeRect(point.x, point.y); + + return local.x >= badge[0] && local.x < badge[0] + badge[2] + && local.y >= badge[1] + && local.y < badge[1] + badge[3]; + }); + } + + public ItemTreeSlot getCollapseArrowMouseOver(Point2D mouse) { + return findVisibleNode(mouse, (node, local) -> { + if (!this.graph.childrenOf.containsKey(node) || node.prefItem == null + || this.collapsedRecipes.contains(node.prefItem.recipeId) && !this.graph.roots.contains(node)) { + return false; + } + + final Point point = this.pointByNode.get(node); + final float iconX = point.x + SLOT_SIZE / 2 - ICON_COLLAPSED.width - 1; + final float iconY = point.y + SLOT_SIZE + 1; + final float iconW = ICON_COLLAPSED.width; + final float iconH = ICON_COLLAPSED.height; + + return local.x >= iconX && local.x < iconX + iconW && local.y >= iconY && local.y < iconY + iconH; + }); + } + + private ItemTreeSlot findVisibleNode(Point2D mouse, BiPredicate hitTest) { + final Point2D.Double local = this.camera.toWorldPoint(mouse); + + for (List row : this.rows.subMap(camera.visibleRowMin(), camera.visibleRowMax()).values()) { + for (ItemTreeSlot node : row) { + if (hitTest.test(node, local)) { + return node; + } + } + } + + return null; + } + + public void draw(Point2D mouse, Set collapsedItems, SearchResult searchResult, + boolean useInventorySnapshot, ItemTreeSlot hoveredNode, ItemTreeSlot deleteHoverNode) { + this.camera.clampToBounds(this.treeWidth, this.treeHeight); + + final float zScrollX = this.camera.scrollX * this.camera.zoomLevel; + final float zScrollY = this.camera.scrollY * this.camera.zoomLevel; + final int cropXMin = (int) ((zScrollX - 32) / this.camera.zoomLevel); + final int cropXMax = (int) ((zScrollX + 32) / this.camera.zoomLevel) + + (int) (this.camera.widgetW / this.camera.zoomLevel); + final ItemTreeSlot collapseHoverNode = this.camera.isDragging() ? null : getCollapseArrowMouseOver(mouse); + + GuiHelper.useScissor(this.camera.widgetX, this.camera.widgetY, this.camera.widgetW, this.camera.widgetH, () -> { + GL11.glPushMatrix(); + GL11.glPushAttrib(GL11.GL_ENABLE_BIT | GL11.GL_COLOR_BUFFER_BIT | GL11.GL_LIGHTING_BIT); + GL11.glColor4f(1, 1, 1, 1); + GL11.glEnable(GL11.GL_BLEND); + GL11.glEnable(GL12.GL_RESCALE_NORMAL); + RenderHelper.enableGUIStandardItemLighting(); + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glTranslatef(this.camera.widgetX - zScrollX, this.camera.widgetY - zScrollY, 0.0f); + GL11.glScalef(this.camera.zoomLevel, this.camera.zoomLevel, 1.0f); + + for (List row : this.rows.subMap(this.camera.visibleRowMin(), this.camera.visibleRowMax()) + .values()) { + for (ItemTreeSlot node : row) { + final Point point = this.pointByNode.get(node); + + drawParentLine(node); + + if (point.x >= cropXMin && point.x <= cropXMax) { + drawSlot(node, useInventorySnapshot, searchResult, collapsedItems, node == hoveredNode); + drawCollapseIcon(node, collapsedItems, node == collapseHoverNode); + + if (node == hoveredNode) { + NEIClientUtils.gl2DRenderContext( + () -> { + GuiDraw.drawRect( + point.x + 1, + point.y + 1, + SLOT_SIZE - 2, + SLOT_SIZE - 2, + HOVER_COLOR); + }); + } + + if (node.prefItem != null && this.graph.childrenOf.containsKey(node)) { + drawSlotBadge(node, point.x, point.y); + + if (node == deleteHoverNode) { + drawDeleteButton(point); + } + } + + } + } + } + + GL11.glPopAttrib(); + GL11.glPopMatrix(); + }); + + drawScrollbars(); + } + + public void drawFull(Set collapsedItems, SearchResult searchResult, boolean useInventorySnapshot) { + GL11.glPushMatrix(); + GL11.glPushAttrib(GL11.GL_ENABLE_BIT | GL11.GL_COLOR_BUFFER_BIT | GL11.GL_LIGHTING_BIT); + GL11.glColor4f(1, 1, 1, 1); + GL11.glEnable(GL11.GL_BLEND); + GL11.glEnable(GL12.GL_RESCALE_NORMAL); + RenderHelper.enableGUIStandardItemLighting(); + GL11.glDisable(GL11.GL_LIGHTING); + + for (List row : this.rows.values()) { + for (ItemTreeSlot node : row) { + final Point point = this.pointByNode.get(node); + + drawParentLine(node); + drawSlot(node, useInventorySnapshot, searchResult, collapsedItems, false); + + if (node.prefItem != null && this.graph.childrenOf.containsKey(node)) { + drawSlotBadge(node, point.x, point.y); + } + } + } + + GL11.glPopAttrib(); + GL11.glPopMatrix(); + } + + private void drawSlot(ItemTreeSlot node, boolean useInventorySnapshot, SearchResult searchResult, + Set collapsedItems, boolean hovered) { + final Point point = this.pointByNode.get(node); + + if (searchResult != null) { + + if (node == searchResult.highlightNode) { + SLOT_HIGHLIGHTED.draw(point.x, point.y); + } else if (searchResult.searchMatches.contains(node)) { + SLOT_DEFAULT.draw(point.x, point.y); + } else { + SLOT_DISABLED.draw(point.x, point.y); + } + + } else if (useInventorySnapshot) { + + if (node.leftAmount == 0) { + + if (this.graph.parentOf.get(node) != null && this.graph.parentOf.get(node).leftAmount == 0 + || node.requestedAmount == 0) { + SLOT_DISABLED.draw(point.x, point.y); + } else { + SLOT_DEFAULT.draw(point.x, point.y); + } + + } else if (!this.graph.childrenOf.containsKey(node)) { + SLOT_MISSING.draw(point.x, point.y); + } else if (collapsedItems.contains(node.ingrItem)) { + + if (this.graph.subtreeMissingCache.contains(node)) { + SLOT_MISSING.draw(point.x, point.y); + } else { + SLOT_CRAFTING.draw(point.x, point.y); + } + + } else if (!this.graph.childrenNeedCraftingCache.contains(node)) { + SLOT_CRAFTING.draw(point.x, point.y); + } else { + SLOT_DEFAULT.draw(point.x, point.y); + } + + } else { + SLOT_DEFAULT.draw(point.x, point.y); + } + + GuiContainerManager + .drawItem(point.x + (SLOT_SIZE - 16) / 2, point.y + (SLOT_SIZE - 16) / 2, node.emptyStack, true); + + if (node.requestedAmount > 0 && node.leftAmount == 0) { + drawStackSize(node, node.ingrItem.getStackSize(node.requestedAmount), point.x, point.y, true); + } else if (node.requestedAmount > 0 && node.leftAmount > 0) { + final long amount = hovered && node.prefItem != null + && this.graph.roots.contains(node) + && this.collapsedRecipes.contains(node.prefItem.recipeId) + && NEIClientUtils.controlKey() ? node.currentRequestedAmount : node.requestedAmount; + + drawStackSize(node, node.ingrItem.getStackSize(amount), point.x, point.y, false); + } + + } + + protected void drawStackSize(ItemTreeSlot node, long stackSize, int x, int y, boolean resolved) { + String amountString = stackSize < 10_000 ? String.valueOf(stackSize) + : ReadableNumberConverter.INSTANCE.toWideReadableForm(stackSize); + + if (node.isFluidDisplay) { + amountString += "L"; + } + + final boolean hasOutputChance = node.prefItem != null && node.prefItem.chance != PositionedStack.CHANCE_FULL; + int color = 0xFFFFFF; + + if (node.ingrItem.chance != PositionedStack.CHANCE_FULL || hasOutputChance || node.isContainerItem) { + amountString = "~" + amountString; + color = 0xFFAA00; + } + + if (resolved) { + color = 0x55FF55; + } + + NEIClientUtils.drawNEIOverlayText( + amountString, + new Rectangle4i( + x + (SLOT_SIZE - 16) / 2, + y + (SLOT_SIZE - 16) / 2, + SLOT_SIZE - (SLOT_SIZE - 16), + SLOT_SIZE - (SLOT_SIZE - 16)), + 1, + color, + true, + node.isFluidDisplay ? Alignment.BottomLeft : Alignment.BottomRight); + } + + protected void drawSlotBadge(ItemTreeSlot node, int x, int y) { + final float[] badge = badgeRect(x, y); + + GL11.glPushMatrix(); + GL11.glColor4f(1, 1, 1, 1); + GL11.glTranslatef(badge[0], badge[1], 4.0f); + GL11.glScalef(BADGE_SCALE, BADGE_SCALE, 1.0f); + + if (node.info.hasImageOrItem()) { + final DrawableResource image = node.info.getImage(); + + if (image != null) { + image.draw(0, 0); + } else { + GuiContainerManager.drawItem(0, 0, node.info.getItemStack(), true); + } + + } else { + ICON_CRAFTING.draw(0, 0); + } + + GL11.glPopMatrix(); + } + + private void drawCollapseIcon(ItemTreeSlot node, Set collapsedItems, boolean hover) { + + if (!this.graph.childrenOf.containsKey(node) || node.prefItem == null + || this.collapsedRecipes.contains(node.prefItem.recipeId) && !this.graph.roots.contains(node)) { + return; + } + + final DrawableResource icon; + + if (collapsedItems.contains(node.ingrItem)) { + icon = hover ? ICON_COLLAPSED_HOVER : ICON_COLLAPSED; + } else { + icon = hover ? ICON_EXPANDED_HOVER : ICON_EXPANDED; + } + + final Point point = this.pointByNode.get(node); + + GL11.glColor4f(1, 1, 1, 1); + icon.draw(point.x + SLOT_SIZE / 2 - icon.width - 1, point.y + SLOT_SIZE + 1); + } + + private void drawDeleteButton(Point point) { + final float[] badge = badgeRect(point.x, point.y); + + GL11.glPushMatrix(); + GL11.glColor4f(1, 1, 1, 1); + GL11.glTranslatef(badge[0], badge[1], 350.0f); + GL11.glScalef(BADGE_SCALE, BADGE_SCALE, 1.0f); + ICON_REMOVE.draw(0, 0); + GL11.glPopMatrix(); + } + + private static float[] badgeRect(float x, float y) { + final float w = ICON_REMOVE.width * BADGE_SCALE; + final float h = ICON_REMOVE.height * BADGE_SCALE; + + return new float[] { x - w / 2 + 1, y - h / 2 + 1, w, h }; + } + + private void drawParentLine(ItemTreeSlot node) { + + if (!this.graph.parentOf.containsKey(node)) { + return; + } + + final Point point = this.pointByNode.get(node); + final float x0 = point.x + SLOT_SIZE / 2f; + final float y0 = point.y - 1; + + final ItemTreeSlot parentNode = this.graph.parentOf.get(node); + final Point parentPoint = this.pointByNode.get(parentNode); + final float x1 = parentPoint.x + SLOT_SIZE / 2f; + final float y1 = parentPoint.y + SLOT_SIZE; + + if (x0 != x1) { + final float midY = (y0 + y1) / 2f; + + NEIClientUtils.drawRect(x0, Math.min(y0, midY), 1, Math.abs(midY - y0) + 1, LINE_COLOR); + NEIClientUtils.drawRect(Math.min(x0, x1), midY, Math.abs(x1 - x0) + 1, 1, LINE_COLOR); + NEIClientUtils.drawRect(x1, Math.min(midY, y1), 1, Math.abs(y1 - midY) + 1, LINE_COLOR); + } else { + NEIClientUtils.drawRect(x0, Math.min(y0, y1), 1, Math.abs(y1 - y0) + 1, LINE_COLOR); + } + } + + private void drawScrollbars() { + if (this.treeWidth <= 0 || this.treeHeight <= 0) { + return; + } + + final float scrollXPct = Camera.clamp(this.camera.scrollX / this.treeWidth, 0.0f, 1.0f); + final float scrollYPct = Camera.clamp(this.camera.scrollY / this.treeHeight, 0.0f, 1.0f); + + GL11.glTranslatef(0, 0, 500.0f); + + SCROLLBAR_H.draw( + this.camera.widgetX + this.camera.widgetW - 3, + (int) (this.camera.widgetY + scrollYPct * (this.camera.widgetH - SCROLLBAR_H.height))); + + SCROLLBAR_W.draw( + (int) (this.camera.widgetX + scrollXPct * (this.camera.widgetW - SCROLLBAR_W.width)), + this.camera.widgetY + this.camera.widgetH - 3); + + GL11.glTranslatef(0, 0, -500.0f); + } + + public void recalculateCoordinates(Set collapsedRecipes, Set collapsedItems, + ItemTreeSlot anchor) { + final Map offsetFromParent = new HashMap<>(); + final Point anchorOldPoint = anchor != null ? resolveAnchorPoint(anchor) : null; + + this.collapsedRecipes.clear(); + this.pointByNode.clear(); + this.rows.clear(); + this.treeWidth = 0; + this.treeHeight = 0; + + this.collapsedRecipes.addAll(collapsedRecipes); + + for (ItemTreeSlot node : this.graph.items) { + this.pointByNode.put(node, new Point(0, 0)); + } + + final List contour = new ArrayList<>(); + + for (ItemTreeSlot root : this.graph.roots) { + final List layout = computeLayout(root, collapsedItems, offsetFromParent); + final int shift = contour.isEmpty() ? 0 : requiredRootShift(contour, layout); + + mergeContour(contour, layout, shift); + assignPositions(root, shift, 0, collapsedItems, offsetFromParent); + } + + int minX = 0; + + for (Point point : this.pointByNode.values()) { + minX = Math.min(minX, point.x); + } + + if (minX < 0) { + for (Point point : this.pointByNode.values()) { + point.x -= minX; + } + } + + for (List row : this.rows.values()) { + for (ItemTreeSlot node : row) { + this.treeWidth = Math.max(this.treeWidth, this.pointByNode.get(node).x + CraftingTreeCanvas.SLOT_SIZE); + this.treeHeight = Math + .max(this.treeHeight, this.pointByNode.get(node).y + CraftingTreeCanvas.SLOT_SIZE); + } + } + + if (anchorOldPoint != null) { + final Point anchorNewPoint = resolveAnchorPoint(anchor); + + if (anchorNewPoint != null) { + this.camera.scrollX += anchorNewPoint.x - anchorOldPoint.x; + this.camera.scrollY += anchorNewPoint.y - anchorOldPoint.y; + } + } + } + + private Point resolveAnchorPoint(ItemTreeSlot anchor) { + Point result = this.pointByNode.get(anchor); + + if (result == null) { + for (Map.Entry entry : this.pointByNode.entrySet()) { + if (entry.getKey().ingrItem.equals(anchor.ingrItem)) { + return entry.getValue(); + } + } + + } + + return result; + } + + private List computeLayout(ItemTreeSlot node, Set collapsedItems, + Map offsetFromParent) { + List children = Collections.emptyList(); + + if (!collapsedItems.contains(node.ingrItem)) { + children = this.graph.childrenOf.getOrDefault(node, Collections.emptyList()); + } + + if (children.isEmpty()) { + final List rows = new ArrayList<>(); + rows.add(new int[] { 0, 0 }); + return rows; + } + + final int[] offsets = new int[children.size()]; + final List combined = new ArrayList<>(); + + for (int i = 0; i < children.size(); i++) { + final List childRows = computeLayout(children.get(i), collapsedItems, offsetFromParent); + final int shift = i == 0 ? 0 : requiredShift(combined, childRows, X_SPACING); + + offsets[i] = shift; + mergeContour(combined, childRows, shift); + } + + final int selfX = (offsets[0] + offsets[children.size() - 1]) / 2; + + for (int i = 0; i < children.size(); i++) { + offsetFromParent.put(children.get(i), offsets[i] - selfX); + } + + for (int[] row : combined) { + row[0] -= selfX; + row[1] -= selfX; + } + + final List rows = new ArrayList<>(combined.size() + 1); + rows.add(new int[] { 0, 0 }); + rows.addAll(combined); + + return rows; + } + + private static int requiredShift(List existing, List incoming, int spacing) { + int shift = 0; + final int rows = Math.min(existing.size(), incoming.size()); + + for (int r = 0; r < rows; r++) { + shift = Math.max(shift, existing.get(r)[1] + spacing - incoming.get(r)[0]); + } + + return shift; + } + + private static int requiredRootShift(List existing, List incoming) { + int existingRight = Integer.MIN_VALUE; + + for (int[] row : existing) { + existingRight = Math.max(existingRight, row[1]); + } + + int incomingLeft = Integer.MAX_VALUE; + + for (int[] row : incoming) { + incomingLeft = Math.min(incomingLeft, row[0]); + } + + return Math.max(0, existingRight + ROOT_SPACING - incomingLeft); + } + + private static void mergeContour(List target, List source, int shift) { + for (int r = 0; r < source.size(); r++) { + final int left = source.get(r)[0] + shift; + final int right = source.get(r)[1] + shift; + + if (r < target.size()) { + target.get(r)[0] = Math.min(target.get(r)[0], left); + target.get(r)[1] = Math.max(target.get(r)[1], right); + } else { + target.add(new int[] { left, right }); + } + } + } + + private void assignPositions(ItemTreeSlot node, int x, int y, Set collapsedItems, + Map offsetFromParent) { + this.rows.computeIfAbsent(y, ignored -> new ArrayList<>()).add(node); + this.pointByNode.put(node, new Point(x, y)); + + final int childY = y + CraftingTreeCanvas.SLOT_SIZE + Y_SPACING; + + if (!collapsedItems.contains(node.ingrItem)) { + for (ItemTreeSlot child : this.graph.childrenOf.getOrDefault(node, Collections.emptyList())) { + assignPositions(child, x + offsetFromParent.get(child), childY, collapsedItems, offsetFromParent); + } + } + + } +} diff --git a/src/main/java/codechicken/nei/bookmark/tree/CraftingTreeGraph.java b/src/main/java/codechicken/nei/bookmark/tree/CraftingTreeGraph.java new file mode 100644 index 00000000..ae68d893 --- /dev/null +++ b/src/main/java/codechicken/nei/bookmark/tree/CraftingTreeGraph.java @@ -0,0 +1,99 @@ +package codechicken.nei.bookmark.tree; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import codechicken.nei.recipe.Recipe.RecipeId; + +public class CraftingTreeGraph { + + public final List roots = new ArrayList<>(); + public final List items = new ArrayList<>(); + public final Map parentOf = new HashMap<>(); + public final Map> childrenOf = new HashMap<>(); + + public final Set subtreeMissingCache = new HashSet<>(); + public final Set childrenNeedCraftingCache = new HashSet<>(); + + public void rebuild(CraftingTreeMath math) { + this.subtreeMissingCache.clear(); + this.childrenNeedCraftingCache.clear(); + + math.refresh(); + + this.roots.clear(); + this.roots.addAll(math.rootSlots); + this.items.clear(); + this.items.addAll(math.items); + this.parentOf.clear(); + this.parentOf.putAll(math.parentOf); + this.childrenOf.clear(); + this.childrenOf.putAll(math.childrenOf); + + for (ItemTreeSlot root : this.roots) { + computeSubtreeMissing(root); + } + + for (Map.Entry> entry : this.childrenOf.entrySet()) { + if (entry.getValue().stream().anyMatch(child -> child.leftAmount != 0)) { + this.childrenNeedCraftingCache.add(entry.getKey()); + } + } + } + + public Set collectUnusedRecipes(ItemTreeSlot node) { + final Map> children = new HashMap<>(); + final Map> consumers = new HashMap<>(); + int rootCount = 0; + + for (ItemTreeSlot item : this.items) { + if (item.prefItem != null) { + children.computeIfAbsent(item.ingrItem.recipeId, k -> new HashSet<>()).add(item.prefItem.recipeId); + consumers.computeIfAbsent(item.prefItem.recipeId, k -> new HashSet<>()).add(item.ingrItem.recipeId); + + if (item.ingrItem.recipeId.equals(node.prefItem.recipeId)) { + rootCount++; + } + } + } + + final Set unusedRecipes = new HashSet<>(); + final RecipeId rootRecipeId = node.prefItem.recipeId; + + if (rootCount <= 1) { + unusedRecipes.add(rootRecipeId); + collectSubtree(rootRecipeId, children, consumers, unusedRecipes); + } + + return unusedRecipes; + } + + private void collectSubtree(RecipeId recipeId, Map> children, + Map> consumers, Set unusedRecipes) { + for (RecipeId child : children.getOrDefault(recipeId, Collections.emptySet())) { + if (consumers.get(child).size() == 1 && unusedRecipes.add(child)) { + collectSubtree(child, children, consumers, unusedRecipes); + } + } + } + + private boolean computeSubtreeMissing(ItemTreeSlot node) { + boolean hasMissing = !this.childrenOf.containsKey(node); + + for (ItemTreeSlot child : this.childrenOf.getOrDefault(node, Collections.emptyList())) { + hasMissing |= child.leftAmount != 0 && computeSubtreeMissing(child); + } + + if (hasMissing) { + this.subtreeMissingCache.add(node); + } + + return hasMissing; + } + +} diff --git a/src/main/java/codechicken/nei/bookmark/tree/CraftingTreeMath.java b/src/main/java/codechicken/nei/bookmark/tree/CraftingTreeMath.java new file mode 100644 index 00000000..48b09518 --- /dev/null +++ b/src/main/java/codechicken/nei/bookmark/tree/CraftingTreeMath.java @@ -0,0 +1,149 @@ +package codechicken.nei.bookmark.tree; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import codechicken.nei.bookmark.BookmarkItem; +import codechicken.nei.bookmark.BookmarkItem.BookmarkItemType; +import codechicken.nei.recipe.Recipe.RecipeId; +import codechicken.nei.recipe.chain.RecipeChainMath; + +public class CraftingTreeMath extends RecipeChainMath { + + public final List rootSlots = new ArrayList<>(); + public final List items = new ArrayList<>(); + public final Map parentOf = new HashMap<>(); + public final Map> childrenOf = new HashMap<>(); + + private final Deque parentStack = new ArrayDeque<>(); + private ItemTreeSlot lastItem = null; + + public CraftingTreeMath(List chainItems, Set collapsedRecipes) { + super(chainItems, collapsedRecipes); + } + + @Override + public RecipeChainMath refresh() { + this.rootSlots.clear(); + this.items.clear(); + this.parentOf.clear(); + this.childrenOf.clear(); + this.parentStack.clear(); + this.lastItem = null; + + return super.refresh(); + } + + @Override + protected void calculationStepTrigger(BookmarkItem ingrItem, BookmarkItem prefItem, long requestedAmount, + long containerAmount, long inventoryAmount, long ingrAmount, long produced) { + + if (ingrItem.type == BookmarkItemType.RESULT) { + final ItemTreeSlot slot = new ItemTreeSlot( + ingrItem, + Math.max(0, this.requiredAmount.get(prefItem) - requestedAmount), + requestedAmount, + this.requiredAmount.get(prefItem), + 0, + prefItem); + + this.rootSlots.add(slot); + this.items.add(slot); + + this.lastItem = slot; + } else { + + if (prefItem != null && hasContainerItem(prefItem.itemStack)) { + + if (requestedAmount > 1) { + final long requestedSteps = getToolsContainerItems( + prefItem.itemStack.copy(), + requestedAmount).leftSteps; + requestedAmount = (long) Math + .ceil((double) requestedAmount / Math.max(1, requestedAmount - requestedSteps)); + } + + if (containerAmount > 1) { + final long containerSteps = getToolsContainerItems( + prefItem.itemStack.copy(), + containerAmount).leftSteps; + containerAmount = (long) Math + .ceil((double) containerAmount / Math.max(1, containerAmount - containerSteps)); + } + } + + final ItemTreeSlot slot = new ItemTreeSlot( + ingrItem, + 0, + requestedAmount, + produced, + containerAmount, + prefItem); + + this.items.add(slot); + this.parentOf.put(slot, this.parentStack.peek()); + this.childrenOf.computeIfAbsent(this.parentStack.peek(), k -> new ArrayList<>()).add(slot); + + this.lastItem = slot; + } + } + + @Override + protected void prepareIngredients(RecipeId recipeId, long multiplier, List visited) { + this.parentStack.push(this.lastItem); + super.prepareIngredients(recipeId, multiplier, visited); + this.lastItem = this.parentStack.pop(); + } + + @Override + protected void calculateSuitableRecipe(BookmarkItem ingrItem, long ingrMultiplier, List visited) { + final BookmarkItem prefItem = this.preferredItems.get(ingrItem); + super.calculateSuitableRecipe(ingrItem, ingrMultiplier, visited); + + if (prefItem != null && !isForeignBoxBoundary(ingrItem) + && !visited.contains(prefItem.recipeId) + && !this.childrenOf.containsKey(this.lastItem)) { + visited.add(prefItem.recipeId); + prepareIngredients(prefItem.recipeId, visited); + visited.remove(prefItem.recipeId); + } + + } + + protected void prepareIngredients(RecipeId recipeId, List visited) { + this.parentStack.push(this.lastItem); + + for (BookmarkItem item : this.ingredientsByRecipe.getOrDefault(recipeId, Collections.emptyList())) { + if (!item.emptyFactor()) { + calculateSuitableRecipe(item, visited); + } + } + + this.lastItem = this.parentStack.pop(); + } + + protected void calculateSuitableRecipe(BookmarkItem ingrItem, List visited) { + final BookmarkItem prefItem = this.preferredItems.get(ingrItem); + final ItemTreeSlot slot = new ItemTreeSlot(ingrItem, 0, 0, 0, 0, prefItem); + + this.items.add(slot); + this.parentOf.put(slot, this.parentStack.peek()); + this.childrenOf.computeIfAbsent(this.parentStack.peek(), k -> new ArrayList<>()).add(slot); + + this.lastItem = slot; + + if (prefItem != null && !isForeignBoxBoundary(ingrItem) && !visited.contains(prefItem.recipeId)) { + visited.add(prefItem.recipeId); + prepareIngredients(prefItem.recipeId, visited); + visited.remove(prefItem.recipeId); + } + + } + +} diff --git a/src/main/java/codechicken/nei/bookmark/tree/CraftingTreeState.java b/src/main/java/codechicken/nei/bookmark/tree/CraftingTreeState.java new file mode 100644 index 00000000..f54a9f53 --- /dev/null +++ b/src/main/java/codechicken/nei/bookmark/tree/CraftingTreeState.java @@ -0,0 +1,105 @@ +package codechicken.nei.bookmark.tree; + +import java.util.EnumSet; +import java.util.HashSet; +import java.util.Set; + +import codechicken.nei.NEIClientConfig; +import codechicken.nei.bookmark.BookmarkItem; + +class CraftingTreeState { + + enum CollapseMode { + EXPANDED, + SMART, + ALL + } + + enum StatsSection { + INGREDIENTS_NEEDED, + INGREDIENTS_AVAILABLE, + CRAFTING_NEEDED, + CRAFTING_AVAILABLE, + RESULTS, + REMAINDERS, + HANDLERS + } + + private static final String KEY_STATS_VISIBLE = "craftingTree.statsVisible"; + private static final String KEY_STATS_SECTIONS = "craftingTree.statsSections"; + private static final String KEY_USE_INVENTORY_SNAPSHOT = "craftingTree.useInventorySnapshot"; + private static final String KEY_COLLAPSE_MODE = "craftingTree.collapseMode"; + + public boolean statsPanelVisible = true; + public final Set visibleStatsSections = EnumSet.allOf(StatsSection.class); + public boolean useInventorySnapshot = false; + public CollapseMode collapseMode = CollapseMode.EXPANDED; + + public final Set collapsedItems = new HashSet<>(); + public ItemTreeSlot replaceRecipeNode; + + public SearchResult searchItemResult; + public SearchResult searchStatsResult; + + public void load() { + this.statsPanelVisible = !NEIClientConfig.world.nbt.hasKey(KEY_STATS_VISIBLE) + || NEIClientConfig.world.nbt.getBoolean(KEY_STATS_VISIBLE); + + final int savedSectionsMask = NEIClientConfig.world.nbt.hasKey(KEY_STATS_SECTIONS) + ? NEIClientConfig.world.nbt.getInteger(KEY_STATS_SECTIONS) + : -1; + + this.visibleStatsSections.clear(); + + for (StatsSection section : StatsSection.values()) { + if ((savedSectionsMask & (1 << section.ordinal())) != 0) { + this.visibleStatsSections.add(section); + } + } + + this.useInventorySnapshot = NEIClientConfig.world.nbt.getBoolean(KEY_USE_INVENTORY_SNAPSHOT); + + final CollapseMode[] collapseModes = CollapseMode.values(); + final int savedCollapseMode = NEIClientConfig.world.nbt.getInteger(KEY_COLLAPSE_MODE); + this.collapseMode = savedCollapseMode >= 0 && savedCollapseMode < collapseModes.length + ? collapseModes[savedCollapseMode] + : CollapseMode.EXPANDED; + this.replaceRecipeNode = null; + } + + public void toggleStatsPanelVisible() { + this.statsPanelVisible = !this.statsPanelVisible; + NEIClientConfig.world.nbt.setBoolean(KEY_STATS_VISIBLE, this.statsPanelVisible); + } + + public void toggleStatsSection(StatsSection section) { + + if (!this.visibleStatsSections.remove(section)) { + this.visibleStatsSections.add(section); + } + + int mask = 0; + + for (StatsSection visibleSection : this.visibleStatsSections) { + mask |= 1 << visibleSection.ordinal(); + } + + NEIClientConfig.world.nbt.setInteger(KEY_STATS_SECTIONS, mask); + } + + public void toggleUseInventorySnapshot() { + this.useInventorySnapshot = !this.useInventorySnapshot; + NEIClientConfig.world.nbt.setBoolean(KEY_USE_INVENTORY_SNAPSHOT, this.useInventorySnapshot); + } + + public void cycleCollapseMode() { + final CollapseMode[] collapseModes = CollapseMode.values(); + this.collapseMode = collapseModes[(this.collapseMode.ordinal() + 1) % collapseModes.length]; + NEIClientConfig.world.nbt.setInteger(KEY_COLLAPSE_MODE, this.collapseMode.ordinal()); + } + + public SearchResult activeSearchResult() { + return this.searchItemResult != null ? this.searchItemResult : this.searchStatsResult; + } + +} diff --git a/src/main/java/codechicken/nei/bookmark/tree/CraftingTreeToolbar.java b/src/main/java/codechicken/nei/bookmark/tree/CraftingTreeToolbar.java new file mode 100644 index 00000000..422e5cf9 --- /dev/null +++ b/src/main/java/codechicken/nei/bookmark/tree/CraftingTreeToolbar.java @@ -0,0 +1,241 @@ +package codechicken.nei.bookmark.tree; + +import java.util.Arrays; +import java.util.List; +import java.util.function.BooleanSupplier; +import java.util.function.Supplier; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiButton; + +import org.lwjgl.opengl.GL11; + +import codechicken.nei.GuiNEIButton; +import codechicken.nei.NEIClientUtils; +import codechicken.nei.drawable.DrawableBuilder; +import codechicken.nei.drawable.DrawableResource; + +class CraftingTreeToolbar { + + private static class GuiStatsButton extends GuiNEIButton { + + private final Supplier icon; + private final BooleanSupplier isEnabled; + private final Supplier tooltip; + + public GuiStatsButton(int buttonId, Supplier icon, Supplier tooltip, + BooleanSupplier isEnabled) { + super(buttonId, 0, 0, 16, 16, ""); + this.tooltip = tooltip; + this.icon = icon; + this.isEnabled = isEnabled; + } + + public String getTooltip() { + return this.tooltip.get(); + } + + @Override + public int getHoverState(boolean mouseOver) { + return this.isEnabled.getAsBoolean() ? 0 : super.getHoverState(mouseOver); + } + + @Override + protected void drawContent(Minecraft minecraft, int y, int x, boolean mouseOver) { + final DrawableResource icon = this.icon.get(); + final int iconX = this.xPosition + (this.width - icon.width / 2) / 2; + final int iconY = this.yPosition + (this.height - icon.height / 2) / 2; + + GL11.glPushMatrix(); + GL11.glColor4f(1, 1, 1, 1); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glTranslated(iconX, iconY, 0); + GL11.glScalef(0.5f, 0.5f, 1); + + icon.bindTexture(); + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR); + icon.draw(0, 0); + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST); + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST); + + GL11.glDisable(GL11.GL_BLEND); + GL11.glPopMatrix(); + } + + } + + private static final DrawableResource STATS_TEXTURE = new DrawableBuilder( + "nei:textures/gui/craftingtree.png", + 2, + 66, + 16, + 16).build(); + + private static final DrawableResource COMPACT_STATS_TEXTURE = new DrawableBuilder( + "nei:textures/gui/craftingtree.png", + 20, + 66, + 16, + 16).build(); + + private static final DrawableResource FIT_TEXTURE = new DrawableBuilder( + "nei:textures/gui/craftingtree.png", + 38, + 66, + 16, + 16).build(); + + private static final DrawableResource INV_TEXTURE = new DrawableBuilder( + "nei:textures/gui/craftingtree.png", + 56, + 66, + 16, + 16).build(); + + private static final DrawableResource LINK_TEXTURE = new DrawableBuilder( + "nei:textures/gui/craftingtree.png", + 74, + 66, + 16, + 16).build(); + + private static final DrawableResource ICON_COLLAPSED = new DrawableBuilder( + "nei:textures/gui/craftingtree.png", + 92, + 66, + 16, + 16).build(); + + private static final DrawableResource ICON_INDETERMINATE = new DrawableBuilder( + "nei:textures/gui/craftingtree.png", + 110, + 66, + 16, + 16).build(); + + private static final DrawableResource ICON_EXPANDED = new DrawableBuilder( + "nei:textures/gui/craftingtree.png", + 128, + 66, + 16, + 16).build(); + + private static final DrawableResource ICON_SCREENSHOT = new DrawableBuilder( + "nei:textures/gui/craftingtree.png", + 146, + 66, + 16, + 16).build(); + + private static final DrawableResource ICON_SETTINGS = new DrawableBuilder( + "nei:textures/gui/craftingtree.png", + 164, + 66, + 16, + 16).build(); + + private static final int BUTTON_SPACING = 20; + + public static final int INV_BUTTON_ID = 0; + public static final int FIT_BUTTON_ID = 1; + public static final int COLLAPSE_ALL_BUTTON_ID = 2; + public static final int TOGGLE_STATS_BUTTON_ID = 3; + public static final int STATS_SECTIONS_BUTTON_ID = 4; + public static final int LINK_BUTTON_ID = 5; + public static final int SCREENSHOT_BUTTON_ID = 6; + + private final CraftingTreeState uiState; + private final GuiStatsButton[] buttons; + + public CraftingTreeToolbar(CraftingTreeState uiState) { + this.uiState = uiState; + + this.buttons = new GuiStatsButton[] { + new GuiStatsButton( + INV_BUTTON_ID, + () -> INV_TEXTURE, + () -> NEIClientUtils.translate( + this.uiState.useInventorySnapshot ? "bookmark.tree.toolbar.inventory_on" + : "bookmark.tree.toolbar.inventory_off"), + () -> this.uiState.useInventorySnapshot), + + new GuiStatsButton( + FIT_BUTTON_ID, + () -> FIT_TEXTURE, + () -> NEIClientUtils.translate("bookmark.tree.toolbar.fit_to_view"), + () -> false), + + new GuiStatsButton(COLLAPSE_ALL_BUTTON_ID, () -> switch (this.uiState.collapseMode) { + case EXPANDED -> ICON_EXPANDED; + case SMART -> ICON_INDETERMINATE; + case ALL -> ICON_COLLAPSED; + }, () -> NEIClientUtils.translate(switch (this.uiState.collapseMode) { + case EXPANDED -> "bookmark.tree.toolbar.collapse_resolved"; + case SMART -> "bookmark.tree.toolbar.collapse_all"; + case ALL -> "bookmark.tree.toolbar.expand_all"; + }), () -> this.uiState.collapseMode != CraftingTreeState.CollapseMode.EXPANDED), + + new GuiStatsButton( + TOGGLE_STATS_BUTTON_ID, + () -> this.uiState.visibleStatsSections.size() == CraftingTreeState.StatsSection.values().length + ? STATS_TEXTURE + : COMPACT_STATS_TEXTURE, + () -> NEIClientUtils.translate( + this.uiState.statsPanelVisible ? "bookmark.tree.toolbar.stats_on" + : "bookmark.tree.toolbar.stats_off"), + () -> this.uiState.statsPanelVisible), + + new GuiStatsButton( + STATS_SECTIONS_BUTTON_ID, + () -> ICON_SETTINGS, + () -> NEIClientUtils.translate("bookmark.tree.toolbar.stats_sections"), + () -> false), + new GuiStatsButton( + LINK_BUTTON_ID, + () -> LINK_TEXTURE, + () -> NEIClientUtils.translate("bookmark.tree.toolbar.link"), + () -> false), + new GuiStatsButton( + SCREENSHOT_BUTTON_ID, + () -> ICON_SCREENSHOT, + () -> NEIClientUtils.translate("bookmark.tree.toolbar.screenshot"), + () -> false) + + }; + } + + public void layout(int x, int y) { + for (int i = 0; i < this.buttons.length; i++) { + this.buttons[i].xPosition = x + BUTTON_SPACING * i; + this.buttons[i].yPosition = y; + } + } + + public void addTo(List buttonList) { + buttonList.addAll(Arrays.asList(this.buttons)); + } + + public GuiButton getButton(int buttonId) { + + for (int i = 0; i < this.buttons.length; i++) { + if (this.buttons[i].id == buttonId) { + return this.buttons[i]; + } + } + + return null; + } + + public String getTooltip(Minecraft mc, int mousex, int mousey) { + for (GuiStatsButton button : this.buttons) { + if (button.mousePressed(mc, mousex, mousey)) { + return button.getTooltip(); + } + } + + return null; + } + +} diff --git a/src/main/java/codechicken/nei/bookmark/tree/GuiCraftingTree.java b/src/main/java/codechicken/nei/bookmark/tree/GuiCraftingTree.java new file mode 100644 index 00000000..37ac8b84 --- /dev/null +++ b/src/main/java/codechicken/nei/bookmark/tree/GuiCraftingTree.java @@ -0,0 +1,1265 @@ +package codechicken.nei.bookmark.tree; + +import java.awt.geom.Point2D; +import java.awt.image.BufferedImage; +import java.io.File; +import java.nio.IntBuffer; +import java.nio.file.FileAlreadyExistsException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +import javax.imageio.ImageIO; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.gui.inventory.GuiContainer; +import net.minecraft.client.renderer.OpenGlHelper; +import net.minecraft.client.shader.Framebuffer; +import net.minecraft.event.ClickEvent; +import net.minecraft.inventory.Slot; +import net.minecraft.item.ItemStack; +import net.minecraft.util.ChatComponentText; +import net.minecraft.util.ChatComponentTranslation; +import net.minecraft.util.EnumChatFormatting; + +import org.lwjgl.BufferUtils; +import org.lwjgl.input.Keyboard; +import org.lwjgl.input.Mouse; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; + +import codechicken.lib.gui.GuiDraw; +import codechicken.nei.KeyManager; +import codechicken.nei.LRUCache; +import codechicken.nei.NEICPH; +import codechicken.nei.NEIClientUtils; +import codechicken.nei.PositionedStack; +import codechicken.nei.SearchField.GuiSearchField; +import codechicken.nei.VisiblityData; +import codechicken.nei.Widget; +import codechicken.nei.api.IGuiContainerOverlay; +import codechicken.nei.api.INEIGuiHandler; +import codechicken.nei.api.ItemFilter; +import codechicken.nei.bookmark.BookmarkGrid; +import codechicken.nei.bookmark.BookmarkGroup; +import codechicken.nei.bookmark.BookmarkItem; +import codechicken.nei.bookmark.BookmarkItem.BookmarkItemType; +import codechicken.nei.bookmark.BookmarkPayload; +import codechicken.nei.drawable.DrawableBuilder; +import codechicken.nei.drawable.DrawableResource; +import codechicken.nei.guihook.IContainerTooltipHandler; +import codechicken.nei.guihook.IGuiClientSide; +import codechicken.nei.guihook.IGuiHandleMouseWheel; +import codechicken.nei.recipe.AcceptsFollowingTooltipLineHandler; +import codechicken.nei.recipe.AutoCraftingManager; +import codechicken.nei.recipe.GuiCraftingRecipe; +import codechicken.nei.recipe.GuiOverlayButton; +import codechicken.nei.recipe.GuiRecipe; +import codechicken.nei.recipe.GuiRecipeButton.UpdateRecipeButtonsEvent; +import codechicken.nei.recipe.GuiRecipeTab; +import codechicken.nei.recipe.Recipe; +import codechicken.nei.recipe.Recipe.RecipeId; +import codechicken.nei.recipe.RecipeTooltipLineHandler; +import codechicken.nei.recipe.StackInfo; +import codechicken.nei.util.EmptyContainer; +import codechicken.nei.util.NEIMouseUtils; +import codechicken.nei.util.ReadableNumberConverter; +import codechicken.nei.util.SlotInaccessible; +import cpw.mods.fml.common.eventhandler.SubscribeEvent; + +public class GuiCraftingTree extends GuiContainer implements INEIGuiHandler, IGuiClientSide, IGuiHandleMouseWheel, + IContainerTooltipHandler, IGuiContainerOverlay { + + public static class EventHandler { + + @SubscribeEvent + public void updateRecipeButtons(UpdateRecipeButtonsEvent.Post event) { + GuiScreen screen = event.gui; + + while (screen instanceof GuiReciperecipeGui) { + screen = recipeGui.prevGui; + } + + if (screen instanceof GuiCraftingTree treeGui) { + for (int index = 0; index < event.buttonList.size(); index++) { + if (event.buttonList.get(index) instanceof GuiOverlayButton button) { + event.buttonList.set( + index, + new TreeOverlayButton(treeGui, button.handlerRef, button.xPosition, button.yPosition)); + } + } + } + + } + } + + private static final DrawableResource BG_TEXTURE = new DrawableBuilder( + "nei:textures/gui/craftingtree.png", + 0, + 0, + 64, + 64).build(); + + private static final DateTimeFormatter SCREENSHOT_DATE_FORMAT = DateTimeFormatter + .ofPattern("yyyy-MM-dd_HH.mm.ss", Locale.ROOT); + + private static final int MARGIN = 4; + private static final int PADDING = 9; + private static final int PADDING_TOP = 22; + + private static LRUCache handlerTitleCache = new LRUCache<>(100); + + private GuiScreen prevGui; + private boolean isFirstScreen = true; + + private final BookmarkGrid grid; + private final int groupId; + private CraftingTreeMath math; + private Set collapsedRecipes; + + private final StatsPanel statsPanel = new StatsPanel(); + private final StatsSectionsMenu statsSectionsMenu = new StatsSectionsMenu(); + private final CraftingTreeCanvas canvas = new CraftingTreeCanvas(); + private boolean statsPanelVisible; + + private GuiSearchField searchField; + + private RecipeTooltipLineHandler recipeTooltipLineHandler; + private AcceptsFollowingTooltipLineHandler acceptsFollowingTooltipLineHandler; + + private final CraftingTreeState uiState = new CraftingTreeState(); + private final CraftingTreeToolbar toolbar; + + private ItemTreeSlot hoveredNode; + private ItemTreeSlot hoveredBadgeHandler; + + protected GuiCraftingTree(GuiScreen prevGui, BookmarkGrid grid, int groupId) { + super(new EmptyContainer()); + this.prevGui = prevGui; + this.grid = grid; + this.groupId = groupId; + + this.toolbar = new CraftingTreeToolbar(this.uiState); + } + + public static void openCraftingTreeGui(BookmarkGrid grid, int groupId) { + final Minecraft mc = NEIClientUtils.mc(); + mc.displayGuiScreen(new GuiCraftingTree(mc.currentScreen, grid, groupId)); + } + + protected void addRecipe(Recipe recipe) { + final ItemTreeSlot replaceNode = this.uiState.replaceRecipeNode; + this.uiState.replaceRecipeNode = null; + + if (recipe != null) { + final RecipeId recipeId = recipe.getRecipeId(); + boolean changed = false; + + if (replaceNode != null) { + final Set unusedRecipes = this.canvas.graph.collectUnusedRecipes(replaceNode); + unusedRecipes.remove(recipeId); + + for (RecipeId unusedId : unusedRecipes) { + changed |= this.grid.removeRecipe(unusedId, this.groupId); + } + } + + if (!this.grid.existsRecipe(recipeId, this.groupId)) { + this.grid.addRecipe(recipe, 1, this.groupId); + changed = true; + } + + if (changed) { + rebuildTree(null); + } + } + + NEIClientUtils.mc().displayGuiScreen(this); + } + + @Override + public void initGui() { + this.xSize = this.width; + this.ySize = this.height; + + super.initGui(); + + this.canvas.setViewport( + MARGIN + PADDING, + MARGIN + PADDING_TOP, + this.width - MARGIN * 2 - PADDING * 2, + this.height - MARGIN * 2 - PADDING_TOP - PADDING); + + this.searchField = new GuiSearchField() { + + @Override + public void mouseClicked(int x, int y, int button) { + + if (button == 1 && x >= this.xPosition + && x < this.xPosition + this.width + && y >= this.yPosition + && y < this.yPosition + this.height) { + setText(""); + onSearchTextChanged(); + } else { + super.mouseClicked(x, y, button); + } + + } + + }; + this.searchField.width = Math.min(140, (this.width - MARGIN * 2 - PADDING * 2) / 3); + this.searchField.height = 14; + this.searchField.xPosition = this.width - MARGIN - this.searchField.width - PADDING; + this.searchField.yPosition = MARGIN + 5; + this.searchField.setMaxStringLength(256); + + this.toolbar.layout(MARGIN + PADDING, MARGIN + 4); + + this.buttonList.clear(); + this.toolbar.addTo(this.buttonList); + + this.uiState.load(); + + rebuildTree(null); + } + + @Override + public boolean doesGuiPauseGame() { + return false; + } + + @Override + public GuiContainer getFirstScreen() { + + if (this.prevGui instanceof IGuiContainerOverlay overlay) { + return overlay.getFirstScreen(); + } + + return this.prevGui instanceof GuiContainer gui ? gui : null; + } + + @Override + public GuiScreen getFirstScreenGeneral() { + return this; + } + + @Override + public void updateScreen() { + this.statsPanel.update(); + + super.updateScreen(); + } + + private void rebuildTree(ItemTreeSlot anchor) { + final BookmarkGroup group = this.grid.getGroup(this.groupId); + final List chainItems = group.crafting != null + ? new ArrayList<>(this.grid.createChainItems(this.groupId).values()) + : Collections.emptyList(); + + this.collapsedRecipes = group.collapsedRecipes; + this.math = new CraftingTreeMath(chainItems, this.collapsedRecipes); + this.math.initialItems.clear(); + + if (this.uiState.useInventorySnapshot) { + final GuiContainer firstGui = getFirstScreen(); + + if (firstGui != null) { + for (ItemStack stack : AutoCraftingManager.getInventoryItems(firstGui).values()) { + if (stack != null) { + this.math.initialItems.add(BookmarkItem.of(-1, stack)); + } + } + } + } + + this.canvas.graph.rebuild(this.math); + + if (this.isFirstScreen) { + updateCollapsedItems(); + } else { + this.canvas.recalculateCoordinates(this.collapsedRecipes, this.uiState.collapsedItems, anchor); + } + + rebuildStatsPanel(); + + if (this.isFirstScreen) { + this.isFirstScreen = false; + this.canvas.fitToView(); + } + } + + private void rebuildStatsPanel() { + this.statsPanel.rebuild( + this.math, + this.canvas.graph, + this.uiState.useInventorySnapshot, + this.uiState.visibleStatsSections, + MARGIN + PADDING, + MARGIN + PADDING_TOP, + this.width - MARGIN * 2 - PADDING * 2, + this.height - MARGIN * 2 - PADDING_TOP - PADDING); + + updateSearchItemMatches(); + updateSearchStatsMatches(); + this.statsPanel.setHighlight(this.uiState.searchStatsResult); + this.statsPanelVisible = this.uiState.statsPanelVisible && !this.statsPanel.getWidgets().isEmpty(); + + this.canvas.setViewport( + MARGIN + PADDING, + MARGIN + PADDING_TOP, + this.width - MARGIN * 2 - PADDING * 2, + this.height - MARGIN * 2 - PADDING_TOP - PADDING - (this.statsPanelVisible ? this.statsPanel.h : 0)); + } + + @Override + protected void drawGuiContainerBackgroundLayer(float partialTicks, int mouseX, int mouseY) { + final boolean canHover = !this.canvas.isDragging() && this.canvas.isInWidget(mouseX, mouseY) + && this.statsPanel.getStackMouseOver(mouseX, mouseY) == null; + this.hoveredBadgeHandler = canHover ? this.canvas.getHandlerBadgeMouseOver(getMousePosition()) : null; + this.hoveredNode = canHover && hoveredBadgeHandler == null ? this.canvas.getNodeMouseOver(getMousePosition()) + : null; + + BG_TEXTURE.draw( + MARGIN, + MARGIN, + this.width - MARGIN * 2, + this.height - MARGIN * 2, + PADDING, + PADDING, + PADDING_TOP, + PADDING); + + if (this.math.outputRecipes.isEmpty()) { + drawCenteredString( + this.fontRendererObj, + NEIClientUtils.translate("bookmark.tree.empty"), + this.width / 2, + MARGIN + PADDING_TOP + (this.height - MARGIN * 2 - PADDING_TOP) / 2, + 0xFFAAAAAA); + } else { + this.canvas.draw( + getMousePosition(), + this.uiState.collapsedItems, + this.uiState.activeSearchResult(), + this.uiState.useInventorySnapshot, + this.hoveredNode, + this.hoveredBadgeHandler); + + if (this.statsPanelVisible) { + this.statsPanel.draw(mouseX, mouseY); + } + } + + drawCenteredString( + this.fontRendererObj, + NEIClientUtils.translate("bookmark.tree.title"), + this.width / 2, + MARGIN + PADDING_TOP / 2 - this.fontRendererObj.FONT_HEIGHT / 2 + 1, + 0xFFFFFFFF); + + this.searchField.drawTextBox(); + + this.statsSectionsMenu.draw(mouseX, mouseY, this.uiState); + } + + @Override + protected void actionPerformed(GuiButton button) { + + switch (button.id) { + case CraftingTreeToolbar.INV_BUTTON_ID: + this.uiState.toggleUseInventorySnapshot(); + rebuildTree(null); + break; + case CraftingTreeToolbar.FIT_BUTTON_ID: + this.canvas.fitToView(); + break; + case CraftingTreeToolbar.TOGGLE_STATS_BUTTON_ID: + this.uiState.toggleStatsPanelVisible(); + rebuildStatsPanel(); + break; + case CraftingTreeToolbar.STATS_SECTIONS_BUTTON_ID: + + if (this.statsSectionsMenu.visible) { + this.statsSectionsMenu.close(); + } else { + this.statsSectionsMenu.open(button.xPosition, button.yPosition + button.height + 2); + } + + break; + case CraftingTreeToolbar.LINK_BUTTON_ID: + NEIClientUtils.sendChatItemLink(BookmarkPayload.of(this.groupId).toNBT()); + break; + case CraftingTreeToolbar.COLLAPSE_ALL_BUTTON_ID: + this.uiState.cycleCollapseMode(); + updateCollapsedItems(); + this.canvas.fitToView(); + break; + case CraftingTreeToolbar.SCREENSHOT_BUTTON_ID: + saveScreenshot(); + break; + default: + return; + } + + } + + private void saveScreenshot() { + + if (this.math.outputRecipes.isEmpty()) { + return; + } + + if (!OpenGlHelper.isFramebufferEnabled()) { + NEIClientUtils.printChatMessage( + new ChatComponentText(NEIClientUtils.translate("bookmark.tree.screenshot.fbo_unsupported"))); + return; + } + + final Minecraft mc = NEIClientUtils.mc(); + + try { + final int screenshotZoom = 2; + final File screenshotsDir = new File(mc.mcDataDir, "screenshots"); + screenshotsDir.mkdirs(); + + final StatsPanel screenshotStats = this.statsPanelVisible ? new StatsPanel() : null; + final int treeWidth = this.canvas.treeWidth; + final int treeHeight = this.canvas.treeHeight + 16; + int statsWidth = 0; + int statsHeight = 0; + + if (screenshotStats != null) { + screenshotStats.rebuild( + this.math, + this.canvas.graph, + this.uiState.useInventorySnapshot, + this.uiState.visibleStatsSections, + 0, + 0, + treeWidth, + Short.MAX_VALUE); + + screenshotStats.x = 0; + screenshotStats.y = treeHeight; + screenshotStats.update(); + statsWidth = screenshotStats.getActualWidth(); + statsHeight = screenshotStats.h = screenshotStats.getActualHeight() + 8; + } + + final int contentWidth = Math.max(treeWidth, statsWidth); + final float treeShiftX = (contentWidth - treeWidth) / 2f; + + if (screenshotStats != null && statsWidth > treeWidth) { + screenshotStats.w = statsWidth; + screenshotStats.x = 0; + screenshotStats.update(); + } + + final int maxGlTexSize = GL11.glGetInteger(GL11.GL_MAX_TEXTURE_SIZE) / 2; + int imgHeight = screenshotZoom * (treeHeight + statsHeight + 16); + int imgWidth = screenshotZoom * (contentWidth + 16); + + // Make sure the image can be actually allocated, worst case it'll be cropped + while ((long) imgWidth * (long) imgHeight >= (long) Integer.MAX_VALUE / 4) { + if (imgWidth > imgHeight) { + imgWidth /= 2; + } else { + imgHeight /= 2; + } + } + + final int xChunks = (imgWidth + maxGlTexSize - 1) / maxGlTexSize; + final int yChunks = (imgHeight + maxGlTexSize - 1) / maxGlTexSize; + final int fbWidth = Math.min(imgWidth, maxGlTexSize); + final int fbHeight = Math.min(imgHeight, maxGlTexSize); + + final BufferedImage outputImg = new BufferedImage(imgWidth, imgHeight, BufferedImage.TYPE_INT_ARGB); + final IntBuffer downloadBuffer = BufferUtils.createIntBuffer(fbWidth * fbHeight); + + GL11.glPushMatrix(); + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); + + final Framebuffer fb = new Framebuffer(fbWidth, fbHeight, true); + + GL11.glMatrixMode(GL11.GL_PROJECTION); + GL11.glLoadIdentity(); + GL11.glOrtho(0, fbWidth / (float) screenshotZoom, fbHeight / (float) screenshotZoom, 0, 1000, 3000); + GL11.glMatrixMode(GL11.GL_MODELVIEW); + GL11.glLoadIdentity(); + GL11.glDisable(GL11.GL_DEPTH_TEST); + + try { + fb.bindFramebuffer(true); + + for (int xChunk = 0; xChunk < xChunks; xChunk++) { + final int xStart = xChunk * maxGlTexSize; + final int xChunkSize = Math.min((xChunk + 1) * maxGlTexSize, imgWidth) - xStart; + + for (int yChunk = 0; yChunk < yChunks; yChunk++) { + final int yStart = yChunk * maxGlTexSize; + final int yChunkSize = Math.min((yChunk + 1) * maxGlTexSize, imgHeight) - yStart; + + GL11.glPushMatrix(); + GL11.glTranslatef( + 8 - xStart / (float) screenshotZoom, + 8 - yStart / (float) screenshotZoom, + -2000.0f); + GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT); + + GL11.glPushMatrix(); + GL11.glTranslatef(treeShiftX, 0, 0); + this.canvas.drawFull( + this.uiState.collapsedItems, + this.uiState.activeSearchResult(), + this.uiState.useInventorySnapshot); + GL11.glPopMatrix(); + + if (screenshotStats != null) { + GL11.glPushMatrix(); + screenshotStats.drawFull(); + GL11.glPopMatrix(); + } + + GL11.glPopMatrix(); + + GL11.glBindTexture(GL11.GL_TEXTURE_2D, fb.framebufferTexture); + GL11.glGetTexImage( + GL11.GL_TEXTURE_2D, + 0, + GL12.GL_BGRA, + GL12.GL_UNSIGNED_INT_8_8_8_8_REV, + downloadBuffer); + + for (int y = 0; y < yChunkSize; y++) { + for (int x = 0; x < xChunkSize; x++) { + outputImg.setRGB( + x + xStart, + y + yStart, + downloadBuffer.get((fbHeight - 1 - y) * fbWidth + x)); + } + } + } + } + } finally { + fb.deleteFramebuffer(); + GL11.glViewport(0, 0, mc.displayWidth, mc.displayHeight); + } + + GL11.glPopAttrib(); + GL11.glPopMatrix(); + + final String date = SCREENSHOT_DATE_FORMAT.format(LocalDateTime.now()); + String filename = String.format("%s-nei.png", date); + File outFile = new File(screenshotsDir, filename); + for (int i = 1; outFile.exists() && i < 99; i++) { + filename = String.format("%s-nei-%d.png", date, i); + outFile = new File(screenshotsDir, filename); + } + if (outFile.exists()) { + throw new FileAlreadyExistsException(filename); + } + ImageIO.write(outputImg, "png", outFile); + + final ChatComponentText chatLink = new ChatComponentText(filename); + chatLink.getChatStyle() + .setChatClickEvent(new ClickEvent(ClickEvent.Action.OPEN_FILE, outFile.getAbsolutePath())); + chatLink.getChatStyle().setUnderlined(true); + NEIClientUtils.printChatMessage(new ChatComponentTranslation("screenshot.success", chatLink)); + } catch (Exception e) { + NEIClientUtils.printChatMessage(new ChatComponentTranslation("screenshot.failure", e.getMessage())); + } + } + + private void updateCollapsedItems() { + this.uiState.collapsedItems.clear(); + + for (ItemTreeSlot node : this.canvas.graph.childrenOf.keySet()) { + + if (this.canvas.graph.roots.contains(node)) { + continue; + } + + final boolean collapse = switch (this.uiState.collapseMode) { + case EXPANDED -> false; + case SMART -> !this.canvas.graph.childrenNeedCraftingCache.contains(node); + case ALL -> true; + }; + + if (collapse) { + this.uiState.collapsedItems.add(node.ingrItem); + } + } + + this.canvas.recalculateCoordinates(this.collapsedRecipes, this.uiState.collapsedItems, null); + } + + protected ItemStack getItemStackMouseOver(int mousex, int mousey) { + + if (this.statsPanelVisible) { + final ItemStack stack = this.statsPanel.getStackMouseOver(mousex, mousey); + + if (stack != null) { + return stack; + } + } + + if (this.hoveredNode != null) { + return this.hoveredNode.itemStack; + } + + return null; + } + + @Override + public Slot getSlotAtPosition(int mousex, int mousey) { + final ItemStack hoveredStack = getItemStackMouseOver(mousex, mousey); + final EmptyContainer slotcontainer = (EmptyContainer) this.inventorySlots; + slotcontainer.setActiveStack(hoveredStack); + + return hoveredStack != null ? new SlotInaccessible(hoveredStack, 0, 0) : null; + } + + @Override + protected void mouseClicked(int mouseX, int mouseY, int button) { + this.searchField.mouseClicked(mouseX, mouseY, button); + + if (this.statsSectionsMenu.visible) { + + if (this.statsSectionsMenu.handleClick(mouseX, mouseY, this.uiState)) { + rebuildStatsPanel(); + return; + } + + if (!isOverButton(this.toolbar.getButton(CraftingTreeToolbar.STATS_SECTIONS_BUTTON_ID), mouseX, mouseY)) { + this.statsSectionsMenu.close(); + } + } + + if (this.statsPanelVisible) { + + if (handleWidgetClick(mouseX, mouseY, button)) { + return; + } + + if (this.statsPanel.handleClick(mouseX, mouseY, button)) { + return; + } + } + + if (button == 0 && handleCanvasClick(mouseX, mouseY)) { + return; + } + + super.mouseClicked(mouseX, mouseY, button); + } + + private static boolean isOverButton(GuiButton button, int mouseX, int mouseY) { + return mouseX >= button.xPosition && mouseX < button.xPosition + button.width + && mouseY >= button.yPosition + && mouseY < button.yPosition + button.height; + } + + private boolean handleWidgetClick(int mouseX, int mouseY, int button) { + final Object widgetGUID = this.statsPanel.getWidgetGUIDUnderMouse(mouseX, mouseY); + + if (widgetGUID == null) { + return false; + } + + if (button == 1 && this.uiState.searchStatsResult != null + && this.uiState.searchStatsResult.matchesGUID(widgetGUID)) { + NEIClientUtils.playClickSound(); + + this.uiState.searchStatsResult = null; + this.statsPanel.setHighlight(null); + onSearchTextChanged(); + return true; + } + + if (button == 0) { + NEIClientUtils.playClickSound(); + + if (this.uiState.searchStatsResult == null || !this.uiState.searchStatsResult.matchesGUID(widgetGUID)) { + this.uiState.searchItemResult = null; + this.uiState.searchStatsResult = new SearchResult(widgetGUID); + updateSearchStatsMatches(); + this.statsPanel.setHighlight(this.uiState.searchStatsResult); + } + + searchGoTo(this.uiState.searchStatsResult, !NEIClientUtils.shiftKey()); + return true; + } + + return false; + } + + private boolean handleCanvasClick(int mouseX, int mouseY) { + + if (!this.canvas.isInWidget(mouseX, mouseY)) { + return false; + } + + final Point2D mouse = getMousePosition(); + final ItemTreeSlot deleteButtonTarget = this.canvas.getHandlerBadgeMouseOver(mouse); + final ItemTreeSlot hoveredNode = deleteButtonTarget != null ? null : this.canvas.getNodeMouseOver(mouse); + + if (hoveredNode != null) { + + if (NEIClientUtils.altKey() && hoveredNode.prefItem != null) { + NEIClientUtils.playClickSound(); + this.grid.toggleCollapsedRecipe(this.groupId, hoveredNode.prefItem.recipeId); + rebuildTree(hoveredNode); + return true; + } else if (this.canvas.graph.childrenOf.containsKey(hoveredNode)) { + this.uiState.replaceRecipeNode = hoveredNode; + } + + } else if (deleteButtonTarget != null) { + NEIClientUtils.playClickSound(); + final RecipeId recipeId = deleteButtonTarget.prefItem.recipeId; + final int itemIndex = this.grid + .indexOf(this.groupId, deleteButtonTarget.prefItem.itemStack, recipeId, false); + + this.grid.removeRecipe(itemIndex, false); + + if (!this.grid.existsRecipe(recipeId, this.groupId)) { + for (RecipeId unusedId : this.canvas.graph.collectUnusedRecipes(deleteButtonTarget)) { + this.grid.removeRecipe(unusedId, this.groupId); + } + } + + rebuildTree(null); + return true; + } else { + final ItemTreeSlot collapseTarget = this.canvas.getCollapseArrowMouseOver(mouse); + + if (collapseTarget != null) { + NEIClientUtils.playClickSound(); + final BookmarkItem item = collapseTarget.ingrItem; + + if (!this.uiState.collapsedItems.remove(item)) { + this.uiState.collapsedItems.add(item); + } + + this.canvas.recalculateCoordinates(this.collapsedRecipes, this.uiState.collapsedItems, collapseTarget); + + return true; + } + + this.canvas.startDrag(mouseX, mouseY); + } + + return false; + } + + @Override + protected void mouseClickMove(int mouseX, int mouseY, int button, long timeSinceLastClick) { + if (button != 0) { + return; + } + + if (this.statsPanelVisible) { + + if (this.statsPanel.isDraggingScrollbar()) { + this.statsPanel.mouseDragged(mouseX, mouseY, button, timeSinceLastClick); + return; + } + + } + + this.canvas.drag(mouseX, mouseY); + } + + @Override + protected void mouseMovedOrUp(int mouseX, int mouseY, int state) { + super.mouseMovedOrUp(mouseX, mouseY, state); + + if (this.statsPanelVisible) { + this.statsPanel.mouseUp(mouseX, mouseY, state); + } + + this.canvas.endDrag(); + } + + @Override + public void mouseScrolled(int wheel) { + final Point2D mouse = getMousePosition(); + + if (handleItemWheel(wheel, mouse)) { + return; + } + + final int direction = wheel > 0 ? 1 : -1; + + if (NEIClientUtils.shiftKey() && this.acceptsFollowingTooltipLineHandler != null) { + final BookmarkItem item = (BookmarkItem) this.acceptsFollowingTooltipLineHandler.tooltipGUID; + final int itemIndex = this.grid + .indexOf(this.groupId, item.itemStack, item.recipeId, item.type == BookmarkItemType.INGREDIENT); + + if (item.permutations.size() > 1) { + final List items = acceptsFollowingTooltipLineHandler.getItems(); + ItemStack activeStack = this.acceptsFollowingTooltipLineHandler.getActiveStack(); + int stackIndex = 0; + + for (int i = 0; i < items.size(); i++) { + if (StackInfo.equalItemAndNBT(activeStack, items.get(i), true)) { + stackIndex = i; + break; + } + } + + activeStack = items.get((items.size() - direction + stackIndex) % items.size()); + + this.acceptsFollowingTooltipLineHandler.setActiveStack(activeStack); + + if (itemIndex != -1) { + final BookmarkItem oldItem = this.grid.getBookmarkItem(itemIndex); + final BookmarkItem newItem = oldItem.copyWithPerm(activeStack); + this.grid.replaceBookmarkItem(itemIndex, newItem); + } else { + this.grid.onItemsChanged(); + } + + item.itemStack = activeStack; + rebuildTree(null); + } + + return; + } + + this.canvas.scroll( + (int) mouse.getX(), + (int) mouse.getY(), + direction, + NEIClientUtils.shiftKey(), + NEIClientUtils.controlKey()); + } + + private boolean handleItemWheel(int wheel, Point2D mouse) { + final int direction = wheel > 0 ? 1 : -1; + final int x = (int) mouse.getX(); + final int y = (int) mouse.getY(); + + if (this.statsPanelVisible && this.statsPanel.contains(x, y) && this.statsPanel.onMouseWheel(direction, x, y) + || !this.canvas.isInWidget(x, y)) { + return true; + } + + if (NEIClientUtils.controlKey()) { + final ItemTreeSlot node = this.canvas.getNodeMouseOver(mouse); + + if (this.canvas.graph.roots.contains(node)) { + final BookmarkItem item = node.ingrItem; + final RecipeId recipeId = item.recipeId; + + if (recipeId != null) { + final long multiplier = NEIClientUtils.altKey() ? NEIClientUtils.getScrollMultiplier(item.itemStack) + : 1; + + this.grid.shiftRecipeAmount(this.groupId, recipeId, multiplier * direction); + rebuildTree(null); + + return true; + } + } + + } + + return false; + } + + @Override + protected void keyTyped(char typedChar, int keyCode) { + + if (keyCode == Keyboard.KEY_ESCAPE) { + this.mc.displayGuiScreen(this.prevGui); + NEICPH.sendRequestContainer(); + return; + } + + if (this.searchField.isFocused()) { + + if (keyCode == Keyboard.KEY_RETURN) { + if (this.uiState.searchItemResult == null) { + onSearchTextChanged(); + } else { + searchGoTo(this.uiState.searchItemResult, !NEIClientUtils.shiftKey()); + } + return; + } + + if (this.searchField.textboxKeyTyped(typedChar, keyCode)) { + onSearchTextChanged(); + return; + } + } + + if (this.statsPanelVisible && KeyManager.isKeyDown("recipe.recipe")) { + final Point2D mouse = getMousePosition(); + final Widget widget = this.statsPanel.getWidgetUnderMouse((int) mouse.getX(), (int) mouse.getY()); + + if (widget != null && widget instanceof StatsPanel.ItemStatsSlot itemStatsSlot + && itemStatsSlot.recipeId != null) { + GuiCraftingRecipe.openRecipeGui("recipeId", itemStatsSlot.recipeId.getResult(), itemStatsSlot.recipeId); + return; + } + } + + super.keyTyped(typedChar, keyCode); + } + + private static String getHandlerTitle(String handlerName) { + return handlerTitleCache.computeIfAbsent( + handlerName, + hname -> Stream + .concat( + GuiCraftingRecipe.craftinghandlers.stream(), + GuiCraftingRecipe.serialCraftingHandlers.stream()) + .filter(handler -> hname.equals(GuiRecipeTab.getHandlerInfo(handler).getHandlerName())) + .findFirst().map(handler -> handler.getRecipeName().trim()).orElse(hname)); + } + + @Override + public List handleTooltip(GuiContainer gui, int mousex, int mousey, List currenttip) { + final String toolbarTooltip = this.toolbar.getTooltip(gui.mc, mousex, mousey); + + if (this.recipeTooltipLineHandler != null && getSlotAtPosition(mousex, mousey) == null) { + this.recipeTooltipLineHandler = null; + } + + if (toolbarTooltip != null) { + currenttip.add(toolbarTooltip); + } else if (this.hoveredBadgeHandler != null) { + final RecipeId recipeId = this.hoveredBadgeHandler.prefItem != null + ? this.hoveredBadgeHandler.prefItem.recipeId + : null; + final long multiplier = this.hoveredBadgeHandler.multiplier; + + addRecipeTooltipLine(recipeId, multiplier, currenttip); + } else { + + if (this.statsPanelVisible) { + final Widget widget = this.statsPanel.getWidgetUnderMouse(mousex, mousey); + + if (widget instanceof StatsPanel.HandlerStatsSlot handlerStatsSlot) { + currenttip.add(getHandlerTitle(handlerStatsSlot.handlerName) + GuiDraw.TOOLTIP_LINESPACE); + currenttip.add( + EnumChatFormatting.GRAY + NEIClientUtils.translate( + "bookmark.tree.stats.handler_recipes", + handlerStatsSlot.stats.recipeCount)); + currenttip.add( + EnumChatFormatting.GRAY + NEIClientUtils.translate( + "bookmark.tree.stats.handler_iterations", + handlerStatsSlot.stats.iterations)); + } + } + + } + + return currenttip; + } + + private void addRecipeTooltipLine(RecipeId recipeId, long multiplier, List currenttip) { + + if (recipeId == null) { + this.recipeTooltipLineHandler = null; + } else + if (this.recipeTooltipLineHandler == null || !this.recipeTooltipLineHandler.getRecipeId().equals(recipeId) + || this.recipeTooltipLineHandler.getMultiplier() != multiplier) { + this.recipeTooltipLineHandler = new RecipeTooltipLineHandler(recipeId, multiplier); + } + + if (this.recipeTooltipLineHandler != null) { + currenttip.add(GuiDraw.TOOLTIP_HANDLER + GuiDraw.getTipLineId(this.recipeTooltipLineHandler)); + } + } + + @Override + public List handleItemTooltip(GuiContainer gui, ItemStack itemstack, int mousex, int mousey, + List currenttip) { + + final ItemTreeSlot hoveredItemNode = this.hoveredNode; + + if (hoveredItemNode != null) { + final BookmarkItem item = hoveredItemNode.ingrItem; + + if (item.chance != PositionedStack.CHANCE_FULL) { + final String chanceText = NEIClientUtils + .formatChance(item.chance / (float) PositionedStack.CHANCE_FULL); + + currenttip.add( + 1, + EnumChatFormatting.GRAY + NEIClientUtils.translate( + item.type == BookmarkItemType.INGREDIENT ? "recipe.badge.chance.consume" + : "recipe.badge.chance.output", + chanceText)); + + } + + if (this.uiState.useInventorySnapshot && hoveredItemNode.requestedAmount > 0) { + + if (hoveredItemNode.leftAmount == 0) { + final ItemStack resolvedStack = (hoveredItemNode.prefItem != null ? hoveredItemNode.prefItem + : hoveredItemNode.ingrItem).itemStack; + final String resolvedName = resolvedStack.getDisplayName(); + + currenttip.add( + 1, + EnumChatFormatting.GRAY + + NEIClientUtils.translate("bookmark.tree.item.state.resolved", resolvedName)); + } else if (hoveredItemNode.leftAmount != hoveredItemNode.requestedAmount) { + final long freshStackSize = item.getStackSize(hoveredItemNode.leftAmount); + String freshAmountText = freshStackSize < 10_000 ? String.valueOf(freshStackSize) + : ReadableNumberConverter.INSTANCE.toWideReadableForm(freshStackSize); + + if (hoveredItemNode.isFluidDisplay) { + freshAmountText += "L"; + } + + currenttip.add( + 1, + EnumChatFormatting.GRAY + + NEIClientUtils.translate("bookmark.tree.item.state.partial", freshAmountText)); + } + + if (hoveredItemNode.leftAmount > 0 && this.canvas.graph.childrenOf.containsKey(hoveredItemNode) + && !this.canvas.graph.childrenNeedCraftingCache.contains(hoveredItemNode)) { + currenttip.add( + 1, + EnumChatFormatting.GRAY + NEIClientUtils.translate("bookmark.tree.item.state.craftable")); + } + + } + + if (hoveredItemNode.prefItem != null && this.canvas.graph.childrenOf.containsKey(hoveredItemNode)) { + final BookmarkItem output = hoveredItemNode.prefItem; + final long producedAmount = output.getAmount(hoveredItemNode.multiplier); + final long surplus = producedAmount - hoveredItemNode.requestedAmount; + + if (surplus > 0) { + String surplusText = surplus < 10_000 ? String.valueOf(surplus) + : ReadableNumberConverter.INSTANCE.toWideReadableForm(surplus); + + if (hoveredItemNode.isFluidDisplay) { + surplusText += "L"; + } + + currenttip.add( + 1, + EnumChatFormatting.GRAY + + NEIClientUtils.translate("bookmark.tree.item.state.surplus", surplusText)); + } + } + + if (item.recipeId == null || hoveredItemNode.prefItem != null + || hoveredItemNode.leftAmount == 0 && hoveredItemNode.requestedAmount > 0) { + this.acceptsFollowingTooltipLineHandler = null; + } else if (this.acceptsFollowingTooltipLineHandler == null + || !item.equals(this.acceptsFollowingTooltipLineHandler.tooltipGUID)) { + this.acceptsFollowingTooltipLineHandler = AcceptsFollowingTooltipLineHandler.of( + item, + new ArrayList<>(item.permutations.values()), + hoveredItemNode.ingrItem.getItemStack(), + 2); + } + + if (this.acceptsFollowingTooltipLineHandler != null) { + currenttip.add(GuiDraw.TOOLTIP_HANDLER + GuiDraw.getTipLineId(this.acceptsFollowingTooltipLineHandler)); + } + + } else { + this.acceptsFollowingTooltipLineHandler = null; + + if (this.statsPanelVisible) { + final Widget widget = this.statsPanel.getWidgetUnderMouse(mousex, mousey); + if (widget instanceof StatsPanel.ItemStatsSlot itemStatsSlot) { + addRecipeTooltipLine(itemStatsSlot.recipeId, itemStatsSlot.multiplier, currenttip); + } + } + + } + + return currenttip; + } + + @Override + public Map handleHotkeys(GuiContainer gui, int mousex, int mousey, Map hotkeys) { + + if (this.hoveredNode != null) { + if (this.hoveredNode.prefItem != null) { + hotkeys.put( + NEIClientUtils.getKeyName(NEIClientUtils.ALT_HASH, NEIMouseUtils.MOUSE_BTN_LMB), + NEIClientUtils.translate("bookmark.tree.item.toggle_recipe")); + } + + if (this.canvas.graph.roots.contains(this.hoveredNode)) { + hotkeys.put( + NEIClientUtils.getKeyName( + NEIClientUtils.CTRL_HASH, + NEIMouseUtils.MOUSE_BTN_NONE + NEIMouseUtils.MOUSE_SCROLL), + NEIClientUtils.translate("bookmark.change_quantity")); + + hotkeys.put( + NEIClientUtils.getKeyName( + NEIClientUtils.CTRL_HASH + NEIClientUtils.ALT_HASH, + NEIMouseUtils.MOUSE_BTN_NONE + NEIMouseUtils.MOUSE_SCROLL), + NEIClientUtils.translate("bookmark.change_quantity_step")); + } + } else if (this.statsPanelVisible) { + final Object widgetGUID = this.statsPanel.getWidgetGUIDUnderMouse(mousex, mousey); + + if (widgetGUID != null) { + + if (this.uiState.searchStatsResult != null && this.uiState.searchStatsResult.matchesGUID(widgetGUID)) { + hotkeys.put( + NEIMouseUtils.getKeyName(NEIMouseUtils.MOUSE_BTN_LMB), + NEIClientUtils.translate("bookmark.tree.stats.next_highlight")); + + hotkeys.put( + NEIClientUtils.getKeyName(NEIClientUtils.SHIFT_HASH, NEIMouseUtils.MOUSE_BTN_LMB), + NEIClientUtils.translate("bookmark.tree.stats.prev_highlight")); + + hotkeys.put( + NEIMouseUtils.getKeyName(NEIMouseUtils.MOUSE_BTN_RMB), + NEIClientUtils.translate("bookmark.tree.stats.clear_highlight")); + } else { + hotkeys.put( + NEIMouseUtils.getKeyName(NEIMouseUtils.MOUSE_BTN_LMB), + NEIClientUtils.translate("bookmark.tree.stats.highlight_handler")); + } + + } + + } + + if (this.acceptsFollowingTooltipLineHandler != null) { + hotkeys.put( + NEIClientUtils.getKeyName( + NEIClientUtils.SHIFT_HASH, + NEIMouseUtils.MOUSE_BTN_NONE + NEIMouseUtils.MOUSE_SCROLL), + NEIClientUtils.translate("recipe.accepts.scroll")); + } + + return hotkeys; + } + + @Override + public VisiblityData modifyVisiblity(GuiContainer gui, VisiblityData currentVisibility) { + currentVisibility.showNEI = false; + return currentVisibility; + } + + private Point2D getMousePosition() { + final float preciseMouseX = Mouse.getX() * (float) this.width / this.mc.displayWidth; + final float preciseMouseY = this.height - Mouse.getY() * (float) this.height / this.mc.displayHeight; + return new Point2D.Float(preciseMouseX, preciseMouseY); + } + + private void onSearchTextChanged() { + final String searchText = this.searchField.getText(); + + if (this.uiState.searchStatsResult != null) { + this.uiState.searchStatsResult = null; + this.statsPanel.setHighlight(null); + } + + if (!searchText.isEmpty()) { + this.uiState.searchItemResult = new SearchResult(searchText); + updateSearchItemMatches(); + searchGoTo(this.uiState.searchItemResult, true); + } else { + this.uiState.searchItemResult = null; + } + + } + + private void searchGoTo(SearchResult searchResult, boolean forward) { + + if (searchResult == null || searchResult.searchMatches.isEmpty()) { + return; + } + + final List searchList = new ArrayList<>(searchResult.searchMatches); + int searchIndex = searchList.indexOf(searchResult.highlightNode); + + if (forward) { + + if (searchIndex >= searchList.size() - 1) { + searchIndex = -1; + } + + searchIndex++; + } else { + + if (searchIndex <= 0) { + searchIndex = searchList.size(); + } + + searchIndex--; + } + + ItemTreeSlot target = searchIndex == -1 ? null : searchList.get(searchIndex); + + if (target != null) { + ItemTreeSlot current = this.canvas.graph.parentOf.get(target); + + while (current != null) { + this.uiState.collapsedItems.remove(current.ingrItem); + current = this.canvas.graph.parentOf.get(current); + } + } + + this.canvas.recalculateCoordinates(this.collapsedRecipes, this.uiState.collapsedItems, null); + + searchResult.highlightNode = target; + this.canvas.scrollTo(target); + } + + private void updateSearchItemMatches() { + + if (this.uiState.searchItemResult != null) { + final ItemFilter filter = this.searchField.getFilter((String) this.uiState.searchItemResult.searchGUID); + this.uiState.searchItemResult.searchMatches.clear(); + this.uiState.searchItemResult.highlightNode = null; + + for (ItemTreeSlot node : this.canvas.graph.items) { + if (filter.matches(node.emptyStack)) { + this.uiState.searchItemResult.searchMatches.add(node); + } + } + } + + } + + private void updateSearchStatsMatches() { + + if (this.uiState.searchStatsResult != null) { + this.uiState.searchStatsResult.searchMatches.clear(); + this.uiState.searchStatsResult.highlightNode = null; + + final boolean byHandler = this.uiState.searchStatsResult.searchGUID instanceof String; + + for (ItemTreeSlot node : this.canvas.graph.items) { + final boolean matches = byHandler + ? node.prefItem != null + && this.uiState.searchStatsResult.matchesGUID(node.prefItem.recipeId.getHandlerName()) + : this.uiState.searchStatsResult.matchesGUID(node.emptyStack); + + if (matches) { + this.uiState.searchStatsResult.searchMatches.add(node); + } + } + } + + } + +} diff --git a/src/main/java/codechicken/nei/bookmark/tree/ItemTreeSlot.java b/src/main/java/codechicken/nei/bookmark/tree/ItemTreeSlot.java new file mode 100644 index 00000000..b29e1c3d --- /dev/null +++ b/src/main/java/codechicken/nei/bookmark/tree/ItemTreeSlot.java @@ -0,0 +1,90 @@ +package codechicken.nei.bookmark.tree; + +import net.minecraft.item.ItemStack; + +import codechicken.nei.bookmark.BookmarkItem; +import codechicken.nei.recipe.GuiRecipeTab; +import codechicken.nei.recipe.HandlerInfo; +import codechicken.nei.recipe.StackInfo; + +class ItemTreeSlot { + + public final BookmarkItem ingrItem; + public final BookmarkItem prefItem; + + public final long ingeritRequestedAmount; + public final long currentRequestedAmount; + public final long requestedAmount; + + public final long containerAmount; + public final long producedAmount; + public final long leftAmount; + + public final ItemStack emptyStack; + public final ItemStack itemStack; + public final boolean isFluidDisplay; + public final boolean isContainerItem; + + public final HandlerInfo info; + public final long multiplier; + + public ItemTreeSlot(BookmarkItem ingrItem, long ingeritRequestedAmount, long currentRequestedAmount, + long producedAmount, long containerAmount, BookmarkItem prefItem) { + + this.ingrItem = ingrItem; + this.ingeritRequestedAmount = ingeritRequestedAmount; + this.currentRequestedAmount = currentRequestedAmount; + this.requestedAmount = ingeritRequestedAmount + currentRequestedAmount; + + this.containerAmount = containerAmount; + this.producedAmount = producedAmount; + this.leftAmount = producedAmount + containerAmount; + this.prefItem = prefItem; + + if (prefItem != null) { + this.info = handlerInfo(prefItem.recipeId.getHandlerName()); + this.multiplier = prefItem.getMultiplierFromAmount(this.requestedAmount); + + this.itemStack = prefItem.getItemStack(this.requestedAmount); + this.emptyStack = prefItem.getItemStack(0); + } else { + this.info = null; + this.multiplier = ingrItem.getMultiplierFromAmount(this.requestedAmount); + this.itemStack = ingrItem.getItemStack(this.requestedAmount); + this.emptyStack = ingrItem.getItemStack(0); + } + + this.isFluidDisplay = StackInfo.itemStackToNBT(this.emptyStack).hasKey("gtFluidName"); + this.isContainerItem = isContainerItem(this.emptyStack); + } + + private static HandlerInfo handlerInfo(String handlerName) { + final HandlerInfo info = GuiRecipeTab.getHandlerInfo(handlerName, null); + return info != null ? info : GuiRecipeTab.DEFAULT_HANDLER_INFO; + } + + private static boolean isContainerItem(ItemStack stack) { + boolean isContainerItem = false; + + if (stack.getItem().hasContainerItem(stack)) { + final boolean isPausedItemDamageSound = StackInfo.isPausedItemDamageSound(); + + try { + + StackInfo.pauseItemDamageSound(true); + + final ItemStack containerItem = stack.getItem().getContainerItem(stack); + + if (containerItem != null) { + isContainerItem = stack.getItem() == containerItem.getItem(); + } + + } finally { + StackInfo.pauseItemDamageSound(isPausedItemDamageSound); + } + } + + return isContainerItem; + } + +} diff --git a/src/main/java/codechicken/nei/bookmark/tree/SearchResult.java b/src/main/java/codechicken/nei/bookmark/tree/SearchResult.java new file mode 100644 index 00000000..24466d6f --- /dev/null +++ b/src/main/java/codechicken/nei/bookmark/tree/SearchResult.java @@ -0,0 +1,28 @@ +package codechicken.nei.bookmark.tree; + +import java.util.LinkedHashSet; +import java.util.Set; + +import net.minecraft.item.ItemStack; + +import codechicken.nei.NEIClientUtils; + +class SearchResult { + + public final Object searchGUID; + public ItemTreeSlot highlightNode; + public final Set searchMatches = new LinkedHashSet<>(); + + public SearchResult(Object searchGUID) { + this.searchGUID = searchGUID; + } + + public boolean matchesGUID(Object guid) { + if (guid instanceof ItemStack stack && this.searchGUID instanceof ItemStack searchStack) { + return NEIClientUtils.areStacksSameTypeCraftingWithNBT(stack, searchStack); + } + + return this.searchGUID.equals(guid); + } + +} diff --git a/src/main/java/codechicken/nei/bookmark/tree/StatsPanel.java b/src/main/java/codechicken/nei/bookmark/tree/StatsPanel.java new file mode 100644 index 00000000..cb0bf178 --- /dev/null +++ b/src/main/java/codechicken/nei/bookmark/tree/StatsPanel.java @@ -0,0 +1,744 @@ +package codechicken.nei.bookmark.tree; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import net.minecraft.client.renderer.RenderHelper; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; + +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; + +import codechicken.lib.gui.GuiDraw; +import codechicken.lib.vec.Rectangle4i; +import codechicken.nei.ItemSorter; +import codechicken.nei.ItemStackAmount; +import codechicken.nei.NEIClientUtils; +import codechicken.nei.NEIClientUtils.Alignment; +import codechicken.nei.Widget; +import codechicken.nei.bookmark.BookmarkItem; +import codechicken.nei.drawable.DrawableBuilder; +import codechicken.nei.drawable.DrawableResource; +import codechicken.nei.guihook.GuiContainerManager; +import codechicken.nei.recipe.GuiRecipeTab; +import codechicken.nei.recipe.HandlerInfo; +import codechicken.nei.recipe.Recipe.RecipeId; +import codechicken.nei.recipe.StackInfo; +import codechicken.nei.recipe.chain.RecipeChainMath; +import codechicken.nei.scroll.ScrollBar; +import codechicken.nei.scroll.ScrollContainer; +import codechicken.nei.util.ReadableNumberConverter; + +class StatsPanel extends ScrollContainer { + + public static class HandlerStats { + + public long iterations; + public long recipeCount; + } + + private static class SectionLabel extends Widget { + + public final String text; + + public SectionLabel(String text) { + this.text = text; + this.w = GuiDraw.fontRenderer.getStringWidth(text); + this.h = GuiDraw.fontRenderer.FONT_HEIGHT + 4; + } + + @Override + public void draw(int mx, int my) { + NEIClientUtils.gl2DRenderContext( + () -> GuiDraw.fontRenderer.drawStringWithShadow(this.text, this.x, this.y, 0xFFFFFFFF)); + } + } + + public static class ItemStatsSlot extends Widget { + + public final ItemStack stack; + public final long stackSize; + public final RecipeId recipeId; + public final long multiplier; + public final HandlerInfo info; + public final DrawableResource icon; + private final boolean isFluidDisplay; + public boolean highlight = false; + + public ItemStatsSlot(ItemStack stack, long stackSize, DrawableResource icon, RecipeId recipeId, + long multiplier) { + this.stack = StackInfo.withAmount(stack, 0); + this.stackSize = stackSize; + this.icon = icon; + this.recipeId = recipeId; + this.multiplier = multiplier; + this.isFluidDisplay = StackInfo.itemStackToNBT(stack).hasKey("gtFluidName"); + this.info = this.recipeId != null ? handlerInfo(this.recipeId.getHandlerName()) : null; + this.w = SLOT_SIZE; + this.h = SLOT_SIZE; + } + + public ItemStatsSlot(ItemStack stack, long stackSize, DrawableResource icon) { + this(stack, stackSize, icon, null, 1); + } + + @Override + public void draw(int mx, int my) { + + if (this.highlight) { + SLOT_HIGHLIGHTED.draw(this.x, this.y); + } else { + this.icon.draw(this.x, this.y); + } + + GuiContainerManager + .drawItem(this.x + (SLOT_SIZE - 16) / 2, this.y + (SLOT_SIZE - 16) / 2, this.stack, true, ""); + + drawStackSize(); + + if (mx >= this.x && mx < this.x + SLOT_SIZE && my >= this.y && my < this.y + SLOT_SIZE) { + NEIClientUtils.gl2DRenderContext( + () -> GuiDraw.drawRect(this.x + 1, this.y + 1, SLOT_SIZE - 2, SLOT_SIZE - 2, HOVER_COLOR)); + } + + if (this.recipeId != null) { + drawHandlerBadge(); + } + } + + private void drawHandlerBadge() { + final float scale = 0.6f; + final float badgeX = x - 6 * scale; + + GL11.glPushMatrix(); + GL11.glColor4f(1, 1, 1, 1); + GL11.glTranslatef(badgeX, y - 6 * scale, 3.0f); + GL11.glScalef(scale, scale, 1.0f); + + if (this.info.hasImageOrItem()) { + final DrawableResource image = this.info.getImage(); + + if (image != null) { + image.draw(0, 0); + } else { + GuiContainerManager.drawItem(0, 0, this.info.getItemStack(), true); + } + + } else { + ICON_CRAFTING.draw(0, 0); + } + + GL11.glPopMatrix(); + } + + private void drawStackSize() { + String amountString = stackSize < 10_000 ? String.valueOf(stackSize) + : ReadableNumberConverter.INSTANCE.toWideReadableForm(stackSize); + + if (this.isFluidDisplay) { + amountString += "L"; + } + + NEIClientUtils.drawNEIOverlayText( + amountString, + new Rectangle4i( + x + (SLOT_SIZE - 16) / 2, + y + (SLOT_SIZE - 16) / 2, + SLOT_SIZE - (SLOT_SIZE - 16), + SLOT_SIZE - (SLOT_SIZE - 16)), + 1, + 0xFFFFFF, + true, + this.isFluidDisplay ? Alignment.BottomLeft : Alignment.BottomRight); + } + + } + + public static class HandlerStatsSlot extends Widget { + + public final String handlerName; + public final HandlerInfo info; + public final HandlerStats stats; + public boolean highlight = false; + + public HandlerStatsSlot(String handlerName, HandlerStats stats) { + this.handlerName = handlerName; + this.info = handlerInfo(handlerName); + this.stats = stats; + this.w = SLOT_SIZE; + this.h = SLOT_SIZE; + } + + @Override + public void draw(int mx, int my) { + + if (this.highlight) { + SLOT_HIGHLIGHTED.draw(this.x, this.y); + } else { + SLOT_DEFAULT.draw(this.x, this.y); + } + + if (this.info.hasImageOrItem()) { + final DrawableResource image = this.info.getImage(); + + if (image != null) { + image.draw(this.x + (SLOT_SIZE - 16) / 2, this.y + (SLOT_SIZE - 16) / 2); + } else { + GuiContainerManager.drawItem( + this.x + (SLOT_SIZE - 16) / 2, + this.y + (SLOT_SIZE - 16) / 2, + this.info.getItemStack(), + true); + } + } else { + ICON_CRAFTING.draw(this.x + (SLOT_SIZE - 16) / 2, this.y + (SLOT_SIZE - 16) / 2); + } + + NEIClientUtils.drawNEIOverlayText( + "x" + ReadableNumberConverter.INSTANCE.toWideReadableForm(this.stats.iterations), + new Rectangle4i(this.x + 1, this.y + 1, SLOT_SIZE - 2, SLOT_SIZE - 2), + 1, + 0xFFFFFF, + true, + Alignment.TopLeft); + + NEIClientUtils.drawNEIOverlayText( + ReadableNumberConverter.INSTANCE.toWideReadableForm(this.stats.recipeCount), + new Rectangle4i(this.x + 1, this.y + 1, SLOT_SIZE - 2, SLOT_SIZE - 2), + 1, + 0xFFFFFF, + true, + Alignment.BottomRight); + + if (mx >= this.x && mx < this.x + SLOT_SIZE && my >= this.y && my < this.y + SLOT_SIZE) { + NEIClientUtils.gl2DRenderContext( + () -> GuiDraw.drawRect(this.x + 1, this.y + 1, SLOT_SIZE - 2, SLOT_SIZE - 2, HOVER_COLOR)); + } + } + + } + + private static final int SLOT_SIZE = 18; + + private static final int SLOT_INLINE_GAP = 2; + private static final int SLOT_BLOCK_GAP = 2; + private static final int COLUMN_GAP = 10; + + private static final int HOVER_COLOR = 0x66999999; + + private static final DrawableResource BG_TEXTURE = new DrawableBuilder( + "nei:textures/gui/craftingtree.png", + 26, + 84, + 46, + 16).build(); + + private static final DrawableResource SLOT_DEFAULT = new DrawableBuilder( + "nei:textures/gui/craftingtree.png", + 68, + 26, + 18, + 18).build(); + + private static final DrawableResource SLOT_MISSING = new DrawableBuilder( + "nei:textures/gui/craftingtree.png", + 92, + 26, + 18, + 18).build(); + + private static final DrawableResource SLOT_CRAFTING = new DrawableBuilder( + "nei:textures/gui/craftingtree.png", + 116, + 26, + 18, + 18).build(); + + private static final DrawableResource SLOT_DISABLED = new DrawableBuilder( + "nei:textures/gui/craftingtree.png", + 140, + 26, + 18, + 18).build(); + + private static final DrawableResource SLOT_HIGHLIGHTED = new DrawableBuilder( + "nei:textures/gui/craftingtree.png", + 164, + 26, + 18, + 18).build(); + + private static final DrawableResource ICON_CRAFTING = new DrawableBuilder( + "nei:textures/gui/craftingtree.png", + 67, + 48, + 16, + 16).build(); + + private static final int Z_OFFSET = 100; + + public StatsPanel() { + final ScrollBar scrollbar = new ScrollBar().setTrackWidth(4) + .setThumbTexture(new DrawableBuilder("nei:textures/gui/craftingtree.png", 26, 100, 24, 4).build(), 0, 0) + .setTrackTexture(new DrawableBuilder("nei:textures/gui/craftingtree.png", 26, 104, 24, 4).build(), 0, 0) + .setTrackPadding(1, -5, 1, 0).setOverflowType(ScrollBar.OverflowType.AUTO); + + setHorizontalScroll(scrollbar); + setPaddingBlock(4, 4); + setPaddingInline(1, 1); + } + + public void rebuild(RecipeChainMath math, CraftingTreeGraph graph, boolean useInventorySnapshot, + Set sections, int widgetX, int widgetY, int widgetW, int widgetH) { + + clear(); + + final List labels = new ArrayList<>(); + final List> sectionGroups = new ArrayList<>(); + final List inputsAndAvailable = new ArrayList<>(); + + if (sections.contains(CraftingTreeState.StatsSection.INGREDIENTS_NEEDED)) { + inputsAndAvailable.addAll(collectInputs(math, useInventorySnapshot)); + } + + if (sections.contains(CraftingTreeState.StatsSection.INGREDIENTS_AVAILABLE)) { + inputsAndAvailable.addAll(collectAvailable(math)); + } + + if (!inputsAndAvailable.isEmpty()) { + labels.add(NEIClientUtils.translate("bookmark.tree.stats.ingredients")); + sectionGroups.add(inputsAndAvailable); + } + + final boolean showCraftingNeeded = sections.contains(CraftingTreeState.StatsSection.CRAFTING_NEEDED); + final boolean showCraftingAvailable = sections.contains(CraftingTreeState.StatsSection.CRAFTING_AVAILABLE); + + if (showCraftingNeeded || showCraftingAvailable) { + final List craftingNeededSlot = new ArrayList<>(); + final List craftingAvailableSlot = new ArrayList<>(); + final Map recipeMissing = new LinkedHashMap<>(); + final Map recipeLevel = new HashMap<>(); + + for (Map.Entry> entry : graph.childrenOf.entrySet()) { + final ItemTreeSlot node = entry.getKey(); + final boolean needsCrafting = graph.childrenNeedCraftingCache.contains(node); + + recipeMissing.merge(node.prefItem, needsCrafting, (a, b) -> a || b); + recipeLevel.merge(node.prefItem, treeLevel(node, graph.parentOf), Math::max); + } + + final List> sortedRecipeMissing = new ArrayList<>( + recipeMissing.entrySet()); + + sortedRecipeMissing.sort( + Comparator.>comparingInt(entry -> recipeLevel.get(entry.getKey())) + .thenComparing(entry -> entry.getKey().itemStack, ItemSorter.instance)); + + for (Map.Entry entry : sortedRecipeMissing) { + final BookmarkItem resultItem = entry.getKey(); + final long amount = resultItem.getAmount(); + + if (amount > 0) { + final boolean missingIngredients = entry.getValue(); + + if (missingIngredients ? !showCraftingNeeded : !showCraftingAvailable) { + continue; + } + + final List slot = missingIngredients ? craftingNeededSlot : craftingAvailableSlot; + + slot.add( + new ItemStatsSlot( + resultItem.itemStack, + resultItem.getStackSize(amount), + missingIngredients ? SLOT_DEFAULT : SLOT_CRAFTING, + resultItem.recipeId, + resultItem.getMultiplier())); + } + } + + final List craftingSlots = new ArrayList<>(craftingNeededSlot); + craftingSlots.addAll(craftingAvailableSlot); + + if (!craftingSlots.isEmpty()) { + labels.add(NEIClientUtils.translate("bookmark.tree.stats.crafting")); + sectionGroups.add(craftingSlots); + } + } + + final List outputsAndRemainders = new ArrayList<>(); + + if (sections.contains(CraftingTreeState.StatsSection.RESULTS)) { + outputsAndRemainders.addAll(collectOutputs(math)); + } + + if (sections.contains(CraftingTreeState.StatsSection.REMAINDERS)) { + outputsAndRemainders.addAll(collectRemainder(math)); + } + + if (!outputsAndRemainders.isEmpty()) { + labels.add(NEIClientUtils.translate("bookmark.tree.stats.results")); + sectionGroups.add(outputsAndRemainders); + } + + if (sections.contains(CraftingTreeState.StatsSection.HANDLERS)) { + final List handlers = collectHandlers(math); + + if (!handlers.isEmpty()) { + labels.add(NEIClientUtils.translate("bookmark.tree.stats.handlers")); + sectionGroups.add(handlers); + } + } + + layoutSections(labels, sectionGroups, widgetX, widgetY, widgetW, widgetH); + } + + private void layoutSections(List labels, List> sections, int widgetX, int widgetY, int widgetW, + int widgetH) { + final int maxRowWidth = Math.max(SLOT_SIZE, widgetW); + final int sectionCount = sections.size(); + final int[] sizes = new int[sectionCount]; + final int[] labelWidths = new int[sectionCount]; + final int[] columns = new int[sectionCount]; + final int maxRows = widgetH / ((GuiDraw.fontRenderer.FONT_HEIGHT + 4) + SLOT_SIZE * 2 + SLOT_BLOCK_GAP) > 3 ? 2 + : 1; + + for (int i = 0; i < sectionCount; i++) { + sizes[i] = sections.get(i).size(); + labelWidths[i] = GuiDraw.fontRenderer.getStringWidth(labels.get(i) + ":"); + columns[i] = sizes[i] == 0 ? 0 : (sizes[i] + maxRows - 1) / maxRows; + } + + int compactWidth = COLUMN_GAP * Math.max(0, sectionCount - 1); + + for (int i = 0; i < sectionCount; i++) { + compactWidth += sectionWidth(columns[i], labelWidths[i]); + } + + if (compactWidth <= maxRowWidth) { + int budget = (maxRowWidth - compactWidth) / (SLOT_SIZE + SLOT_INLINE_GAP); + + while (budget > 0) { + boolean grew = false; + + for (int i = 0; i < sectionCount && budget > 0; i++) { + if (columns[i] < sizes[i]) { + columns[i]++; + --budget; + grew = true; + } + } + + if (!grew) { + break; + } + } + } + + final List widgets = new ArrayList<>(); + int cursorX = 0; + int maxHeight = 0; + + for (int i = 0; i < sectionCount; i++) { + final int height = placeSection(widgets, labels.get(i), sections.get(i), columns[i], cursorX, 0); + maxHeight = Math.max(maxHeight, height); + cursorX += sectionWidth(columns[i], labelWidths[i]) + COLUMN_GAP; + } + + final int contentW = Math.max(0, cursorX - COLUMN_GAP); + final int contentH = maxHeight; + + final int maxVisibleH = maxRows * SLOT_SIZE + Math.max(0, maxRows - 1) * SLOT_BLOCK_GAP + + GuiDraw.fontRenderer.FONT_HEIGHT + + 4; + + final int visibleHeight = Math.min(contentH, maxVisibleH); + + this.w = widgetW; + this.x = widgetX; + this.h = visibleHeight + this.paddingBlockStart + this.paddingBlockEnd; + this.y = widgetY + widgetH - this.h; + + final int xShift = contentW < this.w ? (this.w - contentW) / 2 : 0; + + for (Widget widget : widgets) { + widget.x += xShift; + } + + setWidgets(widgets); + update(); + } + + private static int sectionWidth(int columns, int labelWidth) { + return Math.max(columns * SLOT_SIZE + Math.max(0, columns - 1) * SLOT_INLINE_GAP, labelWidth); + } + + private static int placeSection(List widgets, String label, List group, int columns, int x, int y) { + final SectionLabel sectionLabel = new SectionLabel(label + ":"); + sectionLabel.x = x; + sectionLabel.y = y; + widgets.add(sectionLabel); + + final int gridY = y + GuiDraw.fontRenderer.FONT_HEIGHT + 4; + final int rowWidth = columns * SLOT_SIZE + Math.max(0, columns - 1) * SLOT_INLINE_GAP; + int cursorX = 0; + int row = 0; + boolean any = false; + + for (Widget entry : group) { + + if (cursorX > 0) { + if (cursorX + SLOT_INLINE_GAP + SLOT_SIZE > rowWidth) { + cursorX = 0; + row++; + } else { + cursorX += SLOT_INLINE_GAP; + } + } else if (cursorX + SLOT_SIZE > rowWidth) { + cursorX = 0; + row++; + } + + entry.x = x + cursorX; + entry.y = gridY + row * (SLOT_SIZE + SLOT_BLOCK_GAP); + widgets.add(entry); + + cursorX += SLOT_SIZE; + any = true; + } + + final int rows = any ? row + 1 : 0; + return (gridY - y) + rows * SLOT_SIZE + Math.max(0, rows - 1) * SLOT_BLOCK_GAP; + } + + public ItemStack getStackMouseOver(int mouseX, int mouseY) { + final Widget widget = getWidgetUnderMouse(mouseX, mouseY); + return widget instanceof ItemStatsSlot slot ? slot.stack : null; + } + + public void setHighlight(SearchResult searchResult) { + for (Widget widget : getWidgets()) { + if (widget instanceof HandlerStatsSlot slot) { + slot.highlight = searchResult != null && searchResult.matchesGUID(slot.handlerName); + } else if (widget instanceof ItemStatsSlot slot) { + slot.highlight = searchResult != null && searchResult.matchesGUID(slot.stack); + } + } + } + + public Object getWidgetGUIDUnderMouse(int mouseX, int mouseY) { + final Widget widget = getWidgetUnderMouse(mouseX, mouseY); + + if (widget instanceof HandlerStatsSlot slot) { + return slot.handlerName; + } else if (widget instanceof ItemStatsSlot slot) { + return slot.stack; + } + + return null; + } + + public String getHandlerMouseOver(int mouseX, int mouseY) { + final Widget widget = getWidgetUnderMouse(mouseX, mouseY); + + if (widget instanceof HandlerStatsSlot slot) { + return slot.handlerName; + } else if (widget instanceof ItemStatsSlot slot && slot.recipeId != null) { + return slot.recipeId.getHandlerName(); + } + + return null; + } + + public RecipeId getRecipeIdMouseOver(int mouseX, int mouseY) { + final Widget widget = getWidgetUnderMouse(mouseX, mouseY); + return widget instanceof ItemStatsSlot slot ? slot.recipeId : null; + } + + public boolean isDraggingScrollbar() { + return canScrollHorizontal() && getHorizontalScroll().isMouseDragged(); + } + + @Override + public boolean onMouseWheel(int direction, int mouseX, int mouseY) { + + if (canScrollHorizontal() && boundsOutside().contains(mouseX, mouseY)) { + getHorizontalScroll().setScrollOffset(this, getHorizontalScrollOffset() - direction * MOUSE_SCROLL_SPEED); + return true; + } + + return super.onMouseWheel(direction, mouseX, mouseY); + } + + public void draw(int mouseX, int mouseY) { + GL11.glTranslatef(0, 0, Z_OFFSET); + GL11.glPushAttrib(GL11.GL_ENABLE_BIT | GL11.GL_COLOR_BUFFER_BIT | GL11.GL_LIGHTING_BIT); + GL11.glEnable(GL12.GL_RESCALE_NORMAL); + GL11.glColor4f(1, 1, 1, 1); + RenderHelper.enableGUIStandardItemLighting(); + GL11.glDisable(GL11.GL_LIGHTING); + + BG_TEXTURE.draw(this.x, this.y, this.w, this.h, 0, 0, 2, 0); + + super.draw(mouseX, mouseY); + + GL11.glPopAttrib(); + GL11.glTranslatef(0, 0, -Z_OFFSET); + } + + public void drawFull() { + GL11.glPushAttrib(GL11.GL_ENABLE_BIT | GL11.GL_COLOR_BUFFER_BIT | GL11.GL_LIGHTING_BIT); + GL11.glEnable(GL12.GL_RESCALE_NORMAL); + GL11.glColor4f(1, 1, 1, 1); + RenderHelper.enableGUIStandardItemLighting(); + GL11.glDisable(GL11.GL_LIGHTING); + + drawContent(-1, -1); + GL11.glPopAttrib(); + } + + private List collectAvailable(RecipeChainMath math) { + final ItemStackAmount items = new ItemStackAmount(); + final List widgets = new ArrayList<>(); + + for (BookmarkItem item : math.initialItems) { + final long amount = math.requiredAmount.getOrDefault(item, 0L); + + if (amount > 0) { + items.add(item.itemStack, item.getStackSize(amount)); + } + + } + + for (Map.Entry entry : items.entrySet()) { + if (entry.getValue() > 0) { + widgets.add( + new ItemStatsSlot(StackInfo.loadFromNBT(entry.getKey(), 0), entry.getValue(), SLOT_DEFAULT)); + } + } + + return widgets; + } + + private List collectInputs(RecipeChainMath math, boolean useInventorySnapshot) { + final ItemStackAmount items = new ItemStackAmount(); + final List widgets = new ArrayList<>(); + + for (BookmarkItem item : math.recipeIngredients) { + final long amount = math.requiredAmount.containsKey(math.preferredItems.get(item)) ? 0 + : math.requiredAmount.getOrDefault(item, item.getAmount()); + + if (amount > 0) { + items.add(item.itemStack, item.getStackSize(amount)); + } + } + + for (Map.Entry entry : items.entrySet()) { + if (entry.getValue() > 0) { + widgets.add( + new ItemStatsSlot( + StackInfo.loadFromNBT(entry.getKey(), 0), + entry.getValue(), + useInventorySnapshot ? SLOT_MISSING : SLOT_DEFAULT)); + } + } + + return widgets; + } + + private List collectOutputs(RecipeChainMath math) { + final ItemStackAmount items = new ItemStackAmount(); + final List widgets = new ArrayList<>(); + + for (BookmarkItem item : math.recipeResults) { + final long amount = item.getAmount() - math.requiredAmount.getOrDefault(item, 0L); + + if (amount > 0 && math.outputRecipes.containsKey(item.recipeId)) { + items.add(item.itemStack, item.getStackSize(amount)); + } + } + + for (Map.Entry entry : items.entrySet()) { + if (entry.getValue() > 0) { + widgets.add( + new ItemStatsSlot(StackInfo.loadFromNBT(entry.getKey(), 0), entry.getValue(), SLOT_DEFAULT)); + } + } + + return widgets; + } + + private List collectRemainder(RecipeChainMath math) { + final ItemStackAmount items = new ItemStackAmount(); + final List widgets = new ArrayList<>(); + + for (BookmarkItem item : math.recipeResults) { + final long amount = item.getAmount() - math.requiredAmount.getOrDefault(item, 0L); + + if (amount > 0 && !math.outputRecipes.containsKey(item.recipeId)) { + items.add(item.itemStack, item.getStackSize(amount)); + } + } + + for (ItemStack stack : math.containerItemsInventory) { + if (stack != null) { + items.add(stack); + } + } + + for (ItemStack stack : math.containerItemsCrafting) { + if (stack != null) { + items.add(stack); + } + } + + for (Map.Entry entry : items.entrySet()) { + if (entry.getValue() > 0) { + widgets.add( + new ItemStatsSlot(StackInfo.loadFromNBT(entry.getKey(), 0), entry.getValue(), SLOT_DISABLED)); + } + } + + return widgets; + } + + private List collectHandlers(RecipeChainMath math) { + final Map handlerStats = new HashMap<>(); + final List widgets = new ArrayList<>(); + + for (BookmarkItem item : math.recipeResults) { + final HandlerStats stats = handlerStats + .computeIfAbsent(item.recipeId.getHandlerName(), k -> new HandlerStats()); + stats.iterations += item.getMultiplier(); + stats.recipeCount++; + } + + for (Map.Entry entry : handlerStats.entrySet()) { + widgets.add(new HandlerStatsSlot(entry.getKey(), entry.getValue())); + } + + return widgets; + } + + private static int treeLevel(ItemTreeSlot node, Map parentOf) { + int level = 0; + + for (ItemTreeSlot current = parentOf.get(node); current != null; current = parentOf.get(current)) { + level++; + } + + return level; + } + + private static HandlerInfo handlerInfo(String handlerName) { + final HandlerInfo info = GuiRecipeTab.getHandlerInfo(handlerName, null); + return info != null ? info : GuiRecipeTab.DEFAULT_HANDLER_INFO; + } + +} diff --git a/src/main/java/codechicken/nei/bookmark/tree/StatsSectionsMenu.java b/src/main/java/codechicken/nei/bookmark/tree/StatsSectionsMenu.java new file mode 100644 index 00000000..e4a2b483 --- /dev/null +++ b/src/main/java/codechicken/nei/bookmark/tree/StatsSectionsMenu.java @@ -0,0 +1,121 @@ +package codechicken.nei.bookmark.tree; + +import static codechicken.lib.gui.GuiDraw.drawStringC; + +import org.lwjgl.opengl.GL11; + +import codechicken.lib.gui.GuiDraw; +import codechicken.nei.LayoutManager; +import codechicken.nei.NEIClientUtils; +import codechicken.nei.bookmark.tree.CraftingTreeState.StatsSection; + +class StatsSectionsMenu { + + private static final StatsSection[] SECTIONS = StatsSection.values(); + + private static final int ROW_HEIGHT = 16; + private static final int LABEL_PADDING = 10; + private static final int MIN_WIDTH = 70; + private static final int TOP_PADDING = 4; + private static final int Z_OFFSET = 500; + + private static final int COLOR_HOVER = 0xFFFFFFA0; + private static final int COLOR_ENABLED = 0xFFE0E0E0; + private static final int COLOR_DISABLED = 0xFFA0A0A0; + + public boolean visible = false; + + private int x; + private int y; + private int w; + private int h; + + public void open(int anchorX, int anchorY) { + this.visible = true; + + int maxLabelWidth = 0; + + for (StatsSection section : SECTIONS) { + maxLabelWidth = Math.max(maxLabelWidth, GuiDraw.fontRenderer.getStringWidth(label(section))); + } + + this.w = Math.max(MIN_WIDTH, maxLabelWidth + LABEL_PADDING * 2); + this.h = TOP_PADDING + SECTIONS.length * ROW_HEIGHT; + this.x = anchorX; + this.y = anchorY; + } + + public void close() { + this.visible = false; + } + + public boolean isInWidget(int mouseX, int mouseY) { + return this.visible && mouseX >= this.x + && mouseX < this.x + this.w + && mouseY >= this.y + && mouseY < this.y + this.h; + } + + public boolean handleClick(int mouseX, int mouseY, CraftingTreeState uiState) { + + if (!isInWidget(mouseX, mouseY)) { + return false; + } + + final int relativeY = mouseY - this.y - TOP_PADDING; + + if (relativeY >= 0) { + final int index = relativeY / ROW_HEIGHT; + + if (index < SECTIONS.length) { + uiState.toggleStatsSection(SECTIONS[index]); + } + } + + return true; + } + + public void draw(int mouseX, int mouseY, CraftingTreeState uiState) { + + if (!this.visible) { + return; + } + + GL11.glTranslatef(0, 0, Z_OFFSET); + + for (int i = 0; i < SECTIONS.length; i++) { + final StatsSection section = SECTIONS[i]; + final int rowY = this.y + TOP_PADDING + i * ROW_HEIGHT; + final boolean hover = mouseX >= this.x && mouseX < this.x + this.w + && mouseY >= rowY + && mouseY < rowY + ROW_HEIGHT; + final boolean checked = uiState.visibleStatsSections.contains(section); + final int color = hover ? COLOR_HOVER : checked ? COLOR_ENABLED : COLOR_DISABLED; + + if (checked) { + GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); + LayoutManager.drawButtonBackground(this.x, rowY, this.w, ROW_HEIGHT, false, 1); + } else { + GL11.glColor4f(0.65F, 0.65F, 0.65F, 1.0F); + LayoutManager.drawButtonBackground(this.x, rowY, this.w, ROW_HEIGHT, false, 0); + } + + drawStringC(label(section), this.x, rowY, this.w, ROW_HEIGHT, color); + } + + GL11.glColor4f(1, 1, 1, 1); + GL11.glTranslatef(0, 0, -Z_OFFSET); + } + + private static String label(StatsSection section) { + return NEIClientUtils.translate(switch (section) { + case INGREDIENTS_NEEDED -> "bookmark.tree.stats.ingredients_needed"; + case INGREDIENTS_AVAILABLE -> "bookmark.tree.stats.ingredients_available"; + case CRAFTING_NEEDED -> "bookmark.tree.stats.crafting_needed"; + case CRAFTING_AVAILABLE -> "bookmark.tree.stats.crafting_available"; + case RESULTS -> "bookmark.tree.stats.results"; + case REMAINDERS -> "bookmark.tree.stats.remainders"; + case HANDLERS -> "bookmark.tree.stats.handlers"; + }); + } +} diff --git a/src/main/java/codechicken/nei/bookmark/tree/TreeOverlayButton.java b/src/main/java/codechicken/nei/bookmark/tree/TreeOverlayButton.java new file mode 100644 index 00000000..5becebf2 --- /dev/null +++ b/src/main/java/codechicken/nei/bookmark/tree/TreeOverlayButton.java @@ -0,0 +1,75 @@ +package codechicken.nei.bookmark.tree; + +import java.util.List; +import java.util.Map; + +import net.minecraft.client.Minecraft; + +import org.lwjgl.opengl.GL11; + +import codechicken.nei.NEIClientUtils; +import codechicken.nei.drawable.DrawableBuilder; +import codechicken.nei.drawable.DrawableResource; +import codechicken.nei.recipe.GuiRecipeButton; +import codechicken.nei.recipe.Recipe; +import codechicken.nei.recipe.RecipeHandlerRef; + +public class TreeOverlayButton extends GuiRecipeButton { + + protected static final DrawableResource ICON_FILL = new DrawableBuilder( + "nei:textures/nei_sprites.png", + 28, + 76, + 9, + 10).build(); + protected static final int BUTTON_ID_SHIFT = 4; + + public final GuiCraftingTree treeGui; + + public TreeOverlayButton(GuiCraftingTree treeGui, RecipeHandlerRef handlerRef, int x, int y) { + super(handlerRef, x, y, handlerRef.recipeIndex + BUTTON_ID_SHIFT, "+"); + this.treeGui = treeGui; + } + + @Override + public List handleTooltip(List currenttip) { + currenttip.add(NEIClientUtils.translate("bookmark.tree.overlay")); + return currenttip; + } + + @Override + public Map handleHotkeys(int mousex, int mousey, Map hotkeys) { + return hotkeys; + } + + @Override + public void mouseReleased(int mouseX, int mouseY) { + if (this.treeGui != null) { + this.treeGui.addRecipe(Recipe.of(this.handlerRef)); + } + } + + @Override + protected void drawContent(Minecraft minecraft, int y, int x, boolean mouseOver) { + final int iconX = this.xPosition + (this.width - ICON_FILL.width - 1) / 2; + final int iconY = this.yPosition + (this.height - ICON_FILL.height) / 2; + + GL11.glColor4f(1, 1, 1, this.enabled ? 1 : 0.5f); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + ICON_FILL.draw(iconX, iconY); + GL11.glDisable(GL11.GL_BLEND); + GL11.glColor4f(1, 1, 1, 1); + } + + @Override + public void drawItemOverlay() {} + + public Recipe getRecipe() { + return Recipe.of(this.handlerRef); + } + + @Override + public void lastKeyTyped(char keyChar, int keyID) {} + +} diff --git a/src/main/java/codechicken/nei/drawable/DrawableResource.java b/src/main/java/codechicken/nei/drawable/DrawableResource.java index f73b9fd1..a2d5c8f1 100644 --- a/src/main/java/codechicken/nei/drawable/DrawableResource.java +++ b/src/main/java/codechicken/nei/drawable/DrawableResource.java @@ -40,6 +40,10 @@ public class DrawableResource extends Image { return height + paddingTop + paddingBottom; } + public void bindTexture() { + Minecraft.getMinecraft().getTextureManager().bindTexture(this.resourceLocation); + } + public void draw(int xOffset, int yOffset) { draw(xOffset, yOffset, 0, 0, 0, 0); } @@ -50,7 +54,7 @@ public class DrawableResource extends Image { final int height = this.height - maskBottom - maskTop; GL11.glEnable(GL11.GL_TEXTURE_2D); - Minecraft.getMinecraft().getTextureManager().bindTexture(this.resourceLocation); + bindTexture(); drawModalRectWithCustomSizedTexture(xOffset + maskLeft, yOffset + maskTop, maskLeft, maskTop, width, height); if (!is2DTexture) GL11.glDisable(GL11.GL_TEXTURE_2D); } @@ -72,7 +76,7 @@ public class DrawableResource extends Image { final int remainderHeight = tileHeight - (tileHeightCount * middleHeight); GL11.glEnable(GL11.GL_TEXTURE_2D); - Minecraft.getMinecraft().getTextureManager().bindTexture(this.resourceLocation); + bindTexture(); // middle parts for (int tileW = 0; tileW <= tileWidthCount; tileW++) { diff --git a/src/main/java/codechicken/nei/guihook/GuiContainerManager.java b/src/main/java/codechicken/nei/guihook/GuiContainerManager.java index 4a40f523..07432ed2 100644 --- a/src/main/java/codechicken/nei/guihook/GuiContainerManager.java +++ b/src/main/java/codechicken/nei/guihook/GuiContainerManager.java @@ -620,7 +620,7 @@ public class GuiContainerManager { if (!secondDisplayName.isEmpty()) { tooltip.add(1, EnumChatFormatting.DARK_GRAY + secondDisplayName + GuiDraw.TOOLTIP_LINESPACE); - } else { + } else if (!tooltip.get(0).startsWith(GuiDraw.TOOLTIP_HANDLER)) { tooltip.set(0, tooltip.get(0) + GuiDraw.TOOLTIP_LINESPACE); } diff --git a/src/main/java/codechicken/nei/recipe/GuiFavoriteButton.java b/src/main/java/codechicken/nei/recipe/GuiFavoriteButton.java index 9340c49a..c3be9d55 100644 --- a/src/main/java/codechicken/nei/recipe/GuiFavoriteButton.java +++ b/src/main/java/codechicken/nei/recipe/GuiFavoriteButton.java @@ -23,6 +23,7 @@ import codechicken.nei.KeyManager; import codechicken.nei.LayoutManager; import codechicken.nei.NEIClientConfig; import codechicken.nei.NEIClientUtils; +import codechicken.nei.bookmark.tree.GuiCraftingTree; import codechicken.nei.drawable.DrawableBuilder; import codechicken.nei.drawable.DrawableResource; import codechicken.nei.recipe.Recipe.RecipeId; @@ -127,6 +128,12 @@ public class GuiFavoriteButton extends GuiRecipeButton { saveRecipeInBookmark(); } + if (KeyManager.isKeyDown("bookmark.open_crafting_tree") && NEIClientUtils.shiftKey() + && saveRecipeInBookmark()) { + final int groupId = ItemPanels.bookmarkPanel.getGrid().getNextGroupId() - 1; + GuiCraftingTree.openCraftingTreeGui(ItemPanels.bookmarkPanel.getGrid(), groupId); + } + } @Override @@ -135,6 +142,10 @@ public class GuiFavoriteButton extends GuiRecipeButton { KeyManager.getKeyName("bookmark.add", NEIClientUtils.SHIFT_HASH), translate("recipe.favorite.bookmark_recipe")); + hotkeys.put( + KeyManager.getKeyName("bookmark.open_crafting_tree", NEIClientUtils.SHIFT_HASH), + NEIClientUtils.translate("bookmark.group.open_crafting_tree")); + if (this.recipe.getResults().size() > 1) { hotkeys.put( NEIMouseUtils.getKeyName(NEIMouseUtils.MOUSE_BTN_NONE + NEIMouseUtils.MOUSE_SCROLL), @@ -189,8 +200,8 @@ public class GuiFavoriteButton extends GuiRecipeButton { return true; } - public void saveRecipeInBookmark() { - ItemPanels.bookmarkPanel.addGroup(getRecipesTree(getRecipe()), BookmarkViewMode.TODO_LIST, true); + public boolean saveRecipeInBookmark() { + return ItemPanels.bookmarkPanel.addGroup(getRecipesTree(getRecipe()), BookmarkViewMode.TODO_LIST, true); } private List getRecipesTree(Recipe mainRecipe) { diff --git a/src/main/java/codechicken/nei/recipe/RecipeTooltipLineHandler.java b/src/main/java/codechicken/nei/recipe/RecipeTooltipLineHandler.java index 8e69de7d..147d7a53 100644 --- a/src/main/java/codechicken/nei/recipe/RecipeTooltipLineHandler.java +++ b/src/main/java/codechicken/nei/recipe/RecipeTooltipLineHandler.java @@ -13,6 +13,7 @@ import codechicken.nei.drawable.DrawableResource; import codechicken.nei.guihook.GuiContainerManager; import codechicken.nei.recipe.Recipe.RecipeId; import codechicken.nei.scroll.GuiHelper; +import codechicken.nei.util.ReadableNumberConverter; public class RecipeTooltipLineHandler implements ITooltipLineHandler { @@ -32,9 +33,15 @@ public class RecipeTooltipLineHandler implements ITooltipLineHandler { protected NEIRecipeWidget widget = null; protected RecipeId recipeId = null; protected boolean createdGui = false; + protected long multiplier = 0; + + public RecipeTooltipLineHandler(RecipeId recipeId, long multiplier) { + this.recipeId = recipeId; + this.multiplier = multiplier; + } public RecipeTooltipLineHandler(RecipeId recipeId) { - this.recipeId = recipeId; + this(recipeId, 0); } public ItemStack getItemStack() { @@ -45,6 +52,10 @@ public class RecipeTooltipLineHandler implements ITooltipLineHandler { return this.recipeId; } + public long getMultiplier() { + return this.multiplier; + } + @Override public Dimension getSize() { @@ -104,6 +115,14 @@ public class RecipeTooltipLineHandler implements ITooltipLineHandler { GuiDraw.drawRect(BG_PADDING, BG_PADDING, size.width - BG_PADDING * 2, 12, 0x30000000); GuiDraw.drawStringC(this.recipeName, this.widget.w / 2, BG_PADDING + 2, 0xffffff); + if (this.multiplier > 0) { + GuiDraw.drawString( + "x" + ReadableNumberConverter.INSTANCE.toWideReadableForm(this.multiplier), + BG_PADDING + 2, + BG_PADDING + 2, + 0xffffff); + } + GuiHelper.useScissor( this.widget.x, this.widget.y, diff --git a/src/main/java/codechicken/nei/recipe/chain/RecipeChainMath.java b/src/main/java/codechicken/nei/recipe/chain/RecipeChainMath.java index a0dbb33b..2dda27d4 100644 --- a/src/main/java/codechicken/nei/recipe/chain/RecipeChainMath.java +++ b/src/main/java/codechicken/nei/recipe/chain/RecipeChainMath.java @@ -1,5 +1,6 @@ package codechicken.nei.recipe.chain; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -25,7 +26,7 @@ import codechicken.nei.recipe.StackInfo; public class RecipeChainMath { - private static class ContainerItemResult { + protected static class ContainerItemResult { public ItemStack stack; public ItemStack containerItem; @@ -48,13 +49,19 @@ public class RecipeChainMath { public final List recipeIngredients = new ArrayList<>(); public final List recipeResults = new ArrayList<>(); + protected final Map> ingredientsByRecipe = new HashMap<>(); + protected final Map> resultsByRecipe = new HashMap<>(); + public final Map preferredItems = new HashMap<>(); public final Map requiredAmount = new HashMap<>(); - public final List containerItems = new ArrayList<>(); + + public final List containerItemsCrafting = new ArrayList<>(); + public final List containerItemsInventory = new ArrayList<>(); + private final List containerItemsBlacklist = new ArrayList<>(); - private RecipeChainMath(List recipeItems, Set collapsedRecipes) { + protected RecipeChainMath(List recipeItems, Set collapsedRecipes) { final Map multipliers = new HashMap<>(); for (BookmarkItem item : recipeItems) { @@ -70,6 +77,8 @@ public class RecipeChainMath { } } + rebuildRecipeIndex(); + for (Map.Entry entry : multipliers.entrySet()) { if (entry.getValue() > 1 || collapsedRecipes.contains(entry.getKey())) { collectPreferredItems(entry.getKey(), this.preferredItems, new HashSet<>()); @@ -125,14 +134,26 @@ public class RecipeChainMath { } + private void rebuildRecipeIndex() { + this.ingredientsByRecipe.clear(); + this.resultsByRecipe.clear(); + + for (BookmarkItem item : this.recipeIngredients) { + this.ingredientsByRecipe.computeIfAbsent(item.recipeId, k -> new ArrayList<>()).add(item); + } + + for (BookmarkItem item : this.recipeResults) { + this.resultsByRecipe.computeIfAbsent(item.recipeId, k -> new ArrayList<>()).add(item); + } + } + private void collectPreferredItems(RecipeId recipeId, Map preferredItems, Set visited) { visited.add(recipeId); - for (BookmarkItem ingrItem : this.recipeIngredients) { - if (!ingrItem.emptyFactor() && recipeId.equals(ingrItem.recipeId) - && !preferredItems.containsKey(ingrItem)) { + for (BookmarkItem ingrItem : this.ingredientsByRecipe.getOrDefault(recipeId, Collections.emptyList())) { + if (!ingrItem.emptyFactor() && !preferredItems.containsKey(ingrItem)) { BookmarkItem activeItem = null; BookmarkItem prefItem = null; @@ -178,8 +199,8 @@ public class RecipeChainMath { private void removeLoop(RecipeId recipeId, Map preferredItems, Set visited) { visited.add(recipeId); - for (BookmarkItem ingrItem : this.recipeIngredients) { - if (!ingrItem.emptyFactor() && recipeId.equals(ingrItem.recipeId) && preferredItems.containsKey(ingrItem)) { + for (BookmarkItem ingrItem : this.ingredientsByRecipe.getOrDefault(recipeId, Collections.emptyList())) { + if (!ingrItem.emptyFactor() && preferredItems.containsKey(ingrItem)) { BookmarkItem prefItem = preferredItems.get(ingrItem); if (visited.contains(prefItem.recipeId)) { @@ -194,6 +215,87 @@ public class RecipeChainMath { visited.remove(recipeId); } + private List getOutputRecipeProcessingOrder() { + + if (this.outputRecipes.size() <= 1) { + return new ArrayList<>(this.outputRecipes.keySet()); + } + + final Map> dependents = new HashMap<>(); + final Map inDegree = new HashMap<>(); + + for (RecipeId recipeId : this.outputRecipes.keySet()) { + inDegree.put(recipeId, 0); + } + + for (RecipeId recipeId : this.outputRecipes.keySet()) { + final Set dependencies = new HashSet<>(); + collectBoxDependencies(recipeId, recipeId, dependencies, new HashSet<>()); + dependents.put(recipeId, dependencies); + + for (RecipeId dependency : dependencies) { + inDegree.merge(dependency, 1, Integer::sum); + } + } + + final List order = new ArrayList<>(); + final ArrayDeque queue = new ArrayDeque<>(); + + for (RecipeId recipeId : this.outputRecipes.keySet()) { + if (inDegree.get(recipeId) == 0) { + queue.add(recipeId); + } + } + + while (!queue.isEmpty()) { + final RecipeId recipeId = queue.poll(); + order.add(recipeId); + + for (RecipeId dependency : dependents.getOrDefault(recipeId, Collections.emptySet())) { + if (inDegree.merge(dependency, -1, Integer::sum) == 0) { + queue.add(dependency); + } + } + } + + if (order.size() != this.outputRecipes.size()) { + for (RecipeId recipeId : this.outputRecipes.keySet()) { + if (!order.contains(recipeId)) { + order.add(recipeId); + } + } + } + + return order; + } + + private void collectBoxDependencies(RecipeId boxId, RecipeId currentRecipeId, Set dependencies, + Set visited) { + + if (!visited.add(currentRecipeId)) { + return; + } + + for (BookmarkItem ingrItem : this.ingredientsByRecipe.getOrDefault(currentRecipeId, Collections.emptyList())) { + if (ingrItem.emptyFactor()) { + continue; + } + + final BookmarkItem prefItem = this.preferredItems.get(ingrItem); + if (prefItem == null) { + continue; + } + + if (this.outputRecipes.containsKey(prefItem.recipeId) && !prefItem.recipeId.equals(boxId)) { + dependencies.add(prefItem.recipeId); + } else { + collectBoxDependencies(boxId, prefItem.recipeId, dependencies, visited); + } + } + + visited.remove(currentRecipeId); + } + public RecipeId createMasterRoot() { final List rootIngredients = new ArrayList<>(); @@ -216,6 +318,7 @@ public class RecipeChainMath { BookmarkItem.builder(-1, ROOT_ITEM).factor(1).recipeId(ROOT_RECIPE_ID).type(BookmarkItemType.RESULT) .build()); this.recipeIngredients.addAll(rootIngredients); + rebuildRecipeIndex(); return ROOT_RECIPE_ID; } @@ -276,6 +379,12 @@ public class RecipeChainMath { return missedItems; } + protected boolean isForeignBoxBoundary(BookmarkItem ingrItem) { + final BookmarkItem prefItem = this.preferredItems.get(ingrItem); + return prefItem != null && ingrItem.type != BookmarkItemType.RESULT + && this.outputRecipes.containsKey(prefItem.recipeId); + } + private void resetCalculation() { for (BookmarkItem item : this.recipeIngredients) { @@ -288,7 +397,8 @@ public class RecipeChainMath { this.preferredItems.clear(); this.requiredAmount.clear(); - this.containerItems.clear(); + this.containerItemsCrafting.clear(); + this.containerItemsInventory.clear(); this.containerItemsBlacklist.clear(); for (RecipeId recipeId : this.outputRecipes.keySet()) { @@ -313,9 +423,13 @@ public class RecipeChainMath { } } - for (BookmarkItem prefItem : this.recipeResults) { - if (!prefItem.emptyFactor() && this.outputRecipes.containsKey(prefItem.recipeId)) { - final long prefMultiplier = this.outputRecipes.get(prefItem.recipeId); + for (RecipeId recipeId : getOutputRecipeProcessingOrder()) { + final long prefMultiplier = this.outputRecipes.get(recipeId); + + for (BookmarkItem prefItem : this.resultsByRecipe.getOrDefault(recipeId, Collections.emptyList())) { + if (prefItem.emptyFactor()) { + continue; + } if (prefItem.itemStack.getItem().hasContainerItem(prefItem.itemStack)) { this.containerItemsBlacklist.add(prefItem.itemStack); @@ -341,20 +455,40 @@ public class RecipeChainMath { return this; } - private void calculateSuitableRecipe(BookmarkItem ingrItem, long ingrMultiplier, List visited) { + protected void calculateSuitableRecipe(BookmarkItem ingrItem, long ingrMultiplier, List visited) { final BookmarkItem prefItem = this.preferredItems.get(ingrItem); long ingrAmount = ingrItem .getAmount(ingrMultiplier, prefItem != null ? prefItem.itemStack : ingrItem.itemStack); + final long requested = ingrAmount; + long containerAmount = 0; + long inventoryAmount = 0; // calculate existing containers if (ingrAmount > 0) { for (ItemStack stack : ingrItem.permutations.values()) { if (hasContainerItem(stack)) { - final long stackSize = ingrItem.getStackSize(ingrAmount); - final long shiftSize = shiftContainerItems(stack, stackSize); - if (stackSize != shiftSize && (ingrAmount = shiftSize * ingrItem.fluidCellAmount) == 0) { - break; + long stackSize = ingrItem.getStackSize(ingrAmount); + long shiftSize = shiftContainerItems(stack, stackSize, this.containerItemsCrafting); + + if (stackSize != shiftSize) { + final long newIngrAmount = shiftSize * ingrItem.fluidCellAmount; + containerAmount += ingrAmount - newIngrAmount; + if ((ingrAmount = newIngrAmount) == 0) { + break; + } } + + stackSize = shiftSize; + shiftSize = shiftContainerItems(stack, stackSize, this.containerItemsInventory); + + if (stackSize != shiftSize) { + final long newIngrAmount = shiftSize * ingrItem.fluidCellAmount; + inventoryAmount += ingrAmount - newIngrAmount; + if ((ingrAmount = newIngrAmount) == 0) { + break; + } + } + } } } @@ -362,20 +496,36 @@ public class RecipeChainMath { // calculate existing initial items if (ingrAmount > 0) { for (BookmarkItem item : this.initialItems) { - if (item.containsItems(ingrItem) - && (ingrAmount = addRequiredAmount(item, ingrAmount, item.getAmount())) == 0) { - break; + if (item.containsItems(ingrItem)) { + final long newIngrAmount = addRequiredAmount( + item, + ingrAmount, + item.getAmount(), + this.containerItemsInventory); + inventoryAmount += ingrAmount - newIngrAmount; + if ((ingrAmount = newIngrAmount) == 0) { + break; + } } } } // shift amount - if (prefItem == null) { - addRequiredAmount(ingrItem, ingrAmount, Long.MAX_VALUE); - } else if (visited.contains(prefItem.recipeId)) { - addRequiredAmount(prefItem, ingrAmount, Long.MAX_VALUE); - } else { - addRequiredAmount(prefItem, ingrAmount, Long.MAX_VALUE); + final BookmarkItem producer = prefItem != null ? prefItem : ingrItem; + final long beforeShift = this.requiredAmount.getOrDefault(producer, 0L); + + addRequiredAmount(producer, ingrAmount, Long.MAX_VALUE, this.containerItemsCrafting); + + calculationStepTrigger( + ingrItem, + prefItem, + requested, + containerAmount, + inventoryAmount, + ingrAmount, + this.requiredAmount.getOrDefault(producer, 0L) - beforeShift); + + if (prefItem != null && !isForeignBoxBoundary(ingrItem) && !visited.contains(prefItem.recipeId)) { final long multiplier = Math .max(0, prefItem.getMultiplierFromAmount(this.requiredAmount.get(prefItem)) - prefItem.multiplier); @@ -389,17 +539,21 @@ public class RecipeChainMath { } - private void prepareIngredients(RecipeId recipeId, long multiplier, List visited) { + protected void calculationStepTrigger(BookmarkItem ingrItem, BookmarkItem prefItem, long requested, + long containerAmount, long inventoryAmount, long ingrAmount, long produced) {} - for (BookmarkItem item : this.recipeIngredients) { - if (!item.emptyFactor() && recipeId.equals(item.recipeId)) { + protected void prepareIngredients(RecipeId recipeId, long multiplier, List visited) { + + for (BookmarkItem item : this.ingredientsByRecipe.getOrDefault(recipeId, Collections.emptyList())) { + if (!item.emptyFactor()) { calculateSuitableRecipe(item, multiplier, visited); } } } - private long addRequiredAmount(BookmarkItem prefItem, long ingrAmount, long maxAmount) { + protected long addRequiredAmount(BookmarkItem prefItem, long ingrAmount, long maxAmount, + List containerItems) { long shiftAmount = this.requiredAmount.getOrDefault(prefItem, 0L); if (hasContainerItem(prefItem.itemStack)) { @@ -410,7 +564,7 @@ public class RecipeChainMath { itemStack = itemStack.copy(); itemStack.stackSize = 1; - ingrAmount = shiftContainerItems(itemStack, prefItem.getStackSize(ingrAmount)) + ingrAmount = shiftContainerItems(itemStack, prefItem.getStackSize(ingrAmount), containerItems) * prefItem.fluidCellAmount; if (ingrAmount > 0) { @@ -419,7 +573,8 @@ public class RecipeChainMath { if (result.stack == null) { final long stepsPerReplacement = steps - result.leftSteps; - multiplier = Math.min(steps / stepsPerReplacement, maxAmount - shiftAmount); + multiplier = Math + .min(steps / stepsPerReplacement, (maxAmount - shiftAmount) / prefItem.fluidCellAmount); steps -= multiplier * stepsPerReplacement; } else { steps = result.leftSteps; @@ -431,16 +586,16 @@ public class RecipeChainMath { while (stackSize > Integer.MAX_VALUE) { final ItemStack copy = result.containerItem.copy(); copy.stackSize = Integer.MAX_VALUE; - this.containerItems.add(copy); + containerItems.add(copy); stackSize -= copy.stackSize; } result.containerItem.stackSize = (int) stackSize; - this.containerItems.add(result.containerItem); + containerItems.add(result.containerItem); } if (result.stack != null) { - this.containerItems.add(result.stack); + containerItems.add(result.stack); } ingrAmount = steps * prefItem.fluidCellAmount; @@ -461,11 +616,11 @@ public class RecipeChainMath { return ingrAmount; } - private long shiftContainerItems(ItemStack aStack, long steps) { - final int initialSize = this.containerItems.size(); + private long shiftContainerItems(ItemStack aStack, long steps, List containerItems) { + final int initialSize = containerItems.size(); for (int i = 0; i < initialSize && steps > 0; i++) { - ItemStack bStack = this.containerItems.get(i); + ItemStack bStack = containerItems.get(i); if (bStack != null && NEIClientUtils.areStacksSameTypeCraftingWithNBT(aStack, bStack)) { final ContainerItemResult result = getToolsContainerItems(bStack, steps); @@ -474,20 +629,20 @@ public class RecipeChainMath { steps = result.leftSteps; if (result.containerItem != null) { - this.containerItems.add(result.containerItem); + containerItems.add(result.containerItem); } - this.containerItems.set(i, bStack); + containerItems.set(i, bStack); } } - this.containerItems.removeIf(stack -> stack == null); + containerItems.removeIf(stack -> stack == null); return steps; } - private ContainerItemResult getToolsContainerItems(ItemStack aStack, long steps) { + protected ContainerItemResult getToolsContainerItems(ItemStack aStack, long steps) { final NBTTagCompound tagCompound = aStack.getTagCompound(); if (tagCompound != null && tagCompound.hasKey("GT.ToolStats")) { @@ -541,7 +696,7 @@ public class RecipeChainMath { return 0; } - private boolean hasContainerItem(ItemStack aStack) { + protected boolean hasContainerItem(ItemStack aStack) { if (aStack.getItem().hasContainerItem(aStack)) { @@ -558,16 +713,12 @@ public class RecipeChainMath { } private void addShift(RecipeId recipeId, long shift) { - for (BookmarkItem item : this.recipeIngredients) { - if (recipeId.equals(item.recipeId)) { - item.multiplier += shift; - } + for (BookmarkItem item : this.ingredientsByRecipe.getOrDefault(recipeId, Collections.emptyList())) { + item.multiplier += shift; } - for (BookmarkItem item : this.recipeResults) { - if (recipeId.equals(item.recipeId)) { - item.multiplier += shift; - } + for (BookmarkItem item : this.resultsByRecipe.getOrDefault(recipeId, Collections.emptyList())) { + item.multiplier += shift; } } diff --git a/src/main/java/codechicken/nei/recipe/chain/RecipeChainTooltipLineHandler.java b/src/main/java/codechicken/nei/recipe/chain/RecipeChainTooltipLineHandler.java index abcb9df8..584a7ecd 100644 --- a/src/main/java/codechicken/nei/recipe/chain/RecipeChainTooltipLineHandler.java +++ b/src/main/java/codechicken/nei/recipe/chain/RecipeChainTooltipLineHandler.java @@ -145,7 +145,12 @@ public class RecipeChainTooltipLineHandler implements ITooltipLineHandler { } if (this.lastShiftKey) { - for (ItemStack stack : math.containerItems) { + for (ItemStack stack : math.containerItemsInventory) { + if (stack != null) { + remainder.add(stack.copy()); + } + } + for (ItemStack stack : math.containerItemsCrafting) { if (stack != null) { remainder.add(stack.copy()); } diff --git a/src/main/resources/assets/nei/lang/en_US.lang b/src/main/resources/assets/nei/lang/en_US.lang index c3695353..69c70f64 100644 --- a/src/main/resources/assets/nei/lang/en_US.lang +++ b/src/main/resources/assets/nei/lang/en_US.lang @@ -30,6 +30,7 @@ nei.bookmark.group.sorting=Move Position nei.bookmark.group.remove_recipe=Remove Group nei.bookmark.group.pull_items=Pull all Items nei.bookmark.group.pull_items_shift=Pull missing Items +nei.bookmark.group.open_crafting_tree=Open Crafting Tree nei.bookmark.group.craft_missing=Craft Missing Items nei.bookmark.group.craft_all=Craft Items nei.bookmark.group.chat_link=Send Bookmark Group to Chat @@ -52,6 +53,45 @@ nei.bookmark.crafting_chain.output=Results nei.bookmark.crafting_chain.remainder=Remainders nei.bookmark.crafting_chain.needed=Required Crafts nei.bookmark.crafting_chain.available=Available Items +nei.bookmark.tree.title=Crafting Tree +nei.bookmark.tree.empty=No crafting chain to display +nei.bookmark.tree.overlay=Use for recipe tree +nei.bookmark.tree.item.state.resolved=In inventory: %s +nei.bookmark.tree.item.state.partial=Still needed: %s +nei.bookmark.tree.item.state.craftable=Can be crafted +nei.bookmark.tree.item.state.surplus=Extra produced: %s +nei.bookmark.tree.item.toggle_recipe=Collapse/Expand Recipe + +nei.bookmark.tree.stats.handlers=Handlers +nei.bookmark.tree.stats.ingredients=Ingredients +nei.bookmark.tree.stats.ingredients_needed=Ingredients Needed +nei.bookmark.tree.stats.ingredients_available=Ingredients Available +nei.bookmark.tree.stats.results=Results +nei.bookmark.tree.stats.remainders=Remainders +nei.bookmark.tree.stats.crafting=Crafting +nei.bookmark.tree.stats.crafting_needed=Crafting Needed +nei.bookmark.tree.stats.crafting_available=Crafting Available +nei.bookmark.tree.stats.handler_iterations=Iterations: %s +nei.bookmark.tree.stats.handler_recipes=Unique Recipes: %s + +nei.bookmark.tree.stats.highlight_handler=Highlight Recipe Handler +nei.bookmark.tree.stats.next_highlight=Go to Next Highlighted Handler +nei.bookmark.tree.stats.prev_highlight=Go to Previous Highlighted Handler +nei.bookmark.tree.stats.clear_highlight=Clear Handler Highlight + +nei.bookmark.tree.toolbar.inventory_off=Recompute tree using player inventory (snapshot) +nei.bookmark.tree.toolbar.inventory_on=Using inventory snapshot - click to clear +nei.bookmark.tree.toolbar.fit_to_view=Fit tree to view +nei.bookmark.tree.toolbar.stats_off=Show stats panel +nei.bookmark.tree.toolbar.stats_on=Hide stats panel +nei.bookmark.tree.toolbar.stats_sections=Choose visible stats sections +nei.bookmark.tree.toolbar.link=Send Bookmark Group to Chat +nei.bookmark.tree.toolbar.collapse_resolved=Collapse items with nothing left to craft +nei.bookmark.tree.toolbar.collapse_all=Collapse all nested items +nei.bookmark.tree.toolbar.expand_all=Expand all nested items +nei.bookmark.tree.toolbar.screenshot=Save tree as image +nei.bookmark.tree.screenshot.fbo_unsupported=Could not save crafting tree screenshot: framebuffers are not supported + nei.inventory.search.placeholder=Search... nei.itempanel.quantity.default=One stack nei.itempanel.loading=Loading Item List... @@ -207,6 +247,7 @@ nei.options.keys.gui.search=Focus Search Bar nei.options.keys.bookmark.add=Add Bookmark nei.options.keys.bookmark.remove_recipe=Remove Group from Bookmarks (Shift +) nei.options.keys.bookmark.pull_items=Pull all Bookmarked Items +nei.options.keys.bookmark.open_crafting_tree=Open Crafting Tree Window nei.options.keys.bookmark.hide=Toggle Bookmark Panel Visibility nei.options.keys.bookmark.chat_link=Send link to chat (Ctrl +) nei.options.keys.gui.overlay=Fill/Overlay Crafting Grid diff --git a/src/main/resources/assets/nei/textures/gui/craftingtree.png b/src/main/resources/assets/nei/textures/gui/craftingtree.png new file mode 100644 index 00000000..71b3b39b Binary files /dev/null and b/src/main/resources/assets/nei/textures/gui/craftingtree.png differ