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
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,17 @@ base {

repositories {
maven { url = "https://maven.fabricmc.net/" }
maven { url = "https://api.modrinth.com/maven" }
}

dependencies {
minecraft "com.mojang:minecraft:1.21.1"
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 {
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
48 changes: 47 additions & 1 deletion src/main/java/dev/toolkitmc/guiapi/command/GuiCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
/**
* /guiapi open <namespace:id> [<targets>]
* /guiapi list
* /guiapi reload
* /guiapi help
*
* Permission level 2 required.
*/
Expand Down Expand Up @@ -58,9 +60,17 @@ public static void register(CommandDispatcher<ServerCommandSource> 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<ServerCommandSource> ctx,
Collection<ServerPlayerEntity> targets) {
Identifier id = IdentifierArgumentType.getIdentifier(ctx, "id");
Expand Down Expand Up @@ -90,8 +100,44 @@ private static int listGuis(CommandContext<ServerCommandSource> 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<ServerCommandSource> 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<ServerCommandSource> ctx) {
String help =
"[GuiAPI] Commands (permission level 2):\n" +
" /guiapi open <id> [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;
}
}
39 changes: 32 additions & 7 deletions src/main/java/dev/toolkitmc/guiapi/gui/BarrelGuiHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -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() + "]"
Expand All @@ -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;
}
Expand Down Expand Up @@ -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);

Expand Down
31 changes: 29 additions & 2 deletions src/main/java/dev/toolkitmc/guiapi/gui/GuiDefinition.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -112,6 +133,7 @@ public record Button(
String name,
List<String> lore,
boolean glint,
ClickType clickType,
Optional<ButtonCondition> condition,
List<ButtonAction> actions
) {}
Expand Down Expand Up @@ -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<String> lore = new ArrayList<>();
if (b.has("lore") && b.get("lore").isJsonArray()) {
for (JsonElement l : b.getAsJsonArray("lore"))
Expand Down Expand Up @@ -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) {
Expand All @@ -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<Button> getButtons() { return buttons; }

Expand Down
11 changes: 7 additions & 4 deletions src/main/java/dev/toolkitmc/guiapi/gui/GuiScreenHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,16 @@ public GuiScreenHandler(ScreenHandlerType<?> type, int syncId,

@Override
public void onSlotClick(int slotIndex, int button, SlotActionType actionType, PlayerEntity player) {
if (slotIndex >= 0 && slotIndex < definition.getRows() * 9) {
// `getRows()` is from GenericContainerScreenHandler — always matches
// the actual inventory size, regardless of what GuiDefinition.getRows() returns.
int guiSlotCount = getRows() * 9;
if (slotIndex >= 0 && slotIndex < guiSlotCount) {
if (player instanceof ServerPlayerEntity sp) {
BarrelGuiHandler.handleClick(sp, definition, page, slotIndex, actionType);
BarrelGuiHandler.handleClick(sp, definition, page, slotIndex, button, actionType);
}
return; // consume, don't call super
return; // consume; don't call super
}
// Block player inventory clicks too
// Block player-inventory clicks (slotIndex >= guiSlotCount) as well — no super call.
}

@Override
Expand Down
7 changes: 7 additions & 0 deletions src/main/java/dev/toolkitmc/guiapi/loader/GuiRegistry.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ protected Map<Identifier, GuiDefinition> 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: <ns>:gui/<name>.json
// Compute logical GUI id: <ns>:<name> (strip "gui/" prefix + ".json")
String path = fileId.getPath(); // "gui/my_gui.json"
Expand Down
89 changes: 89 additions & 0 deletions src/main/java/dev/toolkitmc/guiapi/modmenu/GuiApiModMenuEntry.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package dev.toolkitmc.guiapi.modmenu;

import com.terraformersmc.modmenu.api.ConfigScreenFactory;
import com.terraformersmc.modmenu.api.ModMenuApi;
import dev.toolkitmc.guiapi.loader.GuiRegistry;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.screen.multiplayer.AddServerScreen;
import net.minecraft.client.gui.widget.ButtonWidget;
import net.minecraft.client.gui.widget.TextWidget;
import net.minecraft.text.Text;

/**
* Mod Menu integration — shows a simple info screen listing loaded GUIs.
*
* This class is only loaded when Mod Menu is present (modCompileOnly dependency).
* The entrypoint declaration in fabric.mod.json is under "modmenu", which Fabric
* Loader only invokes if the modmenu mod is installed.
*/
public class GuiApiModMenuEntry implements ModMenuApi {

@Override
public ConfigScreenFactory<?> getModConfigScreenFactory() {
return parent -> new GuiApiInfoScreen(parent);
}

// ── Simple info screen ───────────────────────────────────────────────────

private static class GuiApiInfoScreen extends Screen {

private final Screen parent;

protected GuiApiInfoScreen(Screen parent) {
super(Text.literal("GUI API"));
this.parent = parent;
}

@Override
protected void init() {
// Title
addDrawableChild(new TextWidget(
width / 2 - 150, 20, 300, 10,
Text.literal("§6GUI API §7— Datapack-driven chest GUI system"),
textRenderer));

// Loaded GUI count
int count = GuiRegistry.INSTANCE.getAll().size();
addDrawableChild(new TextWidget(
width / 2 - 150, 40, 300, 10,
Text.literal("§7Loaded GUIs: §f" + count +
(count == 0 ? " §c(join a world to load datapacks)" : "")),
textRenderer));

// Loaded GUI list (up to 10)
int y = 60;
int shown = 0;
for (var entry : GuiRegistry.INSTANCE.getAll().entrySet()) {
if (shown >= 10) {
addDrawableChild(new TextWidget(
width / 2 - 150, y, 300, 10,
Text.literal("§8... and " + (count - 10) + " more"),
textRenderer));
break;
}
var def = entry.getValue();
addDrawableChild(new TextWidget(
width / 2 - 150, y, 300, 10,
Text.literal("§a" + entry.getKey() +
" §8[rows=" + def.getRows() +
", pages=" + def.getPageCount() + "]"),
textRenderer));
y += 12;
shown++;
}

// Close button
addDrawableChild(ButtonWidget.builder(
Text.literal("Close"),
btn -> MinecraftClient.getInstance().setScreen(parent))
.dimensions(width / 2 - 50, height - 30, 100, 20)
.build());
}

@Override
public void close() {
MinecraftClient.getInstance().setScreen(parent);
}
}
}
8 changes: 7 additions & 1 deletion src/main/resources/fabric.mod.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,18 @@
"entrypoints": {
"main": [
"dev.toolkitmc.guiapi.GuiApiMod"
],
"modmenu": [
"dev.toolkitmc.guiapi.modmenu.GuiApiModMenuEntry"
]
},
"depends": {
"fabricloader": ">=0.16.5",
"fabric-api": "*",
"minecraft": "~1.21.1",
"java": ">=21"
},
"suggests": {
"modmenu": "*"
}
}
}