diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3a3b5a5..f477c4c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,12 +18,18 @@ jobs: with: distribution: temurin java-version: '25' - server-id: github-pinont # must match the of the GH Packages repo in pom.xml + cache: maven + # Must match pom.xml github-pinont + server-id: github-pinont + server-username: GITHUB_ACTOR + server-password: GITHUB_TOKEN settings-path: ${{ github.workspace }} - # EXPERIMENT: resolves SingularityLib 2.0.0-SNAPSHOT from GitHub Packages using - # this repo's own GITHUB_TOKEN — tests whether cross-repo same-user reads are - # allowed by default without a manual package-access grant. + # Resolves io.github.pinont:singularitylib:2.0.0-SNAPSHOT from + # https://maven.pkg.github.com/Pinont/SingularityLib using this + # workflow's GITHUB_TOKEN (packages:read). setup-java writes + # settings.xml with github-pinont and + # ${env.GITHUB_TOKEN}; pass it with -s so Maven actually uses it. - name: Build (full compile against lib snapshot) - run: mvn -B clean package + run: mvn -B -U clean package -s ${{ github.workspace }}/settings.xml env: GITHUB_TOKEN: ${{ github.token }} diff --git a/.gitignore b/.gitignore index 3b89bd3..884718b 100644 --- a/.gitignore +++ b/.gitignore @@ -96,6 +96,9 @@ $RECYCLE.BIN/ # Windows shortcuts *.lnk +# setup-java writes this into the workspace on CI (settings-path) +/settings.xml + target/ pom.xml.tag diff --git a/AGENTS.md b/AGENTS.md index 184a0e5..6983965 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,9 +19,9 @@ The owner's plan says to "rework DevTool from Kotlin back to Java." **Fact-check mvn clean package # shaded jar into target/ (JDK 25 required) ``` - Requires **JDK 25** (`java.version=25`), targets **Paper API `26.2.build.N-stable`**. -- Depends on `com.github.pinont:SingularityLib:2.0.0-SNAPSHOT` via the `${singularity.version}` property — pinned to the lib's v2 line. -- Resolves SingularityLib from the **public registry** (`https://raw.githubusercontent.com/Pinont/singularity-maven/gh-pages/`) — no auth required; Paper from `repo.papermc.io`. Source compiles against lib `2.0.0-SNAPSHOT` from this registry as of the v2 API migration (see below). -- **Bootstrap model:** SingularityLib is NOT bundled into this jar. Dependency scope is `provided`, and `maven-shade-plugin` `artifactSet` excludes `com.github.pinont:SingularityLib`. At runtime the lib ships as its own plugin on the server. +- Depends on `io.github.pinont:singularitylib:2.0.0-SNAPSHOT` via the `${singularity.version}` property — pinned to the lib's v2 line. +- Resolves SingularityLib from **GitHub Packages** (`https://maven.pkg.github.com/Pinont/SingularityLib`, repository id `github-pinont`). Auth is required even for the public package: CI uses `GITHUB_TOKEN` via `.github/workflows/build.yml` (`packages: read` + `mvn -s settings.xml`); locally put a PAT with `read:packages` in `~/.m2/settings.xml` under `github-pinont`. Paper API comes from `repo.papermc.io`. Do not resolve the lib from `maven.pinont.me` (stale Aug 28 SNAPSHOT). +- **Bootstrap model:** SingularityLib is NOT bundled into this jar. Dependency scope is `provided`, and `maven-shade-plugin` `artifactSet` excludes `io.github.pinont:singularitylib`. At runtime the lib ships as its own plugin on the server. ## Code layout Root package: `com.github.pinont.devtool` (26 files) @@ -29,10 +29,9 @@ Root package: `com.github.pinont.devtool` (26 files) - `DevTool.java` — entry point; extends `CorePlugin`, empty `onPluginStart/Stop` (everything registers via lib mechanisms) - `api/CItemManager.java` — wrapper over lib's CustomItemManager used by menus - `items/Tool.java` — the dev tool item players hold to open the tool -- `commands/` — `/devtool`, `/flyspeed`, `/vanish` (`SimpleCommand` style) -- `events/ChatEvent.java` — chat capture (used by world-name input flows) -- `methods/` — one-off helpers: `CreateWorld`, `ProperWorldName`, `SendChat`, `Blank`, world-creator UI builders, `WorldDeleteButton` -- `menu/DevToolMenu.java` + `menu/submenu/*` (11 menus) — the whole GUI surface: main menu, player management (server/specific player, kick/ban approvals), world management (server worlds, single world, creator, delete approval), custom items, other tools +- `commands/` — `/devtool`, `/flyspeed`, `/vanish` (`SimpleCommand` / `CommandGroup` style) +- `methods/` — one-off helpers: `CreateWorld`, `ProperWorldName`, `StartConversation`, `PromptWorldInput`, snippet export (`ItemSnippet`, `EntitySnippet`, `ExportSnippet`), world-creator UI builders, `WorldDeleteButton` +- `menu/DevToolMenu.java` + `menu/submenu/*` — the whole GUI surface: main menu, player management, world management, Item Studio / Entity Studio (snippet export), config editor, custom items, other tools Resource: `src/main/resources/paper-plugin.yml` — declares `name: SingularityDevTool`, `main: com.github.pinont.devtool.DevTool`, `api-version: '26.2'`, and `folia-supported: true`. @@ -53,9 +52,9 @@ Source migrated to the lib v2 signatures: ## Known issues / tech debt (observed) 1. ~~`paper-plugin.yml` name mismatch~~ — fixed on `rework/v2`: renamed to `SingularityDevTool`. 2. ~~Stale dependency version~~ — fixed on `rework/v2`: `${singularity.version}=2.0.0-SNAPSHOT` tracking the lib v2 line. -3. Chat-input flows (`ChatEvent` + `SendChat`) are fragile global state — replace with conversation API (Paper `ConversationFactory`) during rework. +3. ~~Chat-input flows (`ChatEvent` + `SendChat`) are fragile global state~~ — replaced with Paper `ConversationFactory` via `StartConversation` / `PromptWorldInput` (world name/border/seed and config-editor string keys). Type `cancel` to abort. 4. Menus rebuild state imperatively per open; no shared pagination abstraction yet (lib roadmap item). -5. No tests at all. +5. Test coverage: MockBukkit smoke + command-dispatch + registration tests exist under `src/test/java` (see `DevToolCommandTest`, `DevToolRegistrationTest`, `DevToolTest`). Snippet generation and conversation-start are covered without inventory-click simulation. ## Agent guidance - New features in Java, wired through SingularityLib APIs (`CorePlugin`, `SimpleCommand`, `Menu`) rather than raw Bukkit where possible. diff --git a/README.md b/README.md index 06ef7dc..9c276f5 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,49 @@ # Singularity DevTool Plugin -[![](https://jitpack.io/v/Pinont/Singularity-DevTool.svg)](https://jitpack.io/#Pinont/Singularity-DevTool) +[![](https://img.shields.io/github/license/Pinont/Singularity-DevTool)](https://github.com/Pinont/Singularity-DevTool/blob/main/LICENSE) A Minecraft plugin that introduces a better way to debug your plugins. +## Requirements +- **Paper 26.2+** or **Folia 26.x** (JDK 25) +- **SingularityLib** installed as a server plugin (bootstrap model — see below) + +## Build +```bash +mvn clean package # requires JDK 25 +``` +Place the jar from `target/` into your server's `plugins/` folder. + +## Dependencies +Built on **SingularityLib** via the bootstrap plugin model — the lib runs as its own +plugin; DevTool compiles against it with `provided` scope (never bundled): + +```xml + + io.github.pinont + singularitylib + 2.0.0 + provided + +``` + +```yaml +# paper-plugin.yml +dependencies: + server: + SingularityLib: + load: BEFORE + required: true + join-classpath: true +``` + +Get the latest lib release from +[central.sonatype.com/artifact/io.github.pinont/singularitylib](https://central.sonatype.com/artifact/io.github.pinont/singularitylib), +or dev snapshots from GitHub Packages +(`https://maven.pkg.github.com/Pinont/SingularityLib`, repo id `github-pinont`). + +## Development roadmap (v2) +DevTool v2 becomes an in-server IDE for Singularity plugins: auto-discovers all loaded +Singularity plugins, bridges their commands under `/devtool `, GUI config +editing with live reload, item/entity inspectors, builders that export code snippets, +and Paper conversations for world-name / config-string prompts (no global chat listener). +See the repo's AGENTS.md for the current state. \ No newline at end of file diff --git a/pom.xml b/pom.xml index f343e12..902f6a2 100644 --- a/pom.xml +++ b/pom.xml @@ -12,9 +12,8 @@ SingularityPlugin - + 2.0.0-SNAPSHOT 25 UTF-8 @@ -32,6 +31,11 @@ ${java.version} + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.2 + org.apache.maven.plugins maven-shade-plugin @@ -68,28 +72,21 @@ papermc-repo https://repo.papermc.io/repository/maven-public/ - - + - singularity - https://maven.pinont.me/ + github-pinont + https://maven.pkg.github.com/Pinont/SingularityLib + + true + always + true - @@ -107,5 +104,18 @@ classpath; never bundled (see shade excludes above). --> provided + + + org.mockbukkit.mockbukkit + mockbukkit-v26.2 + 4.116.1 + test + + + org.slf4j + slf4j-simple + 2.0.12 + test + diff --git a/src/main/java/com/github/pinont/devtool/DevTool.java b/src/main/java/com/github/pinont/devtool/DevTool.java index 63b04e7..a14e17a 100644 --- a/src/main/java/com/github/pinont/devtool/DevTool.java +++ b/src/main/java/com/github/pinont/devtool/DevTool.java @@ -1,11 +1,14 @@ package com.github.pinont.devtool; +import com.github.pinont.devtool.commands.DevToolCommand; import com.github.pinont.singularitylib.plugin.CorePlugin; -public final class DevTool extends CorePlugin { +public class DevTool extends CorePlugin { @Override public void onPluginStart() { + // Explicit registration (v2 DSL) — no @AutoRegister classpath scanning. + registerComponents(new DevToolCommand()); } @Override diff --git a/src/main/java/com/github/pinont/devtool/commands/DevTool.java b/src/main/java/com/github/pinont/devtool/commands/DevTool.java deleted file mode 100644 index dc2b527..0000000 --- a/src/main/java/com/github/pinont/devtool/commands/DevTool.java +++ /dev/null @@ -1,93 +0,0 @@ -package com.github.pinont.devtool.commands; - -import com.github.pinont.devtool.items.Tool; -import com.github.pinont.devtool.menu.DevToolMenu; -import com.github.pinont.singularitylib.api.annotation.AutoRegister; -import com.github.pinont.singularitylib.api.command.SimpleCommand; -import io.papermc.paper.command.brigadier.CommandSourceStack; -import org.bukkit.Bukkit; -import org.bukkit.ChatColor; -import org.bukkit.World; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.bukkit.inventory.ItemStack; -import org.jetbrains.annotations.NotNull; - -import java.util.Collection; -import java.util.List; - -@AutoRegister -public class DevTool implements SimpleCommand { - @Override - public void execute(@NotNull CommandSourceStack commandSourceStack, String @NotNull [] strings) { - if (commandSourceStack.getSender() instanceof Player player) { - switch (strings.length) { - case 0: { - DevToolMenu.openDevTool(player); - break; - } - case 1: { - ItemStack devToolItem = new Tool().getItem(); - if (strings[0].equalsIgnoreCase("get") || strings[0].equalsIgnoreCase("getItem")) { - player.getInventory().addItem(devToolItem); - } - break; - } - case 3: { - if (strings[0].equalsIgnoreCase("world")) { - if (strings[1].equalsIgnoreCase("teleport")) { - World world = Bukkit.getWorld(strings[2]); - if (world != null) { - player.teleport(world.getSpawnLocation()); - player.sendMessage(ChatColor.GRAY + "Teleporting to " + world.getName() + "..."); - } else { - player.sendMessage(ChatColor.RED + "World not found!"); - } - } - break; - } - } - } - return; - } - commandSourceStack.getSender().sendMessage("This command can only be executed by a player!"); - } - - @Override - public @NotNull Collection suggest(@NotNull CommandSourceStack commandSourceStack, String @NotNull [] args) { - switch (args.length) { - case 0, 1: { - return List.of("get", "getItem", "world"); - } - case 2: { - if (args[0].equalsIgnoreCase("world")) { - return List.of("teleport"); - } - break; - } - case 3: { - if (args[0].equalsIgnoreCase("world") && args[1].equalsIgnoreCase("teleport")) { - return List.of(Bukkit.getWorlds().stream().map(World::getName).toArray(String[]::new)); - } - break; - } - } - return List.of(); - } - - - @Override - public boolean canUse(@NotNull CommandSender sender) { - if (sender instanceof Player) { - return sender.hasPermission("pinont.devtool"); - } else { - return false; - } - } - - @Override - public String getName() { - return "devTool"; - } - -} diff --git a/src/main/java/com/github/pinont/devtool/commands/DevToolCommand.java b/src/main/java/com/github/pinont/devtool/commands/DevToolCommand.java new file mode 100644 index 0000000..f23eacf --- /dev/null +++ b/src/main/java/com/github/pinont/devtool/commands/DevToolCommand.java @@ -0,0 +1,267 @@ +package com.github.pinont.devtool.commands; + +import com.github.pinont.devtool.menu.DevToolMenu; +import com.github.pinont.devtool.menu.submenu.PluginPanelMenu; +import com.github.pinont.singularitylib.api.command.CommandGroup; +import com.github.pinont.singularitylib.api.command.SubCommand; +import com.github.pinont.singularitylib.api.registry.PluginRegistry; +import com.github.pinont.singularitylib.plugin.CorePlugin; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +/** + * DevTool v2 root command. + * + *

{@code /devtool} → opens the plugin auto-discovery menu. + * {@code /devtool } → opens that plugin's panel. + * {@code /devtool help} → lists discovered plugins + usage. + */ +public class DevToolCommand extends CommandGroup { + + public DevToolCommand() { + registerSubcommand(new HelpSub()); + registerSubcommand(new ItemStudioSub()); + registerSubcommand(new EntityStudioSub()); + registerSubcommand(new SnippetSub()); + registerSubcommand(new RecipeSub()); + registerSubcommand(new ConfigSub()); + registerSubcommand(new InspectSub()); + } + + @Override + public String getName() { + return "devtool:dt"; + } + + @Override + public String usage(Boolean bool) { + return "/devtool [plugin]"; + } + + @Override + public void execute(CommandSender sender, String[] args) { + if (args.length == 0) { + // Root: open the auto-discovery plugin menu + if (sender instanceof Player player) { + DevToolMenu.openDevTool(player); + } else { + sender.sendMessage(Component.text("DevTool: /devtool — players only for the GUI.", NamedTextColor.YELLOW)); + } + return; + } + + // First arg is a known subcommand (help)? Dispatch normally. + if (getSubcommandNames().contains(args[0].toLowerCase())) { + super.execute(sender, args); + return; + } + + // Otherwise treat first arg as a plugin name to open its panel. + CorePlugin plugin = PluginRegistry.get(args[0]); + if (plugin == null) { + sender.sendMessage(Component.text("Unknown Singularity plugin: " + args[0] + + ". Use /devtool help to list them.", NamedTextColor.RED)); + return; + } + if (sender instanceof Player player) { + PluginPanelMenu.open(player, plugin); + } else { + sender.sendMessage(Component.text(plugin.getName() + " v" + plugin.getPluginMeta().getVersion(), NamedTextColor.YELLOW)); + } + } + + /** + * /devtool itemstudio — open the Item Studio material picker. + */ + static class ItemStudioSub extends SubCommand { + + @Override + public String getName() { + return "itemstudio"; + } + + @Override + public String getDescription() { + return "Open the Item Studio (try-create tool)"; + } + + @Override + public void execute(CommandSender sender, String[] args) { + if (sender instanceof Player player) { + com.github.pinont.devtool.menu.submenu.ItemStudioMenu.open(player); + } else { + sender.sendMessage(Component.text("Players only.", NamedTextColor.RED)); + } + } + } + + /** + * /devtool entitystudio — open the Entity Studio type picker. + */ + static class EntityStudioSub extends SubCommand { + + @Override + public String getName() { + return "entitystudio"; + } + + @Override + public String getDescription() { + return "Open the Entity Studio (spawn-snippet tool)"; + } + + @Override + public void execute(CommandSender sender, String[] args) { + if (sender instanceof Player player) { + com.github.pinont.devtool.menu.submenu.EntityStudioMenu.open(player); + } else { + sender.sendMessage(Component.text("Players only.", NamedTextColor.RED)); + } + } + } + + /** + * /devtool snippet — export a Java snippet for the held item. + */ + static class SnippetSub extends SubCommand { + + @Override + public String getName() { + return "snippet"; + } + + @Override + public String getDescription() { + return "Export a Java snippet for the item in your hand"; + } + + @Override + public void execute(CommandSender sender, String[] args) { + if (sender instanceof Player player) { + com.github.pinont.devtool.methods.ExportSnippet.fromHeldItem(player); + } else { + sender.sendMessage(Component.text("Players only.", NamedTextColor.RED)); + } + } + } + + /** + * /devtool recipe — register a demo furnace recipe via RecipeRegistry. + */ + static class RecipeSub extends SubCommand { + + @Override + public String getName() { + return "recipe"; + } + + @Override + public String getDescription() { + return "Register a demo recipe (RecipeRegistry)"; + } + + @Override + public void execute(CommandSender sender, String[] args) { + com.github.pinont.singularitylib.api.recipes.RecipeRegistry.registerFurnace( + com.github.pinont.singularitylib.plugin.CorePlugin.getInstance(), + "devtool_iron", + new org.bukkit.inventory.ItemStack(org.bukkit.Material.IRON_INGOT), + new org.bukkit.inventory.ItemStack(org.bukkit.Material.DIAMOND), + 0.5f + ); + sender.sendMessage(Component.text("Registered recipe 'devtool_iron' (iron→diamond, furnace).", NamedTextColor.GREEN)); + } + } + + /** + * /devtool config [plugin] — live config editor for a discovered plugin. + */ + static class ConfigSub extends SubCommand { + + @Override + public String getName() { + return "config"; + } + + @Override + public String getDescription() { + return "Live-edit a plugin's config.yml"; + } + + @Override + public void execute(org.bukkit.command.CommandSender sender, String[] args) { + if (!(sender instanceof Player player)) { + sender.sendMessage(Component.text("Players only.", NamedTextColor.RED)); + return; + } + if (args.length < 1) { + player.sendMessage(Component.text("Usage: /devtool config ", NamedTextColor.YELLOW)); + return; + } + var plugin = PluginRegistry.get(args[0]); + if (plugin == null) { + player.sendMessage(Component.text("Unknown Singularity plugin: " + args[0], NamedTextColor.RED)); + return; + } + com.github.pinont.devtool.menu.submenu.ConfigEditorMenu.open(player, plugin); + } + } + + /** + * /devtool inspect [--entity] — vanilla NBT of held item or target entity. + */ + static class InspectSub extends SubCommand { + + @Override + public String getName() { + return "inspect"; + } + + @Override + public String getDescription() { + return "Inspect vanilla NBT (held item or --entity target)"; + } + + @Override + public void execute(org.bukkit.command.CommandSender sender, String[] args) { + if (!(sender instanceof Player player)) { + sender.sendMessage(Component.text("Players only.", NamedTextColor.RED)); + return; + } + boolean entity = args.length > 0 && args[0].equalsIgnoreCase("--entity"); + if (entity) { + com.github.pinont.devtool.tools.NbtInspector.inspectEntity(player); + } else { + com.github.pinont.devtool.tools.NbtInspector.inspectItem(player); + } + } + } + + /** + * /devtool help — lists discovered plugins + usage. + */ + static class HelpSub extends SubCommand { + + @Override + public String getName() { + return "help"; + } + + @Override + public String getDescription() { + return "List discovered plugins and usage"; + } + + @Override + public void execute(CommandSender sender, String[] args) { + sender.sendMessage(Component.text("—— Singularity plugins (" + PluginRegistry.count() + ") ——", NamedTextColor.GOLD)); + for (CorePlugin plugin : PluginRegistry.plugins()) { + sender.sendMessage(Component.text("• " + plugin.getName(), NamedTextColor.YELLOW) + .append(Component.text(" v" + plugin.getPluginMeta().getVersion(), NamedTextColor.GRAY))); + } + sender.sendMessage(Component.text("Usage: /devtool ", NamedTextColor.WHITE)); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/github/pinont/devtool/events/ChatEvent.java b/src/main/java/com/github/pinont/devtool/events/ChatEvent.java deleted file mode 100644 index 7c0530b..0000000 --- a/src/main/java/com/github/pinont/devtool/events/ChatEvent.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.github.pinont.devtool.events; - -import com.github.pinont.devtool.methods.SendChat; -import com.github.pinont.singularitylib.api.annotation.AutoRegister; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerChatEvent; - -@AutoRegister -public class ChatEvent implements Listener { - @EventHandler - public void sendChat(PlayerChatEvent event) { - SendChat.sendChat(event); - } -} diff --git a/src/main/java/com/github/pinont/devtool/menu/DevToolMenu.java b/src/main/java/com/github/pinont/devtool/menu/DevToolMenu.java index 1beab5e..107d68b 100644 --- a/src/main/java/com/github/pinont/devtool/menu/DevToolMenu.java +++ b/src/main/java/com/github/pinont/devtool/menu/DevToolMenu.java @@ -11,6 +11,8 @@ import com.github.pinont.singularitylib.api.ui.Layout; import com.github.pinont.singularitylib.api.ui.Menu; import com.github.pinont.singularitylib.plugin.CorePlugin; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.*; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; @@ -37,7 +39,15 @@ public static void openDevTool(Player player) { @Override public ItemStack getItem() { - return new ItemCreator(CorePlugin.getInstance(), new ItemStack(Material.GRASS_BLOCK)).setName(ChatColor.GREEN + "Server Info").addLore(ChatColor.GRAY + "Server: " + ChatColor.YELLOW + Bukkit.getServer().getName(), ChatColor.GRAY + "Version: " + ChatColor.YELLOW + Bukkit.getServer().getVersion(), ChatColor.GRAY + "Plugins (" + ChatColor.YELLOW + Bukkit.getServer().getPluginManager().getPlugins().length + ChatColor.GRAY + ")").create(); + // Components (not legacy ChatColor strings): ItemCreator's string + // setters run MiniMessage, which rejects legacy § codes. + return new ItemCreator(CorePlugin.getInstance(), new ItemStack(Material.GRASS_BLOCK)) + .setName(Component.text("Server Info", NamedTextColor.GREEN)) + .addLore( + Component.text("Server: ", NamedTextColor.GRAY).append(Component.text(Bukkit.getServer().getName(), NamedTextColor.YELLOW)), + Component.text("Version: ", NamedTextColor.GRAY).append(Component.text(Bukkit.getServer().getVersion(), NamedTextColor.YELLOW)), + Component.text("Plugins (", NamedTextColor.GRAY).append(Component.text(Bukkit.getServer().getPluginManager().getPlugins().length, NamedTextColor.YELLOW)).append(Component.text(")", NamedTextColor.GRAY)) + ).create(); } @Override @@ -48,7 +58,9 @@ public void onClick(Player player) { new Layout('p', new Button() { // player list @Override public ItemStack getItem() { - return new ItemHeadCreator(CorePlugin.getInstance(), new ItemStack(Material.PLAYER_HEAD)).setOwner(player.getName()).setName("Player List").create(); + return new ItemHeadCreator(CorePlugin.getInstance(), new ItemStack(Material.PLAYER_HEAD)) + .setOwner(player.getName()) + .setName(Component.text("Player List")).create(); } @Override @@ -59,7 +71,7 @@ public void onClick(Player player) { new Layout('w', new Button() { // worldcreator @Override public ItemStack getItem() { - return new ItemCreator(CorePlugin.getInstance(), new ItemStack(Material.COARSE_DIRT)).setName("Worlds").create(); + return new ItemCreator(CorePlugin.getInstance(), new ItemStack(Material.COARSE_DIRT)).setName(Component.text("Worlds")).create(); } @Override @@ -70,7 +82,9 @@ public void onClick(Player player) { new Layout('t', new Button() { // tools @Override public ItemStack getItem() { - return new ItemCreator(CorePlugin.getInstance(), Material.STICK).setName("Tools").addLore("More Tools").create(); + return new ItemCreator(CorePlugin.getInstance(), Material.STICK) + .setName(Component.text("Tools")) + .addLore(Component.text("More Tools")).create(); } @Override @@ -81,7 +95,9 @@ public void onClick(Player player) { new Layout('c', new Button() { // customItem @Override public ItemStack getItem() { - return new ItemCreator(CorePlugin.getInstance(), Material.CHEST).setName("Custom Items").addLore("Open Custom Item Creator").create(); + return new ItemCreator(CorePlugin.getInstance(), Material.CHEST) + .setName(Component.text("Custom Items")) + .addLore(Component.text("Open Custom Item Creator")).create(); } @Override diff --git a/src/main/java/com/github/pinont/devtool/menu/submenu/ConfigEditorMenu.java b/src/main/java/com/github/pinont/devtool/menu/submenu/ConfigEditorMenu.java new file mode 100644 index 0000000..0b2ee06 --- /dev/null +++ b/src/main/java/com/github/pinont/devtool/menu/submenu/ConfigEditorMenu.java @@ -0,0 +1,130 @@ +package com.github.pinont.devtool.menu.submenu; + +import com.github.pinont.devtool.methods.StartConversation; +import com.github.pinont.singularitylib.api.items.ItemCreator; +import com.github.pinont.singularitylib.api.manager.CommentConfigManager; +import com.github.pinont.singularitylib.api.ui.Button; +import com.github.pinont.singularitylib.api.ui.PaginatedMenu; +import com.github.pinont.singularitylib.api.ui.StandardButtons; +import com.github.pinont.singularitylib.plugin.CorePlugin; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import java.util.ArrayList; +import java.util.List; + +/** + * DevTool v2: LIVE config editor for a plugin's {@code config.yml}. + * + *

Renders every key as a paginated button: + *

+ * Save & Reload persists via {@link CommentConfigManager#save()} and triggers + * the plugin's {@code reloadReloadables()} hook; Revert closes without saving. + */ +public final class ConfigEditorMenu { + + private ConfigEditorMenu() { + } + + public static void open(Player player, CorePlugin plugin) { + new Editor(plugin, new CommentConfigManager(plugin, "config.yml")).show(player); + } + + private static int[] contentSlots() { + int[] s = new int[27]; + for (int i = 0; i < 27; i++) s[i] = 10 + (i / 7) * 9 + (i % 7); // 3x7 grid in the middle + return s; + } + + static final class Editor extends PaginatedMenu { + + private final CorePlugin target; + private final CommentConfigManager manager; + + Editor(CorePlugin target, CommentConfigManager manager) { + super(target, "Config: " + target.getName() + "/config.yml", 54, 27, + ConfigEditorMenu.contentSlots(), new ArrayList<>(), null, null); + this.target = target; + this.manager = manager; + } + + @Override + public void show(Player player) { + List keys = new ArrayList<>(manager.getConfig().getValues(false).keySet()); + updateItems(keys, this::renderKey, this::handleClick); + super.show(player); + } + + private void handleClick(Player player, String key) { + Object value = manager.get(key); + if (value instanceof Boolean b) { + manager.set(key, !b); + player.sendMessage(Component.text(key + " -> " + (!b), NamedTextColor.GREEN)); + show(player); + } else if (value instanceof Number n) { + manager.set(key, n.intValue() + (player.isSneaking() ? -1 : 1)); + player.sendMessage(Component.text(key + " -> " + manager.get(key), NamedTextColor.GREEN)); + show(player); + } else if (value instanceof String s) { + player.closeInventory(); + StartConversation.ask(player, + "Type the new value for '" + key + "' (current: '" + s + + "'), or 'cancel' to abort.", + input -> { + manager.set(key, input); + player.sendMessage(Component.text(key + " -> '" + input + + "' (use Save to persist).", NamedTextColor.GREEN)); + show(player); + }, + () -> { + player.sendMessage(Component.text("Edit cancelled.", NamedTextColor.GRAY)); + show(player); + }); + } else { + player.sendMessage(Component.text("Cannot edit key '" + key + "' (" + value + ")", NamedTextColor.RED)); + } + } + + private ItemStack renderKey(String key) { + Object v = manager.get(key); + Material mat = v instanceof Boolean ? (Boolean) v ? Material.GREEN_WOOL : Material.RED_WOOL + : v instanceof Number ? Material.REPEATER + : v instanceof String ? Material.PAPER + : Material.PAPER; + String val = v == null ? "null" : v.toString(); + String type = v instanceof Boolean ? "boolean" + : v instanceof Number ? "number" + : v instanceof String ? "string" + : v.getClass().getSimpleName(); + return new ItemCreator(target, mat) + .setName(Component.text(key, NamedTextColor.WHITE)) + .addLore(Component.text("Type: " + type, NamedTextColor.GRAY), + Component.text("Value: " + val, NamedTextColor.YELLOW), + Component.text("Click to edit", NamedTextColor.DARK_GRAY)) + .create(); + } + + @Override + protected void addExtraBottomRowInto(List