From 18341767186595013d2f3b83288780237d711163 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 16 May 2026 13:36:00 +0000 Subject: [PATCH 1/2] fix: correct click handling, state race condition, item fallback, type guard, version mismatch - BarrelGuiHandler: consume ALL non-PICKUP slot actions (THROW, CLONE, QUICK_MOVE, etc.) to prevent item manipulation; only PICKUP triggers button actions. Previously QUICK_MOVE was incorrectly allowed to trigger actions. - BarrelGuiHandler: move OPEN_GUIS.put before buildInventory to eliminate race condition where handleClick could fire before state was registered. - BarrelGuiHandler: log WARN when unknown item id falls back to stone instead of silently substituting. - GuiRegistry: skip and warn on unsupported GUI types (e.g. legacy 'dialog') instead of silently loading them as empty GUIs. - gradle.properties: sync fabric_version to 0.116.12+1.21.1 to match build.gradle. --- gradle.properties | 2 +- .../toolkitmc/guiapi/gui/BarrelGuiHandler.java | 18 ++++++++++++++---- .../toolkitmc/guiapi/loader/GuiRegistry.java | 7 +++++++ 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/gradle.properties b/gradle.properties index e1dc041..099df92 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,4 +8,4 @@ archives_base_name=guiapi # Dependencies — keep in sync with build.gradle minecraft_version=1.21.1 loader_version=0.16.5 -fabric_version=0.104.0+1.21.1 +fabric_version=0.116.12+1.21.1 diff --git a/src/main/java/dev/toolkitmc/guiapi/gui/BarrelGuiHandler.java b/src/main/java/dev/toolkitmc/guiapi/gui/BarrelGuiHandler.java index 165460d..f7ee03a 100644 --- a/src/main/java/dev/toolkitmc/guiapi/gui/BarrelGuiHandler.java +++ b/src/main/java/dev/toolkitmc/guiapi/gui/BarrelGuiHandler.java @@ -60,8 +60,10 @@ public static void open(ServerPlayerEntity player, GuiDefinition def, int page) int rows = Math.clamp(def.getRows(), 1, 6); int finalPage = page; - SimpleInventory inv = buildInventory(player, def, page, rows * 9); + // Register state BEFORE building inventory so that any handleClick call + // triggered during screen open (edge case) already sees the correct state. OPEN_GUIS.put(player.getUuid(), new OpenState(def, page)); + SimpleInventory inv = buildInventory(player, def, page, rows * 9); String pageIndicator = def.getPageCount() > 1 ? " §8[" + (page + 1) + "/" + def.getPageCount() + "]" @@ -83,8 +85,10 @@ public net.minecraft.screen.ScreenHandler createMenu( public static boolean handleClick(ServerPlayerEntity player, GuiDefinition def, int page, int slot, SlotActionType actionType) { - if (actionType != SlotActionType.PICKUP && actionType != SlotActionType.QUICK_MOVE) - return true; // consume but ignore + // Only PICKUP (left/right click) triggers button actions. + // All other action types (QUICK_MOVE, THROW, CLONE, etc.) are consumed silently + // to prevent any item manipulation inside the GUI inventory. + if (actionType != SlotActionType.PICKUP) return true; for (GuiDefinition.Button btn : def.getButtonsForPage(page)) { if (btn.slot() != slot) continue; @@ -122,7 +126,13 @@ private static SimpleInventory buildInventory(ServerPlayerEntity player, private static ItemStack buildStack(GuiDefinition.Button btn) { Identifier itemId = Identifier.tryParse(btn.item()); - Item item = itemId != null ? Registries.ITEM.get(itemId) : Items.STONE; + Item item; + if (itemId != null && Registries.ITEM.containsId(itemId)) { + item = Registries.ITEM.get(itemId); + } else { + GuiApiMod.LOGGER.warn("[GuiAPI] Unknown item '{}' in slot {}, falling back to stone.", btn.item(), btn.slot()); + item = Items.STONE; + } ItemStack stack = new ItemStack(item); diff --git a/src/main/java/dev/toolkitmc/guiapi/loader/GuiRegistry.java b/src/main/java/dev/toolkitmc/guiapi/loader/GuiRegistry.java index b3a26fd..5f177c9 100644 --- a/src/main/java/dev/toolkitmc/guiapi/loader/GuiRegistry.java +++ b/src/main/java/dev/toolkitmc/guiapi/loader/GuiRegistry.java @@ -53,6 +53,13 @@ protected Map prepare(ResourceManager manager, Profil JsonObject json = GSON.fromJson(reader, JsonObject.class); + // Reject unsupported GUI types explicitly (e.g. legacy "dialog" format) + if (json.has("type") && !json.get("type").getAsString().equals("barrel")) { + GuiApiMod.LOGGER.warn("[GuiAPI] Skipping {} — unsupported type '{}'. Only chest/barrel GUIs are supported.", + fileId, json.get("type").getAsString()); + return; + } + // fileId looks like: :gui/.json // Compute logical GUI id: : (strip "gui/" prefix + ".json") String path = fileId.getPath(); // "gui/my_gui.json" From 0cd0f5524e971223e1bec57c01361c649d9ba183 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 16 May 2026 13:41:17 +0000 Subject: [PATCH 2/2] feat: fix rows, add left/right/shift click, help+reload commands, Mod Menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Fix rows not working - GuiScreenHandler.onSlotClick: replaced definition.getRows()*9 with GenericContainerScreenHandler.getRows()*9 — these must match the actual inventory size, not the raw JSON value. - GuiDefinition.getRows(): now always returns Math.clamp(rows,1,6) so callers never get an out-of-range value. 2. Left / Right / Shift click support - Added ClickType enum: ANY | LEFT | RIGHT | SHIFT - Added click_type field to Button record (default: ANY) - GuiScreenHandler passes mouseButton int to handleClick - handleClick resolves isLeft/isRight/isShift from mouseButton + SlotActionType, then matches against button's ClickType filter. 3. /guiapi reload and /guiapi help subcommands - reload: delegates to server.reloadResources() — same pipeline as /reload - help: prints all commands and button JSON field reference - list: now also shows rows count alongside pages 4. Mod Menu integration - build.gradle: modCompileOnly modmenu:11.0.3 via Modrinth maven - GuiApiModMenuEntry: info screen showing loaded GUIs with rows/pages - fabric.mod.json: modmenu entrypoint + suggests: modmenu --- build.gradle | 4 + .../toolkitmc/guiapi/command/GuiCommand.java | 48 +++++++++- .../guiapi/gui/BarrelGuiHandler.java | 29 ++++-- .../toolkitmc/guiapi/gui/GuiDefinition.java | 31 ++++++- .../guiapi/gui/GuiScreenHandler.java | 11 ++- .../guiapi/modmenu/GuiApiModMenuEntry.java | 89 +++++++++++++++++++ src/main/resources/fabric.mod.json | 8 +- 7 files changed, 205 insertions(+), 15 deletions(-) create mode 100644 src/main/java/dev/toolkitmc/guiapi/modmenu/GuiApiModMenuEntry.java diff --git a/build.gradle b/build.gradle index 815e5fe..8faa09f 100644 --- a/build.gradle +++ b/build.gradle @@ -12,6 +12,7 @@ base { repositories { maven { url = "https://maven.fabricmc.net/" } + maven { url = "https://api.modrinth.com/maven" } } dependencies { @@ -19,6 +20,9 @@ dependencies { mappings "net.fabricmc:yarn:1.21.1+build.3:v2" modImplementation "net.fabricmc:fabric-loader:0.16.5" modImplementation "net.fabricmc.fabric-api:fabric-api:0.116.12+1.21.1" + + // Mod Menu — optional. Compile-only so the jar doesn't require it at runtime. + modCompileOnly "maven.modrinth:modmenu:11.0.3" } processResources { diff --git a/src/main/java/dev/toolkitmc/guiapi/command/GuiCommand.java b/src/main/java/dev/toolkitmc/guiapi/command/GuiCommand.java index 998386f..4086111 100644 --- a/src/main/java/dev/toolkitmc/guiapi/command/GuiCommand.java +++ b/src/main/java/dev/toolkitmc/guiapi/command/GuiCommand.java @@ -19,6 +19,8 @@ /** * /guiapi open [] * /guiapi list + * /guiapi reload + * /guiapi help * * Permission level 2 required. */ @@ -58,9 +60,17 @@ public static void register(CommandDispatcher dispatcher) { .then(CommandManager.literal("list") .executes(GuiCommand::listGuis)) + + .then(CommandManager.literal("reload") + .executes(GuiCommand::reloadGuis)) + + .then(CommandManager.literal("help") + .executes(GuiCommand::showHelp)) ); } + // ── Subcommand handlers ────────────────────────────────────────────────── + private static int openGui(CommandContext ctx, Collection targets) { Identifier id = IdentifierArgumentType.getIdentifier(ctx, "id"); @@ -90,8 +100,44 @@ private static int listGuis(CommandContext ctx) { StringBuilder sb = new StringBuilder("[GuiAPI] Loaded GUIs (" + all.size() + "):\n"); all.forEach((id, def) -> sb.append(" ").append(id) - .append(" [pages=").append(def.getPageCount()).append("]\n")); + .append(" [rows=").append(def.getRows()) + .append(", pages=").append(def.getPageCount()).append("]\n")); ctx.getSource().sendFeedback(() -> Text.literal(sb.toString().trim()), false); return all.size(); } + + private static int reloadGuis(CommandContext ctx) { + // Delegates to the server's full resource reload so GuiRegistry.apply() + // fires through the normal Fabric reload pipeline — same as /reload. + ctx.getSource().getServer() + .reloadResources(ctx.getSource().getServer().getDataPackManager().getEnabledIds()) + .thenRun(() -> ctx.getSource().sendFeedback( + () -> Text.literal("[GuiAPI] Reload complete. " + + GuiRegistry.INSTANCE.getAll().size() + " GUI(s) loaded."), + true)) + .exceptionally(ex -> { + ctx.getSource().sendError( + Text.literal("[GuiAPI] Reload failed: " + ex.getMessage())); + return null; + }); + return 1; + } + + private static int showHelp(CommandContext ctx) { + String help = + "[GuiAPI] Commands (permission level 2):\n" + + " /guiapi open [targets] - Open a GUI for yourself or target players\n" + + " /guiapi list - List all loaded GUI definitions\n" + + " /guiapi reload - Reload all datapack resources (including GUIs)\n" + + " /guiapi help - Show this help message\n" + + "\n" + + "Button JSON fields:\n" + + " slot, page, item, name, lore, glint\n" + + " click_type: any | left | right | shift (default: any)\n" + + " condition: has_tag | score_gt | score_lt | score_eq\n" + + " actions: run_command | close | open_gui | message\n" + + " next_page | prev_page | goto_page"; + ctx.getSource().sendFeedback(() -> Text.literal(help), false); + return 1; + } } diff --git a/src/main/java/dev/toolkitmc/guiapi/gui/BarrelGuiHandler.java b/src/main/java/dev/toolkitmc/guiapi/gui/BarrelGuiHandler.java index f7ee03a..888d0b7 100644 --- a/src/main/java/dev/toolkitmc/guiapi/gui/BarrelGuiHandler.java +++ b/src/main/java/dev/toolkitmc/guiapi/gui/BarrelGuiHandler.java @@ -84,19 +84,34 @@ public net.minecraft.screen.ScreenHandler createMenu( } public static boolean handleClick(ServerPlayerEntity player, GuiDefinition def, - int page, int slot, SlotActionType actionType) { - // Only PICKUP (left/right click) triggers button actions. - // All other action types (QUICK_MOVE, THROW, CLONE, etc.) are consumed silently - // to prevent any item manipulation inside the GUI inventory. - if (actionType != SlotActionType.PICKUP) return true; + int page, int slot, int mouseButton, SlotActionType actionType) { + // Resolve what kind of click this actually is. + // mouseButton: 0 = left, 1 = right (Minecraft protocol) + // actionType QUICK_MOVE = shift+click + final boolean isShift = actionType == SlotActionType.QUICK_MOVE; + final boolean isLeft = !isShift && mouseButton == 0 && actionType == SlotActionType.PICKUP; + final boolean isRight = !isShift && mouseButton == 1 && actionType == SlotActionType.PICKUP; + + // Consume every action type to block item manipulation. + // Only PICKUP and QUICK_MOVE can actually trigger button actions. + if (!isLeft && !isRight && !isShift) return true; for (GuiDefinition.Button btn : def.getButtonsForPage(page)) { if (btn.slot() != slot) continue; - if (!evaluateCondition(player, btn)) continue; // invisible button, ignore + if (!evaluateCondition(player, btn)) continue; + + // Check click_type filter + boolean matches = switch (btn.clickType()) { + case LEFT -> isLeft; + case RIGHT -> isRight; + case SHIFT -> isShift; + case ANY -> isLeft || isRight || isShift; + }; + if (!matches) continue; for (GuiDefinition.ButtonAction action : btn.actions()) { boolean shouldBreak = executeAction(player, def, page, action); - if (shouldBreak) break; // close/open_gui terminates chain + if (shouldBreak) break; } return true; } diff --git a/src/main/java/dev/toolkitmc/guiapi/gui/GuiDefinition.java b/src/main/java/dev/toolkitmc/guiapi/gui/GuiDefinition.java index dd16b8a..84d1069 100644 --- a/src/main/java/dev/toolkitmc/guiapi/gui/GuiDefinition.java +++ b/src/main/java/dev/toolkitmc/guiapi/gui/GuiDefinition.java @@ -23,6 +23,7 @@ * "name": "§bClick Me", * "lore": ["§7Line 1"], * "glint": true, // enchantment glint effect + * "click_type": "any", // any (default) | left | right | shift * "condition": { // optional visibility condition * "type": "has_tag", // has_tag | score_gt | score_lt | score_eq * "value": "my_tag" // tag name OR "objective:min:max" for score @@ -58,6 +59,26 @@ public class GuiDefinition { // ── Enums ──────────────────────────────────────────────────────────────── + /** + * Which mouse button triggers this button's actions. + * ANY — left or right click (default, original behaviour) + * LEFT — only left click + * RIGHT — only right click + * SHIFT — only shift+left click (QUICK_MOVE) + */ + public enum ClickType { + ANY, LEFT, RIGHT, SHIFT; + + public static ClickType fromString(String s) { + return switch (s.toLowerCase()) { + case "left" -> LEFT; + case "right" -> RIGHT; + case "shift" -> SHIFT; + default -> ANY; + }; + } + } + public enum ActionType { RUN_COMMAND, CLOSE, OPEN_GUI, MESSAGE, NEXT_PAGE, PREV_PAGE, GOTO_PAGE; @@ -112,6 +133,7 @@ public record Button( String name, List lore, boolean glint, + ClickType clickType, Optional condition, List actions ) {} @@ -157,6 +179,10 @@ private static Button parseButton(JsonObject b) { String name = b.has("name") ? b.get("name").getAsString() : ""; boolean glint = b.has("glint") && b.get("glint").getAsBoolean(); + ClickType clickType = b.has("click_type") + ? ClickType.fromString(b.get("click_type").getAsString()) + : ClickType.ANY; + List lore = new ArrayList<>(); if (b.has("lore") && b.get("lore").isJsonArray()) { for (JsonElement l : b.getAsJsonArray("lore")) @@ -184,7 +210,7 @@ private static Button parseButton(JsonObject b) { if (actions.isEmpty()) actions.add(new ButtonAction(ActionType.CLOSE, "")); - return new Button(slot, page, item, name, lore, glint, condition, actions); + return new Button(slot, page, item, name, lore, glint, clickType, condition, actions); } private static ButtonAction parseAction(JsonObject a) { @@ -201,7 +227,8 @@ private static ButtonAction parseAction(JsonObject a) { public Identifier getId() { return id; } public String getTitle() { return title; } - public int getRows() { return rows; } + /** Always in [1, 6]. */ + public int getRows() { return Math.clamp(rows, 1, 6); } public int getPageCount() { return pageCount; } public List