关于 minecraft-plugin-dev
面向 Minecraft 服务端插件开发的技能,覆盖 Paper、Spigot、Bukkit 和 Folia。提供项目构建、事件与命令处理、数据存储和验证方面的指导。
主要功能
- 01配置 Gradle 项目与插件描述文件
- 02实现事件监听器和命令
- 03处理 Paper 与 Folia 调度器
- 04使用 PDC、YAML 和 Adventure 文本
- 05运行插件布局验证脚本
适用场景
- 01创建或维护 Paper、Spigot 插件
- 02为 Folia 适配任务调度逻辑
- 03实现玩家数据、配置和命令处理
- 04构建并检查插件 JAR 与描述文件
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ jahrome907/ minecraft-agent-skills --skill minecraft-plugin-dev安装后在 Claude Code 中输入/minecraft-plugin-dev确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/paper-plugin-commands.mdruntime-patterns.mdscripts/validate-plugin-layout.shscripts/vendor/js-yaml.min.cjsLICENSE.js-yaml详细介绍
Minecraft 插件开发技能
平台概览
| 平台 | 基础 API | 说明 |
|---|---|---|
| Paper | Bukkit/Spigot + Paper 扩展 | 推荐;支持异步区块加载,原生支持 Adventure |
| Spigot | Bukkit + Spigot 扩展 | 旧版;API 较少,速度较慢 |
| Bukkit | 仅基础 API | 新插件应避免使用 |
| Folia | Paper 分支 | 按区域分线程;需要专用调度器 API |
推荐以 Paper 为目标平台。Paper 包含所有 Bukkit 和 Spigot API,以及 显著的性能提升和其他 API。
适用边界
Use when:目标是服务端 Paper/Bukkit/Spigot 插件行为,并使用 JavaPlugin API。Do not use when:任务需要客户端可安装的模组或加载器 API(minecraft-modding/minecraft-multiloader)。Do not use when:任务仅涉及原版数据包/命令内容(minecraft-datapack/minecraft-commands-scripting)。
随附参考资料
- 如果任务涉及调度、Folia 支持、PDC、Adventure/MiniMessage、YAML 配置、Vault 或 Paper 专属 API,请阅读
references/runtime-patterns.md。 - 对于仅面向 Paper、使用
paper-plugin.yml的项目,或 Brigadier 命令注册,请阅读references/paper-plugin-commands.md。
项目设置
以下示例面向当前的 Paper 26.2 和 Java 25。对于现有的
1.21.x 插件,请保留其 1.21.x-R0.1-SNAPSHOT 依赖、Java 21
工具链和匹配的 api-version,直到有意迁移项目为止。
settings.gradle.kts
rootProject.name = "my-plugin"
build.gradle.kts
plugins {
java
}
group = "com.example"
version = "1.0.0-SNAPSHOT"
repositories {
mavenCentral()
maven("https://repo.papermc.io/repository/maven-public/")
}
dependencies {
compileOnly("io.papermc.paper:paper-api:26.2.build.+")
}
java {
toolchain.languageVersion.set(JavaLanguageVersion.of(25))
}
tasks {
processResources {
// Substitutes ${version} in plugin.yml with the Gradle project version
filesMatching(listOf("plugin.yml", "paper-plugin.yml")) {
expand("version" to project.version)
}
}
}
仅当插件有必须打包并重定位的运行时库时才添加 Shadow。Paper 和 Vault 等
可选插件 API 应保持为 compileOnly,不得打包进插件 JAR。
gradle/wrapper/gradle-wrapper.properties
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
Gradle 8.8 无法在 Java 25 上运行。当前的 Java 25 项目请使用 Gradle 9.1 或更高版本。对于旧版 1.21.x 项目,请保留现有的 Wrapper 和 Java 21 工具链,除非有意升级并验证构建。
项目结构
my-plugin/
├── build.gradle.kts
├── settings.gradle.kts
├── gradle/
│ └── wrapper/
│ └── gradle-wrapper.properties
└── src/main/
├── java/com/example/myplugin/
│ ├── MyPlugin.java ← main class (extends JavaPlugin)
│ ├── listeners/
│ │ └── PlayerListener.java
│ ├── commands/
│ │ └── MyCommand.java
│ └── managers/
│ └── DataManager.java
└── resources/
├── plugin.yml ← Bukkit-compatible descriptor
├── paper-plugin.yml ← active descriptor for a Paper plugin
└── config.yml
核心文件
plugin.yml(默认兼容 Bukkit)
name: MyPlugin
version: "${version}"
main: com.example.myplugin.MyPlugin
description: An example Paper plugin
author: YourName
website: https://github.com/example/my-plugin
api-version: '26.2'
commands:
myplugin:
description: Main plugin command
usage: /myplugin <subcommand>
permission: myplugin.use
aliases: [mp]
permissions:
myplugin.use:
description: Allows use of /myplugin
default: true
myplugin.admin:
description: Admin access
default: op
将
api-version设置为插件有意支持的最早 Paper API 版本。 当前 Paper 示例使用26.2;旧服务器仍可使用1.21和正数形式的1.21.<patch>值。低于所声明版本的服务器会拒绝加载插件。
paper-plugin.yml(实验性 Paper 专属格式)
对于兼容 Bukkit 的插件,优先使用 plugin.yml。仅当 JAR 有意仅面向 Paper,
且需要引导程序、加载器或类加载隔离等 Paper 插件行为时,才使用 paper-plugin.yml。
它可以是唯一的描述文件,但不能直接替代 plugin.yml:Paper 插件不使用
commands 字段或 getCommand(...) 注册。请阅读
references/paper-plugin-commands.md
了解配套的描述文件、主类和 Brigadier 生命周期注册。
如果同一个 JAR 同时包含这两种描述文件,请确保它们的共有元数据和主类
保持一致。不要将仅面向 Paper 的示例与下方兼容 Bukkit 的
MyPlugin 示例混用。
兼容 Bukkit 的主类
package com.example.myplugin;
import com.example.myplugin.commands.MyCommand;
import com.example.myplugin.listeners.PlayerListener;
import org.bukkit.plugin.java.JavaPlugin;
public final class MyPlugin extends JavaPlugin {
@Override
public void onEnable() {
saveDefaultConfig();
// Register listeners
getServer().getPluginManager().registerEvents(new PlayerListener(), this);
// Register commands
var cmd = getCommand("myplugin");
if (cmd == null) {
throw new IllegalStateException("myplugin command is missing from plugin.yml");
}
var handler = new MyCommand(this);
cmd.setExecutor(handler);
cmd.setTabCompleter(handler);
getLogger().info("MyPlugin enabled!");
}
@Override
public void onDisable() {
getLogger().info("MyPlugin disabled.");
}
}
事件监听器
package com.example.myplugin.listeners;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
public class PlayerListener implements Listener {
@EventHandler(priority = EventPriority.NORMAL)
public void onPlayerJoin(PlayerJoinEvent event) {
event.joinMessage(
Component.text(event.getPlayer().getName() + " joined!", NamedTextColor.GREEN)
);
}
@EventHandler
public void onPlayerQuit(PlayerQuitEvent event) {
event.quitMessage(
Component.text(event.getPlayer().getName() + " left.", NamedTextColor.YELLOW)
);
}
@EventHandler
public void onPlayerDeath(PlayerDeathEvent event) {
// Modify death message using Adventure components
event.deathMessage(
Component.text("☠ ", NamedTextColor.RED)
.append(Component.text(event.getPlayer().getName(), NamedTextColor.WHITE))
.append(Component.text(" died!", NamedTextColor.RED))
);
}
}
EventPriority 顺序
LOWEST → LOW → NORMAL → HIGH → HIGHEST → MONITOR
仅将 MONITOR 用于日志记录(切勿修改结果)。对于实现
Cancellable 的事件,除非需要已取消的事件,否则使用 ignoreCancelled = true。
可取消事件
@EventHandler
public void onBlockBreak(BlockBreakEvent event) {
if (event.getPlayer().hasPermission("myplugin.break.deny")) {
event.setCancelled(true);
event.getPlayer().sendMessage(Component.text("You cannot break blocks!", NamedTextColor.RED));
}
}
命令
本节适用于上文的 plugin.yml 方式。其声明的命令会启用
getCommand("myplugin")。对于仅面向 Paper 的描述文件,请使用 references/paper-plugin-commands.md 中的 Brigadier 生命周期示例。
package com.example.myplugin.commands;
import com.example.myplugin.MyPlugin;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Locale;
public class MyCommand implements CommandExecutor, TabCompleter {
private final MyPlugin plugin;
public MyCommand(MyPlugin plugin) {
this.plugin = plugin;
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String label, @NotNull String[] args) {
if (!(sender instanceof Player player)) {
sender.sendMessage(Component.text("Only players can use this command.", NamedTextColor.RED));
return true;
}
if (!player.hasPermission("myplugin.use")) {
player.sendMessage(Component.text("No permission.", NamedTextColor.RED));
return true;
}
if (args.length == 0) {
player.sendMessage(Component.text("Usage: /myplugin <reload|info>", NamedTextColor.YELLOW));
return true;
}
return switch (args[0].toLowerCase(Locale.ROOT)) {
case "reload" -> {
plugin.reloadConfig();
player.sendMessage(Component.text("Config reloaded.", NamedTextColor.GREEN));
yield true;
}
case "info" -> {
player.sendMessage(Component.text("Version: " + plugin.getDescription().getVersion(), NamedTextColor.AQUA));
yield true;
}
default -> {
player.sendMessage(Component.text("Unknown subcommand.", NamedTextColor.RED));
yield true;
}
};
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String label, @NotNull String[] args) {
if (args.length == 1) {
return List.of("reload", "info").stream()
.filter(s -> s.startsWith(args[0].toLowerCase(Locale.ROOT)))
.toList();
}
return List.of();
}
}
调度器
对于经典 Paper 插件,BukkitScheduler 仍然适用。如果你声称支持 Folia,
则应通过相应的 Folia 感知型调度器处理玩家、实体、区域、全局和异步任务。如果同一个插件
必须同时支持 Paper 和 Folia,请将调度逻辑封装在项目本地的小型接口后面。
有关可直接复制的同步、异步、可取消和 Folia 安全调度器示例,请参阅
references/runtime-patterns.md。
持久化数据容器(PDC)
PDC 可在任何 PersistentDataHolder(玩家、实体、物品、区块)上存储任意数据。
数据会随世界一同保存,并在重启后保留。
NamespacedKey 实例只创建一次,发布后保持数据类型稳定,并使用
PDC 存储少量元数据,而非大型数据集。对于大型或查询频繁的插件状态,优先使用配置文件或数据库。
有关玩家、物品、区块和世界 PDC 示例,请参阅 references/runtime-patterns.md。
Adventure 文本组件
Paper 原生使用 Adventure 处理所有文本。不使用旧版聊天颜色。
代码中的消息使用 Component 构建器,由配置驱动的消息使用 MiniMessage。除非目标项目已依赖
旧版 ChatColor 以实现兼容性,否则应避免使用它。
有关简单消息、悬停/点击事件、
MiniMessage 解析、标题和动作栏,请参阅 references/runtime-patterns.md。
配置(YAML)
src/main/resources/config.yml
# Default config
settings:
max-players: 20
welcome-message: "<green>Welcome to the server!"
cooldown-seconds: 30
database:
host: localhost
port: 3306
name: myplugin_db
访问配置值
在 onEnable() 中调用 saveDefaultConfig(),读取
值时提供明确的默认值,并在启动长时间运行的任务前验证配置结构。
自定义配置文件
只有在将用户配置与可变插件数据分开确实值得增加文件处理工作时,才使用自定义 YAML 文件。 避免在高频事件处理路径中执行阻塞式磁盘写入。
有关配置读写和自定义 YAML 示例,请参阅 references/runtime-patterns.md。
Vault 集成(经济系统/权限)
将 Vault 声明为 compileOnly,在插件元数据中将其设为软依赖,并在服务提供方不可用时妥善禁用经济功能。不能仅因为 Vault 本身存在,就假设已安装兼容 Vault 的经济插件。
需要支持 Vault 时,请在 Gradle 构建中添加 JitPack 仓库和
compileOnly("com.github.MilkBowl:VaultAPI:1.7")。
有关最简经济系统设置和扣费示例,请参阅 references/runtime-patterns.md。
Paper 专属 API
当 Paper API 能避免主线程阻塞或简化原生 Adventure 行为时,请使用它。可选插件集成应通过存在性检查和元数据软依赖进行处理。
有关异步区块加载、自定义物品元数据、
档案查找和保护插件集成示例,请参阅 references/runtime-patterns.md。
常见任务检查清单
创建新的事件监听器
- 创建实现
Listener的类 - 使用
@EventHandler注解方法 - 在
onEnable()中调用getServer().getPluginManager().registerEvents(listener, plugin) - 对于可取消事件,除非需要处理已取消的事件,否则添加
ignoreCancelled = true
添加新命令
- 对于兼容 Bukkit 的插件,在
plugin.yml中定义命令,创建CommandExecutor,并通过getCommand("name")注册 - 对于仅支持 Paper 的插件,通过
LifecycleEvents.COMMANDS注册命令;不要添加commands字段
保存插件数据
- 对于简单值:通过
getConfig()/saveConfig()使用config.yml - 对于每个实体的数据:使用带有
NamespacedKey的 PDC - 对于大型数据集:使用异步调度器加文件 I/O,或使用数据库
调度重复任务
- 确定任务是否需要主线程(使用
runTaskTimer)或是否属于 I/O(使用runTaskTimerAsynchronously) - 保存
BukkitTask引用,以便在onDisable()中取消任务 - 在
onDisable()中取消所有任务,或使用getServer().getScheduler().cancelTasks(plugin)
构建、验证和运行
- 构建插件 JAR:
./gradlew build # Output: build/libs/my-plugin-1.0.0-SNAPSHOT.jar - 运行随附的验证器,以检查配置和目录结构错误:
验证器需要 Node,并自带用于检查描述符的 YAML 解析器。./scripts/validate-plugin-layout.sh --root /path/to/plugin-project # Strict mode treats warnings as failures: ./scripts/validate-plugin-layout.sh --root /path/to/plugin-project --strict - 修复报告的错误,然后重新运行,直到验证通过。
- 部署:将构建好的 JAR 复制到
server/plugins/,然后重启 Paper 服务端。 如果实际项目已应用 Paper 开发服务端插件(例如xyz.jpenilla.run-paper),请使用该项目文档中说明的开发任务,而不要假定存在./gradlew runServer。
验证器会检查:
- 当前启用的
plugin.yml或paper-plugin.yml是否包含必需键(name、version、main、api-version),以及仓库支持的当前26.<release>或旧版1.21/ 正数1.21.<patch>版本值;对于高于文档示例的版本会发出警告 - 两个描述符均存在时,检查其元数据是否一致;
paper-plugin.yml会被选为当前启用的描述符 - 当前启用的主类路径是否存在,以及该类是否继承
JavaPlugin - 实际服务端
/reload反模式,例如调用Bukkit.reload()或分派服务端重载命令
参考资料
- Paper API Javadoc:https://jd.papermc.io/paper/
- Paper 插件描述符:https://docs.papermc.io/paper/dev/plugin-yml/
- Paper 插件:https://docs.papermc.io/paper/dev/getting-started/paper-plugins/
- Paper Brigadier 注册:https://docs.papermc.io/paper/dev/command-api/basics/registration/
- Gradle Java 兼容性:https://docs.gradle.org/current/userguide/compatibility.html
- Adventure(文本 API):https://docs.advntr.dev/
- MiniMessage 格式:https://docs.advntr.dev/minimessage/format.html
- Vault API:https://github.com/MilkBowl/VaultAPI
- Bukkit API Javadoc:https://javadoc.io/doc/org.bukkit/bukkit/
- run-task Gradle 插件:https://github.com/jpenilla/run-task