From 7290aca2269ed1e394cd7469fdab8386e69b6759 Mon Sep 17 00:00:00 2001 From: Pinont Date: Fri, 28 Aug 2026 17:52:36 +0000 Subject: [PATCH 01/13] docs: Central-first, io.github.pinont coords, bootstrap model --- README.md | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 06ef7dc..186a15e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,47 @@ # 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 https://maven.pinont.me. + +## 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, and builders that export code snippets. +See the repo's AGENTS.md for the current state. \ No newline at end of file From fbe6bae3cf895f3e8372ddee219b8d4c366ae0da Mon Sep 17 00:00:00 2001 From: Pinont Date: Fri, 28 Aug 2026 17:53:07 +0000 Subject: [PATCH 02/13] docs: remove stale jitpack comment + fix AGENTS.md coords --- AGENTS.md | 2 +- pom.xml | 12 ++---------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 184a0e5..5102c1d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,7 +19,7 @@ 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. +- 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 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. diff --git a/pom.xml b/pom.xml index f343e12..1d71aa3 100644 --- a/pom.xml +++ b/pom.xml @@ -80,16 +80,8 @@ true - + From 216c040a3bb0f40851caae384d83cdac8335b404 Mon Sep 17 00:00:00 2001 From: Pinont Date: Fri, 28 Aug 2026 17:58:09 +0000 Subject: [PATCH 03/13] test: add MockBukkit smoke tests + un-final main class for testability --- pom.xml | 13 +++++ .../com/github/pinont/devtool/DevTool.java | 2 +- .../github/pinont/devtool/DevToolTest.java | 49 +++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/github/pinont/devtool/DevToolTest.java diff --git a/pom.xml b/pom.xml index 1d71aa3..e423e93 100644 --- a/pom.xml +++ b/pom.xml @@ -99,5 +99,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..a046265 100644 --- a/src/main/java/com/github/pinont/devtool/DevTool.java +++ b/src/main/java/com/github/pinont/devtool/DevTool.java @@ -2,7 +2,7 @@ import com.github.pinont.singularitylib.plugin.CorePlugin; -public final class DevTool extends CorePlugin { +public class DevTool extends CorePlugin { @Override public void onPluginStart() { diff --git a/src/test/java/com/github/pinont/devtool/DevToolTest.java b/src/test/java/com/github/pinont/devtool/DevToolTest.java new file mode 100644 index 0000000..d6afd78 --- /dev/null +++ b/src/test/java/com/github/pinont/devtool/DevToolTest.java @@ -0,0 +1,49 @@ +package com.github.pinont.devtool; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.MockBukkit; + +/** + * Smoke tests: DevTool boots correctly under a mocked Paper 26.2 server + * (bootstrap model — depends on SingularityLib for its plugin base class). + */ +public class DevToolTest { + + private DevTool plugin; + + @BeforeEach + public void setUp() { + MockBukkit.mock(); + this.plugin = MockBukkit.load(DevTool.class); + } + + @AfterEach + public void tearDown() { + MockBukkit.unmock(); + } + + @Test + @DisplayName("DevTool loads and is enabled") + public void loads() { + Assertions.assertNotNull(plugin, "plugin should load under MockBukkit"); + Assertions.assertTrue(plugin.isEnabled(), "plugin should be enabled"); + } + + @Test + @DisplayName("DevTool's plugin name is correct") + public void name() { + Assertions.assertEquals("SingularityDevTool", plugin.getName()); + } + + @Test + @DisplayName("DevTool extends CorePlugin (bootstrap model)") + public void extendsCorePlugin() { + Assertions.assertTrue( + com.github.pinont.singularitylib.plugin.CorePlugin.class.isAssignableFrom(plugin.getClass()), + "DevTool should be a CorePlugin"); + } +} \ No newline at end of file From 5a9417e4d889fcb88751dafc1e2b9588ad72cc42 Mon Sep 17 00:00:00 2001 From: Pinont Date: Fri, 28 Aug 2026 18:01:36 +0000 Subject: [PATCH 04/13] ci: force snapshot re-resolution (-U) until pages-repo auto-publish is wired --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3a3b5a5..5fd06fd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,6 +24,6 @@ jobs: # this repo's own GITHUB_TOKEN — tests whether cross-repo same-user reads are # allowed by default without a manual package-access grant. - name: Build (full compile against lib snapshot) - run: mvn -B clean package + run: mvn -B -U clean package env: GITHUB_TOKEN: ${{ github.token }} From 9aeeb8e5fbf682d84c1f82c7ea835b88a4266b50 Mon Sep 17 00:00:00 2001 From: Pinont Date: Fri, 28 Aug 2026 19:17:23 +0000 Subject: [PATCH 05/13] =?UTF-8?q?feat(v2):=20DevTool=20command=20bridge=20?= =?UTF-8?q?=E2=80=94=20plugin=20auto-discovery=20panel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DevToolCommand extends lib CommandGroup: /devtool [plugin] | help - /devtool opens DevToolMenu; /devtool opens PluginPanelMenu (auto-discovered via PluginRegistry); unknown plugin → error/help - PluginPanelMenu: PaginatedMenu showing plugin name/version/class/status, component slots reserved for the lib component registry - DevTool.java now uses explicit registerComponents() DSL (no @AutoRegister scan) - old monolithic commands/DevTool.java removed (superseded) --- .../com/github/pinont/devtool/DevTool.java | 5 +- .../pinont/devtool/commands/DevTool.java | 93 ------------------ .../devtool/commands/DevToolCommand.java | 94 +++++++++++++++++++ .../devtool/menu/submenu/PluginPanelMenu.java | 69 ++++++++++++++ 4 files changed, 167 insertions(+), 94 deletions(-) delete mode 100644 src/main/java/com/github/pinont/devtool/commands/DevTool.java create mode 100644 src/main/java/com/github/pinont/devtool/commands/DevToolCommand.java create mode 100644 src/main/java/com/github/pinont/devtool/menu/submenu/PluginPanelMenu.java diff --git a/src/main/java/com/github/pinont/devtool/DevTool.java b/src/main/java/com/github/pinont/devtool/DevTool.java index a046265..4c06146 100644 --- a/src/main/java/com/github/pinont/devtool/DevTool.java +++ b/src/main/java/com/github/pinont/devtool/DevTool.java @@ -1,14 +1,17 @@ package com.github.pinont.devtool; +import com.github.pinont.devtool.commands.DevToolCommand; import com.github.pinont.singularitylib.plugin.CorePlugin; public class DevTool extends CorePlugin { @Override public void onPluginStart() { + // Explicit registration (v2 DSL) — no @AutoRegister classpath scanning. + registerComponents(new DevToolCommand()); } @Override public void onPluginStop() { } -} +} \ No newline at end of file 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..66627f2 --- /dev/null +++ b/src/main/java/com/github/pinont/devtool/commands/DevToolCommand.java @@ -0,0 +1,94 @@ +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()); + } + + @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 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/menu/submenu/PluginPanelMenu.java b/src/main/java/com/github/pinont/devtool/menu/submenu/PluginPanelMenu.java new file mode 100644 index 0000000..58ef55a --- /dev/null +++ b/src/main/java/com/github/pinont/devtool/menu/submenu/PluginPanelMenu.java @@ -0,0 +1,69 @@ +package com.github.pinont.devtool.menu.submenu; + +import com.github.pinont.singularitylib.api.items.ItemCreator; +import com.github.pinont.singularitylib.api.ui.Menu; +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: panel for a single auto-discovered Singularity plugin. + * Uses the lib's PaginatedMenu to show plugin info + a placeholder + * component list (commands/listeners/items come from the lib registry in a + * later iteration — slot is reserved). + */ +public final class PluginPanelMenu { + + private PluginPanelMenu() { + } + + public static void open(Player player, CorePlugin plugin) { + List rows = new ArrayList<>(); + rows.add("Name: " + plugin.getName()); + rows.add("Version: " + plugin.getPluginMeta().getVersion()); + rows.add("Class: " + plugin.getClass().getSimpleName()); + rows.add("Status: enabled=" + plugin.isEnabled()); + rows.add("—"); + rows.add("Components (coming with lib registry v2):"); + rows.add(" commands, listeners, custom items, scheduled tasks"); + + PluginPanel panel = new PluginPanel(plugin, rows); + panel.show(player); + } + + /** + * Concrete PaginatedMenu over the info rows. + */ + private static final class PluginPanel extends PaginatedMenu { + + PluginPanel(CorePlugin plugin, List rows) { + super( + com.github.pinont.singularitylib.plugin.CorePlugin.getInstance(), + "Plugin: " + plugin.getName(), + 27, // 3 rows + 21 - 9 + 7, // room for nav row; 7 content slots + new int[]{10, 11, 12, 13, 14, 15, 16}, + rows, + PluginPanel::renderRow, + (p, row) -> { + } + ); + addButton(StandardButtons.close(com.github.pinont.singularitylib.plugin.CorePlugin.getInstance())); + } + + private static ItemStack renderRow(String row) { + return new ItemCreator( + com.github.pinont.singularitylib.plugin.CorePlugin.getInstance(), + Material.PAPER + ).setName(Component.text(row, NamedTextColor.WHITE)).create(); + } + } +} \ No newline at end of file From b06ae50669c6e8cfbab2f7cb11c1bb61535217f9 Mon Sep 17 00:00:00 2001 From: Pinont Date: Fri, 28 Aug 2026 19:49:13 +0000 Subject: [PATCH 06/13] =?UTF-8?q?feat(v2):=20Item=20Studio=20+=20recipe=20?= =?UTF-8?q?demo=20=E2=80=94=20builders=20wired=20to=20lib=20toolkit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /devtool itemstudio: PaginatedMenu over every item material; picking one gives a creator-built item with Attributes applied (try-create) - /devtool recipe : registers a furnace recipe via RecipeRegistry - demonstrates lib PaginatedMenu + Attributes + RecipeRegistry integration --- .../devtool/commands/DevToolCommand.java | 55 +++++++++++ .../devtool/menu/submenu/ItemStudioMenu.java | 97 +++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 src/main/java/com/github/pinont/devtool/menu/submenu/ItemStudioMenu.java diff --git a/src/main/java/com/github/pinont/devtool/commands/DevToolCommand.java b/src/main/java/com/github/pinont/devtool/commands/DevToolCommand.java index 66627f2..145f43b 100644 --- a/src/main/java/com/github/pinont/devtool/commands/DevToolCommand.java +++ b/src/main/java/com/github/pinont/devtool/commands/DevToolCommand.java @@ -22,6 +22,8 @@ public class DevToolCommand extends CommandGroup { public DevToolCommand() { registerSubcommand(new HelpSub()); + registerSubcommand(new ItemStudioSub()); + registerSubcommand(new RecipeSub()); } @Override @@ -66,6 +68,59 @@ public void execute(CommandSender sender, String[] args) { } } + /** + * /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 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 help — lists discovered plugins + usage. */ diff --git a/src/main/java/com/github/pinont/devtool/menu/submenu/ItemStudioMenu.java b/src/main/java/com/github/pinont/devtool/menu/submenu/ItemStudioMenu.java new file mode 100644 index 0000000..c11dd8a --- /dev/null +++ b/src/main/java/com/github/pinont/devtool/menu/submenu/ItemStudioMenu.java @@ -0,0 +1,97 @@ +package com.github.pinont.devtool.menu.submenu; + +import com.github.pinont.singularitylib.api.items.Attributes; +import com.github.pinont.singularitylib.api.items.ItemCreator; +import com.github.pinont.singularitylib.api.ui.Button; +import com.github.pinont.singularitylib.api.ui.Menu; +import com.github.pinont.singularitylib.api.ui.PaginatedMenu; +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.enchantments.Enchantment; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiConsumer; + +/** + * DevTool v2: Item Studio — a paginated material picker that lets a dev "try + * create stuff": pick a material, get a pre-built item with the lib's ItemCreator + * + Attributes, and copy the recipe to… (export snippet comes in a later pass). + * + *

Demonstrates the lib's new PaginatedMenu + Attributes + ItemCreator toolkit. + */ +public final class ItemStudioMenu { + + private ItemStudioMenu() { + } + + public static void open(Player player) { + List materials = new ArrayList<>(); + for (Material m : Material.values()) { + if (m.isItem() && !m.isAir()) { + materials.add(m); + } + } + new StudioMenu(materials).show(player); + } + + private static final class StudioMenu extends PaginatedMenu { + + StudioMenu(List materials) { + super(CorePlugin.getInstance(), + "Item Studio (pick a material)", + 54, + 36, + contentSlots(), + materials, + ItemStudioMenu::renderMaterial, + ItemStudioMenu::onPick + ); + // bottom-right close + addButton(new Button() { + @Override public int getSlot() { return 53; } + @Override public ItemStack getItem() { + return new ItemCreator(CorePlugin.getInstance(), Material.BARRIER) + .setName(Component.text("Close", NamedTextColor.RED)).create(); + } + @Override public void onClick(Player p) { p.closeInventory(); } + }); + } + + private static int[] contentSlots() { + int[] s = new int[36]; + for (int i = 0; i < 36; i++) s[i] = 9 + i; + return s; + } + } + + private static ItemStack renderMaterial(Material m) { + return new ItemCreator(CorePlugin.getInstance(), m) + .setName(Component.text(format(m), NamedTextColor.WHITE)) + .create(); + } + + private static void onPick(Player player, Material m) { + // "try create stuff": give a starter item with a couple of attributes + ItemStack base = new ItemCreator(CorePlugin.getInstance(), m) + .setName(Component.text("Studio: " + format(m), NamedTextColor.LIGHT_PURPLE)) + .addLore(Component.text("Made in the DevTool Item Studio", NamedTextColor.GRAY)) + .create(); + // Add a generic damage/armor attribute where applicable (harmless if unsupported) + ItemStack boosted = Attributes.setAttribute(base, + com.github.pinont.singularitylib.api.enums.AttributeType.ATTACK_DAMAGE, + 5.0, + org.bukkit.attribute.AttributeModifier.Operation.ADD_NUMBER); + player.getInventory().addItem(boosted); + player.sendMessage(Component.text("Added " + format(m) + " to your inventory (via ItemCreator + Attributes).", NamedTextColor.GREEN)); + } + + private static String format(Material m) { + String n = m.name().toLowerCase().replace('_', ' '); + return Character.toUpperCase(n.charAt(0)) + n.substring(1); + } +} \ No newline at end of file From 9cf87e7ded0be2b51c5c5918856417c94796a385 Mon Sep 17 00:00:00 2001 From: Pinont Date: Fri, 28 Aug 2026 20:12:18 +0000 Subject: [PATCH 07/13] test: real feature + failure-path coverage for DevTool command dispatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DevToolCommandTest (5 tests): exercises DevToolCommand through the CommandGroup sender-based execute(CommandSender, String[]) path: root menu for a player (asserts a 45-slot inventory opens), help for console, unknown-plugin failure path, and the known-plugin console + player panel paths (both via PluginRegistry lookup). - DevToolRegistrationTest (3 tests): verifies CorePlugin lifecycle registration into PluginRegistry (count > 0, get("SingularityDevTool") != null), the explicit onPluginStart registerComponents wiring that publishes the devtool:dt command, and isEnabled(). - fix(menu): DevToolMenu item builders passed legacy ChatColor strings to ItemCreator.setName/addLore(String), which deserialize via MiniMessage and throw ParsingException on legacy '§' codes. The player /devtool menu crashed for everyone. Replaced with the Component-based overloads (colors preserved via NamedTextColor). - docs: AGENTS.md known-issues no longer claims "no tests at all". --- AGENTS.md | 2 +- .../pinont/devtool/menu/DevToolMenu.java | 26 ++- .../devtool/DevToolRegistrationTest.java | 72 +++++++++ .../devtool/commands/DevToolCommandTest.java | 149 ++++++++++++++++++ 4 files changed, 243 insertions(+), 6 deletions(-) create mode 100644 src/test/java/com/github/pinont/devtool/DevToolRegistrationTest.java create mode 100644 src/test/java/com/github/pinont/devtool/commands/DevToolCommandTest.java diff --git a/AGENTS.md b/AGENTS.md index 5102c1d..ee8f37a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -55,7 +55,7 @@ Source migrated to the lib v2 signatures: 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. 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`). Deeper menu/click-simulation coverage is still thin by design (MockBukkit inventory clicks are flaky). ## Agent guidance - New features in Java, wired through SingularityLib APIs (`CorePlugin`, `SimpleCommand`, `Menu`) rather than raw Bukkit where possible. 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/test/java/com/github/pinont/devtool/DevToolRegistrationTest.java b/src/test/java/com/github/pinont/devtool/DevToolRegistrationTest.java new file mode 100644 index 0000000..e6cfd13 --- /dev/null +++ b/src/test/java/com/github/pinont/devtool/DevToolRegistrationTest.java @@ -0,0 +1,72 @@ +package com.github.pinont.devtool; + +import com.github.pinont.singularitylib.api.registry.PluginRegistry; +import com.github.pinont.singularitylib.plugin.CorePlugin; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.MockBukkit; + +/** + * Verifies DevTool's lifecycle wiring under MockBukkit: + *

    + *
  • {@code CorePlugin.onEnable} registers the plugin in + * {@link PluginRegistry} (auto-discovery surface);
  • + *
  • {@code DevTool.onPluginStart} performs the explicit v2 registration + * ({@code registerComponents(new DevToolCommand())}) and the command + * shows up in the registry's per-plugin component snapshot;
  • + *
  • the plugin is enabled.
  • + *
+ */ +public class DevToolRegistrationTest { + + private DevTool plugin; + + @BeforeEach + public void setUp() { + MockBukkit.mock(); + PluginRegistry.clear(); + this.plugin = MockBukkit.load(DevTool.class); + } + + @AfterEach + public void tearDown() { + MockBukkit.unmock(); + PluginRegistry.clear(); + } + + @Test + @DisplayName("DevTool registers itself in PluginRegistry on load") + public void registersItself() { + Assertions.assertTrue(PluginRegistry.count() > 0, + "PluginRegistry should contain at least DevTool after load"); + Assertions.assertNotNull(PluginRegistry.get("SingularityDevTool"), + "PluginRegistry should resolve DevTool by its plugin name"); + Assertions.assertSame(plugin, PluginRegistry.get("SingularityDevTool"), + "registry should hold the loaded plugin instance"); + } + + @Test + @DisplayName("onPluginStart explicit registration publishes the devtool command component") + public void registersComponentsExplicitly() { + CorePlugin registered = PluginRegistry.get("SingularityDevTool"); + Assertions.assertNotNull(registered, "plugin registered before component snapshot check"); + + var components = PluginRegistry.componentsOf(registered); + Assertions.assertNotNull(components, "component snapshot present"); + Assertions.assertFalse(components.commands().isEmpty(), + "DevToolCommand should be published via registerComponents"); + Assertions.assertTrue( + components.commands().stream() + .anyMatch(c -> c.getName().equals("devtool:dt")), + "the published command should be the devtool command group"); + } + + @Test + @DisplayName("DevTool is enabled after load") + public void isEnabled() { + Assertions.assertTrue(plugin.isEnabled(), "plugin should be enabled"); + } +} \ No newline at end of file diff --git a/src/test/java/com/github/pinont/devtool/commands/DevToolCommandTest.java b/src/test/java/com/github/pinont/devtool/commands/DevToolCommandTest.java new file mode 100644 index 0000000..a9c98b3 --- /dev/null +++ b/src/test/java/com/github/pinont/devtool/commands/DevToolCommandTest.java @@ -0,0 +1,149 @@ +package com.github.pinont.devtool.commands; + +import com.github.pinont.devtool.DevTool; +import com.github.pinont.singularitylib.api.registry.PluginRegistry; +import org.bukkit.command.ConsoleCommandSender; +import org.bukkit.entity.Player; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.MockBukkit; +import org.mockbukkit.mockbukkit.ServerMock; +import org.mockbukkit.mockbukkit.command.MessageTarget; + +/** + * Exercises {@link DevToolCommand} through the {@code CommandGroup} + * sender-based dispatch path ({@code execute(CommandSender, String[])}) + * — no CommandSourceStack / brigadier needed. + * + *

Goal: cover the real dispatch + {@link PluginRegistry} lookup logic + * (root menu, help subcommand, unknown-plugin failure path, known-plugin + * panel path) with robust no-crash assertions. Deep MockBukkit + * inventory/click simulation is deliberately avoided. + * + *

DevTool is loaded in {@code setUp} so {@code onPluginStart}'s explicit + * registration puts "SingularityDevTool" into {@link PluginRegistry} — that + * is what the {@code /devtool } path resolves against. + */ +public class DevToolCommandTest { + + private ServerMock server; + private DevToolCommand command; + + @BeforeEach + public void setUp() { + MockBukkit.mock(); + PluginRegistry.clear(); + // Loading DevTool runs CorePlugin.onEnable -> PluginRegistry.register + // ("SingularityDevTool") and DevTool.onPluginStart -> + // registerComponents(new DevToolCommand()). + MockBukkit.load(DevTool.class); + server = MockBukkit.getMock(); + command = new DevToolCommand(); + } + + @AfterEach + public void tearDown() { + MockBukkit.unmock(); + PluginRegistry.clear(); + } + + // -- helpers ----------------------------------------------------------- + + private Player addPlayer(String name) { + return server.addPlayer(name); + } + + /** + * Scans the console's queued messages for one containing {@code needle}. + * The queue also holds startup chatter from CorePlugin.onEnable, so we + * search instead of assuming queue position. + */ + private static boolean sawMessageContaining(MessageTarget target, String needle) { + String message; + while ((message = target.nextMessage()) != null) { + if (message.contains(needle)) { + return true; + } + } + return false; + } + + // -- tests ------------------------------------------------------------- + + @Test + @DisplayName("/devtool (no args) by a player opens the dev menu without crashing") + public void noArgsByPlayerOpensMenu() { + Player player = addPlayer("Pinont"); + + Assertions.assertDoesNotThrow( + () -> command.execute(player, new String[]{}), + "/devtool with no args by a player should not throw"); + + // DevToolMenu builds a 9x5 = 45-slot chest and opens it on success. + Assertions.assertEquals( + 45, + player.getOpenInventory().getTopInventory().getSize(), + "player should be viewing the 45-slot developer tools menu"); + } + + @Test + @DisplayName("/devtool help by console sends the plugin listing without crashing") + public void helpByConsoleSendsListing() { + ConsoleCommandSender console = server.getConsoleSender(); + + Assertions.assertDoesNotThrow( + () -> command.execute(console, new String[]{"help"}), + "/devtool help should not throw"); + + Assertions.assertTrue( + sawMessageContaining((MessageTarget) console, "Singularity plugins"), + "console should have received the /devtool help plugin listing"); + } + + @Test + @DisplayName("/devtool by console reports unknown plugin without crashing") + public void unknownPluginByConsoleDoesNotThrow() { + ConsoleCommandSender console = server.getConsoleSender(); + + Assertions.assertDoesNotThrow( + () -> command.execute(console, new String[]{"unknownplugin"}), + "/devtool should not throw"); + + Assertions.assertTrue( + sawMessageContaining((MessageTarget) console, "Unknown Singularity plugin"), + "console should have received the unknown-plugin failure message"); + } + + @Test + @DisplayName("/devtool SingularityDevTool by console reports name + version") + public void knownPluginByConsoleSendsInfo() { + ConsoleCommandSender console = server.getConsoleSender(); + + Assertions.assertDoesNotThrow( + () -> command.execute(console, new String[]{"SingularityDevTool"}), + "/devtool by console should not throw"); + + Assertions.assertTrue( + sawMessageContaining((MessageTarget) console, "SingularityDevTool v"), + "console should have received the discovered plugin name + version"); + } + + @Test + @DisplayName("/devtool SingularityDevTool by a player opens the plugin panel without crashing") + public void knownPluginByPlayerOpensPanel() { + Player player = addPlayer("Pinont"); + + Assertions.assertDoesNotThrow( + () -> command.execute(player, new String[]{"SingularityDevTool"}), + "/devtool by a player should not throw"); + + // PluginPanelMenu is a 27-slot chest (3 rows). + Assertions.assertEquals( + 27, + player.getOpenInventory().getTopInventory().getSize(), + "player should be viewing the 27-slot plugin panel"); + } +} \ No newline at end of file From 2466537c761f181b2574f9e64dcf6ce4133fc8d7 Mon Sep 17 00:00:00 2001 From: Pinont Date: Fri, 28 Aug 2026 21:01:32 +0000 Subject: [PATCH 08/13] feat(v2): LIVE config editor (ConfigEditorMenu + ChatPrompt + /devtool config) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step 4 of the plan — the flagship DevTool feature: - ConfigEditorMenu: PaginatedMenu over config.yml keys; booleans toggle on click, numbers step ±1 (sneak reverses), strings prompt in chat - ChatPrompt: scoped one-shot per-player chat input (replaces old global ChatEvent hack); 'cancel' aborts; re-opens editor after capture - Save & Reload button persists via CommentConfigManager.save() and triggers CorePlugin.reloadReloadables(); Revert closes - /devtool config subcommand wired - 11 tests green --- .../com/github/pinont/devtool/DevTool.java | 3 +- .../devtool/commands/DevToolCommand.java | 35 +++++ .../devtool/menu/submenu/ChatPrompt.java | 66 ++++++++++ .../menu/submenu/ConfigEditorMenu.java | 120 ++++++++++++++++++ 4 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/github/pinont/devtool/menu/submenu/ChatPrompt.java create mode 100644 src/main/java/com/github/pinont/devtool/menu/submenu/ConfigEditorMenu.java diff --git a/src/main/java/com/github/pinont/devtool/DevTool.java b/src/main/java/com/github/pinont/devtool/DevTool.java index 4c06146..4d76e69 100644 --- a/src/main/java/com/github/pinont/devtool/DevTool.java +++ b/src/main/java/com/github/pinont/devtool/DevTool.java @@ -1,6 +1,7 @@ package com.github.pinont.devtool; import com.github.pinont.devtool.commands.DevToolCommand; +import com.github.pinont.devtool.menu.submenu.ChatPrompt; import com.github.pinont.singularitylib.plugin.CorePlugin; public class DevTool extends CorePlugin { @@ -8,7 +9,7 @@ public class DevTool extends CorePlugin { @Override public void onPluginStart() { // Explicit registration (v2 DSL) — no @AutoRegister classpath scanning. - registerComponents(new DevToolCommand()); + registerComponents(new DevToolCommand(), new ChatPrompt()); } @Override diff --git a/src/main/java/com/github/pinont/devtool/commands/DevToolCommand.java b/src/main/java/com/github/pinont/devtool/commands/DevToolCommand.java index 145f43b..ef9ac3a 100644 --- a/src/main/java/com/github/pinont/devtool/commands/DevToolCommand.java +++ b/src/main/java/com/github/pinont/devtool/commands/DevToolCommand.java @@ -24,6 +24,7 @@ public DevToolCommand() { registerSubcommand(new HelpSub()); registerSubcommand(new ItemStudioSub()); registerSubcommand(new RecipeSub()); + registerSubcommand(new ConfigSub()); } @Override @@ -121,6 +122,40 @@ public void execute(CommandSender sender, String[] args) { } } + /** + * /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 help — lists discovered plugins + usage. */ diff --git a/src/main/java/com/github/pinont/devtool/menu/submenu/ChatPrompt.java b/src/main/java/com/github/pinont/devtool/menu/submenu/ChatPrompt.java new file mode 100644 index 0000000..9bef08a --- /dev/null +++ b/src/main/java/com/github/pinont/devtool/menu/submenu/ChatPrompt.java @@ -0,0 +1,66 @@ +package com.github.pinont.devtool.menu.submenu; + +import com.github.pinont.singularitylib.api.manager.CommentConfigManager; +import com.github.pinont.singularitylib.plugin.CorePlugin; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.AsyncPlayerChatEvent; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * DevTool: minimal chat-prompt for editing string config values. + * + *

Used by {@link ConfigEditorMenu} — a player clicks a string key, the editor asks + * them to type a value, and the next chat message is captured (then re-opens the menu). + * This is a scoped, per-player conversation (no global chat hijacking like the old + * DevTool ChatEvent). It replaces the previous all-messages-are-input approach with a + * strict one-shot prompt. + */ +public final class ChatPrompt implements Listener { + + /** pending prompts: player -> (plugin, key, manager, menuToReopen) */ + private static final Map PENDING = new ConcurrentHashMap<>(); + + public ChatPrompt() { + } + + public static void setPending(CorePlugin plugin, Player player, String key, + CommentConfigManager manager, ConfigEditorMenu.Editor editor) { + PENDING.put(player.getUniqueId(), new Pending(plugin, key, manager, editor)); + } + + public static boolean hasPending(Player player) { + return PENDING.containsKey(player.getUniqueId()); + } + + @EventHandler(priority = EventPriority.HIGH) + public void onChat(AsyncPlayerChatEvent event) { + Pending pending = PENDING.remove(event.getPlayer().getUniqueId()); + if (pending == null) { + return; + } + event.setCancelled(true); + Player player = event.getPlayer(); + String input = event.getMessage().trim(); + if (input.equalsIgnoreCase("cancel")) { + player.sendMessage(Component.text("Edit cancelled.", NamedTextColor.GRAY)); + pending.editor.show(player); + return; + } + pending.manager.set(pending.key, input); + player.sendMessage(Component.text(pending.key + " -> '" + input + "' (use Save to persist).", NamedTextColor.GREEN)); + // Re-open on the main thread (chat is async) + org.bukkit.Bukkit.getScheduler().runTask(pending.plugin, () -> pending.editor.show(player)); + } + + private record Pending(CorePlugin plugin, String key, CommentConfigManager manager, + ConfigEditorMenu.Editor editor) { + } +} \ No newline at end of file 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..10a2536 --- /dev/null +++ b/src/main/java/com/github/pinont/devtool/menu/submenu/ConfigEditorMenu.java @@ -0,0 +1,120 @@ +package com.github.pinont.devtool.menu.submenu; + +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: + *

    + *
  • Boolean → click toggles the value;
  • + *
  • Number → left-click +1, right-click −1 (stepless ± by shift);
  • + *
  • String → a chat prompt appears to type the new value.
  • + *
+ * 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) { + // chat prompt (conversation-style): ask for a value + player.closeInventory(); + player.sendMessage(Component.text("Type the new value for '" + key + + "' (current: '" + s + "'), or 'cancel' to abort.", NamedTextColor.YELLOW)); + ChatPrompt.setPending(target, player, key, manager, this); + } 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