Implement Bookmark Tree Screen (#1018)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
slprimeandgithub-actions[bot] authored and GitHub committed 2026-09-08 13:56:58 +03:00
1 parent fda7688807
commit 3d6c1c6e19
30 files changed
+4135 -135

No files matched your search

@@ -1048,15 +1048,9 @@ public class BookmarkPanel extends PanelWidget<BookmarkGrid> {
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;
@@ -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();
@@ -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);
@@ -79,6 +79,10 @@ public class ItemsTooltipLineHandler implements ITooltipLineHandler {
return this.activeStackIndex == -1 ? null : this.items.get(this.activeStackIndex);
}
public List<ItemStack> getItems() {
return this.items;
}
public boolean isEmpty() {
return this.items.isEmpty();
}
@@ -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);
@@ -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);
});
}
@@ -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<Integer> getItemSpawnSlots(GuiContainer gui, ItemStack item) {
return Collections.emptyList();
}
@Override
public List<TaggedInventoryArea> 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");
@@ -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<Integer> getItemSpawnSlots(GuiContainer gui, ItemStack item);
default Iterable<Integer> getItemSpawnSlots(GuiContainer gui, ItemStack item) {
return Collections.emptyList();
}
/**
* @return A list of TaggedInventoryAreas that will be used with the savestates.
*/
public List<TaggedInventoryArea> getInventoryAreas(GuiContainer gui);
default List<TaggedInventoryArea> 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;
}
}
@@ -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<String, String> 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),
@@ -179,11 +179,15 @@ public class BookmarkGrid extends ItemsGrid<BookmarksGridSlot, BookmarkGrid.Book
}
public int addGroup(BookmarkGroup group) {
int groupId = this.bookmarkItems.stream().mapToInt(item -> 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<BookmarksGridSlot, BookmarkGrid.Book
}
public void removeRecipe(int itemIndex, boolean removeFullRecipe) {
final BookmarkItem item = getCalculatedItem(itemIndex);
final BookmarkItem item = getBookmarkItem(itemIndex);
if (item == null) {
return;
@@ -1214,28 +1218,10 @@ public class BookmarkGrid extends ItemsGrid<BookmarksGridSlot, BookmarkGrid.Book
if (group.collapsed) {
shiftGroupAmount(targetItem.groupId, shift);
} else if (targetItem.recipeId != null && targetItem.type != BookmarkItemType.ITEM) {
final RecipeId recipeId = this.gridGenerator.itemToRecipe
.getOrDefault(targetItemIndex, targetItem.recipeId);
final boolean recipeInMiddle = group.crafting != null && group.crafting.recipeInMiddle.contains(recipeId);
long multiplier = Integer.MAX_VALUE;
for (BookmarkItem item : this.bookmarkItems) {
if (item.equalsRecipe(recipeId, targetItem.groupId) && !item.emptyFactor()) {
multiplier = Math.min(
shiftMultiplier(
Math.max(recipeInMiddle ? 1 : 0, item.multiplier),
shift,
recipeInMiddle ? 1 : 0),
multiplier);
}
}
for (BookmarkItem item : this.bookmarkItems) {
if (item.equalsRecipe(recipeId, targetItem.groupId) && !item.emptyFactor()) {
item.multiplier = multiplier;
}
}
shiftRecipeAmount(
targetItem.groupId,
this.gridGenerator.itemToRecipe.getOrDefault(targetItemIndex, targetItem.recipeId),
shift);
} else {
for (BookmarkItem item : this.bookmarkItems) {
@@ -1245,6 +1231,32 @@ public class BookmarkGrid extends ItemsGrid<BookmarksGridSlot, BookmarkGrid.Book
}
}
this.onItemsChanged();
}
}
public void shiftRecipeAmount(int targetGroupId, RecipeId recipeId, long shift) {
final BookmarkGroup group = getGroup(targetGroupId);
final boolean recipeInMiddle = group != null && group.crafting != null
&& group.crafting.recipeInMiddle.contains(recipeId);
long multiplier = Integer.MAX_VALUE;
for (BookmarkItem item : this.bookmarkItems) {
if (item.equalsRecipe(recipeId, targetGroupId) && !item.emptyFactor()) {
multiplier = Math.min(
shiftMultiplier(
Math.max(recipeInMiddle ? 1 : 0, item.multiplier),
shift,
recipeInMiddle ? 1 : 0),
multiplier);
}
}
for (BookmarkItem item : this.bookmarkItems) {
if (item.equalsRecipe(recipeId, targetGroupId) && !item.emptyFactor()) {
item.multiplier = multiplier;
}
}
this.onItemsChanged();
@@ -0,0 +1,119 @@
package codechicken.nei.bookmark.tree;
import java.awt.geom.Point2D;
class Camera {
private static final float ZOOM_MIN = 0.1f;
private static final float ZOOM_MAX = 8.0f;
private static final double ZOOM_STEP_BASE = 1.4;
private static final float SCROLL_STEP_DIVISOR = 5f;
private static final int CULL_MARGIN = 32;
private static final int CULL_ROW_MARGIN = 16;
private static final float INITIAL_SCROLL = -8f;
public int widgetX;
public int widgetY;
public int widgetW;
public int widgetH;
public float scrollX = INITIAL_SCROLL;
public float scrollY = INITIAL_SCROLL;
public float zoomLevel = 1.0f;
private float lastDragX;
private float lastDragY;
private boolean dragging;
public void setViewport(int x, int y, int w, int h) {
this.widgetX = x;
this.widgetY = y;
this.widgetW = w;
this.widgetH = h;
}
public boolean isInWidget(int x, int y) {
return x >= 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);
}
}
@@ -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<ItemTreeSlot, Point> pointByNode = new HashMap<>();
public final TreeMap<Integer, List<ItemTreeSlot>> rows = new TreeMap<>();
public final Set<RecipeId> 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<ItemTreeSlot, Point2D.Double> hitTest) {
final Point2D.Double local = this.camera.toWorldPoint(mouse);
for (List<ItemTreeSlot> 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<BookmarkItem> 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<ItemTreeSlot> 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<BookmarkItem> 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<ItemTreeSlot> 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<BookmarkItem> 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<BookmarkItem> 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<RecipeId> collapsedRecipes, Set<BookmarkItem> collapsedItems,
ItemTreeSlot anchor) {
final Map<ItemTreeSlot, Integer> 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<int[]> contour = new ArrayList<>();
for (ItemTreeSlot root : this.graph.roots) {
final List<int[]> 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<ItemTreeSlot> 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<ItemTreeSlot, Point> entry : this.pointByNode.entrySet()) {
if (entry.getKey().ingrItem.equals(anchor.ingrItem)) {
return entry.getValue();
}
}
}
return result;
}
private List<int[]> computeLayout(ItemTreeSlot node, Set<BookmarkItem> collapsedItems,
Map<ItemTreeSlot, Integer> offsetFromParent) {
List<ItemTreeSlot> children = Collections.emptyList();
if (!collapsedItems.contains(node.ingrItem)) {
children = this.graph.childrenOf.getOrDefault(node, Collections.emptyList());
}
if (children.isEmpty()) {
final List<int[]> rows = new ArrayList<>();
rows.add(new int[] { 0, 0 });
return rows;
}
final int[] offsets = new int[children.size()];
final List<int[]> combined = new ArrayList<>();
for (int i = 0; i < children.size(); i++) {
final List<int[]> 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<int[]> rows = new ArrayList<>(combined.size() + 1);
rows.add(new int[] { 0, 0 });
rows.addAll(combined);
return rows;
}
private static int requiredShift(List<int[]> existing, List<int[]> 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<int[]> existing, List<int[]> 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<int[]> target, List<int[]> 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<BookmarkItem> collapsedItems,
Map<ItemTreeSlot, Integer> 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);
}
}
}
}
@@ -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<ItemTreeSlot> roots = new ArrayList<>();
public final List<ItemTreeSlot> items = new ArrayList<>();
public final Map<ItemTreeSlot, ItemTreeSlot> parentOf = new HashMap<>();
public final Map<ItemTreeSlot, List<ItemTreeSlot>> childrenOf = new HashMap<>();
public final Set<ItemTreeSlot> subtreeMissingCache = new HashSet<>();
public final Set<ItemTreeSlot> 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<ItemTreeSlot, List<ItemTreeSlot>> entry : this.childrenOf.entrySet()) {
if (entry.getValue().stream().anyMatch(child -> child.leftAmount != 0)) {
this.childrenNeedCraftingCache.add(entry.getKey());
}
}
}
public Set<RecipeId> collectUnusedRecipes(ItemTreeSlot node) {
final Map<RecipeId, Set<RecipeId>> children = new HashMap<>();
final Map<RecipeId, Set<RecipeId>> 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<RecipeId> 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<RecipeId, Set<RecipeId>> children,
Map<RecipeId, Set<RecipeId>> consumers, Set<RecipeId> 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;
}
}
@@ -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<ItemTreeSlot> rootSlots = new ArrayList<>();
public final List<ItemTreeSlot> items = new ArrayList<>();
public final Map<ItemTreeSlot, ItemTreeSlot> parentOf = new HashMap<>();
public final Map<ItemTreeSlot, List<ItemTreeSlot>> childrenOf = new HashMap<>();
private final Deque<ItemTreeSlot> parentStack = new ArrayDeque<>();
private ItemTreeSlot lastItem = null;
public CraftingTreeMath(List<BookmarkItem> chainItems, Set<RecipeId> 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<RecipeId> 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<RecipeId> 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<RecipeId> 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<RecipeId> 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);
}
}
}
@@ -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<StatsSection> visibleStatsSections = EnumSet.allOf(StatsSection.class);
public boolean useInventorySnapshot = false;
public CollapseMode collapseMode = CollapseMode.EXPANDED;
public final Set<BookmarkItem> 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;
}
}
@@ -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<DrawableResource> icon;
private final BooleanSupplier isEnabled;
private final Supplier<String> tooltip;
public GuiStatsButton(int buttonId, Supplier<DrawableResource> icon, Supplier<String> 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<GuiButton> 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;
}
}
File diff suppressed because it is too large. Load diff
@@ -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;
}
}
@@ -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<ItemTreeSlot> 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);
}
}
@@ -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<CraftingTreeState.StatsSection> sections, int widgetX, int widgetY, int widgetW, int widgetH) {
clear();
final List<String> labels = new ArrayList<>();
final List<List<Widget>> sectionGroups = new ArrayList<>();
final List<Widget> 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<Widget> craftingNeededSlot = new ArrayList<>();
final List<Widget> craftingAvailableSlot = new ArrayList<>();
final Map<BookmarkItem, Boolean> recipeMissing = new LinkedHashMap<>();
final Map<BookmarkItem, Integer> recipeLevel = new HashMap<>();
for (Map.Entry<ItemTreeSlot, List<ItemTreeSlot>> 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<Map.Entry<BookmarkItem, Boolean>> sortedRecipeMissing = new ArrayList<>(
recipeMissing.entrySet());
sortedRecipeMissing.sort(
Comparator.<Map.Entry<BookmarkItem, Boolean>>comparingInt(entry -> recipeLevel.get(entry.getKey()))
.thenComparing(entry -> entry.getKey().itemStack, ItemSorter.instance));
for (Map.Entry<BookmarkItem, Boolean> 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<Widget> slot = missingIngredients ? craftingNeededSlot : craftingAvailableSlot;
slot.add(
new ItemStatsSlot(
resultItem.itemStack,
resultItem.getStackSize(amount),
missingIngredients ? SLOT_DEFAULT : SLOT_CRAFTING,
resultItem.recipeId,
resultItem.getMultiplier()));
}
}
final List<Widget> craftingSlots = new ArrayList<>(craftingNeededSlot);
craftingSlots.addAll(craftingAvailableSlot);
if (!craftingSlots.isEmpty()) {
labels.add(NEIClientUtils.translate("bookmark.tree.stats.crafting"));
sectionGroups.add(craftingSlots);
}
}
final List<Widget> 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<Widget> 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<String> labels, List<List<Widget>> 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<Widget> 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<Widget> widgets, String label, List<Widget> 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<Widget> collectAvailable(RecipeChainMath math) {
final ItemStackAmount items = new ItemStackAmount();
final List<Widget> 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<NBTTagCompound, Long> entry : items.entrySet()) {
if (entry.getValue() > 0) {
widgets.add(
new ItemStatsSlot(StackInfo.loadFromNBT(entry.getKey(), 0), entry.getValue(), SLOT_DEFAULT));
}
}
return widgets;
}
private List<Widget> collectInputs(RecipeChainMath math, boolean useInventorySnapshot) {
final ItemStackAmount items = new ItemStackAmount();
final List<Widget> 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<NBTTagCompound, Long> 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<Widget> collectOutputs(RecipeChainMath math) {
final ItemStackAmount items = new ItemStackAmount();
final List<Widget> 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<NBTTagCompound, Long> entry : items.entrySet()) {
if (entry.getValue() > 0) {
widgets.add(
new ItemStatsSlot(StackInfo.loadFromNBT(entry.getKey(), 0), entry.getValue(), SLOT_DEFAULT));
}
}
return widgets;
}
private List<Widget> collectRemainder(RecipeChainMath math) {
final ItemStackAmount items = new ItemStackAmount();
final List<Widget> 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<NBTTagCompound, Long> entry : items.entrySet()) {
if (entry.getValue() > 0) {
widgets.add(
new ItemStatsSlot(StackInfo.loadFromNBT(entry.getKey(), 0), entry.getValue(), SLOT_DISABLED));
}
}
return widgets;
}
private List<Widget> collectHandlers(RecipeChainMath math) {
final Map<String, HandlerStats> handlerStats = new HashMap<>();
final List<Widget> 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<String, HandlerStats> entry : handlerStats.entrySet()) {
widgets.add(new HandlerStatsSlot(entry.getKey(), entry.getValue()));
}
return widgets;
}
private static int treeLevel(ItemTreeSlot node, Map<ItemTreeSlot, ItemTreeSlot> 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;
}
}
@@ -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";
});
}
}
@@ -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<String> handleTooltip(List<String> currenttip) {
currenttip.add(NEIClientUtils.translate("bookmark.tree.overlay"));
return currenttip;
}
@Override
public Map<String, String> handleHotkeys(int mousex, int mousey, Map<String, String> 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) {}
}
@@ -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++) {
@@ -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);
}
@@ -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<Recipe> getRecipesTree(Recipe mainRecipe) {
@@ -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,
@@ -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<BookmarkItem> recipeIngredients = new ArrayList<>();
public final List<BookmarkItem> recipeResults = new ArrayList<>();
protected final Map<RecipeId, List<BookmarkItem>> ingredientsByRecipe = new HashMap<>();
protected final Map<RecipeId, List<BookmarkItem>> resultsByRecipe = new HashMap<>();
public final Map<BookmarkItem, BookmarkItem> preferredItems = new HashMap<>();
public final Map<BookmarkItem, Long> requiredAmount = new HashMap<>();
public final List<ItemStack> containerItems = new ArrayList<>();
public final List<ItemStack> containerItemsCrafting = new ArrayList<>();
public final List<ItemStack> containerItemsInventory = new ArrayList<>();
private final List<ItemStack> containerItemsBlacklist = new ArrayList<>();
private RecipeChainMath(List<BookmarkItem> recipeItems, Set<RecipeId> collapsedRecipes) {
protected RecipeChainMath(List<BookmarkItem> recipeItems, Set<RecipeId> collapsedRecipes) {
final Map<RecipeId, Long> multipliers = new HashMap<>();
for (BookmarkItem item : recipeItems) {
@@ -70,6 +77,8 @@ public class RecipeChainMath {
}
}
rebuildRecipeIndex();
for (Map.Entry<RecipeId, Long> 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<BookmarkItem, BookmarkItem> preferredItems,
Set<RecipeId> 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<BookmarkItem, BookmarkItem> preferredItems, Set<RecipeId> 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<RecipeId> getOutputRecipeProcessingOrder() {
if (this.outputRecipes.size() <= 1) {
return new ArrayList<>(this.outputRecipes.keySet());
}
final Map<RecipeId, Set<RecipeId>> dependents = new HashMap<>();
final Map<RecipeId, Integer> inDegree = new HashMap<>();
for (RecipeId recipeId : this.outputRecipes.keySet()) {
inDegree.put(recipeId, 0);
}
for (RecipeId recipeId : this.outputRecipes.keySet()) {
final Set<RecipeId> 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<RecipeId> order = new ArrayList<>();
final ArrayDeque<RecipeId> 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<RecipeId> dependencies,
Set<RecipeId> 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<BookmarkItem> 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<RecipeId> visited) {
protected void calculateSuitableRecipe(BookmarkItem ingrItem, long ingrMultiplier, List<RecipeId> 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<RecipeId> 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<RecipeId> 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<ItemStack> 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<ItemStack> 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;
}
}
@@ -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());
}
@@ -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
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB