minecraft-plugin-dev
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMinecraft Plugin Development Skill
Minecraft 插件开发 Skill
Platform Overview
平台概览
| Platform | Base API | Notes |
|---|---|---|
| Paper | Bukkit/Spigot + Paper extensions | Recommended; async chunk loading, Adventure native |
| Spigot | Bukkit + Spigot extensions | Legacy; fewer APIs, slower |
| Bukkit | Base API only | Avoid for new plugins |
| Folia | Paper fork | Region-threaded; requires special scheduler APIs |
Paper is the recommended target. Paper includes all Bukkit and Spigot APIs plus significant performance improvements and additional APIs.
| 平台 | 基础 API | 说明 |
|---|---|---|
| Paper | Bukkit/Spigot + Paper 扩展 | 推荐;异步区块加载,原生支持 Adventure |
| Spigot | Bukkit + Spigot 扩展 | 旧版;API 更少,速度更慢 |
| Bukkit | 仅基础 API | 新插件应避免使用 |
| Folia | Paper 分支 | 区域线程;需要特殊调度器 API |
推荐以 Paper 为目标。Paper 包含所有 Bukkit 和 Spigot API,同时提供 显著的性能改进和额外 API。
Routing Boundaries
路由边界
- : the target is server-side Paper/Bukkit/Spigot plugin behavior with JavaPlugin APIs.
Use when - : the task requires client-side installable mods or loader APIs (
Do not use when/minecraft-modding).minecraft-multiloader - : the task is pure vanilla datapack/command content (
Do not use when/minecraft-datapack).minecraft-commands-scripting
- :目标是通过 JavaPlugin API 实现服务端 Paper/Bukkit/Spigot 插件行为。
适用于 - :任务需要使用客户端可安装模组或加载器 API(
不适用于/minecraft-modding)。minecraft-multiloader - :任务是纯原版数据包或命令内容(
不适用于/minecraft-datapack)。minecraft-commands-scripting
Bundled References
内置参考资料
- Read when the task touches scheduling, Folia support, PDC, Adventure/MiniMessage, YAML config, Vault, or Paper-specific APIs.
references/runtime-patterns.md - Read for a Paper-only
references/paper-plugin-commands.mdproject or Brigadier command registration.paper-plugin.yml
- 当任务涉及调度、Folia 支持、PDC、Adventure/MiniMessage、YAML 配置、Vault 或 Paper 专属 API 时,读取 。
references/runtime-patterns.md - 对于仅限 Paper 的 项目或 Brigadier 命令注册,读取
paper-plugin.yml。references/paper-plugin-commands.md
Project Setup
项目设置
The examples below target current Paper 26.3 and Java 25. For an existing
1.21.x plugin, preserve its dependency, Java 21
toolchain, and matching until the project is intentionally ported.
1.21.x-R0.1-SNAPSHOTapi-version以下示例面向当前的 Paper 26.3 和 Java 25。对于现有的
1.21.x 插件,在有意完成移植之前,保留其 依赖、Java 21
工具链和匹配的 。
1.21.x-R0.1-SNAPSHOTapi-versionsettings.gradle.kts
settings.gradle.ktssettings.gradle.kts
settings.gradle.ktskotlin
rootProject.name = "my-plugin"kotlin
rootProject.name = "my-plugin"build.gradle.kts
build.gradle.ktsbuild.gradle.kts
build.gradle.ktskotlin
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.3.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)
}
}
}Add Shadow only when the plugin has runtime libraries that must be bundled and
relocated. Paper and optional plugin APIs such as Vault remain and
must not be shaded into the plugin JAR.
compileOnlykotlin
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.3.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 保持 ,不得被 shade
进插件 JAR。
compileOnlygradle/wrapper/gradle-wrapper.properties
gradle/wrapper/gradle-wrapper.propertiesgradle/wrapper/gradle-wrapper.properties
gradle/wrapper/gradle-wrapper.propertiesproperties
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zipGradle 8.8 cannot run on Java 25. Use Gradle 9.1 or newer for a current Java 25
project. Preserve the existing wrapper and Java 21 toolchain for a legacy 1.21.x
project unless its build is intentionally upgraded and verified.
properties
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zipGradle 8.8 无法在 Java 25 上运行。当前 Java 25 项目应使用 Gradle 9.1
或更高版本。对于旧版 1.21.x 项目,除非有意升级并验证其构建,否则保留
现有 wrapper 和 Java 21 工具链。
Project Layout
项目布局
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.ymlmy-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.ymlCore Files
核心文件
plugin.yml
(Bukkit-compatible default)
plugin.ymlplugin.yml
(Bukkit 兼容的默认格式)
plugin.ymlyaml
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.3'
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: opMatchto the oldest Paper API the plugin intentionally supports. Current Paper examples useapi-version; legacy26.3and positive1.21values remain valid for older servers. A server older than the declared value refuses to load the plugin.1.21.<patch>
yaml
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.3'
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将与插件有意支持的最旧 Paper API 匹配。 当前 Paper 示例使用api-version;旧版26.3和1.21形式的正值 对旧服务器仍然有效。比所声明值更旧的服务器将拒绝加载该插件。1.21.<patch>
paper-plugin.yml
(experimental Paper-only format)
paper-plugin.ymlpaper-plugin.yml
(实验性的 Paper 专属格式)
paper-plugin.ymlPrefer for Bukkit-compatible plugins. Use only
when the JAR is intentionally Paper-only and needs Paper-plugin behavior such
as bootstrapping, loaders, or classloading isolation. It can be the only
descriptor, but is not a drop-in replacement: Paper plugins do not use a
field or registration. Read
for the paired descriptor, main class, and Brigadier lifecycle registration.
plugin.ymlpaper-plugin.ymlcommandsgetCommand(...)references/paper-plugin-commands.mdWhen one JAR ships both descriptors, keep their shared metadata and main class
aligned. Do not combine the Paper-only sample with the Bukkit-compatible
sample below.
MyPlugin对于需要兼容 Bukkit 的插件,优先使用 。仅当 JAR 有意为 Paper 专属,且需要引导、加载器或类加载隔离等 Paper 插件行为时,才使用 。它可以是唯一的描述文件,但不能作为即插即用替代品:Paper 插件不使用 字段,也不进行 注册。阅读
,了解配对的描述文件、主类和 Brigadier 生命周期注册。
plugin.ymlpaper-plugin.ymlcommandsgetCommand(...)references/paper-plugin-commands.md当一个 JAR 同时包含两种描述文件时,应确保它们的共享元数据和主类保持一致。不要将仅限 Paper 的示例与下方兼容 Bukkit 的 示例结合使用。
MyPluginBukkit-compatible main class
Bukkit 兼容的主类
java
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.");
}
}java
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.");
}
}Event Listeners
事件监听器
java
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))
);
}
}java
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 order
EventPriority 顺序
LOWEST → LOW → NORMAL → HIGH → HIGHEST → MONITORUse
MONITORCancellableignoreCancelled = trueLOWEST → LOW → NORMAL → HIGH → HIGHEST → MONITOR仅将
MONITORCancellableignoreCancelled = trueCancellable events
可取消事件
java
@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));
}
}java
@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));
}
}Commands
命令
This section is for the path above. Its declared command enables
. For a Paper-only descriptor, use the Brigadier
lifecycle example in .
plugin.ymlgetCommand("myplugin")references/paper-plugin-commands.mdjava
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 的描述文件,请使用
中的 Brigadier 生命周期示例。
plugin.ymlgetCommand("myplugin")references/paper-plugin-commands.mdjava
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();
}
}Schedulers
调度器
For classic Paper plugins, is still fine. If you claim Folia support,
route player, entity, region, global, and async work through the matching Folia-aware
scheduler. Keep scheduling behind a small project-local interface when one plugin must
support both Paper and Folia.
BukkitSchedulerSee for copy-ready sync, async, cancelable, and
Folia-safe scheduler examples.
references/runtime-patterns.md对于经典 Paper 插件, 仍然适用。如果声明支持 Folia,
则玩家、实体、区域、全局和异步工作都应通过相应的 Folia 感知
调度器路由。当一个插件必须同时支持 Paper 和 Folia 时,应将调度逻辑
封装在一个小型、项目本地的接口之后。
BukkitScheduler请参阅 ,获取可直接复制使用的同步、异步、可取消和
Folia 安全调度器示例。
references/runtime-patterns.mdPersistent Data Container (PDC)
Persistent Data Container(PDC)
PDC stores arbitrary data on any (players, entities, items, chunks).
Data is saved with the world and persists across restarts.
PersistentDataHolderCreate instances once, keep data types stable after release, and use
PDC for small metadata rather than large datasets. Prefer config files or a database
for large or query-heavy plugin state.
NamespacedKeySee for player, item, chunk, and world PDC examples.
references/runtime-patterns.mdPDC 可在任意 (玩家、实体、物品、区块)上存储任意数据。
数据随世界保存,并在服务器重启后继续保留。
PersistentDataHolder只创建一次 实例,发布后保持数据类型稳定,并将
PDC 用于小型元数据,而非大型数据集。对于大型或查询密集型插件状态,
优先使用配置文件或数据库。
NamespacedKey请参阅 ,获取玩家、物品、区块和世界的 PDC 示例。
references/runtime-patterns.mdAdventure Text Components
Adventure 文本组件
Paper uses Adventure natively for all text. No legacy chat colors.
Use builders for code-owned messages and MiniMessage for config-driven
messages. Avoid legacy unless the target project already depends on it
for compatibility.
ComponentChatColorSee for simple messages, hover/click events,
MiniMessage parsing, titles, and action bars.
references/runtime-patterns.mdPaper 原生使用 Adventure 处理所有文本。不再使用旧版聊天颜色。
对代码拥有的消息使用 builder,对配置驱动的消息使用 MiniMessage。
除非目标项目已为兼容性依赖 ,否则避免使用旧版 。
ComponentChatColorChatColor请参阅 ,获取简单消息、悬停/点击事件、
MiniMessage 解析、标题和操作栏的示例。
references/runtime-patterns.mdConfiguration (YAML)
配置(YAML)
src/main/resources/config.yml
src/main/resources/config.ymlsrc/main/resources/config.yml
src/main/resources/config.ymlyaml
undefinedyaml
undefinedDefault config
Default config
settings:
max-players: 20
welcome-message: "<green>Welcome to the server!"
cooldown-seconds: 30
database:
host: localhost
port: 3306
name: myplugin_db
undefinedsettings:
max-players: 20
welcome-message: "<green>Welcome to the server!"
cooldown-seconds: 30
database:
host: localhost
port: 3306
name: myplugin_db
undefinedAccessing config values
读取配置值
Call in , provide explicit defaults when reading
values, and validate config shape before starting long-running tasks.
saveDefaultConfig()onEnable()在 中调用 ,读取值时提供明确的默认值,
并在启动长时间运行的任务之前验证配置结构。
onEnable()saveDefaultConfig()Custom config file
自定义配置文件
Use custom YAML files only when separating user config from mutable plugin data is
worth the extra file handling. Keep blocking disk writes off hot event paths.
See for config read/write and custom YAML examples.
references/runtime-patterns.md仅当将用户配置与可变插件数据分离值得额外文件处理成本时,
才使用自定义 YAML 文件。让阻塞式磁盘写入远离高频事件路径。
请参阅 ,获取配置读写和自定义 YAML 示例。
references/runtime-patterns.mdVault Integration (Economy / Permissions)
Vault 集成(经济 / 权限)
Declare Vault as , soft-depend on it in plugin metadata, and disable
economy features cleanly when the service provider is unavailable. Never assume a
Vault-compatible economy plugin is installed just because Vault itself is present.
compileOnlyWhen Vault support is required, add the JitPack repository and
to the Gradle build.
compileOnly("com.github.MilkBowl:VaultAPI:1.7")See for a minimal economy setup and charge example.
references/runtime-patterns.md将 Vault 声明为 ,在插件元数据中对其设置 soft-depend,并在服务提供者
不可用时妥善禁用经济功能。绝不能仅因为安装了 Vault,就假定已安装兼容 Vault 的
经济插件。
compileOnly需要 Vault 支持时,在 Gradle 构建中添加 JitPack 仓库和
。
compileOnly("com.github.MilkBowl:VaultAPI:1.7")请参阅 ,获取最小化经济设置和扣费示例。
references/runtime-patterns.mdPaper-Specific APIs
Paper 专属 API
Use Paper APIs when they remove main-thread blocking or simplify Adventure-native
behavior. Keep optional plugin integrations behind presence checks and metadata
soft-dependencies.
See for async chunk loading, custom item meta,
profile lookup, and protection-plugin integration examples.
references/runtime-patterns.md当 Paper API 能避免主线程阻塞或简化基于 Adventure 的行为时,使用 Paper API。
将可选插件集成置于存在性检查和元数据 soft-depend 之后。
请参阅 ,获取异步区块加载、自定义物品元数据、
资料查找和保护类插件集成示例。
references/runtime-patterns.mdCommon Tasks Checklist
常见任务清单
Creating a new event listener
创建新的事件监听器
- Create class implementing
Listener - Annotate methods with
@EventHandler - Call in
getServer().getPluginManager().registerEvents(listener, plugin)onEnable() - On cancellable events, add unless you need cancelled events
ignoreCancelled = true
- 创建实现 的类
Listener - 使用 标注方法
@EventHandler - 在 中调用
onEnable()getServer().getPluginManager().registerEvents(listener, plugin) - 对于可取消事件,除非需要处理已取消的事件,否则添加
ignoreCancelled = true
Adding a new command
添加新命令
- For a Bukkit-compatible plugin, define the command in , create a
plugin.yml, and register it withCommandExecutorgetCommand("name") - For a Paper-only plugin, register the command through ; do not add a
LifecycleEvents.COMMANDSfieldcommands
- 对于 Bukkit 兼容插件,在 中定义命令,创建
plugin.yml,并通过CommandExecutor注册getCommand("name") - 对于仅限 Paper 的插件,通过 注册命令;不要添加
LifecycleEvents.COMMANDS字段commands
Saving plugin data
保存插件数据
- For simple values: use via
config.yml/getConfig()saveConfig() - For per-entity data: use PDC with a
NamespacedKey - For large datasets: use async scheduler + file I/O or a database
- 对于简单值:通过 /
getConfig()使用saveConfig()config.yml - 对于实体级数据:使用带 的 PDC
NamespacedKey - 对于大型数据集:使用异步调度器、文件 I/O 或数据库
Scheduling a repeating task
调度重复任务
- Determine if task needs main thread (use ) or is I/O (use
runTaskTimer)runTaskTimerAsynchronously - Store the reference so you can cancel in
BukkitTaskonDisable() - Cancel all tasks in or use
onDisable()getServer().getScheduler().cancelTasks(plugin)
- 确定任务是否需要主线程(使用 ),或者是否属于 I/O(使用
runTaskTimer)runTaskTimerAsynchronously - 保存 引用,以便在
BukkitTask中取消任务onDisable() - 在 中取消所有任务,或使用
onDisable()getServer().getScheduler().cancelTasks(plugin)
Build, Validate, and Run
构建、验证和运行
- Build the plugin JAR:
bash
./gradlew build # Output: build/libs/my-plugin-1.0.0-SNAPSHOT.jar - Run the bundled validator to catch config and layout errors:
The validator requires Node and includes its own YAML parser for descriptor checks.bash
./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 - Fix any reported errors and re-run until clean.
- Deploy: copy the built JAR to and restart the Paper server. If the real project already applies a Paper dev-server plugin such as
server/plugins/, use that project's documented dev task instead of assumingxyz.jpenilla.run-paperexists../gradlew runServer
The validator checks:
- active or
plugin.ymlrequired keys (paper-plugin.yml,name,version,main) and repo-supported currentapi-versionor legacy26.<release>/ positive1.21values, with warnings for versions newer than the documented examples1.21.<patch> - cross-descriptor metadata consistency when both descriptors are present; is selected as active
paper-plugin.yml - Active main class path exists and extends
JavaPlugin - actual server anti-patterns such as
/reloador dispatching the server reload commandBukkit.reload()
- 构建插件 JAR:
bash
./gradlew build # Output: build/libs/my-plugin-1.0.0-SNAPSHOT.jar - 运行内置验证器以捕获配置和布局错误:
验证器要求安装 Node,并内置用于检查描述文件的 YAML 解析器。bash
./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 复制到 并重启 Paper 服务器。 如果实际项目已经应用了 Paper 开发服务器插件(例如
server/plugins/), 请使用该项目文档中的开发任务,不要假定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()
References
参考资料
- Paper API Javadoc: https://jd.papermc.io/paper/
- Paper plugin descriptor: https://docs.papermc.io/paper/dev/plugin-yml/
- Paper plugins: https://docs.papermc.io/paper/dev/getting-started/paper-plugins/
- Paper Brigadier registration: https://docs.papermc.io/paper/dev/command-api/basics/registration/
- Gradle Java compatibility: https://docs.gradle.org/current/userguide/compatibility.html
- Adventure (text API): https://docs.advntr.dev/
- MiniMessage format: 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 plugin: https://github.com/jpenilla/run-task
- 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