[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6EBZHPHTKG3KTHS2NGTJR83":3,"skill-same:01M6EBZHPHTKG3KTHS2NGTJR83":122},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":23,"publishedAt":44,"linkOnly":45,"markdownContent":46,"markdownBody":47,"frontMatter":48,"refName":49,"commitSha":50,"skillDirectory":51,"forksCount":52,"repositoryHtmlUrl":53,"repositoryHomepageUrl":54,"ownerHtmlUrl":55,"ownerBlogUrl":6,"ownerDescription":56,"updatedAt":57,"install":58,"resources":71},"01M6EBZHPHTKG3KTHS2NGTJR83","minecraft-plugin-dev",null,"Develop Minecraft server plugins using the Paper\u002FBukkit\u002FSpigot API for Minecraft 1.21.x. Handles creating Paper plugins with JavaPlugin, event listeners with @EventHandler,  commands, schedulers (sync\u002Fasync), Persistent Data Container (PDC), Adventure text components, Vault economy integration, BungeeCord\u002FVelocity messaging, plugin.yml configuration, YAML config management, and Paper-specific enhancement APIs. Always targets Paper API 1.21.x (Java 21) with Gradle (Kotlin DSL). Distinguishes plugin development from mod development: plugins run server-side only and do not require client installation.","ComeOnOliver\u002Fskillshub","ComeOnOliver","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F43215937?v=4","01M6E5JNFECJAAVFE0W7SMWWD2","Haoran Shu","01M6E5JNNDQS7AJZDYFQ8TPZCK",66,"MIT","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002FJahrome907\u002Fminecraft-codex-skills\u002Fminecraft-plugin-dev\u002FSKILL.md",[18],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000010","后端与接口","SCENARIO","使用场景",[24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43],"minecraft plugin dev","minecraft plugin","plugin","dev","plugin dev","minecraft","text","gradle","config management","commands","velocity","only","components","api","data","server","messaging","run","config","sync","2026-11-10 10:12:30",false,"---\nname: minecraft-plugin-dev\ndescription: >\n  Develop Minecraft server plugins using the Paper\u002FBukkit\u002FSpigot API for Minecraft 1.21.x.\n  Handles creating Paper plugins with JavaPlugin, event listeners with @EventHandler, \n  commands, schedulers (sync\u002Fasync), Persistent Data Container (PDC), Adventure text components,\n  Vault economy integration, BungeeCord\u002FVelocity messaging, plugin.yml configuration,\n  YAML config management, and Paper-specific enhancement APIs. Always targets Paper API\n  1.21.x (Java 21) with Gradle (Kotlin DSL). Distinguishes plugin development from mod\n  development: plugins run server-side only and do not require client installation.\n---\n\n# Minecraft Plugin Development Skill\n\n## Platform Overview\n\n| Platform | Base API | Notes |\n|----------|----------|-------|\n| **Paper** | Bukkit\u002FSpigot + Paper extensions | Recommended; async chunk loading, Adventure native |\n| **Spigot** | Bukkit + Spigot extensions | Legacy; fewer APIs, slower |\n| **Bukkit** | Base API only | Avoid for new plugins |\n| **Folia** | Paper fork | Region-threaded; requires special scheduler APIs |\n\n> Paper is the recommended target. Paper includes all Bukkit and Spigot APIs plus\n> significant performance improvements and additional APIs.\n\n### Routing Boundaries\n- `Use when`: the target is server-side Paper\u002FBukkit\u002FSpigot plugin behavior with JavaPlugin APIs.\n- `Do not use when`: the task requires client-side installable mods or loader APIs (`minecraft-modding` \u002F `minecraft-multiloader`).\n- `Do not use when`: the task is pure vanilla datapack\u002Fcommand content (`minecraft-datapack` \u002F `minecraft-commands-scripting`).\n\n---\n\n## Project Setup\n\n### `settings.gradle.kts`\n```kotlin\nrootProject.name = \"my-plugin\"\n```\n\n### `build.gradle.kts`\n```kotlin\nplugins {\n    java\n    id(\"com.gradleup.shadow\") version \"8.3.0\"\n}\n\ngroup = \"com.example\"\nversion = \"1.0.0-SNAPSHOT\"\n\nrepositories {\n    mavenCentral()\n    maven(\"https:\u002F\u002Frepo.papermc.io\u002Frepository\u002Fmaven-public\u002F\")\n    \u002F\u002F For Vault (economy API)\n    maven(\"https:\u002F\u002Fjitpack.io\")\n}\n\ndependencies {\n    compileOnly(\"io.papermc.paper:paper-api:1.21.11-R0.1-SNAPSHOT\")\n    \u002F\u002F Optional: Vault economy\u002Fpermission integration\n    compileOnly(\"com.github.MilkBowl:VaultAPI:1.7\")\n}\n\njava {\n    toolchain.languageVersion.set(JavaLanguageVersion.of(21))\n}\n\ntasks {\n    processResources {\n        \u002F\u002F Substitutes ${version} in plugin.yml with the Gradle project version\n        filesMatching(\"plugin.yml\") {\n            expand(\"version\" to project.version)\n        }\n    }\n    shadowJar {\n        archiveClassifier.set(\"\")\n    }\n    build {\n        dependsOn(shadowJar)\n    }\n}\n```\n\n### `gradle\u002Fwrapper\u002Fgradle-wrapper.properties`\n```properties\ndistributionUrl=https\\:\u002F\u002Fservices.gradle.org\u002Fdistributions\u002Fgradle-8.8-bin.zip\n```\n\n---\n\n## Project Layout\n```\nmy-plugin\u002F\n├── build.gradle.kts\n├── settings.gradle.kts\n├── gradle\u002F\n│   └── wrapper\u002F\n│       └── gradle-wrapper.properties\n└── src\u002Fmain\u002F\n    ├── java\u002Fcom\u002Fexample\u002Fmyplugin\u002F\n    │   ├── MyPlugin.java          ← main class (extends JavaPlugin)\n    │   ├── listeners\u002F\n    │   │   └── PlayerListener.java\n    │   ├── commands\u002F\n    │   │   └── MyCommand.java\n    │   └── managers\u002F\n    │       └── DataManager.java\n    └── resources\u002F\n        ├── plugin.yml\n        └── config.yml\n```\n\n---\n\n## Core Files\n\n### `plugin.yml` (required)\n```yaml\nname: MyPlugin\nversion: \"${version}\"\nmain: com.example.myplugin.MyPlugin\ndescription: An example Paper plugin\nauthor: YourName\nwebsite: https:\u002F\u002Fgithub.com\u002Fexample\u002Fmy-plugin\napi-version: '1.21.11'\n\ncommands:\n  myplugin:\n    description: Main plugin command\n    usage: \u002Fmyplugin \u003Csubcommand>\n    permission: myplugin.use\n    aliases: [mp]\n\npermissions:\n  myplugin.use:\n    description: Allows use of \u002Fmyplugin\n    default: true\n  myplugin.admin:\n    description: Admin access\n    default: op\n```\n\n> Paper 1.20.5+ supports major\u002Fminor\u002Fpatch `api-version` values.\n> Use `api-version: '1.21.11'` when you target that Paper patch specifically, or `api-version: '1.21'`\n> only when you intentionally support the broader 1.21.x line.\n\n### Main Plugin Class\n```java\npackage com.example.myplugin;\n\nimport com.example.myplugin.commands.MyCommand;\nimport com.example.myplugin.listeners.PlayerListener;\nimport org.bukkit.plugin.java.JavaPlugin;\n\npublic final class MyPlugin extends JavaPlugin {\n\n    private static MyPlugin instance;\n\n    @Override\n    public void onEnable() {\n        instance = this;\n        saveDefaultConfig();\n\n        \u002F\u002F Register listeners\n        getServer().getPluginManager().registerEvents(new PlayerListener(this), this);\n\n        \u002F\u002F Register commands\n        var cmd = getCommand(\"myplugin\");\n        if (cmd != null) {\n            cmd.setExecutor(new MyCommand(this));\n            cmd.setTabCompleter(new MyCommand(this));\n        }\n\n        getLogger().info(\"MyPlugin enabled!\");\n    }\n\n    @Override\n    public void onDisable() {\n        getLogger().info(\"MyPlugin disabled.\");\n    }\n\n    public static MyPlugin getInstance() {\n        return instance;\n    }\n}\n```\n\n---\n\n## Event Listeners\n\n```java\npackage com.example.myplugin.listeners;\n\nimport com.example.myplugin.MyPlugin;\nimport net.kyori.adventure.text.Component;\nimport net.kyori.adventure.text.format.NamedTextColor;\nimport org.bukkit.event.EventHandler;\nimport org.bukkit.event.EventPriority;\nimport org.bukkit.event.Listener;\nimport org.bukkit.event.entity.PlayerDeathEvent;\nimport org.bukkit.event.player.PlayerJoinEvent;\nimport org.bukkit.event.player.PlayerQuitEvent;\n\npublic class PlayerListener implements Listener {\n\n    private final MyPlugin plugin;\n\n    public PlayerListener(MyPlugin plugin) {\n        this.plugin = plugin;\n    }\n\n    @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)\n    public void onPlayerJoin(PlayerJoinEvent event) {\n        event.joinMessage(\n            Component.text(event.getPlayer().getName() + \" joined!\", NamedTextColor.GREEN)\n        );\n    }\n\n    @EventHandler\n    public void onPlayerQuit(PlayerQuitEvent event) {\n        event.quitMessage(\n            Component.text(event.getPlayer().getName() + \" left.\", NamedTextColor.YELLOW)\n        );\n    }\n\n    @EventHandler(ignoreCancelled = true)\n    public void onPlayerDeath(PlayerDeathEvent event) {\n        \u002F\u002F Modify death message using Adventure components\n        event.deathMessage(\n            Component.text(\"☠ \", NamedTextColor.RED)\n                .append(Component.text(event.getPlayer().getName(), NamedTextColor.WHITE))\n                .append(Component.text(\" died!\", NamedTextColor.RED))\n        );\n    }\n}\n```\n\n### EventPriority order\n`LOWEST → LOW → NORMAL → HIGH → HIGHEST → MONITOR`  \nUse `MONITOR` for logging only (never modify outcome). Use `ignoreCancelled = true` unless\nyou have a specific reason to handle cancelled events.\n\n### Cancellable events\n```java\n@EventHandler\npublic void onBlockBreak(BlockBreakEvent event) {\n    if (event.getPlayer().hasPermission(\"myplugin.break.deny\")) {\n        event.setCancelled(true);\n        event.getPlayer().sendMessage(Component.text(\"You cannot break blocks!\", NamedTextColor.RED));\n    }\n}\n```\n\n---\n\n## Commands\n\n```java\npackage com.example.myplugin.commands;\n\nimport com.example.myplugin.MyPlugin;\nimport net.kyori.adventure.text.Component;\nimport net.kyori.adventure.text.format.NamedTextColor;\nimport org.bukkit.command.Command;\nimport org.bukkit.command.CommandExecutor;\nimport org.bukkit.command.CommandSender;\nimport org.bukkit.command.TabCompleter;\nimport org.bukkit.entity.Player;\nimport org.jetbrains.annotations.NotNull;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.List;\n\npublic class MyCommand implements CommandExecutor, TabCompleter {\n\n    private final MyPlugin plugin;\n\n    public MyCommand(MyPlugin plugin) {\n        this.plugin = plugin;\n    }\n\n    @Override\n    public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command,\n                             @NotNull String label, @NotNull String[] args) {\n        if (!(sender instanceof Player player)) {\n            sender.sendMessage(Component.text(\"Only players can use this command.\", NamedTextColor.RED));\n            return true;\n        }\n\n        if (!player.hasPermission(\"myplugin.use\")) {\n            player.sendMessage(Component.text(\"No permission.\", NamedTextColor.RED));\n            return true;\n        }\n\n        if (args.length == 0) {\n            player.sendMessage(Component.text(\"Usage: \u002Fmyplugin \u003Creload|info>\", NamedTextColor.YELLOW));\n            return true;\n        }\n\n        return switch (args[0].toLowerCase()) {\n            case \"reload\" -> {\n                plugin.reloadConfig();\n                player.sendMessage(Component.text(\"Config reloaded.\", NamedTextColor.GREEN));\n                yield true;\n            }\n            case \"info\" -> {\n                player.sendMessage(Component.text(\"Version: \" + plugin.getDescription().getVersion(), NamedTextColor.AQUA));\n                yield true;\n            }\n            default -> {\n                player.sendMessage(Component.text(\"Unknown subcommand.\", NamedTextColor.RED));\n                yield false;\n            }\n        };\n    }\n\n    @Override\n    public @Nullable List\u003CString> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,\n                                                @NotNull String label, @NotNull String[] args) {\n        if (args.length == 1) {\n            return List.of(\"reload\", \"info\").stream()\n                .filter(s -> s.startsWith(args[0].toLowerCase()))\n                .toList();\n        }\n        return List.of();\n    }\n}\n```\n\n---\n\n## Schedulers\n\n### Synchronous (runs on main thread)\n```java\n\u002F\u002F Run once after 20 ticks (1 second)\nplugin.getServer().getScheduler().runTaskLater(plugin, () -> {\n    \u002F\u002F safe to access Bukkit API here\n}, 20L);\n\n\u002F\u002F Repeating task every 40 ticks (2 seconds), starts after 0 ticks\nplugin.getServer().getScheduler().runTaskTimer(plugin, () -> {\n    \u002F\u002F runs on main thread\n}, 0L, 40L);\n```\n\n### Asynchronous (for I\u002FO \u002F database work)\n```java\n\u002F\u002F Never touch Bukkit API in async tasks!\nplugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> {\n    \u002F\u002F safe: file I\u002FO, HTTP requests, DB queries\n    String data = fetchFromDatabase();\n    \u002F\u002F Switch back to main thread to use Bukkit API\n    plugin.getServer().getScheduler().runTask(plugin, () -> {\n        Bukkit.broadcastMessage(data);\n    });\n});\n```\n\n### BukkitRunnable (cancelable tasks)\n```java\nnew BukkitRunnable() {\n    int count = 0;\n\n    @Override\n    public void run() {\n        count++;\n        if (count >= 10) {\n            cancel(); \u002F\u002F stop after 10 executions\n            return;\n        }\n        \u002F\u002F task logic\n    }\n}.runTaskTimer(plugin, 0L, 20L);\n```\n\n---\n\n## Persistent Data Container (PDC)\n\nPDC stores arbitrary data on any `PersistentDataHolder` (players, entities, items, chunks).\nData is saved with the world and persists across restarts.\n\n```java\nimport org.bukkit.NamespacedKey;\nimport org.bukkit.persistence.PersistentDataType;\n\n\u002F\u002F Define keys (reuse instances — create once in your plugin class)\nNamespacedKey killKey = new NamespacedKey(plugin, \"kill_count\");\nNamespacedKey flagKey = new NamespacedKey(plugin, \"vip\");\n\n\u002F\u002F Write\nplayer.getPersistentDataContainer().set(killKey, PersistentDataType.INTEGER, 42);\nplayer.getPersistentDataContainer().set(flagKey, PersistentDataType.BOOLEAN, true);\n\n\u002F\u002F Read\nint kills = player.getPersistentDataContainer()\n    .getOrDefault(killKey, PersistentDataType.INTEGER, 0);\n\nboolean isVip = player.getPersistentDataContainer()\n    .getOrDefault(flagKey, PersistentDataType.BOOLEAN, false);\n\n\u002F\u002F Check existence\nboolean hasData = player.getPersistentDataContainer().has(killKey, PersistentDataType.INTEGER);\n\n\u002F\u002F Remove\nplayer.getPersistentDataContainer().remove(killKey);\n```\n\n### PDC on ItemStack\n```java\nItemStack item = new ItemStack(Material.DIAMOND_SWORD);\nItemMeta meta = item.getItemMeta();\nmeta.getPersistentDataContainer().set(new NamespacedKey(plugin, \"custom_id\"),\n    PersistentDataType.STRING, \"special_sword\");\nitem.setItemMeta(meta);\n```\n\n---\n\n## Adventure Text Components\n\nPaper uses [Adventure](https:\u002F\u002Fdocs.advntr.dev\u002F) natively for all text. No legacy chat colors.\n\n```java\nimport net.kyori.adventure.text.Component;\nimport net.kyori.adventure.text.format.NamedTextColor;\nimport net.kyori.adventure.text.format.TextDecoration;\nimport net.kyori.adventure.text.event.ClickEvent;\nimport net.kyori.adventure.text.event.HoverEvent;\n\n\u002F\u002F Simple components\nplayer.sendMessage(Component.text(\"Hello!\", NamedTextColor.GREEN));\nplayer.sendMessage(Component.text(\"Bold warning\", NamedTextColor.RED, TextDecoration.BOLD));\n\n\u002F\u002F Compound component\nComponent message = Component.text()\n    .append(Component.text(\"[Click Me]\", NamedTextColor.AQUA)\n        .clickEvent(ClickEvent.runCommand(\"\u002Fmyplugin info\"))\n        .hoverEvent(HoverEvent.showText(Component.text(\"Run \u002Fmyplugin info\"))))\n    .append(Component.text(\" to see plugin info.\", NamedTextColor.WHITE))\n    .build();\nplayer.sendMessage(message);\n\n\u002F\u002F MiniMessage (recommended for config-driven text)\nimport net.kyori.adventure.text.minimessage.MiniMessage;\nComponent parsed = MiniMessage.miniMessage().deserialize(\n    \"\u003Cgradient:red:yellow>Hello World\u003C\u002Fgradient>\"\n);\n\n\u002F\u002F Titles \u002F action bars\nplayer.showTitle(Title.title(\n    Component.text(\"Welcome!\", NamedTextColor.GOLD),\n    Component.text(\"To \" + player.getWorld().getName(), NamedTextColor.YELLOW),\n    Title.Times.times(Duration.ofMillis(500), Duration.ofSeconds(3), Duration.ofMillis(500))\n));\nplayer.sendActionBar(Component.text(\"Health: \" + player.getHealth(), NamedTextColor.RED));\n```\n\n---\n\n## Configuration (YAML)\n\n### `src\u002Fmain\u002Fresources\u002Fconfig.yml`\n```yaml\n# Default config\nsettings:\n  max-players: 20\n  welcome-message: \"\u003Cgreen>Welcome to the server!\"\n  cooldown-seconds: 30\n\ndatabase:\n  host: localhost\n  port: 3306\n  name: myplugin_db\n```\n\n### Accessing config values\n```java\n\u002F\u002F In onEnable():\nsaveDefaultConfig(); \u002F\u002F writes config.yml if absent\n\n\u002F\u002F Reading values\nint maxPlayers = getConfig().getInt(\"settings.max-players\", 20);\nString message  = getConfig().getString(\"settings.welcome-message\", \"Welcome!\");\nboolean enabled = getConfig().getBoolean(\"features.pvp\", true);\n\n\u002F\u002F Reloading\nreloadConfig();\n\n\u002F\u002F Writing values\ngetConfig().set(\"settings.max-players\", 30);\nsaveConfig();\n```\n\n### Custom config file\n```java\nFile customFile = new File(getDataFolder(), \"data.yml\");\nif (!customFile.exists()) {\n    saveResource(\"data.yml\", false); \u002F\u002F copies from resources\u002F\n}\nFileConfiguration customConfig = YamlConfiguration.loadConfiguration(customFile);\ncustomConfig.set(\"some.key\", \"value\");\ncustomConfig.save(customFile);\n```\n\n---\n\n## Vault Integration (Economy \u002F Permissions)\n\n```java\nimport net.milkbowl.vault.economy.Economy;\nimport org.bukkit.plugin.RegisteredServiceProvider;\n\npublic class MyPlugin extends JavaPlugin {\n    private Economy economy;\n\n    @Override\n    public void onEnable() {\n        if (!setupEconomy()) {\n            getLogger().severe(\"Vault not found! Economy features disabled.\");\n        }\n    }\n\n    private boolean setupEconomy() {\n        if (getServer().getPluginManager().getPlugin(\"Vault\") == null) return false;\n        RegisteredServiceProvider\u003CEconomy> rsp =\n            getServer().getServicesManager().getRegistration(Economy.class);\n        if (rsp == null) return false;\n        economy = rsp.getProvider();\n        return economy != null;\n    }\n\n    \u002F\u002F Usage\n    public void chargePlayer(Player player, double amount) {\n        if (economy != null && economy.has(player, amount)) {\n            economy.withdrawPlayer(player, amount);\n        }\n    }\n}\n```\n\n---\n\n## Paper-Specific APIs\n\n### Async chunk loading\n```java\n\u002F\u002F Paper: load chunk without blocking main thread\nworld.getChunkAtAsync(x, z).thenAccept(chunk -> {\n    \u002F\u002F runs on main thread after chunk loads\n    chunk.getBlock(0, 64, 0).setType(Material.GOLD_BLOCK);\n});\n```\n\n### Custom item meta\n```java\n\u002F\u002F Set custom model data (for resource packs)\nItemStack item = new ItemStack(Material.STICK);\nItemMeta meta = item.getItemMeta();\nmeta.setCustomModelData(1001);\nmeta.displayName(Component.text(\"Magic Wand\", NamedTextColor.LIGHT_PURPLE));\nitem.setItemMeta(meta);\n```\n\n### Player profile (async)\n```java\n\u002F\u002F Paper: async profile lookup (no blocking main thread)\nBukkit.createProfile(UUID.fromString(\"...\")).update().thenAccept(profile -> {\n    String name = profile.getName();\n});\n```\n\n### GriefPrevention \u002F WorldGuard bypass\n```java\n\u002F\u002F Check if location is protected (WorldGuard example)\n\u002F\u002F Always soft-depend on protection plugins\nif (getServer().getPluginManager().getPlugin(\"WorldGuard\") != null) {\n    \u002F\u002F use WorldGuard API\n}\n```\n\n---\n\n## Common Tasks Checklist\n\n### Creating a new event listener\n- [ ] Create class implementing `Listener`\n- [ ] Annotate methods with `@EventHandler`\n- [ ] Call `getServer().getPluginManager().registerEvents(listener, plugin)` in `onEnable()`\n- [ ] Add `ignoreCancelled = true` unless you need cancelled events\n\n### Adding a new command\n- [ ] Define command in `plugin.yml` under `commands:`\n- [ ] Create executor class implementing `CommandExecutor`\n- [ ] (Optional) implement `TabCompleter` for autocomplete\n- [ ] Register with `getCommand(\"name\").setExecutor(new MyExecutor())`\n\n### Saving plugin data\n- [ ] For simple values: use `config.yml` via `getConfig()` \u002F `saveConfig()`\n- [ ] For per-entity data: use PDC with a `NamespacedKey`\n- [ ] For large datasets: use async scheduler + file I\u002FO or a database\n\n### Scheduling a repeating task\n- [ ] Determine if task needs main thread (use `runTaskTimer`) or is I\u002FO (use `runTaskTimerAsynchronously`)\n- [ ] Store the `BukkitTask` reference so you can cancel in `onDisable()`\n- [ ] Cancel all tasks in `onDisable()` or use `getServer().getScheduler().cancelTasks(plugin)`\n\n---\n\n## Build & Run\n\n```bash\n# Build plugin JAR\n.\u002Fgradlew shadowJar\n\n# Output: build\u002Flibs\u002Fmy-plugin-1.0.0-SNAPSHOT.jar\n# Copy to server\u002Fplugins\u002F and restart the server\n\n# Run Paper dev server (with run-task plugin)\n.\u002Fgradlew runServer\n```\n\n## Validator Script\n\nUse the bundled validator before publishing a Paper plugin:\n\n```bash\n# Run from the installed skill directory (for example `.agents\u002Fskills\u002Fminecraft-plugin-dev`):\n.\u002Fscripts\u002Fvalidate-plugin-layout.sh --root \u002Fpath\u002Fto\u002Fplugin-project\n\n# Strict mode treats warnings as failures:\n.\u002Fscripts\u002Fvalidate-plugin-layout.sh --root \u002Fpath\u002Fto\u002Fplugin-project --strict\n```\n\nWhat it checks:\n- `plugin.yml` required keys (`name`, `version`, `main`, `api-version`)\n- Main class path exists and extends `JavaPlugin`\n- `\u002Freload` anti-pattern detection in source snippets\n\n---\n\n## References\n\n- Paper API Javadoc: https:\u002F\u002Fjd.papermc.io\u002Fpaper\u002F1.21\u002F\n- Paper Dev Docs: https:\u002F\u002Fdocs.papermc.io\u002Fpaper\u002Fdev\u002Fgetting-started\u002F\n- Adventure (text API): https:\u002F\u002Fdocs.advntr.dev\u002F\n- MiniMessage format: https:\u002F\u002Fdocs.advntr.dev\u002Fminimessage\u002Fformat.html\n- Vault API: https:\u002F\u002Fgithub.com\u002FMilkBowl\u002FVaultAPI\n- Bukkit API Javadoc: https:\u002F\u002Fjavadoc.io\u002Fdoc\u002Forg.bukkit\u002Fbukkit\u002F\n- run-task Gradle plugin: https:\u002F\u002Fgithub.com\u002Fjpenilla\u002Frun-task\n\n","# Minecraft Plugin Development Skill\n\n## Platform Overview\n\n| Platform | Base API | Notes |\n|----------|----------|-------|\n| **Paper** | Bukkit\u002FSpigot + Paper extensions | Recommended; async chunk loading, Adventure native |\n| **Spigot** | Bukkit + Spigot extensions | Legacy; fewer APIs, slower |\n| **Bukkit** | Base API only | Avoid for new plugins |\n| **Folia** | Paper fork | Region-threaded; requires special scheduler APIs |\n\n> Paper is the recommended target. Paper includes all Bukkit and Spigot APIs plus\n> significant performance improvements and additional APIs.\n\n### Routing Boundaries\n- `Use when`: the target is server-side Paper\u002FBukkit\u002FSpigot plugin behavior with JavaPlugin APIs.\n- `Do not use when`: the task requires client-side installable mods or loader APIs (`minecraft-modding` \u002F `minecraft-multiloader`).\n- `Do not use when`: the task is pure vanilla datapack\u002Fcommand content (`minecraft-datapack` \u002F `minecraft-commands-scripting`).\n\n---\n\n## Project Setup\n\n### `settings.gradle.kts`\n```kotlin\nrootProject.name = \"my-plugin\"\n```\n\n### `build.gradle.kts`\n```kotlin\nplugins {\n    java\n    id(\"com.gradleup.shadow\") version \"8.3.0\"\n}\n\ngroup = \"com.example\"\nversion = \"1.0.0-SNAPSHOT\"\n\nrepositories {\n    mavenCentral()\n    maven(\"https:\u002F\u002Frepo.papermc.io\u002Frepository\u002Fmaven-public\u002F\")\n    \u002F\u002F For Vault (economy API)\n    maven(\"https:\u002F\u002Fjitpack.io\")\n}\n\ndependencies {\n    compileOnly(\"io.papermc.paper:paper-api:1.21.11-R0.1-SNAPSHOT\")\n    \u002F\u002F Optional: Vault economy\u002Fpermission integration\n    compileOnly(\"com.github.MilkBowl:VaultAPI:1.7\")\n}\n\njava {\n    toolchain.languageVersion.set(JavaLanguageVersion.of(21))\n}\n\ntasks {\n    processResources {\n        \u002F\u002F Substitutes ${version} in plugin.yml with the Gradle project version\n        filesMatching(\"plugin.yml\") {\n            expand(\"version\" to project.version)\n        }\n    }\n    shadowJar {\n        archiveClassifier.set(\"\")\n    }\n    build {\n        dependsOn(shadowJar)\n    }\n}\n```\n\n### `gradle\u002Fwrapper\u002Fgradle-wrapper.properties`\n```properties\ndistributionUrl=https\\:\u002F\u002Fservices.gradle.org\u002Fdistributions\u002Fgradle-8.8-bin.zip\n```\n\n---\n\n## Project Layout\n```\nmy-plugin\u002F\n├── build.gradle.kts\n├── settings.gradle.kts\n├── gradle\u002F\n│   └── wrapper\u002F\n│       └── gradle-wrapper.properties\n└── src\u002Fmain\u002F\n    ├── java\u002Fcom\u002Fexample\u002Fmyplugin\u002F\n    │   ├── MyPlugin.java          ← main class (extends JavaPlugin)\n    │   ├── listeners\u002F\n    │   │   └── PlayerListener.java\n    │   ├── commands\u002F\n    │   │   └── MyCommand.java\n    │   └── managers\u002F\n    │       └── DataManager.java\n    └── resources\u002F\n        ├── plugin.yml\n        └── config.yml\n```\n\n---\n\n## Core Files\n\n### `plugin.yml` (required)\n```yaml\nname: MyPlugin\nversion: \"${version}\"\nmain: com.example.myplugin.MyPlugin\ndescription: An example Paper plugin\nauthor: YourName\nwebsite: https:\u002F\u002Fgithub.com\u002Fexample\u002Fmy-plugin\napi-version: '1.21.11'\n\ncommands:\n  myplugin:\n    description: Main plugin command\n    usage: \u002Fmyplugin \u003Csubcommand>\n    permission: myplugin.use\n    aliases: [mp]\n\npermissions:\n  myplugin.use:\n    description: Allows use of \u002Fmyplugin\n    default: true\n  myplugin.admin:\n    description: Admin access\n    default: op\n```\n\n> Paper 1.20.5+ supports major\u002Fminor\u002Fpatch `api-version` values.\n> Use `api-version: '1.21.11'` when you target that Paper patch specifically, or `api-version: '1.21'`\n> only when you intentionally support the broader 1.21.x line.\n\n### Main Plugin Class\n```java\npackage com.example.myplugin;\n\nimport com.example.myplugin.commands.MyCommand;\nimport com.example.myplugin.listeners.PlayerListener;\nimport org.bukkit.plugin.java.JavaPlugin;\n\npublic final class MyPlugin extends JavaPlugin {\n\n    private static MyPlugin instance;\n\n    @Override\n    public void onEnable() {\n        instance = this;\n        saveDefaultConfig();\n\n        \u002F\u002F Register listeners\n        getServer().getPluginManager().registerEvents(new PlayerListener(this), this);\n\n        \u002F\u002F Register commands\n        var cmd = getCommand(\"myplugin\");\n        if (cmd != null) {\n            cmd.setExecutor(new MyCommand(this));\n            cmd.setTabCompleter(new MyCommand(this));\n        }\n\n        getLogger().info(\"MyPlugin enabled!\");\n    }\n\n    @Override\n    public void onDisable() {\n        getLogger().info(\"MyPlugin disabled.\");\n    }\n\n    public static MyPlugin getInstance() {\n        return instance;\n    }\n}\n```\n\n---\n\n## Event Listeners\n\n```java\npackage com.example.myplugin.listeners;\n\nimport com.example.myplugin.MyPlugin;\nimport net.kyori.adventure.text.Component;\nimport net.kyori.adventure.text.format.NamedTextColor;\nimport org.bukkit.event.EventHandler;\nimport org.bukkit.event.EventPriority;\nimport org.bukkit.event.Listener;\nimport org.bukkit.event.entity.PlayerDeathEvent;\nimport org.bukkit.event.player.PlayerJoinEvent;\nimport org.bukkit.event.player.PlayerQuitEvent;\n\npublic class PlayerListener implements Listener {\n\n    private final MyPlugin plugin;\n\n    public PlayerListener(MyPlugin plugin) {\n        this.plugin = plugin;\n    }\n\n    @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)\n    public void onPlayerJoin(PlayerJoinEvent event) {\n        event.joinMessage(\n            Component.text(event.getPlayer().getName() + \" joined!\", NamedTextColor.GREEN)\n        );\n    }\n\n    @EventHandler\n    public void onPlayerQuit(PlayerQuitEvent event) {\n        event.quitMessage(\n            Component.text(event.getPlayer().getName() + \" left.\", NamedTextColor.YELLOW)\n        );\n    }\n\n    @EventHandler(ignoreCancelled = true)\n    public void onPlayerDeath(PlayerDeathEvent event) {\n        \u002F\u002F Modify death message using Adventure components\n        event.deathMessage(\n            Component.text(\"☠ \", NamedTextColor.RED)\n                .append(Component.text(event.getPlayer().getName(), NamedTextColor.WHITE))\n                .append(Component.text(\" died!\", NamedTextColor.RED))\n        );\n    }\n}\n```\n\n### EventPriority order\n`LOWEST → LOW → NORMAL → HIGH → HIGHEST → MONITOR`  \nUse `MONITOR` for logging only (never modify outcome). Use `ignoreCancelled = true` unless\nyou have a specific reason to handle cancelled events.\n\n### Cancellable events\n```java\n@EventHandler\npublic void onBlockBreak(BlockBreakEvent event) {\n    if (event.getPlayer().hasPermission(\"myplugin.break.deny\")) {\n        event.setCancelled(true);\n        event.getPlayer().sendMessage(Component.text(\"You cannot break blocks!\", NamedTextColor.RED));\n    }\n}\n```\n\n---\n\n## Commands\n\n```java\npackage com.example.myplugin.commands;\n\nimport com.example.myplugin.MyPlugin;\nimport net.kyori.adventure.text.Component;\nimport net.kyori.adventure.text.format.NamedTextColor;\nimport org.bukkit.command.Command;\nimport org.bukkit.command.CommandExecutor;\nimport org.bukkit.command.CommandSender;\nimport org.bukkit.command.TabCompleter;\nimport org.bukkit.entity.Player;\nimport org.jetbrains.annotations.NotNull;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.List;\n\npublic class MyCommand implements CommandExecutor, TabCompleter {\n\n    private final MyPlugin plugin;\n\n    public MyCommand(MyPlugin plugin) {\n        this.plugin = plugin;\n    }\n\n    @Override\n    public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command,\n                             @NotNull String label, @NotNull String[] args) {\n        if (!(sender instanceof Player player)) {\n            sender.sendMessage(Component.text(\"Only players can use this command.\", NamedTextColor.RED));\n            return true;\n        }\n\n        if (!player.hasPermission(\"myplugin.use\")) {\n            player.sendMessage(Component.text(\"No permission.\", NamedTextColor.RED));\n            return true;\n        }\n\n        if (args.length == 0) {\n            player.sendMessage(Component.text(\"Usage: \u002Fmyplugin \u003Creload|info>\", NamedTextColor.YELLOW));\n            return true;\n        }\n\n        return switch (args[0].toLowerCase()) {\n            case \"reload\" -> {\n                plugin.reloadConfig();\n                player.sendMessage(Component.text(\"Config reloaded.\", NamedTextColor.GREEN));\n                yield true;\n            }\n            case \"info\" -> {\n                player.sendMessage(Component.text(\"Version: \" + plugin.getDescription().getVersion(), NamedTextColor.AQUA));\n                yield true;\n            }\n            default -> {\n                player.sendMessage(Component.text(\"Unknown subcommand.\", NamedTextColor.RED));\n                yield false;\n            }\n        };\n    }\n\n    @Override\n    public @Nullable List\u003CString> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,\n                                                @NotNull String label, @NotNull String[] args) {\n        if (args.length == 1) {\n            return List.of(\"reload\", \"info\").stream()\n                .filter(s -> s.startsWith(args[0].toLowerCase()))\n                .toList();\n        }\n        return List.of();\n    }\n}\n```\n\n---\n\n## Schedulers\n\n### Synchronous (runs on main thread)\n```java\n\u002F\u002F Run once after 20 ticks (1 second)\nplugin.getServer().getScheduler().runTaskLater(plugin, () -> {\n    \u002F\u002F safe to access Bukkit API here\n}, 20L);\n\n\u002F\u002F Repeating task every 40 ticks (2 seconds), starts after 0 ticks\nplugin.getServer().getScheduler().runTaskTimer(plugin, () -> {\n    \u002F\u002F runs on main thread\n}, 0L, 40L);\n```\n\n### Asynchronous (for I\u002FO \u002F database work)\n```java\n\u002F\u002F Never touch Bukkit API in async tasks!\nplugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> {\n    \u002F\u002F safe: file I\u002FO, HTTP requests, DB queries\n    String data = fetchFromDatabase();\n    \u002F\u002F Switch back to main thread to use Bukkit API\n    plugin.getServer().getScheduler().runTask(plugin, () -> {\n        Bukkit.broadcastMessage(data);\n    });\n});\n```\n\n### BukkitRunnable (cancelable tasks)\n```java\nnew BukkitRunnable() {\n    int count = 0;\n\n    @Override\n    public void run() {\n        count++;\n        if (count >= 10) {\n            cancel(); \u002F\u002F stop after 10 executions\n            return;\n        }\n        \u002F\u002F task logic\n    }\n}.runTaskTimer(plugin, 0L, 20L);\n```\n\n---\n\n## Persistent Data Container (PDC)\n\nPDC stores arbitrary data on any `PersistentDataHolder` (players, entities, items, chunks).\nData is saved with the world and persists across restarts.\n\n```java\nimport org.bukkit.NamespacedKey;\nimport org.bukkit.persistence.PersistentDataType;\n\n\u002F\u002F Define keys (reuse instances — create once in your plugin class)\nNamespacedKey killKey = new NamespacedKey(plugin, \"kill_count\");\nNamespacedKey flagKey = new NamespacedKey(plugin, \"vip\");\n\n\u002F\u002F Write\nplayer.getPersistentDataContainer().set(killKey, PersistentDataType.INTEGER, 42);\nplayer.getPersistentDataContainer().set(flagKey, PersistentDataType.BOOLEAN, true);\n\n\u002F\u002F Read\nint kills = player.getPersistentDataContainer()\n    .getOrDefault(killKey, PersistentDataType.INTEGER, 0);\n\nboolean isVip = player.getPersistentDataContainer()\n    .getOrDefault(flagKey, PersistentDataType.BOOLEAN, false);\n\n\u002F\u002F Check existence\nboolean hasData = player.getPersistentDataContainer().has(killKey, PersistentDataType.INTEGER);\n\n\u002F\u002F Remove\nplayer.getPersistentDataContainer().remove(killKey);\n```\n\n### PDC on ItemStack\n```java\nItemStack item = new ItemStack(Material.DIAMOND_SWORD);\nItemMeta meta = item.getItemMeta();\nmeta.getPersistentDataContainer().set(new NamespacedKey(plugin, \"custom_id\"),\n    PersistentDataType.STRING, \"special_sword\");\nitem.setItemMeta(meta);\n```\n\n---\n\n## Adventure Text Components\n\nPaper uses [Adventure](https:\u002F\u002Fdocs.advntr.dev\u002F) natively for all text. No legacy chat colors.\n\n```java\nimport net.kyori.adventure.text.Component;\nimport net.kyori.adventure.text.format.NamedTextColor;\nimport net.kyori.adventure.text.format.TextDecoration;\nimport net.kyori.adventure.text.event.ClickEvent;\nimport net.kyori.adventure.text.event.HoverEvent;\n\n\u002F\u002F Simple components\nplayer.sendMessage(Component.text(\"Hello!\", NamedTextColor.GREEN));\nplayer.sendMessage(Component.text(\"Bold warning\", NamedTextColor.RED, TextDecoration.BOLD));\n\n\u002F\u002F Compound component\nComponent message = Component.text()\n    .append(Component.text(\"[Click Me]\", NamedTextColor.AQUA)\n        .clickEvent(ClickEvent.runCommand(\"\u002Fmyplugin info\"))\n        .hoverEvent(HoverEvent.showText(Component.text(\"Run \u002Fmyplugin info\"))))\n    .append(Component.text(\" to see plugin info.\", NamedTextColor.WHITE))\n    .build();\nplayer.sendMessage(message);\n\n\u002F\u002F MiniMessage (recommended for config-driven text)\nimport net.kyori.adventure.text.minimessage.MiniMessage;\nComponent parsed = MiniMessage.miniMessage().deserialize(\n    \"\u003Cgradient:red:yellow>Hello World\u003C\u002Fgradient>\"\n);\n\n\u002F\u002F Titles \u002F action bars\nplayer.showTitle(Title.title(\n    Component.text(\"Welcome!\", NamedTextColor.GOLD),\n    Component.text(\"To \" + player.getWorld().getName(), NamedTextColor.YELLOW),\n    Title.Times.times(Duration.ofMillis(500), Duration.ofSeconds(3), Duration.ofMillis(500))\n));\nplayer.sendActionBar(Component.text(\"Health: \" + player.getHealth(), NamedTextColor.RED));\n```\n\n---\n\n## Configuration (YAML)\n\n### `src\u002Fmain\u002Fresources\u002Fconfig.yml`\n```yaml\n# Default config\nsettings:\n  max-players: 20\n  welcome-message: \"\u003Cgreen>Welcome to the server!\"\n  cooldown-seconds: 30\n\ndatabase:\n  host: localhost\n  port: 3306\n  name: myplugin_db\n```\n\n### Accessing config values\n```java\n\u002F\u002F In onEnable():\nsaveDefaultConfig(); \u002F\u002F writes config.yml if absent\n\n\u002F\u002F Reading values\nint maxPlayers = getConfig().getInt(\"settings.max-players\", 20);\nString message  = getConfig().getString(\"settings.welcome-message\", \"Welcome!\");\nboolean enabled = getConfig().getBoolean(\"features.pvp\", true);\n\n\u002F\u002F Reloading\nreloadConfig();\n\n\u002F\u002F Writing values\ngetConfig().set(\"settings.max-players\", 30);\nsaveConfig();\n```\n\n### Custom config file\n```java\nFile customFile = new File(getDataFolder(), \"data.yml\");\nif (!customFile.exists()) {\n    saveResource(\"data.yml\", false); \u002F\u002F copies from resources\u002F\n}\nFileConfiguration customConfig = YamlConfiguration.loadConfiguration(customFile);\ncustomConfig.set(\"some.key\", \"value\");\ncustomConfig.save(customFile);\n```\n\n---\n\n## Vault Integration (Economy \u002F Permissions)\n\n```java\nimport net.milkbowl.vault.economy.Economy;\nimport org.bukkit.plugin.RegisteredServiceProvider;\n\npublic class MyPlugin extends JavaPlugin {\n    private Economy economy;\n\n    @Override\n    public void onEnable() {\n        if (!setupEconomy()) {\n            getLogger().severe(\"Vault not found! Economy features disabled.\");\n        }\n    }\n\n    private boolean setupEconomy() {\n        if (getServer().getPluginManager().getPlugin(\"Vault\") == null) return false;\n        RegisteredServiceProvider\u003CEconomy> rsp =\n            getServer().getServicesManager().getRegistration(Economy.class);\n        if (rsp == null) return false;\n        economy = rsp.getProvider();\n        return economy != null;\n    }\n\n    \u002F\u002F Usage\n    public void chargePlayer(Player player, double amount) {\n        if (economy != null && economy.has(player, amount)) {\n            economy.withdrawPlayer(player, amount);\n        }\n    }\n}\n```\n\n---\n\n## Paper-Specific APIs\n\n### Async chunk loading\n```java\n\u002F\u002F Paper: load chunk without blocking main thread\nworld.getChunkAtAsync(x, z).thenAccept(chunk -> {\n    \u002F\u002F runs on main thread after chunk loads\n    chunk.getBlock(0, 64, 0).setType(Material.GOLD_BLOCK);\n});\n```\n\n### Custom item meta\n```java\n\u002F\u002F Set custom model data (for resource packs)\nItemStack item = new ItemStack(Material.STICK);\nItemMeta meta = item.getItemMeta();\nmeta.setCustomModelData(1001);\nmeta.displayName(Component.text(\"Magic Wand\", NamedTextColor.LIGHT_PURPLE));\nitem.setItemMeta(meta);\n```\n\n### Player profile (async)\n```java\n\u002F\u002F Paper: async profile lookup (no blocking main thread)\nBukkit.createProfile(UUID.fromString(\"...\")).update().thenAccept(profile -> {\n    String name = profile.getName();\n});\n```\n\n### GriefPrevention \u002F WorldGuard bypass\n```java\n\u002F\u002F Check if location is protected (WorldGuard example)\n\u002F\u002F Always soft-depend on protection plugins\nif (getServer().getPluginManager().getPlugin(\"WorldGuard\") != null) {\n    \u002F\u002F use WorldGuard API\n}\n```\n\n---\n\n## Common Tasks Checklist\n\n### Creating a new event listener\n- [ ] Create class implementing `Listener`\n- [ ] Annotate methods with `@EventHandler`\n- [ ] Call `getServer().getPluginManager().registerEvents(listener, plugin)` in `onEnable()`\n- [ ] Add `ignoreCancelled = true` unless you need cancelled events\n\n### Adding a new command\n- [ ] Define command in `plugin.yml` under `commands:`\n- [ ] Create executor class implementing `CommandExecutor`\n- [ ] (Optional) implement `TabCompleter` for autocomplete\n- [ ] Register with `getCommand(\"name\").setExecutor(new MyExecutor())`\n\n### Saving plugin data\n- [ ] For simple values: use `config.yml` via `getConfig()` \u002F `saveConfig()`\n- [ ] For per-entity data: use PDC with a `NamespacedKey`\n- [ ] For large datasets: use async scheduler + file I\u002FO or a database\n\n### Scheduling a repeating task\n- [ ] Determine if task needs main thread (use `runTaskTimer`) or is I\u002FO (use `runTaskTimerAsynchronously`)\n- [ ] Store the `BukkitTask` reference so you can cancel in `onDisable()`\n- [ ] Cancel all tasks in `onDisable()` or use `getServer().getScheduler().cancelTasks(plugin)`\n\n---\n\n## Build & Run\n\n```bash\n# Build plugin JAR\n.\u002Fgradlew shadowJar\n\n# Output: build\u002Flibs\u002Fmy-plugin-1.0.0-SNAPSHOT.jar\n# Copy to server\u002Fplugins\u002F and restart the server\n\n# Run Paper dev server (with run-task plugin)\n.\u002Fgradlew runServer\n```\n\n## Validator Script\n\nUse the bundled validator before publishing a Paper plugin:\n\n```bash\n# Run from the installed skill directory (for example `.agents\u002Fskills\u002Fminecraft-plugin-dev`):\n.\u002Fscripts\u002Fvalidate-plugin-layout.sh --root \u002Fpath\u002Fto\u002Fplugin-project\n\n# Strict mode treats warnings as failures:\n.\u002Fscripts\u002Fvalidate-plugin-layout.sh --root \u002Fpath\u002Fto\u002Fplugin-project --strict\n```\n\nWhat it checks:\n- `plugin.yml` required keys (`name`, `version`, `main`, `api-version`)\n- Main class path exists and extends `JavaPlugin`\n- `\u002Freload` anti-pattern detection in source snippets\n\n---\n\n## References\n\n- Paper API Javadoc: https:\u002F\u002Fjd.papermc.io\u002Fpaper\u002F1.21\u002F\n- Paper Dev Docs: https:\u002F\u002Fdocs.papermc.io\u002Fpaper\u002Fdev\u002Fgetting-started\u002F\n- Adventure (text API): https:\u002F\u002Fdocs.advntr.dev\u002F\n- MiniMessage format: https:\u002F\u002Fdocs.advntr.dev\u002Fminimessage\u002Fformat.html\n- Vault API: https:\u002F\u002Fgithub.com\u002FMilkBowl\u002FVaultAPI\n- Bukkit API Javadoc: https:\u002F\u002Fjavadoc.io\u002Fdoc\u002Forg.bukkit\u002Fbukkit\u002F\n- run-task Gradle plugin: https:\u002F\u002Fgithub.com\u002Fjpenilla\u002Frun-task\n\n",{"name":5,"description":7},"main","def8531e65114c0fca8fb8551c1871ee0eed705c","skills\u002FJahrome907\u002Fminecraft-codex-skills\u002Fminecraft-plugin-dev",24,"https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub","https:\u002F\u002Fskillshub.wtf","https:\u002F\u002Fgithub.com\u002FComeOnOliver","AI & Blockchain Engineer\r\n","2026-06-24 22:07:19",{"source":8,"skillName":5,"commands":59,"prompt":69,"zipAvailable":70},[60,63,66],{"tool":61,"command":62},"npx","npx skills add ComeOnOliver\u002Fskillshub --skill minecraft-plugin-dev",{"tool":64,"command":65},"bunx","bunx skills add ComeOnOliver\u002Fskillshub --skill minecraft-plugin-dev",{"tool":67,"command":68},"pnpm","pnpm dlx skills add ComeOnOliver\u002Fskillshub --skill minecraft-plugin-dev","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002FJahrome907\u002Fminecraft-codex-skills\u002Fminecraft-plugin-dev\u002FSKILL.md",true,[72,78,84,88,94,98,102,106,110,114,118],{"path":73,"resourceType":74,"resourceTypeDesc":75,"sizeBytes":76,"sourceHtmlUrl":77},"forms.md","REFERENCE","参考文档",11854,"https:\u002F\u002Fgithub.com\u002Fa1627418541\u002F-\u002Fblob\u002Fmain\u002F.qoder\u002Fskills\u002Fpdf\u002Fforms.md",{"path":79,"resourceType":80,"resourceTypeDesc":81,"sizeBytes":82,"sourceHtmlUrl":83},"LICENSE.txt","LICENSE","许可证",1467,"https:\u002F\u002Fgithub.com\u002Fa1627418541\u002F-\u002Fblob\u002Fmain\u002F.qoder\u002Fskills\u002Fpdf\u002FLICENSE.txt",{"path":85,"resourceType":74,"resourceTypeDesc":75,"sizeBytes":86,"sourceHtmlUrl":87},"reference.md",16692,"https:\u002F\u002Fgithub.com\u002Fa1627418541\u002F-\u002Fblob\u002Fmain\u002F.qoder\u002Fskills\u002Fpdf\u002Freference.md",{"path":89,"resourceType":90,"resourceTypeDesc":91,"sizeBytes":92,"sourceHtmlUrl":93},"scripts\u002Fcheck_bounding_boxes.py","SCRIPT","脚本",2774,"https:\u002F\u002Fgithub.com\u002Fa1627418541\u002F-\u002Fblob\u002Fmain\u002F.qoder\u002Fskills\u002Fpdf\u002Fscripts\u002Fcheck_bounding_boxes.py",{"path":95,"resourceType":90,"resourceTypeDesc":91,"sizeBytes":96,"sourceHtmlUrl":97},"scripts\u002Fcheck_fillable_fields.py",268,"https:\u002F\u002Fgithub.com\u002Fa1627418541\u002F-\u002Fblob\u002Fmain\u002F.qoder\u002Fskills\u002Fpdf\u002Fscripts\u002Fcheck_fillable_fields.py",{"path":99,"resourceType":90,"resourceTypeDesc":91,"sizeBytes":100,"sourceHtmlUrl":101},"scripts\u002Fconvert_pdf_to_images.py",1008,"https:\u002F\u002Fgithub.com\u002Fa1627418541\u002F-\u002Fblob\u002Fmain\u002F.qoder\u002Fskills\u002Fpdf\u002Fscripts\u002Fconvert_pdf_to_images.py",{"path":103,"resourceType":90,"resourceTypeDesc":91,"sizeBytes":104,"sourceHtmlUrl":105},"scripts\u002Fcreate_validation_image.py",1258,"https:\u002F\u002Fgithub.com\u002Fa1627418541\u002F-\u002Fblob\u002Fmain\u002F.qoder\u002Fskills\u002Fpdf\u002Fscripts\u002Fcreate_validation_image.py",{"path":107,"resourceType":90,"resourceTypeDesc":91,"sizeBytes":108,"sourceHtmlUrl":109},"scripts\u002Fextract_form_field_info.py",4300,"https:\u002F\u002Fgithub.com\u002Fa1627418541\u002F-\u002Fblob\u002Fmain\u002F.qoder\u002Fskills\u002Fpdf\u002Fscripts\u002Fextract_form_field_info.py",{"path":111,"resourceType":90,"resourceTypeDesc":91,"sizeBytes":112,"sourceHtmlUrl":113},"scripts\u002Fextract_form_structure.py",3945,"https:\u002F\u002Fgithub.com\u002Fa1627418541\u002F-\u002Fblob\u002Fmain\u002F.qoder\u002Fskills\u002Fpdf\u002Fscripts\u002Fextract_form_structure.py",{"path":115,"resourceType":90,"resourceTypeDesc":91,"sizeBytes":116,"sourceHtmlUrl":117},"scripts\u002Ffill_fillable_fields.py",3819,"https:\u002F\u002Fgithub.com\u002Fa1627418541\u002F-\u002Fblob\u002Fmain\u002F.qoder\u002Fskills\u002Fpdf\u002Fscripts\u002Ffill_fillable_fields.py",{"path":119,"resourceType":90,"resourceTypeDesc":91,"sizeBytes":120,"sourceHtmlUrl":121},"scripts\u002Ffill_pdf_form_with_annotations.py",3235,"https:\u002F\u002Fgithub.com\u002Fa1627418541\u002F-\u002Fblob\u002Fmain\u002F.qoder\u002Fskills\u002Fpdf\u002Fscripts\u002Ffill_pdf_form_with_annotations.py",[123,135,145,154,160,167],{"skillNo":124,"name":125,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":126,"categories":127,"keywords":131,"publishedAt":44},"01M6FKJN13GMG4KY57J3QD8GY5","model-hierarchy-skill","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzscole\u002Fmodel-hierarchy-skill\u002Fmodel-hierarchy-skill\u002FSKILL.md",[128],{"categoryNo":129,"categoryName":130,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发",[132,133,134],"model","model hierarchy","model hierarchy skill",{"skillNo":136,"name":137,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":138,"categories":139,"keywords":141,"publishedAt":44},"01M6FKJJJ6GCAMR4PPPRD12M5X","zoom-mcp","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fzoom-mcp\u002FSKILL.md",[140],{"categoryNo":129,"categoryName":130,"taxonomyType":21,"taxonomyTypeDesc":22},[142,143,144],"MCP","zoom","zoom mcp",{"skillNo":146,"name":147,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":148,"categories":149,"keywords":150,"publishedAt":44},"01M6FKJE3KYT3KPEZMEEHYKA6K","zoom-apps-sdk","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fzoom-apps-sdk\u002FSKILL.md",[],[151,152,153,143],"sdk","zoom apps sdk","apps",{"skillNo":155,"name":156,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":157,"categories":158,"keywords":159,"publishedAt":44},"01M6FKJ9EDH9Q5VWDF8HAJRSP0","windows","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwindows\u002FSKILL.md",[],[156],{"skillNo":161,"name":162,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":163,"categories":164,"keywords":166,"publishedAt":44},"01M6FKJ666BEEFC3AE9GJ72Q93","whiteboard","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwhiteboard\u002FSKILL.md",[165],{"categoryNo":129,"categoryName":130,"taxonomyType":21,"taxonomyTypeDesc":22},[162],{"skillNo":168,"name":169,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":170,"categories":171,"keywords":172,"publishedAt":44},"01M6FKJ21WFCDGTJQ2Q91T9DVW","websockets","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwebsockets\u002FSKILL.md",[],[169]]