Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ rad.json
TestResults.xml

# Gradle
.gradle/
.gradle/
build/
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

Expand Down Expand Up @@ -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**.
Expand Down
70 changes: 70 additions & 0 deletions example-datapack/data/example/gui/macro_and_input_demo.json
Original file line number Diff line number Diff line change
@@ -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" }
]
}
]
}
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
13 changes: 11 additions & 2 deletions src/main/java/dev/toolkitmc/guiapi/command/GuiCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -189,15 +189,24 @@ private static int showHelp(CommandContext<ServerCommandSource> 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:<macro_name>\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" +
" click_type: any | left | right | shift\n" +
" 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;
}
Expand Down
64 changes: 32 additions & 32 deletions src/main/java/dev/toolkitmc/guiapi/gui/AnvilGuiHandler.java
Original file line number Diff line number Diff line change
@@ -1,65 +1,66 @@
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;
import net.minecraft.item.Items;
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 {

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) {
return true;
}

@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);
}
Expand All @@ -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;
Expand Down
42 changes: 30 additions & 12 deletions src/main/java/dev/toolkitmc/guiapi/gui/BarrelGuiHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand All @@ -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());
}

/**
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<GuiDefinition.ButtonAction> macroActions = def.getMacros().get(macroName);
if (macroActions != null && !macroActions.isEmpty()) {
executeDelayedActionChain(player, def, currentPage, macroActions, 0, false);
}
}
case CLOSE -> {
Expand Down
Loading