diff --git a/.gitignore b/.gitignore index 72b1b4b..125dae0 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,5 @@ rad.json TestResults.xml # Gradle -.gradle/ \ No newline at end of file +.gradle/ +build/ \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cbf6a13..7aa7375 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,7 +22,7 @@ cd guiAPI ./gradlew build ``` -The built jar is at `build/libs/guiapi-1.0.3.jar`. +The built jar is at `build/libs/guiapi-1.0.5.jar`. To run in a local Minecraft instance, use Fabric's `runServer` task or drop the jar into a test server's `mods/` folder. diff --git a/README.md b/README.md index a71cc97..9fb0eb7 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ No client mod required. No macros. No external dependencies beyond Fabric API. ## Installation -1. Drop `guiapi-1.0.4.jar` into your `mods/` folder. +1. Drop `guiapi-1.0.5.jar` into your `mods/` folder. 2. Drop your datapack into `world/datapacks/`. 3. Run `/reload` or `/guiapi reload`. @@ -220,7 +220,7 @@ Please refer to the updated `example-datapack` directory in the repository sourc ```bash chmod +x gradlew ./gradlew build -# Output: build/libs/guiapi-1.0.4.jar +# Output: build/libs/guiapi-1.0.5.jar ``` Requires **Java 21**. diff --git a/example-datapack/data/example/gui/macro_and_input_demo.json b/example-datapack/data/example/gui/macro_and_input_demo.json new file mode 100644 index 0000000..3f7c181 --- /dev/null +++ b/example-datapack/data/example/gui/macro_and_input_demo.json @@ -0,0 +1,70 @@ +{ + "title": "§bMacro & Input Demo", + "rows": 3, + "macros": { + "give_reward": [ + { "type": "run_command", "value": "give @s minecraft:diamond 1", "run_with": "console" }, + { "type": "sound", "value": "minecraft:entity.player.levelup" }, + { "type": "action_bar", "value": "§aReward claimed!" } + ], + "show_info": [ + { "type": "message", "value": "§7XP: §a{xp} §7| Input: §f{input} §7| Var: §e{var:myVar}" } + ] + }, + "buttons": [ + { + "slot": 10, + "item": "minecraft:writable_book", + "name": "§eEnter Your Name", + "lore": [ + "§7Click to open anvil input", + "§7Saves to {input} and {var:myVar}" + ], + "actions": [ + { "type": "anvil_input", "var": "myVar", "value": "Enter Name|{var:myVar}" } + ] + }, + { + "slot": 12, + "item": "minecraft:diamond", + "name": "§bRun Macro: give_reward", + "lore": [ + "§7Executes the 'give_reward' macro", + "§7defined in this JSON's macros block." + ], + "actions": [ + { "type": "run_function", "value": "give_reward" } + ] + }, + { + "slot": 14, + "item": "minecraft:book", + "name": "§dShow Info Macro", + "lore": [ + "§7Displays {xp}, {input} and {var:myVar}" + ], + "actions": [ + { "type": "run_function", "value": "show_info" } + ] + }, + { + "slot": 16, + "item": "minecraft:experience_bottle", + "name": "§aShow XP Level", + "lore": [ + "§7Your current XP level: §f{xp}" + ], + "actions": [ + { "type": "message", "value": "§7Your XP level is §a{xp}§7!" } + ] + }, + { + "slot": 22, + "item": "minecraft:barrier", + "name": "§cClose", + "actions": [ + { "type": "close" } + ] + } + ] +} diff --git a/gradle.properties b/gradle.properties index f8f52a5..141adf2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ org.gradle.workers.max=1 org.gradle.daemon=false # Project -mod_version=1.0.4+1.21.8 +mod_version=1.0.5+1.21.8 maven_group=dev.toolkitmc archives_base_name=guiapi diff --git a/src/main/java/dev/toolkitmc/guiapi/command/GuiCommand.java b/src/main/java/dev/toolkitmc/guiapi/command/GuiCommand.java index cd77250..be4334d 100644 --- a/src/main/java/dev/toolkitmc/guiapi/command/GuiCommand.java +++ b/src/main/java/dev/toolkitmc/guiapi/command/GuiCommand.java @@ -189,6 +189,14 @@ private static int showHelp(CommandContext ctx) { "Variable actions: set_var | add_var | sub_var | reset_var | clear_vars\n" + "Variable conditions: var_eq | var_gt | var_lt | var_set\n" + "Variable placeholder: {var:key}\n" + + "Input placeholder: {input} (last anvil input)\n" + + "XP placeholder: {xp} (player experience level)\n" + + "\n" + + "Macro functions: define reusable action blocks in JSON with \"macros\": {}\n" + + " actions: run_function:\n" + + "\n" + + "Anvil input: anvil_input action saves text to a variable and {input}\n" + + " Example: {\"type\": \"anvil_input\", \"var\": \"myVar\", \"value\": \"Enter name|Default\"}\n" + "\n" + "Button JSON fields:\n" + " slot, page, item, name, lore, glint\n" + @@ -196,8 +204,9 @@ private static int showHelp(CommandContext ctx) { " condition: has_tag | not_tag | score_gt | score_lt | score_eq\n" + " var_eq | var_gt | var_lt | var_set\n" + " actions: run_command | close | open_gui | message | sound\n" + - " next_page | prev_page | goto_page\n" + - " set_var | add_var | sub_var | reset_var | clear_vars"; + " next_page | prev_page | goto_page | run_function\n" + + " set_var | add_var | sub_var | reset_var | clear_vars\n" + + " anvil_input" ; ctx.getSource().sendFeedback(() -> Text.literal(help), false); return 1; } diff --git a/src/main/java/dev/toolkitmc/guiapi/gui/AnvilGuiHandler.java b/src/main/java/dev/toolkitmc/guiapi/gui/AnvilGuiHandler.java index 038554e..7c872d2 100644 --- a/src/main/java/dev/toolkitmc/guiapi/gui/AnvilGuiHandler.java +++ b/src/main/java/dev/toolkitmc/guiapi/gui/AnvilGuiHandler.java @@ -1,5 +1,6 @@ package dev.toolkitmc.guiapi.gui; +import net.minecraft.component.DataComponentTypes; import net.minecraft.entity.player.PlayerEntity; import net.minecraft.entity.player.PlayerInventory; import net.minecraft.item.ItemStack; @@ -7,8 +8,10 @@ import net.minecraft.screen.AnvilScreenHandler; import net.minecraft.screen.NamedScreenHandlerFactory; import net.minecraft.screen.ScreenHandlerContext; +import net.minecraft.screen.slot.SlotActionType; import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.text.Text; +import net.minecraft.util.Formatting; public class AnvilGuiHandler { @@ -16,39 +19,36 @@ public interface AnvilCallback { void onInput(ServerPlayerEntity player, String text); } - private static String getNewItemNameReflected(AnvilScreenHandler handler) { - try { - java.lang.reflect.Field field = AnvilScreenHandler.class.getDeclaredField("newItemName"); - field.setAccessible(true); - String val = (String) field.get(handler); - return val != null ? val : ""; - } catch (Exception e) { - try { - java.lang.reflect.Field field = AnvilScreenHandler.class.getDeclaredField("field_30755"); - field.setAccessible(true); - String val = (String) field.get(handler); - return val != null ? val : ""; - } catch (Exception e2) { - ItemStack stack = handler.getSlot(2).getStack(); - if (!stack.isEmpty()) { - return stack.getName().getString(); - } - return ""; - } - } + public static void openInput(ServerPlayerEntity player, String title, String defaultText, AnvilCallback callback) { + openInput(player, Text.literal(title).formatted(Formatting.GOLD, Formatting.BOLD), defaultText, callback); } - public static void openInput(ServerPlayerEntity player, String title, String defaultText, AnvilCallback callback) { + public static void openInput(ServerPlayerEntity player, Text title, String defaultText, AnvilCallback callback) { player.openHandledScreen(new NamedScreenHandlerFactory() { @Override public Text getDisplayName() { - return Text.literal(title); + return title; } @Override - public net.minecraft.screen.ScreenHandler createMenu(int syncId, PlayerInventory playerInv, PlayerEntity p) { + public AnvilScreenHandler createMenu(int syncId, PlayerInventory playerInv, PlayerEntity p) { AnvilScreenHandler handler = new AnvilScreenHandler(syncId, playerInv, ScreenHandlerContext.EMPTY) { - private boolean initializing = false; + + // REFLECTION REMOVED: there is no "newItemName" FIELD on AnvilScreenHandler + // (removed in Yarn 1.21.4+/1.21.8). getNewItemNameReflected used to always + // fall through to the bottom catch block and read the slot 2 item name — + // which was never the text the player actually typed. The correct approach + // is to override setNewItemName(String) instead. + private String currentInputText = defaultText; + + @Override + public boolean setNewItemName(String newItemName) { + this.currentInputText = newItemName; + // super is not called: the vanilla behavior computes an XP cost and + // tries to place a renamed item into the output slot, which is not + // wanted in this GUI. + return true; + } @Override public boolean canUse(PlayerEntity player) { @@ -56,10 +56,11 @@ public boolean canUse(PlayerEntity player) { } @Override - public void onSlotClick(int slotIndex, int button, net.minecraft.screen.slot.SlotActionType actionType, PlayerEntity playerEntity) { - if (slotIndex == 2) { - String text = getNewItemNameReflected(this); + public void onSlotClick(int slotIndex, int button, SlotActionType actionType, PlayerEntity playerEntity) { + // All three slots (0, 1, 2) act as submit; player inventory slots (3+) stay vanilla. + if (slotIndex == 0 || slotIndex == 1 || slotIndex == 2) { if (playerEntity instanceof ServerPlayerEntity sp) { + String text = this.currentInputText != null ? this.currentInputText : ""; sp.closeHandledScreen(); callback.onInput(sp, text); } @@ -70,15 +71,14 @@ public void onSlotClick(int slotIndex, int button, net.minecraft.screen.slot.Slo @Override public void updateResult() { - // Do not call setStack here — it triggers markDirty → onSlotChange → updateResult loop. - // Output slot is managed by super; just update the display name on the output. - super.updateResult(); + // no-op: super.updateResult() triggers the vanilla repair-cost logic, + // risking a setStack -> markDirty -> onContentChanged -> updateResult loop. + // Fully disabled since the output slot is unused here. } }; - // Set the input item once, outside of updateResult, to avoid the recursive loop. ItemStack paper = new ItemStack(Items.PAPER); - paper.set(net.minecraft.component.DataComponentTypes.CUSTOM_NAME, Text.literal(defaultText)); + paper.set(DataComponentTypes.CUSTOM_NAME, Text.literal(defaultText)); handler.getSlot(0).setStack(paper); return handler; diff --git a/src/main/java/dev/toolkitmc/guiapi/gui/BarrelGuiHandler.java b/src/main/java/dev/toolkitmc/guiapi/gui/BarrelGuiHandler.java index aae8b28..16b40a2 100644 --- a/src/main/java/dev/toolkitmc/guiapi/gui/BarrelGuiHandler.java +++ b/src/main/java/dev/toolkitmc/guiapi/gui/BarrelGuiHandler.java @@ -304,6 +304,7 @@ public static void executeDelayedActionChain(ServerPlayerEntity player, GuiDefin public static void onClose(UUID playerUuid) { if (OPEN_GUIS.remove(playerUuid) != null) { GuiVarStore.INSTANCE.clear(playerUuid); + GuiInputStore.INSTANCE.clear(playerUuid); } } @@ -323,6 +324,7 @@ public static void onClose(ServerPlayerEntity player) { executeAction(player, state.def, state.page, action); } GuiVarStore.INSTANCE.clear(player.getUuid()); + GuiInputStore.INSTANCE.clear(player.getUuid()); } /** @@ -517,6 +519,8 @@ static String resolve(String text, ServerPlayerEntity player, text = text.replace("{page}", String.valueOf(page)); text = text.replace("{page1}", String.valueOf(page + 1)); text = text.replace("{pages}", String.valueOf(def.getPageCount())); + text = text.replace("{xp}", String.valueOf(player.experienceLevel)); + text = text.replace("{input}", GuiInputStore.INSTANCE.get(player.getUuid())); // {score:objective} int idx; @@ -686,18 +690,32 @@ static boolean executeAction(ServerPlayerEntity player, GuiDefinition def, } case ANVIL_INPUT -> { String resolved = resolve(action.value(), player, def, currentPage); - String[] parts = resolved.split(":", 2); - if (parts.length == 2) { - String varKey = parts[0]; - String anvilTitle = parts[1]; - final Identifier previousGuiId = def.getId(); - final int previousPage = currentPage; - - AnvilGuiHandler.openInput(player, anvilTitle, "Type here...", (sp, text) -> { - GuiVarStore.INSTANCE.set(sp.getUuid(), varKey, text); - dev.toolkitmc.guiapi.loader.GuiRegistry.INSTANCE.get(previousGuiId) - .ifPresent(target -> open(sp, target, previousPage)); - }); + String varKey = action.var().isEmpty() ? "input" : action.var(); + String anvilTitle; + String defaultText; + if (resolved.contains("|")) { + String[] p = resolved.split("\\|", 2); + anvilTitle = p[0]; + defaultText = p[1]; + } else { + anvilTitle = resolved; + defaultText = "Type here..."; + } + final Identifier previousGuiId = def.getId(); + final int previousPage = currentPage; + + AnvilGuiHandler.openInput(player, anvilTitle, defaultText, (sp, text) -> { + GuiVarStore.INSTANCE.set(sp.getUuid(), varKey, text); + GuiInputStore.INSTANCE.set(sp.getUuid(), text); + dev.toolkitmc.guiapi.loader.GuiRegistry.INSTANCE.get(previousGuiId) + .ifPresent(target -> open(sp, target, previousPage)); + }); + } + case RUN_FUNCTION -> { + String macroName = resolve(action.value(), player, def, currentPage); + java.util.List macroActions = def.getMacros().get(macroName); + if (macroActions != null && !macroActions.isEmpty()) { + executeDelayedActionChain(player, def, currentPage, macroActions, 0, false); } } case CLOSE -> { diff --git a/src/main/java/dev/toolkitmc/guiapi/gui/GuiDefinition.java b/src/main/java/dev/toolkitmc/guiapi/gui/GuiDefinition.java index e50c1d1..c4f0314 100644 --- a/src/main/java/dev/toolkitmc/guiapi/gui/GuiDefinition.java +++ b/src/main/java/dev/toolkitmc/guiapi/gui/GuiDefinition.java @@ -52,7 +52,7 @@ public enum ActionType { RUN_COMMAND, CLOSE, OPEN_GUI, MESSAGE, NEXT_PAGE, PREV_PAGE, GOTO_PAGE, SOUND, SET_VAR, ADD_VAR, SUB_VAR, RESET_VAR, CLEAR_VARS, REFRESH, TAKE_ITEM, SET_SCORE, ADD_SCORE, SUB_SCORE, ACTION_BAR, - ADD_EFFECT, REMOVE_EFFECT, CLEAR_EFFECTS, NONE, ANVIL_INPUT; + ADD_EFFECT, REMOVE_EFFECT, CLEAR_EFFECTS, NONE, ANVIL_INPUT, RUN_FUNCTION; public static ActionType fromString(String s) { return switch (s.toLowerCase()) { @@ -80,6 +80,7 @@ public static ActionType fromString(String s) { case "clear_effects" -> CLEAR_EFFECTS; case "none" -> NONE; case "anvil_input" -> ANVIL_INPUT; + case "run_function" -> RUN_FUNCTION; default -> NONE; }; } @@ -220,6 +221,7 @@ public record Button( private final int tickRate; private final boolean closeOnMove; private final ContainerType containerType; + private final java.util.Map> macros; // ── Constructor ────────────────────────────────────────────────────────── @@ -230,7 +232,7 @@ public GuiDefinition(Identifier id, String title, int rows, Optional filler, int tickRate, boolean closeOnMove) { - this(id, title, rows, buttons, onOpen, onClose, filler, tickRate, closeOnMove, ContainerType.BARREL); + this(id, title, rows, buttons, onOpen, onClose, filler, tickRate, closeOnMove, ContainerType.BARREL, java.util.Map.of()); } public GuiDefinition(Identifier id, String title, int rows, @@ -241,6 +243,18 @@ public GuiDefinition(Identifier id, String title, int rows, int tickRate, boolean closeOnMove, ContainerType containerType) { + this(id, title, rows, buttons, onOpen, onClose, filler, tickRate, closeOnMove, containerType, java.util.Map.of()); + } + + public GuiDefinition(Identifier id, String title, int rows, + List