From f2dc75d6c8be42529652b90ff9969cf0265eb7c0 Mon Sep 17 00:00:00 2001 From: Darto KLoning Date: Sat, 3 Oct 2026 23:50:40 +0700 Subject: [PATCH] config gui --- README.md | 15 ++ gradle.properties | 2 +- .../lobbysystems/AuthManager.java | 11 + .../dartokloning/lobbysystems/ConfigGui.java | 225 ++++++++++++++++++ .../lobbysystems/LobbySystemsMod.java | 3 + 5 files changed, 255 insertions(+), 1 deletion(-) create mode 100644 src/main/java/id/dartokloning/lobbysystems/ConfigGui.java diff --git a/README.md b/README.md index 849e894..d2c2539 100644 --- a/README.md +++ b/README.md @@ -257,3 +257,18 @@ Local/cracked identity tidak mempunyai Mojang profile public key. Dengan `enforc ## License All Rights Reserved. Source dan asset Lobby Systems tidak boleh didistribusikan atau digunakan ulang di luar izin pemilik project. + +## v1.10.0 - Configuration GUI + +Operator-only `/ls config` opens a vanilla 6-row chest menu named **Lobby Systems Configuration**. No client-side mod is required for the menu itself. + +The first GUI release focuses on settings that can be changed safely with inventory clicks: + +- **Lobby**: minimum play time, portal check interval, portal cooldown. +- **Authentication**: enabled, premium auto-login, premium username protection, require-login-every-join, authentication timeout, max attempts, minimum password length, PBKDF2 iterations. +- **Server Rules**: enabled, rules version, rules timeout. +- **Dimensions**, **Starter Kits**, and **Portals** are represented in the root menu and direct administrators to their existing `/ls` management commands. This deliberately avoids turning structured lists and world-specific records into unsafe one-click edits. + +Controls: left click increases numeric values, right click decreases them; boolean entries toggle on click. Changes are saved immediately. Auth/Rules changes are also applied to the current runtime state, so a separate `/ls reload` is not required for GUI edits. Manual JSON edits still use `/ls reload`. + +The configuration GUI edits configuration only. `player.json` and `starterkit/recipients.json` remain persistent runtime databases and are not exposed as raw editable GUI data. diff --git a/gradle.properties b/gradle.properties index c1ac1c2..b62a709 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,6 +5,6 @@ minecraft_version=26.2 loader_version=0.19.3 loom_version=1.17-SNAPSHOT fabric_api_version=0.156.0+26.2 -mod_version=1.9.0 +mod_version=1.10.1 maven_group=id.dartokloning archives_base_name=lobby-systems diff --git a/src/main/java/id/dartokloning/lobbysystems/AuthManager.java b/src/main/java/id/dartokloning/lobbysystems/AuthManager.java index 606bd0a..7594264 100644 --- a/src/main/java/id/dartokloning/lobbysystems/AuthManager.java +++ b/src/main/java/id/dartokloning/lobbysystems/AuthManager.java @@ -76,6 +76,17 @@ public final class AuthManager { } public AuthConfig config() { return config; } + public RulesConfig rules() { return rules; } + + public void saveConfiguration() { + try { + Files.createDirectories(ConfigPaths.AUTH_DIRECTORY); + saveConfig(); + saveRules(); + } catch (Exception e) { + LobbySystemsMod.LOGGER.error("Failed to save Lobby Systems Auth configuration", e); + } + } /** Re-applies the freshly loaded auth configuration to players already online. */ public void applyReload(MinecraftServer server) { diff --git a/src/main/java/id/dartokloning/lobbysystems/ConfigGui.java b/src/main/java/id/dartokloning/lobbysystems/ConfigGui.java new file mode 100644 index 0000000..74502b2 --- /dev/null +++ b/src/main/java/id/dartokloning/lobbysystems/ConfigGui.java @@ -0,0 +1,225 @@ +package id.dartokloning.lobbysystems; + +import net.minecraft.ChatFormatting; +import net.minecraft.core.component.DataComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.ChestMenu; +import net.minecraft.world.inventory.ContainerInput; +import net.minecraft.world.inventory.MenuType; +import net.minecraft.world.SimpleMenuProvider; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; + +/** Server-side vanilla chest GUI for Lobby Systems administration. */ +public final class ConfigGui { + private static final int SIZE = 54; + private final ConfigManager configManager; + private final AuthManager authManager; + + public ConfigGui(ConfigManager configManager, AuthManager authManager) { + this.configManager = configManager; + this.authManager = authManager; + } + + public void open(ServerPlayer player) { open(player, Page.ROOT); } + + private void open(ServerPlayer player, Page page) { + Component title = Component.literal(switch (page) { + case ROOT -> "Lobby Systems Configuration"; + case AUTH -> "Authentication Configuration"; + case RULES -> "Server Rules Configuration"; + case LOBBY -> "Lobby Configuration"; + }); + player.openMenu(new SimpleMenuProvider( + (id, inventory, ignored) -> new GuiMenu(id, inventory, player, page), title)); + } + + private final class GuiMenu extends ChestMenu { + private final ServerPlayer viewer; + private final Page page; + private final SimpleContainer gui; + + GuiMenu(int id, Inventory inventory, ServerPlayer viewer, Page page) { + this(id, inventory, viewer, page, new SimpleContainer(SIZE)); + } + + private GuiMenu(int id, Inventory inventory, ServerPlayer viewer, Page page, SimpleContainer gui) { + super(MenuType.GENERIC_9x6, id, inventory, gui, 6); + this.viewer = viewer; + this.page = page; + this.gui = gui; + render(); + } + + @Override + public void clicked(int slotIndex, int buttonNum, ContainerInput input, Player player) { + if (slotIndex < 0 || slotIndex >= SIZE) return; + if (input != ContainerInput.PICKUP && input != ContainerInput.QUICK_MOVE) return; + handle(slotIndex, buttonNum); + } + + @Override + public ItemStack quickMoveStack(Player player, int slotIndex) { return ItemStack.EMPTY; } + + @Override + public boolean stillValid(Player player) { return true; } + + private void handle(int slot, int button) { + boolean right = button == 1; + switch (page) { + case ROOT -> { + if (slot == 20) open(viewer, Page.LOBBY); + else if (slot == 22) open(viewer, Page.AUTH); + else if (slot == 24) open(viewer, Page.RULES); + else if (slot == 38) viewer.sendSystemMessage(Component.literal("Dimension settings are managed by /ls gamemode and /ls gamerule.").withStyle(ChatFormatting.YELLOW)); + else if (slot == 40) viewer.sendSystemMessage(Component.literal("Starter Kits are managed by /ls starterkit commands.").withStyle(ChatFormatting.YELLOW)); + else if (slot == 42) viewer.sendSystemMessage(Component.literal("Portals are managed by /ls portal commands.").withStyle(ChatFormatting.YELLOW)); + else if (slot == 49) viewer.closeContainer(); + } + case AUTH -> handleAuth(slot, right); + case RULES -> handleRules(slot, right); + case LOBBY -> handleLobby(slot, right); + } + } + + private void handleAuth(int slot, boolean right) { + AuthConfig c = authManager.config(); + boolean changed = true; + switch (slot) { + case 10 -> c.enabled = !c.enabled; + case 12 -> c.premiumAutoLogin = !c.premiumAutoLogin; + case 14 -> c.protectPremiumUsernames = !c.protectPremiumUsernames; + case 16 -> c.requireLoginEveryJoin = !c.requireLoginEveryJoin; + case 28 -> c.authenticationTimeoutSeconds = adjust(c.authenticationTimeoutSeconds, right ? -30 : 30, 30, 3600); + case 30 -> c.maxLoginAttempts = adjust(c.maxLoginAttempts, right ? -1 : 1, 1, 20); + case 32 -> c.minimumPasswordLength = adjust(c.minimumPasswordLength, right ? -1 : 1, 4, 128); + case 34 -> c.pbkdf2Iterations = adjust(c.pbkdf2Iterations, right ? -10000 : 10000, 50000, 2000000); + case 45 -> { open(viewer, Page.ROOT); return; } + case 49 -> { viewer.closeContainer(); return; } + default -> changed = false; + } + if (changed) { + authManager.saveConfiguration(); + authManager.applyReload(viewer.level().getServer()); + render(); + } + } + + private void handleRules(int slot, boolean right) { + RulesConfig c = authManager.rules(); + boolean changed = true; + switch (slot) { + case 11 -> c.enabled = !c.enabled; + case 13 -> c.version = adjust(c.version, right ? -1 : 1, 1, Integer.MAX_VALUE); + case 15 -> c.timeoutSeconds = adjust(c.timeoutSeconds, right ? -15 : 15, 15, 3600); + case 45 -> { open(viewer, Page.ROOT); return; } + case 49 -> { viewer.closeContainer(); return; } + default -> changed = false; + } + if (changed) { + authManager.saveConfiguration(); + authManager.applyReload(viewer.level().getServer()); + render(); + } + } + + private void handleLobby(int slot, boolean right) { + LobbyConfig c = configManager.get(); + boolean changed = true; + switch (slot) { + case 20 -> c.minimumPlaySeconds = adjust(c.minimumPlaySeconds, right ? -30 : 30, 0, 86400); + case 22 -> c.portalCheckIntervalTicks = adjust(c.portalCheckIntervalTicks, right ? -1 : 1, 1, 200); + case 24 -> c.portalCooldownTicks = adjust(c.portalCooldownTicks, right ? -10 : 10, 1, 1200); + case 45 -> { open(viewer, Page.ROOT); return; } + case 49 -> { viewer.closeContainer(); return; } + default -> changed = false; + } + if (changed) { configManager.save(); render(); } + } + + private void render() { + gui.clearContent(); + switch (page) { + case ROOT -> renderRoot(); + case AUTH -> renderAuth(); + case RULES -> renderRules(); + case LOBBY -> renderLobby(); + } + broadcastFullState(); + } + + private void renderRoot() { + set(20, Items.COMPASS, "Lobby", ChatFormatting.AQUA); + set(22, Items.IRON_DOOR, "Authentication", ChatFormatting.GREEN); + set(24, Items.WRITABLE_BOOK, "Server Rules", ChatFormatting.YELLOW); + set(38, Items.GRASS_BLOCK, "Dimensions", ChatFormatting.GREEN); + set(40, Items.CHEST, "Starter Kits", ChatFormatting.GOLD); + set(42, Items.ENDER_PEARL, "Portals", ChatFormatting.LIGHT_PURPLE); + set(49, Items.BARRIER, "Close", ChatFormatting.RED); + } + + private void renderAuth() { + AuthConfig c = authManager.config(); + toggle(10, Items.LEVER, "Auth Enabled", c.enabled); + toggle(12, Items.GOLD_INGOT, "Premium Auto Login", c.premiumAutoLogin); + toggle(14, Items.SHIELD, "Protect Premium Usernames", c.protectPremiumUsernames); + toggle(16, Items.NAME_TAG, "Require Login Every Join", c.requireLoginEveryJoin); + number(28, Items.CLOCK, "Authentication Timeout", c.authenticationTimeoutSeconds, "seconds", 30); + number(30, Items.REDSTONE, "Maximum Login Attempts", c.maxLoginAttempts, "attempts", 1); + number(32, Items.PAPER, "Minimum Password Length", c.minimumPasswordLength, "characters", 1); + number(34, Items.REPEATER, "PBKDF2 Iterations", c.pbkdf2Iterations, "iterations", 10000); + navigation(); + } + + private void renderRules() { + RulesConfig c = authManager.rules(); + toggle(11, Items.LEVER, "Rules Enabled", c.enabled); + number(13, Items.BOOK, "Rules Version", c.version, "", 1); + number(15, Items.CLOCK, "Rules Timeout", c.timeoutSeconds, "seconds", 15); + set(31, Items.WRITABLE_BOOK, "Rule text: edit auth/rules.json", ChatFormatting.GRAY); + navigation(); + } + + private void renderLobby() { + LobbyConfig c = configManager.get(); + set(13, Items.COMPASS, "Lobby Dimension: " + c.lobbyDimension, ChatFormatting.AQUA); + number(20, Items.CLOCK, "Minimum Play Time", c.minimumPlaySeconds, "seconds", 30); + number(22, Items.ENDER_EYE, "Portal Check Interval", c.portalCheckIntervalTicks, "ticks", 1); + number(24, Items.RECOVERY_COMPASS, "Portal Cooldown", c.portalCooldownTicks, "ticks", 10); + navigation(); + } + + private void navigation() { + set(45, Items.ARROW, "Back", ChatFormatting.YELLOW); + set(49, Items.BARRIER, "Close", ChatFormatting.RED); + } + + private void toggle(int slot, Item item, String label, boolean value) { + set(slot, item, label + ": " + (value ? "ON" : "OFF"), value ? ChatFormatting.GREEN : ChatFormatting.RED); + } + + private void number(int slot, Item item, String label, int value, String unit, int step) { + String suffix = unit.isBlank() ? "" : " " + unit; + set(slot, item, label + ": " + value + suffix + " (L +" + step + " / R -" + step + ")", ChatFormatting.YELLOW); + } + + private void set(int slot, Item item, String name, ChatFormatting color) { + ItemStack stack = new ItemStack(item); + stack.set(DataComponents.CUSTOM_NAME, Component.literal(name).withStyle(color)); + gui.setItem(slot, stack); + } + } + + private static int adjust(int value, int delta, int min, int max) { + long n = (long) value + delta; + return (int) Math.max(min, Math.min(max, n)); + } + + private enum Page { ROOT, AUTH, RULES, LOBBY } +} diff --git a/src/main/java/id/dartokloning/lobbysystems/LobbySystemsMod.java b/src/main/java/id/dartokloning/lobbysystems/LobbySystemsMod.java index 1e0538f..8de28ce 100644 --- a/src/main/java/id/dartokloning/lobbysystems/LobbySystemsMod.java +++ b/src/main/java/id/dartokloning/lobbysystems/LobbySystemsMod.java @@ -48,6 +48,7 @@ public final class LobbySystemsMod implements ModInitializer { private static final DimensionGameRuleManager DIMENSION_GAMERULES = new DimensionGameRuleManager(); private static final StarterKitManager STARTER_KITS = new StarterKitManager(); private static final AuthManager AUTH = new AuthManager(); + private static final ConfigGui CONFIG_GUI = new ConfigGui(CONFIG, AUTH); private static int saveTicker; private static final java.util.Set LOCAL_LOGIN_NAMES = java.util.concurrent.ConcurrentHashMap.newKeySet(); @@ -271,6 +272,8 @@ public final class LobbySystemsMod implements ModInitializer { dispatcher.register(Commands.literal("ls") .requires(Commands.hasPermission(Commands.LEVEL_ADMINS)) + .then(Commands.literal("config") + .executes(ctx -> { CONFIG_GUI.open(ctx.getSource().getPlayerOrException()); return 1; })) .then(Commands.literal("reload") .executes(ctx -> reloadAll(ctx.getSource().getServer(), ctx.getSource()))) .then(Commands.literal("tp")