package codechicken.nei.recipe.debug; import java.awt.Point; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.IntPredicate; import java.util.regex.Matcher; import java.util.stream.Collectors; import net.minecraft.client.gui.inventory.GuiContainer; import net.minecraft.util.EnumChatFormatting; import org.apache.commons.io.IOUtils; import org.lwjgl.input.Keyboard; import org.lwjgl.opengl.GL11; import codechicken.lib.gui.GuiDraw; import codechicken.lib.vec.Rectangle4i; import codechicken.nei.Button; import codechicken.nei.ClientHandler; import codechicken.nei.NEIClientConfig; import codechicken.nei.NEIClientUtils; import codechicken.nei.Widget; import codechicken.nei.api.API; import codechicken.nei.drawable.DrawableBuilder; import codechicken.nei.drawable.DrawableResource; import codechicken.nei.guihook.GuiContainerManager; import codechicken.nei.guihook.IContainerTooltipHandler; import codechicken.nei.recipe.GuiRecipe; import codechicken.nei.recipe.GuiRecipeButton; import codechicken.nei.recipe.GuiRecipeTab; import codechicken.nei.recipe.HandlerInfo; import codechicken.nei.recipe.IRecipeHandler; import codechicken.nei.recipe.RecipeHandlerRef; import codechicken.nei.recipe.ShapedRecipeHandler; import codechicken.nei.recipe.TemplateRecipeHandler; import codechicken.nei.recipe.TemplateRecipeHandler.RecipeTransferRect; import codechicken.nei.recipe.widget.RecipeWidget; import codechicken.nei.scroll.ScrollContainer; public class DebugHandlerWidget extends Widget implements IContainerTooltipHandler { private static class HandlerInfoRecord { public HandlerInfo info; public String handlerKey; public int yShift; public int width; public int height; public boolean multiWidgets; public int order; public boolean allowOverflowX; public boolean allowOverflowY; public boolean showFavorites; public boolean showOverlay; public boolean showBadge; private String csvLine; public HandlerInfoRecord(String handlerKey, HandlerInfo info) { this.info = info; this.handlerKey = handlerKey; this.yShift = info.getYShift(); this.width = info.getWidth(); this.height = info.getHeight(); this.multiWidgets = info.isMultipleWidgetsAllowed(); this.order = NEIClientConfig.handlerOrdering.getOrDefault(handlerKey, 0); this.allowOverflowX = info.isAllowOverflowX(); this.allowOverflowY = info.isAllowOverflowY(); this.showFavorites = info.getShowFavoritesButton(); this.showOverlay = info.getShowOverlayButton(); this.showBadge = info.getShowBadge(); this.csvLine = toCsvLine(); } public void apply() { this.info.setYShift(this.yShift); this.info.setHandlerDimensions(this.width, this.height); this.info.setMultipleWidgetsAllowed(this.multiWidgets); this.info.setAllowOverflowX(this.allowOverflowX); this.info.setAllowOverflowY(this.allowOverflowY); this.info.setShowFavoritesButton(this.showFavorites); this.info.setShowOverlayButton(this.showOverlay); this.info.setShowBadge(this.showBadge); NEIClientConfig.handlerOrdering.put(this.handlerKey, this.order); } public void apply(String csvLine) { final String[] parts = csvLine.split(","); this.yShift = intOrDefault(parts[1], this.yShift); this.height = intOrDefault(parts[2], this.height); this.width = intOrDefault(parts[3], this.width); this.multiWidgets = intOrDefault(parts[4], this.multiWidgets ? 1 : 0) == 1; this.order = intOrDefault(parts[5], this.order); this.allowOverflowX = intOrDefault(parts[6], this.allowOverflowX ? 1 : 0) == 1; this.allowOverflowY = intOrDefault(parts[7], this.allowOverflowY ? 1 : 0) == 1; this.showFavorites = intOrDefault(parts[8], this.showFavorites ? 1 : 0) == 1; this.showOverlay = intOrDefault(parts[9], this.showOverlay ? 1 : 0) == 1; this.showBadge = intOrDefault(parts[10], this.showBadge ? 1 : 0) == 1; apply(); } public String toCsvLine() { return String.join( ",", this.handlerKey, String.valueOf(this.yShift), String.valueOf(this.height), String.valueOf(this.width), String.valueOf(this.multiWidgets ? 1 : 0), String.valueOf(this.order), String.valueOf(this.allowOverflowX ? 1 : 0), String.valueOf(this.allowOverflowY ? 1 : 0), String.valueOf(this.showFavorites ? 1 : 0), String.valueOf(this.showOverlay ? 1 : 0), String.valueOf(this.showBadge ? 1 : 0)); } public boolean isUnmodified() { return this.csvLine.equals(this.toCsvLine()); } private int intOrDefault(String str, int defaultValue) { if (str == null || str.isEmpty()) return defaultValue; try { return Integer.parseInt(str); } catch (NumberFormatException e) { return defaultValue; } } } private static final int WINDOW_WIDTH = 176; private static final int LINE_HEIGHT = 16; private static final int INLINE_PADDING = 6; private static final int LABEL_WIDTH = 50; private static final String TOOLTIP_PREFIX = "debug.RecipeHandler."; private static final String HEADER = "HandlerId,YShift,Height,Width,MultiWidgets,Order,AllowOverflowX,AllowOverflowY,ShowFavorites,ShowOverlay,ShowBadge"; private final int[] COLORS = new int[] { 0x2200FF00, 0x22FF0000, 0x220000FF, 0x2200FFFF, 0x22FF00FF, 0x22FFFF00 }; public static DebugHandlerWidget instance = new DebugHandlerWidget(); private ScrollContainer container = new ScrollContainer(); private final Map values = new LinkedHashMap<>(); private Map patches = new HashMap<>(); private HandlerInfoRecord record; private RecipeWidget hoveredRecipe; private boolean duplicatesOnly = false; private String duplicatesHandlerId; private Point dragPoint = null; private boolean showWidget = false; private Button resetButton; private final DrawableResource BG_TEXTURE = new DrawableBuilder( "nei:textures/gui/recipebg.png", 0, 0, 176 + TRANSPARENCY_BORDER * 2, 166 + TRANSPARENCY_BORDER * 2) .setSlices( BORDER_PADDING + TRANSPARENCY_BORDER, BORDER_PADDING + TRANSPARENCY_BORDER, BORDER_PADDING + TRANSPARENCY_BORDER, BORDER_PADDING + TRANSPARENCY_BORDER) .build(); private static final int BORDER_PADDING = 6; private static final int TRANSPARENCY_BORDER = 4; protected DebugHandlerWidget() { GuiContainerManager.addTooltipHandler(this); this.w = WINDOW_WIDTH; this.x = 10; this.y = 50; this.z = 4; this.container.x = this.x + INLINE_PADDING; this.container.y = this.y + 5 + LINE_HEIGHT; this.container.w = this.w - INLINE_PADDING * 2; int y = 0; y = addInfoWidget("Name", y); y = addInfoWidget("Id", y); y = addInfoWidget("Key", y); y = addInfoWidget("HHack", y); y = addInfoWidget("ModName", y); y = addInfoWidget("ModId", y); y = addOverrideInfoWidget("Override", y); y = addTextFieldWidget("Order", 0, (field, oldText) -> { record.order = field.getInteger(); saveHandlerInfoPatch(); }, null, y); y = addTextFieldWidget("yShift", 0, (field, oldText) -> { record.yShift = field.getInteger(); saveHandlerInfoPatch(); }, null, y); y = addCheckboxWidget("MultiWidgets", button -> { record.multiWidgets = button.value; saveHandlerInfoPatch(); }, y); final LabelWidget readonly = new LabelWidget(0xffffff, true); readonly.y = y; readonly.w = 57; readonly.x = this.w - INLINE_PADDING * 2 - readonly.w; this.values.put("height_readonly", readonly); y = addTextFieldWidget("Height", HandlerInfo.DEFAULT_HEIGHT, (field, oldText) -> { record.height = field.getInteger(); saveHandlerInfoPatch(); }, newValue -> newValue > 0, y); y = addTextFieldWidget("Width", HandlerInfo.DEFAULT_WIDTH, (field, oldText) -> { record.width = field.getInteger(); saveHandlerInfoPatch(); }, newValue -> newValue >= HandlerInfo.DEFAULT_WIDTH, y); y = addCheckboxWidget("AllowOverflowX", button -> { record.allowOverflowX = button.value; saveHandlerInfoPatch(); }, y); y = addCheckboxWidget("AllowOverflowY", button -> { record.allowOverflowY = button.value; saveHandlerInfoPatch(); }, y); y = addCheckboxWidget("ShowFavorites", button -> { record.showFavorites = button.value; saveHandlerInfoPatch(); }, y); y = addCheckboxWidget("ShowOverlay", button -> { record.showOverlay = button.value; saveHandlerInfoPatch(); }, y); y = addCheckboxWidget("ShowBadge", button -> { record.showBadge = button.value; saveHandlerInfoPatch(); }, y); if (Boolean.getBoolean("nei.recipeOrigins")) { y = addCheckboxWidget("DuplicatesOnly", button -> { this.duplicatesOnly = button.value; if (NEIClientUtils.getGuiContainer() instanceof GuiReciperecipe) { this.duplicatesHandlerId = getHandlerID(recipe.getHandler()); recipe.forceRefreshPage(); } }, y + LINE_HEIGHT / 2); // only the handler the checkbox was toggled on is filtered, so switching handlers does not pay for a rescan API.addRecipeFilter(() -> this.duplicatesOnly ? this::isDuplicate : null); } this.h = 10 + LINE_HEIGHT + y; this.container.h = this.h - INLINE_PADDING * 2 - LINE_HEIGHT; } private int addInfoWidget(String key, int y) { final LabelWidget label = new LabelWidget(0x888888, false); final LabelWidget value = new LabelWidget(0xffffff, true); label.y = value.y = y; label.w = LABEL_WIDTH; value.x = LABEL_WIDTH + 5; value.w = this.w - INLINE_PADDING * 2 - value.x; label.updateValue(NEIClientUtils.translate(TOOLTIP_PREFIX + key)); this.container.addWidget(label); this.container.addWidget(value); this.values.put(key.toLowerCase(), value); return y + value.h + 1; } private int addTextFieldWidget(String key, int defaultValue, BiConsumer onChange, IntPredicate validator, int y) { final LabelWidget label = new LabelWidget(0x888888, false); final IntegerField value = new IntegerField(key, onChange, validator, defaultValue); label.y = value.y = y; value.w = 60; value.x = this.w - INLINE_PADDING * 2 - value.w; label.w = value.x; label.updateValue(NEIClientUtils.translate(TOOLTIP_PREFIX + key)); this.container.addWidget(label); this.container.addWidget(value); this.values.put(key.toLowerCase(), value); return y + value.h + 1; } private int addCheckboxWidget(String key, Consumer onChange, int y) { final LabelWidget label = new LabelWidget(0x888888, false); final CheckboxWidget value = new CheckboxWidget(onChange); label.y = value.y = y; value.w = 58; value.x = this.w - INLINE_PADDING * 2 - value.w - 1; label.w = value.x; label.updateValue(NEIClientUtils.translate(TOOLTIP_PREFIX + key)); this.container.addWidget(label); this.container.addWidget(value); this.values.put(key.toLowerCase(), value); return y + value.h + 1; } private int addOverrideInfoWidget(String key, int y) { final LabelWidget label = new LabelWidget(0x888888, false); final LabelWidget value = new LabelWidget(0xffffff, true); this.resetButton = new Button(NEIClientUtils.translate("debug.RecipeHandler.reset")) { @Override public boolean onButtonPress(boolean rightclick) { if (record != null) { record.apply(record.csvLine); saveHandlerInfoPatch(); record = null;// force update of button state } return true; } @Override public void draw(int mousex, int mousey) { this.state = record != null && record.isUnmodified() ? 2 : 0; super.draw(mousex, mousey); } }; this.resetButton.w = 58; this.resetButton.h = 16; this.resetButton.y = label.y = value.y = y; this.resetButton.x = this.w - INLINE_PADDING * 2 - this.resetButton.w - 1; label.w = LABEL_WIDTH; value.x = LABEL_WIDTH + 5; value.w = this.resetButton.x - value.x - 2; label.updateValue(NEIClientUtils.translate(TOOLTIP_PREFIX + key)); this.container.addWidget(label); this.container.addWidget(value); this.values.put(key.toLowerCase(), value); this.container.addWidget(this.resetButton); return y + value.h + 1; } @Override public void draw(int mx, int my) { if (this.record != null) { GL11.glColor4f(1F, 1F, 1F, 1F); GL11.glScaled(1, 1, 2f); GL11.glDisable(GL11.GL_DEPTH_TEST); BG_TEXTURE.draw( this.x - TRANSPARENCY_BORDER, this.y - TRANSPARENCY_BORDER, this.w + TRANSPARENCY_BORDER * 2, this.h + TRANSPARENCY_BORDER * 2); GuiDraw.drawRect( this.x + INLINE_PADDING, this.y + BORDER_PADDING, this.w - INLINE_PADDING * 2, 12, 0x30000000); GuiDraw.drawStringC( NEIClientUtils.translate("debug.RecipeHandler.title"), this.x + this.w / 2, this.y + BORDER_PADDING + 2, 0xffffff); this.container.draw(mx, my); GL11.glScaled(1, 1, 1 / 2f); } } @Override public void update() { final GuiContainer gui = NEIClientUtils.getGuiContainer(); if (this.showWidget && gui instanceof GuiReciperecipe) { if (this.record == null || !this.record.handlerKey.equals(getHandlerID(recipe.getHandler()))) { final IRecipeHandler handler = recipe.getHandler(); this.record = this.patches.computeIfAbsent( getHandlerID(handler), handlerKey -> new HandlerInfoRecord(handlerKey, GuiRecipeTab.getHandlerInfo(handler))); final boolean isHeightHackApplied = NEIClientConfig.heightHackHandlerRegex.stream() .map(pattern -> pattern.matcher(handler.getHandlerId())).anyMatch(Matcher::matches); this.values.get("name").updateValue(handler.getRecipeName()); this.values.get("id").updateValue(handler.getOverlayIdentifier()); this.values.get("key").updateValue(handler.getHandlerId()); this.values.get("hhack").updateValue( isHeightHackApplied ? NEIClientUtils.translate("debug.RecipeHandler.yes") : NEIClientUtils.translate("debug.RecipeHandler.no")); this.values.get("modname").updateValue(this.record.info.getModName()); this.values.get("modid").updateValue(this.record.info.getModId()); this.values.get("order").updateValue(String.valueOf(this.record.order)); this.values.get("yshift").updateValue(String.valueOf(this.record.yShift)); this.values.get("height").updateValue(String.valueOf(this.record.height)); this.values.get("height_readonly").updateValue(String.valueOf(this.record.height)); this.values.get("width").updateValue(String.valueOf(this.record.width)); this.values.get("multiwidgets").updateValue(String.valueOf(this.record.multiWidgets)); this.values.get("allowoverflowx").updateValue(String.valueOf(this.record.allowOverflowX)); this.values.get("allowoverflowy").updateValue(String.valueOf(this.record.allowOverflowY)); this.values.get("showfavorites").updateValue(String.valueOf(this.record.showFavorites)); this.values.get("showoverlay").updateValue(String.valueOf(this.record.showOverlay)); this.values.get("showbadge").updateValue(String.valueOf(this.record.showBadge)); updateOverride(); if (this.values.get("duplicatesonly") instanceof CheckboxWidget duplicates) { duplicates.state = handler instanceof ShapedRecipeHandler ? 0 : 2; resetDuplicatesOnly(); } this.container.removeIf( widget -> this.values.get("height") == widget || this.values.get("height_readonly") == widget); if (handler.getRecipeHeight(0) > 0) { this.container.addWidget((Widget) this.values.get("height_readonly")); } else { this.container.addWidget((Widget) this.values.get("height")); } } } else { this.record = null; this.showWidget = false; resetDuplicatesOnly(); } this.container.update(); } private void resetDuplicatesOnly() { if (!this.duplicatesOnly) return; this.duplicatesOnly = false; if (this.values.get("duplicatesonly") instanceof CheckboxWidget duplicates) { duplicates.updateValue("false"); } if (NEIClientUtils.getGuiContainer() instanceof GuiReciperecipe) { recipe.forceRefreshPage(); } } @Override public List handleTooltip(int mx, int my, List tooltip) { return this.record != null ? this.container.handleTooltip(mx, my, tooltip) : tooltip; } @Override public Map handleHotkeys(int mousex, int mousey, Map hotkeys) { return this.record != null ? this.container.handleHotkeys(mousex, mousey, hotkeys) : hotkeys; } @Override public boolean handleKeyPress(int keyID, char keyChar) { if (NEIClientConfig.getBooleanSetting("inventory.guirecipe.handlerInfo") && keyID == Keyboard.KEY_D && NEIClientUtils.shiftKey()) { this.showWidget = !this.showWidget; return true; } else if (this.record != null) { if (this.container.handleKeyPress(keyID, keyChar)) { return true; } this.container.lastKeyTyped(keyID, keyChar); return this.container.isFocused(); } return false; } @Override public boolean onHotkeyPressed() { return this.record != null && this.container.onHotkeyPressed(); } @Override public void onGuiClick(int mx, int my) { if (this.record != null) { this.container.onGuiClick(mx, my); } } @Override public boolean handleClick(int mx, int my, int button) { if (this.record != null) { if (new Rectangle4i(this.x + INLINE_PADDING, this.y + BORDER_PADDING, this.w - INLINE_PADDING * 2, 12) .contains(mx, my)) { this.dragPoint = new Point(mx, my); } this.container.handleClick(mx, my, button); } return this.record != null; } @Override public boolean contains(int px, int py) { return this.record != null && super.contains(px, py); } @Override public void mouseUp(int mx, int my, int button) { this.dragPoint = null; if (this.record != null) { this.container.mouseUp(mx, my, button); } } @Override public boolean onMouseWheel(int scrolled, int mousex, int mousey) { if (this.record != null && contains(mousex, mousey)) { this.container.onMouseWheel(scrolled, mousex, mousey); return true; } return false; } @Override public boolean handleClickExt(int mx, int my, int button) { return this.record != null && this.container.handleClickExt(mx, my, button); } @Override public void mouseDragged(int mx, int my, int button, long heldTime) { if (this.dragPoint != null) { this.x -= this.dragPoint.x - mx; this.y -= this.dragPoint.y - my; this.container.y = this.y + INLINE_PADDING + LINE_HEIGHT; this.container.x = this.x + INLINE_PADDING; this.dragPoint.move(mx, my); } } @Override public List handleTooltip(GuiContainer gui, int mousex, int mousey, List tooltip) { final RecipeWidget widget = this.hoveredRecipe; this.hoveredRecipe = null; if (widget == null || !this.showWidget || !tooltip.isEmpty()) { return tooltip; } final RecipeHandlerRef ref = widget.getRecipeHandlerRef(); for (List origin : RecipeOrigins.findOrigins(ref.handler, ref.recipeIndex)) { addOriginLines(tooltip, origin); } return tooltip; } private static void addOriginLines(List tooltip, List lines) { tooltip.add( EnumChatFormatting.WHITE + NEIClientUtils.translate("debug.RecipeHandler.RegisteredBy", lines.get(0)) + GuiDraw.TOOLTIP_LINESPACE); for (int i = 1; i < lines.size(); i++) { tooltip.add(EnumChatFormatting.GRAY + lines.get(i)); } tooltip.add(""); } // only the handler the checkbox was toggled on is filtered private boolean isDuplicate(IRecipeHandler handler, int recipeIndex) { return !getHandlerID(handler).equals(this.duplicatesHandlerId) || RecipeOrigins.hasDuplicates(handler, recipeIndex); } public void drawGuiPlaceholder(RecipeWidget widget) { if (!this.showWidget) return; final Point mouse = GuiDraw.getMousePosition(); if (widget.getRecipeHandlerRef().handler instanceof ShapedRecipeHandler && widget.contains(mouse.x, mouse.y) && widget.getStackMouseOver(mouse.x, mouse.y) == null) { this.hoveredRecipe = widget; } NEIClientUtils.gl2DRenderContext(() -> { // background GuiDraw.drawRect( widget.x, widget.y, widget.w, widget.h, COLORS[widget.getRecipeHandlerRef().recipeIndex % COLORS.length]); // center cross GuiDraw.drawRect(widget.x + widget.w / 2 - 1, widget.y, 1, widget.h, 0x88ffffff); GuiDraw.drawRect(widget.x, widget.y + widget.h / 2 - 1, widget.w, 1, 0x88ffffff); // blue-line-top (grid-top) GuiDraw.drawRect(widget.x + 14, widget.y + 5, widget.w - 38, 1, 0xff0000aa); // blue-line-bottom {grid-bottom} GuiDraw.drawRect(widget.x + 14, widget.y + widget.h - 7, widget.w - 38, 1, 0xff0000aa); // blue-left (grid start) GuiDraw.drawRect(widget.x + 24, widget.y + (widget.h - 16) / 2, 1, 16, 0xffaa00aa); // purple (before favorite) GuiDraw.drawRect( widget.x + Math.min(168, widget.w) - 27, widget.y + widget.h - 6 - GuiRecipeButton.BUTTON_HEIGHT * 2 - 4, GuiRecipeButton.BUTTON_WIDTH, 1, 0xffaa00aa); // green () GuiDraw.drawRect( widget.x + Math.min(168, widget.w) - 35, widget.y + widget.h - 6 - GuiRecipeButton.BUTTON_HEIGHT - 3, 20, 1, 0xff00aa00); // purple space before bottom grid GuiDraw.drawRect( widget.x + 14, widget.y + widget.h - 7 - GuiRecipeButton.BUTTON_WIDTH / 2, 38, 1, 0xffaa00aa); if (widget.getRecipeHandlerRef().handler instanceof TemplateRecipeHandler handler) { int yShift = widget.getHandlerInfo().getYShift(); for (RecipeTransferRect rect : handler.transferRects) { GuiDraw.drawRect( widget.x + rect.getRect().x, widget.y + yShift + rect.getRect().y, rect.getRect().width, rect.getRect().height, 0x40ff0000); } } }); } public void loadHandlerInfoPatch() { ClientHandler.loadSettingsFile("handlers.patch", lines -> { final List linesList = lines.collect(Collectors.toCollection(ArrayList::new)); if (linesList.isEmpty() || !HEADER.equals(linesList.get(0))) { return; } linesList.remove(0); for (String line : linesList) { final String[] parts = line.split(","); final String handlerKey = parts[0]; final HandlerInfoRecord record = patches.computeIfAbsent( handlerKey, key -> new HandlerInfoRecord( key, GuiRecipeTab.handlerMap.getOrDefault(key, GuiRecipeTab.DEFAULT_HANDLER_INFO))); record.apply((line + ",null,null,null,null,null,null,null,null,null,null")); } }); } private void saveHandlerInfoPatch() { final List lines = new ArrayList<>(); lines.add(HEADER); for (HandlerInfoRecord record : patches.values()) { if (!record.csvLine.equals(record.toCsvLine())) { lines.add(record.toCsvLine()); } } final File path = new File(NEIClientConfig.configDir, "handlers.patch"); try (FileOutputStream output = new FileOutputStream(path)) { IOUtils.writeLines(lines, "\n", output, StandardCharsets.UTF_8); } catch (IOException e) {} if (this.record != null) { updateOverride(); record.apply(); } if (NEIClientUtils.getGuiContainer() instanceof GuiReciperecipe) { recipe.forceRefreshPage(); } } private void updateOverride() { this.container.removeIf(widget -> widget == this.resetButton); if (this.record.isUnmodified()) { this.values.get("override").updateValue(NEIClientUtils.translate("debug.RecipeHandler.no")); } else { this.values.get("override") .updateValue(EnumChatFormatting.RED + NEIClientUtils.translate("debug.RecipeHandler.yes")); this.container.addWidget(this.resetButton); } } private String getHandlerID(IRecipeHandler handler) { if (!GuiRecipeTab.handlerMap.containsKey(handler.getHandlerId()) && handler instanceof TemplateRecipeHandler && handler.getOverlayIdentifier() != null) { return handler.getOverlayIdentifier(); } return handler.getHandlerId(); } }