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/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/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 165460d..888d0b7 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() + "]" @@ -82,17 +84,34 @@ 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 + 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; } @@ -122,7 +141,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/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