package codechicken.nei; import java.awt.Point; import java.io.File; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; import net.minecraft.client.Minecraft; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; import net.minecraft.nbt.NBTTagString; import net.minecraft.util.EnumChatFormatting; import org.lwjgl.opengl.GL11; import com.google.gson.JsonParser; import codechicken.core.gui.GuiScrollSlot; import codechicken.lib.gui.GuiDraw; import codechicken.lib.vec.Rectangle4i; import codechicken.nei.ItemList.AnyMultiItemFilter; import codechicken.nei.ItemList.NothingItemFilter; import codechicken.nei.SearchTokenParser.ISearchParserProvider; import codechicken.nei.SearchTokenParser.SearchMode; import codechicken.nei.api.API; import codechicken.nei.api.ItemFilter; import codechicken.nei.api.ItemFilter.ItemFilterProvider; import codechicken.nei.guihook.GuiContainerManager; import codechicken.nei.recipe.GuiCraftingRecipe; import codechicken.nei.recipe.GuiRecipeTab; import codechicken.nei.recipe.ICraftingHandler; import codechicken.nei.recipe.IRecipeHandler; import codechicken.nei.recipe.StackInfo; import codechicken.nei.util.NBTJson; import codechicken.nei.util.NEIMouseUtils; public class SubsetWidget extends Button implements ItemFilterProvider { protected static final int SLOT_HEIGHT = 18; protected static final int MARGIN = 2; protected static final char PREFIX = '%'; private static final Object hiddenLock = new Object(); public static class SubsetTag { protected class SubsetSlot extends GuiScrollSlot { public SubsetSlot() { super(0, 0, 0, 0); setSmoothScroll(false); } @Override public int getSlotHeight(int slot) { return SubsetWidget.SLOT_HEIGHT; } @Override protected int getNumSlots() { return children.size() + items.size(); } @Override protected void slotClicked(int slot, int button, int mx, int my, int count) { if (slot < children.size()) { SubsetTag tag = children.get(slot); if (SubsetWidget.enableSearchBySubsets && NEIClientUtils.shiftKey()) { LayoutManager.searchField .setText(SearchField.searchParser.getRedefinedPrefix(SubsetWidget.PREFIX) + tag.path); } else if (button == 0 && count >= 2) { SubsetWidget.showOnly(tag); } else if (button == 0 || button == 1) { SubsetWidget.setHidden(tag, button == 1); } } else { ItemStack item = items.get(slot - children.size()); if (NEIClientUtils.controlKey() && NEIClientConfig.canCheatItem(item)) { NEIClientUtils.cheatItem(item, button, -1); } else if (SubsetWidget.enableSearchBySubsets && NEIClientUtils.shiftKey()) { LayoutManager.searchField.setText(SearchField.getEscapedSearchText(item)); } else if (button == 0 || button == 1) { SubsetWidget.setHidden(item, button == 1); } } } @Override protected void drawSlot(int slot, int x, int y, int mx, int my, float frame) { int w = windowBounds().width; Rectangle4i r = new Rectangle4i(0, 0, w, getSlotHeight(slot)); if (slot < children.size()) { SubsetTag tag = children.get(slot); LayoutManager.getLayoutStyle() .drawSubsetTag(tag.displayName(), x, y, r.w, r.h, tag.state, r.contains(mx, my)); if (r.contains(mx, my)) { SubsetWidget.hoverTag = tag; } } else { ItemStack stack = items.get(slot - children.size()); boolean hidden = SubsetWidget.isHidden(stack); LayoutManager.getLayoutStyle() .drawSubsetTag(null, x, y, r.w, r.h, hidden ? 0 : 2, r.contains(mx, my)); GuiContainerManager.drawItem(x + (w / 2 - 8), y + 1, stack); if (r.contains(mx, my)) { SubsetWidget.hoverStack = stack; } } } @Override public void drawOverlay(float frame) { /** disabled draw overlay */ } @Override public void drawBackground(float frame) { drawRect(x, y, x + width, y + height, 0xFF202020); } @Override public int scrollbarAlignment() { return marginleft == 0 ? 1 : -1; } @Override public void drawScrollbar(float frame) { if (hasScrollbar()) super.drawScrollbar(frame); } @Override public int scrollbarGuideAlignment() { return 0; } } public final String fullname; public final String path; public final String parentPath; public final ItemFilter filter; // 0 - full hidden // 1 - partitial hidden // 2 - enabled public int state = 2; public volatile int calculatedWidth = -1; public final List items; public final List children = new ArrayList<>(); protected final SubsetSlot slot = new SubsetSlot(); private SubsetTag selectedChild; private int visible; public SubsetTag(String fullname) { this(fullname, null); } public SubsetTag(String fullname, ItemFilter filter) { this.fullname = EnumChatFormatting.getTextWithoutFormattingCodes(fullname); this.filter = filter == null ? new NothingItemFilter() : filter; this.items = new ArrayList<>(); if (this.fullname != null) { this.path = this.fullname.replaceAll("\\s+", "").toLowerCase(); int idx = this.path.lastIndexOf('.'); this.parentPath = idx < 0 ? null : this.path.substring(0, idx); } else { this.parentPath = this.path = null; } } public String displayName() { final String translated = NEIClientUtils.translate("subsets." + this.fullname); return translated.startsWith("nei.") ? name() : translated; } public String name() { int idx = this.fullname.lastIndexOf('.'); return idx < 0 ? this.fullname : this.fullname.substring(idx + 1); } public String parent() { int idx = this.fullname.lastIndexOf('.'); return idx < 0 ? null : this.fullname.substring(0, idx); } public void clearCache() { this.state = 2; this.items.clear(); this.children.clear(); this.calculatedWidth = -1; } public void updateVisiblity(int mx, int my) { if (selectedChild != null) { selectedChild.updateVisiblity(mx, my); if (!selectedChild.isVisible()) { selectedChild = null; } } if (slot.contains(mx, my) && (selectedChild == null || !selectedChild.contains(mx, my))) { int mslot = slot.getClickedSlot(my); if (mslot >= 0 && mslot < children.size()) { SubsetTag mtag = children.get(mslot); if (mtag != null) { if (mtag != selectedChild && selectedChild != null) { selectedChild.setHidden(); } selectedChild = mtag; selectedChild.setVisible(); } } setVisible(); } if (this.selectedChild == null) { countdownVisible(); } } public void setHidden() { this.visible = 0; slot.mouseMovedOrUp(0, 0, 0); // cancel any scrolling if (this.selectedChild != null) { this.selectedChild.setHidden(); this.selectedChild = null; } } public void setVisible() { this.visible = 10; } private void countdownVisible() { if (this.visible > 0 && --this.visible == 0) { setHidden(); } } public void resize(Rectangle4i screen, Rectangle4i parent, boolean dropRight) { if (calculatedWidth < 0) { calculatedWidth = this.children.stream().mapToInt(SubsetTag::nameWidth).max().orElse(0); } int height = Math.min(slot.contentHeight(), screen.h); int width = Math.max(calculatedWidth + 2, this.items.isEmpty() ? 0 : 16 + MARGIN * 2); int scrollbarWidth = slot.scrollbarDim().width; if (slot.contentHeight() > height) { width += scrollbarWidth; } int slotY = parent.y1() + Math.min(0, alignValueToStep(screen.y2() - parent.y1() - height)); int slotX = dropRight ? parent.x2() : parent.x1() - width; if (slotX + width >= screen.x2()) { slotX = parent.x1() - width; dropRight = false; } else if (slotX <= screen.x1()) { slotX = parent.x2(); dropRight = true; } slot.setSize(slotX, slotY, width, height); if (dropRight) { slot.setMargins(0, 0, slot.hasScrollbar() ? scrollbarWidth : 0, 0); } else { slot.setMargins(slot.hasScrollbar() ? scrollbarWidth : 0, 0, 0, 0); } if (selectedChild != null) { selectedChild.resize( screen, new Rectangle4i( slot.x, slot.y + slot.getSlotY(this.children.indexOf(selectedChild)) - slot.scrolledPixels(), width, SLOT_HEIGHT), dropRight); } } protected int nameWidth() { return Minecraft.getMinecraft().fontRenderer.getStringWidth(displayName()) + 4; } public boolean isVisible() { return this.visible > 0; } public void draw(int mx, int my) { slot.draw(mx, my, 0); if (selectedChild != null) selectedChild.draw(mx, my); } public boolean contains(int px, int py) { return slot.contains(px, py) || selectedChild != null && selectedChild.contains(px, py); } public void mouseClicked(int mx, int my, int button) { if (selectedChild != null && selectedChild.contains(mx, my)) { selectedChild.mouseClicked(mx, my, button); } else if (slot.contains(mx, my)) { slot.mouseClicked(mx, my, button); } } public void mouseDragged(int mx, int my, int button, long heldTime) { slot.mouseDragged(mx, my, button, heldTime); if (selectedChild != null) { selectedChild.mouseDragged(mx, my, button, heldTime); } } public void mouseUp(int mx, int my, int button) { slot.mouseMovedOrUp(mx, my, button); if (selectedChild != null) { selectedChild.mouseUp(mx, my, button); } } public boolean mouseScrolled(int mx, int my, int scroll) { if (slot.hasScrollbar() && slot.contains(mx, my)) { slot.scroll(scroll); return true; } if (selectedChild != null && selectedChild.mouseScrolled(mx, my, scroll)) return true; if (slot.hasScrollbar() && !contains(mx, my)) { slot.scroll(scroll); return true; } return false; } public boolean isScrolling() { return slot.isScrolling() || selectedChild != null && selectedChild.isScrolling(); } } private static class DefaultParserProvider implements ISearchParserProvider { public ItemFilter getFilter(String searchText) { final int patternMode = NEIClientConfig.getIntSetting("inventory.search.patternMode"); Pattern pattern = null; if (patternMode != 3) { searchText = searchText.replaceAll("\\s+", "").toLowerCase(); } else { pattern = SearchField.getPattern(searchText, patternMode); } final AnyMultiItemFilter filter = new AnyMultiItemFilter(); final Set filteredItems = new HashSet<>(); final List snapshot; synchronized (tags) { snapshot = new ArrayList<>(tags.values()); } for (SubsetTag tag : snapshot) { if (tag.filter != null && matches(tag.path, searchText, pattern)) { synchronized (tag.items) { filteredItems.addAll(tag.items); filter.filters.add(tag.filter); } } } return stack -> filteredItems.contains(stack) || !ItemList.items.contains(stack) && filter.matches(stack); } public char getPrefix() { return SubsetWidget.PREFIX; } public EnumChatFormatting getHighlightedColor() { return EnumChatFormatting.DARK_PURPLE; } @Override public String getName() { return "subsets"; } private boolean matches(String name, String searchText, Pattern pattern) { if (pattern != null) { return pattern.matcher(name).find(); } else { return name.contains(searchText); } } } /** * Canonical {@link StackInfo#getItemStackGUID(ItemStack)} of every hidden item, stored by GUID so the state * survives world reloads. All operations should be synchronised. */ private static final Set hiddenItems = new HashSet<>(); private static final Map tags = new HashMap<>(); private static final SubsetTag root = new SubsetTag(""); protected static boolean enableSearchBySubsets = false; protected static ItemStack hoverStack; protected static SubsetTag hoverTag; private long lastclicktime; public static void addTag(SubsetTag tag) { updateState.stop(); synchronized (tags) { tags.put(tag.path, tag); String parentname = tag.parent(); while (parentname != null && !tags.containsKey(parentname.replaceAll("\\s+", "").toLowerCase())) { SubsetTag parent = new SubsetTag(parentname); tags.put(parent.path, parent); parentname = parent.parent(); } updateHiddenItems(); SearchField.searchParser.clearCache(); } } public static void removeTag(String path) { updateState.stop(); synchronized (tags) { tags.keySet().removeIf( parentpath -> parentpath.equals(path.toLowerCase()) || parentpath.startsWith(path.toLowerCase() + ".")); updateHiddenItems(); SearchField.searchParser.clearCache(); } } public static boolean isHidden(ItemStack item) { final String guid = StackInfo.getItemStackGUID(item); synchronized (hiddenLock) { return hiddenItems.contains(guid); } } private static List toGUIDs(List items) { return items.stream().map(StackInfo::getItemStackGUID).collect(Collectors.toList()); } private static List getItems(SubsetTag tag, List items) { items.addAll(tag.items); for (SubsetTag child : tag.children) { getItems(child, items); } return items; } public static void showOnly(SubsetTag tag) { final List allItems = toGUIDs(getItems(root, new ArrayList<>())); final List tagItems = toGUIDs(getItems(tag, new ArrayList<>())); synchronized (hiddenLock) { hiddenItems.clear(); hiddenItems.addAll(allItems); hiddenItems.removeAll(tagItems); } calculateVisibility(); } public static void setHidden(SubsetTag tag, boolean hidden) { final List tagItems = toGUIDs(getItems(tag, new ArrayList<>())); synchronized (hiddenLock) { if (hidden) { hiddenItems.addAll(tagItems); } else { hiddenItems.removeAll(tagItems); } } calculateVisibility(); } public static void setHidden(ItemStack item, boolean hidden) { final String guid = StackInfo.getItemStackGUID(item); synchronized (hiddenLock) { if (hidden) { hiddenItems.add(guid); } else { hiddenItems.remove(guid); } } calculateVisibility(); } public static void unhideAll() { synchronized (hiddenLock) { hiddenItems.clear(); } calculateVisibility(); } public static void updateHiddenItems() { if (ItemList.loadFinished) { updateState.restart(); } } public static void loadCustomSubsets() { File dir = new File(NEIClientConfig.configDir, "subsets"); if (!dir.exists() || !dir.isDirectory()) { return; } Stream.of(dir.listFiles()).filter(file -> !file.isDirectory()).forEach(file -> { ClientHandler.loadSettingsFile( "subsets/" + file.getName(), lines -> parseFile(file.getName(), lines.collect(Collectors.toList()))); }); } private static void parseFile(String resource, List itemStrings) { final JsonParser parser = new JsonParser(); final int dotIndex = resource.lastIndexOf('.'); final String subsetNamespace = dotIndex > 0 ? resource.substring(0, dotIndex) : resource; SubsetTag processedTag = new SubsetTag(subsetNamespace, new ItemStackSet()); for (String itemStr : itemStrings) { try { if (itemStr.startsWith("; ")) { addTag(processedTag); final String handlerName = itemStr.substring(2); final IRecipeHandler recipeHandler = Stream .concat( GuiCraftingRecipe.craftinghandlers.stream(), GuiCraftingRecipe.serialCraftingHandlers.stream()) .filter(handler -> handlerName.equals(getHandlerName(handler))).findFirst().orElse(null); String recipeName = handlerName; if (recipeHandler != null) { recipeName = recipeHandler.getRecipeName().trim(); } processedTag = new SubsetTag( subsetNamespace + "." + recipeName.replace(".", ""), new ItemStackSet()); } else { final NBTBase nbt = NBTJson.toNbt(parser.parse(itemStr)); if (nbt instanceof NBTTagCompound tag) { ((ItemStackSet) processedTag.filter).add(StackInfo.loadFromNBT(tag)); } else { throw new IllegalArgumentException( "Expected NBTTagCompound but got " + nbt.getClass().getSimpleName()); } } } catch (Exception e) { NEIClientConfig.logger.error("Failed to load custom subset items from json string:\n{}", itemStr); } } addTag(processedTag); } private static String getHandlerName(ICraftingHandler handler) { return GuiRecipeTab.getHandlerInfo(handler).getHandlerName(); } public static void loadHidden() { final Set guids = new HashSet<>(); try { final NBTTagList guidList = NEIClientConfig.world.nbt.getTagList("hiddenItems", 8); for (int i = 0; i < guidList.tagCount(); i++) { guids.add(guidList.getStringTagAt(i)); } final NBTTagList list = NEIClientConfig.world.nbt.getTagList("hiddenItems", 10); for (int i = 0; i < list.tagCount(); i++) { final ItemStack stack = ItemStack.loadItemStackFromNBT(list.getCompoundTagAt(i)); if (stack != null) { guids.add(StackInfo.getItemStackGUID(stack)); } } } catch (Exception e) { NEIClientConfig.logger.error("Error loading hiddenItems", e); return; } synchronized (hiddenLock) { hiddenItems.clear(); hiddenItems.addAll(guids); } } public static void saveHidden() { if (NEIClientConfig.world == null) return; final NBTTagList list = new NBTTagList(); synchronized (hiddenLock) { for (String guid : hiddenItems) { list.appendTag(new NBTTagString(guid)); } } NEIClientConfig.world.nbt.setTag("hiddenItems", list); } private static final UpdateStateTask updateState = new UpdateStateTask(); private static class UpdateStateTask extends RestartableTask { public UpdateStateTask() { super("NEI Subset Item Allocation"); } @Override public void execute() { try { final List list; synchronized (tags) { list = new ArrayList<>(tags.values()); } root.clearCache(); list.parallelStream().forEach(tag -> { tag.clearCache(); if (!(tag.filter instanceof NothingItemFilter)) { ItemList.items.stream().filter(tag.filter::matches) .collect(Collectors.toCollection(() -> tag.items)); } }); for (SubsetTag tag : list) { if (tag.parentPath == null) { root.children.add(tag); } else { tags.get(tag.parentPath).children.add(tag); } } boolean changed = false; do { changed = false; for (SubsetTag tag : list) { changed = tag.children.removeIf(child -> child.children.isEmpty() && child.items.isEmpty()) || changed; } } while (changed); for (SubsetTag tag : list) { tag.children.sort(Comparator.comparing(SubsetTag::displayName)); tag.calculatedWidth = -1; } root.children.removeIf(child -> child.children.isEmpty() && child.items.isEmpty()); root.children.sort(Comparator.comparing(SubsetTag::displayName)); root.calculatedWidth = -1; calculateVisibility(root); } catch (Throwable e) { e.printStackTrace(); } SearchField.searchParser.clearCache(); } } public SubsetWidget() { super("NEI Subsets"); API.addItemFilter(this); API.addSearchProvider(new DefaultParserProvider()); this.z = 1; } @Override public int getRenderLabelColor() { return NEIClientConfig.subsetWidgetOnTop() ? 0 : ColorUtils.subsetWidget.getColor(); } @Override public String getRenderLabel() { if (NEIClientConfig.subsetWidgetOnTop()) { return NEIClientUtils.translate("inventory.item_subsets"); } else { return String.valueOf(SearchField.searchParser.getRedefinedPrefix(PREFIX)); } } @Override public void draw(int mx, int my) { super.draw(mx, my); SubsetWidget.hoverTag = null; SubsetWidget.hoverStack = null; if (root.isVisible()) { final Minecraft mc = NEIClientUtils.mc(); final Rectangle4i screen = new Rectangle4i( MARGIN, MARGIN, mc.currentScreen.width - MARGIN * 2, mc.currentScreen.height - MARGIN * 2); final Rectangle4i parent = new Rectangle4i(); final boolean dropRight = this.x < (screen.x + screen.w / 2); final boolean dropDown = this.y < (screen.y + screen.h / 2); if (dropRight) { parent.x = this.x; } else { parent.x = this.x + this.w; } if (dropDown) { screen.y = parent.y = this.y + this.h; screen.h = alignValueToStep(mc.currentScreen.height - MARGIN - screen.y); } else { screen.h = alignValueToStep(this.y - screen.y); screen.y = this.y - screen.h; parent.y = this.y; } root.resize(screen, parent, dropRight); GL11.glPushAttrib(GL11.GL_ENABLE_BIT | GL11.GL_COLOR_BUFFER_BIT | GL11.GL_LIGHTING_BIT); GuiContainerManager.enable2DRender(); GuiContainerManager.drawItems.zLevel += 100; root.draw(mx, my); GuiContainerManager.drawItems.zLevel -= 100; GL11.glPopAttrib(); } } private static int alignValueToStep(int height) { return (height / SLOT_HEIGHT) * SLOT_HEIGHT; } protected static void calculateVisibility() { calculateVisibility(root); ItemList.updateFilter.restart(); } protected static void calculateVisibility(SubsetTag tag) { // 0 - full hidden // 1 - partitial hidden // 2 - enabled int hidden = 0; tag.state = 2; for (SubsetTag child : tag.children) { calculateVisibility(child); if (child.state == 1) { tag.state = 1; } else if (child.state == 0) { hidden++; } } if (tag.state == 1) return; for (ItemStack item : tag.items) { if (isHidden(item)) { hidden++; } else if (hidden > 0) { break; } } if (hidden == tag.children.size() + tag.items.size()) { tag.state = 0; } else if (hidden > 0) { tag.state = 1; } } @Override public void update() { SubsetWidget.enableSearchBySubsets = SearchMode .fromInt(NEIClientConfig.getIntSetting("inventory.search.subsetsSearchMode")) == SearchMode.PREFIX; Point mouse = GuiDraw.getMousePosition(); updateVisiblity(mouse.x, mouse.y); } private void updateVisiblity(int mx, int my) { if (!root.isVisible() || root.isScrolling()) return; root.updateVisiblity(mx, my); if (!root.isVisible() && bounds().contains(mx, my)) { root.setVisible(); } } @Override public boolean contains(int px, int py) { return super.contains(px, py) || root.isVisible() && root.contains(px, py); } @Override public boolean handleClick(int mx, int my, int button) { if (root.isVisible() && root.contains(mx, my)) { root.mouseClicked(mx, my, button); return true; } if (button == 0) { if (System.currentTimeMillis() - lastclicktime < 500) { unhideAll(); } else { root.setVisible(); } NEIClientUtils.playClickSound(); lastclicktime = System.currentTimeMillis(); } return true; } /** * Not called */ @Override public boolean onButtonPress(boolean rightclick) { return false; } @Override public void mouseDragged(int mx, int my, int button, long heldTime) { if (root.isVisible()) { root.mouseDragged(mx, my, button, heldTime); } } @Override public void mouseUp(int mx, int my, int button) { if (root.isVisible()) root.mouseUp(mx, my, button); } @Override public boolean onMouseWheel(int i, int mx, int my) { return root.isVisible() && root.mouseScrolled(mx, my, -i) || contains(mx, my); } @Override public void onGuiClick(int mx, int my) { if (!contains(mx, my)) { root.setHidden(); } } @Override public ItemStack getStackMouseOver(int mx, int my) { return SubsetWidget.hoverStack; } @Override public ItemFilter getFilter() { return item -> !isHidden(item); } @Override public void addTooltips(List tooltip) { if (SubsetWidget.hoverStack == null && SubsetWidget.hoverTag != null) { tooltip.add(SubsetWidget.hoverTag.displayName() + GuiDraw.TOOLTIP_LINESPACE); } } @Override public Map handleHotkeys(int mousex, int mousey, Map hotkeys) { if (SubsetWidget.hoverStack != null) { if (NEIClientConfig.canCheatItem(SubsetWidget.hoverStack)) { hotkeys.put( NEIClientUtils.getKeyName(NEIClientUtils.CTRL_HASH, NEIMouseUtils.MOUSE_BTN_LMB), NEIClientUtils.translate("subsets.item.cheat")); } if (SubsetWidget.enableSearchBySubsets) { hotkeys.put( NEIClientUtils.getKeyName(NEIClientUtils.SHIFT_HASH, NEIMouseUtils.MOUSE_BTN_LMB), NEIClientUtils.translate("subsets.item.search")); } hotkeys.put( NEIMouseUtils.getKeyName(NEIMouseUtils.MOUSE_BTN_LMB), NEIClientUtils.translate("subsets.item.show")); hotkeys.put( NEIMouseUtils.getKeyName(NEIMouseUtils.MOUSE_BTN_RMB), NEIClientUtils.translate("subsets.item.hide")); } else if (SubsetWidget.hoverTag != null) { hotkeys.put( NEIClientUtils.translate("subsets.tag.onlythis.key"), NEIClientUtils.translate("subsets.tag.onlythis")); if (SubsetWidget.enableSearchBySubsets) { hotkeys.put( NEIClientUtils.getKeyName(NEIClientUtils.SHIFT_HASH, NEIMouseUtils.MOUSE_BTN_LMB), NEIClientUtils.translate("subsets.tag.search")); } hotkeys.put( NEIMouseUtils.getKeyName(NEIMouseUtils.MOUSE_BTN_LMB), NEIClientUtils.translate("subsets.tag.show")); hotkeys.put( NEIMouseUtils.getKeyName(NEIMouseUtils.MOUSE_BTN_RMB), NEIClientUtils.translate("subsets.tag.hide")); } return hotkeys; } }