minecraft-plugin-dev

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Minecraft Plugin Development Skill

Minecraft 插件开发 Skill

Platform Overview

平台概览

PlatformBase APINotes
PaperBukkit/Spigot + Paper extensionsRecommended; async chunk loading, Adventure native
SpigotBukkit + Spigot extensionsLegacy; fewer APIs, slower
BukkitBase API onlyAvoid for new plugins
FoliaPaper forkRegion-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说明
PaperBukkit/Spigot + Paper 扩展推荐;异步区块加载,原生支持 Adventure
SpigotBukkit + Spigot 扩展旧版;API 更少,速度更慢
Bukkit仅基础 API新插件应避免使用
FoliaPaper 分支区域线程;需要特殊调度器 API
推荐以 Paper 为目标。Paper 包含所有 Bukkit 和 Spigot API,同时提供 显著的性能改进和额外 API。

Routing Boundaries

路由边界

  • Use when
    : the target is server-side Paper/Bukkit/Spigot plugin behavior with JavaPlugin APIs.
  • Do not use when
    : the task requires client-side installable mods or loader APIs (
    minecraft-modding
    /
    minecraft-multiloader
    ).
  • Do not use when
    : the task is pure vanilla datapack/command content (
    minecraft-datapack
    /
    minecraft-commands-scripting
    ).
  • 适用于
    :目标是通过 JavaPlugin API 实现服务端 Paper/Bukkit/Spigot 插件行为。
  • 不适用于
    :任务需要使用客户端可安装模组或加载器 API(
    minecraft-modding
    /
    minecraft-multiloader
    )。
  • 不适用于
    :任务是纯原版数据包或命令内容(
    minecraft-datapack
    /
    minecraft-commands-scripting
    )。

Bundled References

内置参考资料

  • Read
    references/runtime-patterns.md
    when the task touches scheduling, Folia support, PDC, Adventure/MiniMessage, YAML config, Vault, or Paper-specific APIs.
  • Read
    references/paper-plugin-commands.md
    for a Paper-only
    paper-plugin.yml
    project or Brigadier command registration.

  • 当任务涉及调度、Folia 支持、PDC、Adventure/MiniMessage、YAML 配置、Vault 或 Paper 专属 API 时,读取
    references/runtime-patterns.md
    。
  • 对于仅限 Paper 的
    paper-plugin.yml
    项目或 Brigadier 命令注册,读取
    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
1.21.x-R0.1-SNAPSHOT
dependency, Java 21 toolchain, and matching
api-version
until the project is intentionally ported.
以下示例面向当前的 Paper 26.3 和 Java 25。对于现有的 1.21.x 插件,在有意完成移植之前,保留其
1.21.x-R0.1-SNAPSHOT
依赖、Java 21 工具链和匹配的
api-version
。

settings.gradle.kts

settings.gradle.kts

kotlin
rootProject.name = "my-plugin"
kotlin
rootProject.name = "my-plugin"

build.gradle.kts

build.gradle.kts

kotlin
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
compileOnly
and must not be shaded into the plugin JAR.
kotlin
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 保持
compileOnly
,不得被 shade 进插件 JAR。

gradle/wrapper/gradle-wrapper.properties

gradle/wrapper/gradle-wrapper.properties

properties
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
Gradle 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.zip
Gradle 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.yml

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

Core Files

核心文件

plugin.yml
(Bukkit-compatible default)

plugin.yml
(Bukkit 兼容的默认格式)

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
Match
api-version
to the oldest Paper API the plugin intentionally supports. Current Paper examples use
26.3
; legacy
1.21
and positive
1.21.<patch>
values remain valid for older servers. A server older than the declared value refuses to load the plugin.
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
将
api-version
与插件有意支持的最旧 Paper API 匹配。 当前 Paper 示例使用
26.3
;旧版
1.21
和
1.21.<patch>
形式的正值 对旧服务器仍然有效。比所声明值更旧的服务器将拒绝加载该插件。

paper-plugin.yml
(experimental Paper-only format)

paper-plugin.yml
(实验性的 Paper 专属格式)

Prefer
plugin.yml
for Bukkit-compatible plugins. Use
paper-plugin.yml
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
commands
field or
getCommand(...)
registration. Read
references/paper-plugin-commands.md
for the paired descriptor, main class, and Brigadier lifecycle registration.
When one JAR ships both descriptors, keep their shared metadata and main class aligned. Do not combine the Paper-only sample with the Bukkit-compatible
MyPlugin
sample below.
对于需要兼容 Bukkit 的插件,优先使用
plugin.yml
。仅当 JAR 有意为 Paper 专属,且需要引导、加载器或类加载隔离等 Paper 插件行为时,才使用
paper-plugin.yml
。它可以是唯一的描述文件,但不能作为即插即用替代品:Paper 插件不使用
commands
字段,也不进行
getCommand(...)
注册。阅读
references/paper-plugin-commands.md
,了解配对的描述文件、主类和 Brigadier 生命周期注册。
当一个 JAR 同时包含两种描述文件时,应确保它们的共享元数据和主类保持一致。不要将仅限 Paper 的示例与下方兼容 Bukkit 的
MyPlugin
示例结合使用。

Bukkit-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 → MONITOR

Use
MONITOR
for logging only (never modify outcome). On events that implement
Cancellable
, use
ignoreCancelled = true
unless you need cancelled events.
LOWEST → LOW → NORMAL → HIGH → HIGHEST → MONITOR

仅将
MONITOR
用于日志记录(绝不修改结果)。对于实现了
Cancellable
的事件,除非需要处理已取消的事件,否则使用
ignoreCancelled = true
。

Cancellable 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
plugin.yml
path above. Its declared command enables
getCommand("myplugin")
. For a Paper-only descriptor, use the Brigadier lifecycle example in
references/paper-plugin-commands.md
.
java
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();
    }
}

本节对应上面的
plugin.yml
路径。在其中声明的命令可启用
getCommand("myplugin")
。对于仅限 Paper 的描述文件,请使用
references/paper-plugin-commands.md
中的 Brigadier 生命周期示例。
java
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,
BukkitScheduler
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.
See
references/runtime-patterns.md
for copy-ready sync, async, cancelable, and Folia-safe scheduler examples.

对于经典 Paper 插件,
BukkitScheduler
仍然适用。如果声明支持 Folia, 则玩家、实体、区域、全局和异步工作都应通过相应的 Folia 感知 调度器路由。当一个插件必须同时支持 Paper 和 Folia 时,应将调度逻辑 封装在一个小型、项目本地的接口之后。
请参阅
references/runtime-patterns.md
,获取可直接复制使用的同步、异步、可取消和 Folia 安全调度器示例。

Persistent Data Container (PDC)

Persistent Data Container(PDC)

PDC stores arbitrary data on any
PersistentDataHolder
(players, entities, items, chunks). Data is saved with the world and persists across restarts.
Create
NamespacedKey
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.
See
references/runtime-patterns.md
for player, item, chunk, and world PDC examples.

PDC 可在任意
PersistentDataHolder
(玩家、实体、物品、区块)上存储任意数据。 数据随世界保存,并在服务器重启后继续保留。
只创建一次
NamespacedKey
实例,发布后保持数据类型稳定,并将 PDC 用于小型元数据,而非大型数据集。对于大型或查询密集型插件状态, 优先使用配置文件或数据库。
请参阅
references/runtime-patterns.md
,获取玩家、物品、区块和世界的 PDC 示例。

Adventure Text Components

Adventure 文本组件

Paper uses Adventure natively for all text. No legacy chat colors. Use
Component
builders for code-owned messages and MiniMessage for config-driven messages. Avoid legacy
ChatColor
unless the target project already depends on it for compatibility.
See
references/runtime-patterns.md
for simple messages, hover/click events, MiniMessage parsing, titles, and action bars.

Paper 原生使用 Adventure 处理所有文本。不再使用旧版聊天颜色。 对代码拥有的消息使用
Component
builder,对配置驱动的消息使用 MiniMessage。 除非目标项目已为兼容性依赖
ChatColor
,否则避免使用旧版
ChatColor
。
请参阅
references/runtime-patterns.md
,获取简单消息、悬停/点击事件、 MiniMessage 解析、标题和操作栏的示例。

Configuration (YAML)

配置(YAML)

src/main/resources/config.yml

src/main/resources/config.yml

yaml
undefined
yaml
undefined

Default 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
undefined
settings: max-players: 20 welcome-message: "<green>Welcome to the server!" cooldown-seconds: 30
database: host: localhost port: 3306 name: myplugin_db
undefined

Accessing config values

读取配置值

Call
saveDefaultConfig()
in
onEnable()
, provide explicit defaults when reading values, and validate config shape before starting long-running tasks.
在
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
references/runtime-patterns.md
for config read/write and custom YAML examples.

仅当将用户配置与可变插件数据分离值得额外文件处理成本时, 才使用自定义 YAML 文件。让阻塞式磁盘写入远离高频事件路径。
请参阅
references/runtime-patterns.md
,获取配置读写和自定义 YAML 示例。

Vault Integration (Economy / Permissions)

Vault 集成(经济 / 权限)

Declare Vault as
compileOnly
, 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.
When Vault support is required, add the JitPack repository and
compileOnly("com.github.MilkBowl:VaultAPI:1.7")
to the Gradle build.
See
references/runtime-patterns.md
for a minimal economy setup and charge example.

将 Vault 声明为
compileOnly
,在插件元数据中对其设置 soft-depend,并在服务提供者 不可用时妥善禁用经济功能。绝不能仅因为安装了 Vault,就假定已安装兼容 Vault 的 经济插件。
需要 Vault 支持时,在 Gradle 构建中添加 JitPack 仓库和
compileOnly("com.github.MilkBowl:VaultAPI:1.7")
。
请参阅
references/runtime-patterns.md
,获取最小化经济设置和扣费示例。

Paper-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
references/runtime-patterns.md
for async chunk loading, custom item meta, profile lookup, and protection-plugin integration examples.

当 Paper API 能避免主线程阻塞或简化基于 Adventure 的行为时,使用 Paper API。 将可选插件集成置于存在性检查和元数据 soft-depend 之后。
请参阅
references/runtime-patterns.md
,获取异步区块加载、自定义物品元数据、 资料查找和保护类插件集成示例。

Common Tasks Checklist

常见任务清单

Creating a new event listener

创建新的事件监听器

  • Create class implementing
    Listener
  • Annotate methods with
    @EventHandler
  • Call
    getServer().getPluginManager().registerEvents(listener, plugin)
    in
    onEnable()
  • On cancellable events, add
    ignoreCancelled = true
    unless you need cancelled events
  • 创建实现
    Listener
    的类
  • 使用
    @EventHandler
    标注方法
  • 在
    onEnable()
    中调用
    getServer().getPluginManager().registerEvents(listener, plugin)
  • 对于可取消事件,除非需要处理已取消的事件,否则添加
    ignoreCancelled = true

Adding a new command

添加新命令

  • For a Bukkit-compatible plugin, define the command in
    plugin.yml
    , create a
    CommandExecutor
    , and register it with
    getCommand("name")
  • For a Paper-only plugin, register the command through
    LifecycleEvents.COMMANDS
    ; do not add a
    commands
    field
  • 对于 Bukkit 兼容插件,在
    plugin.yml
    中定义命令,创建
    CommandExecutor
    ,并通过
    getCommand("name")
    注册
  • 对于仅限 Paper 的插件,通过
    LifecycleEvents.COMMANDS
    注册命令;不要添加
    commands
    字段

Saving plugin data

保存插件数据

  • For simple values: use
    config.yml
    via
    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
  • 对于实体级数据:使用带
    NamespacedKey
    的 PDC
  • 对于大型数据集:使用异步调度器、文件 I/O 或数据库

Scheduling a repeating task

调度重复任务

  • Determine if task needs main thread (use
    runTaskTimer
    ) or is I/O (use
    runTaskTimerAsynchronously
    )
  • Store the
    BukkitTask
    reference so you can cancel in
    onDisable()
  • Cancel all tasks in
    onDisable()
    or use
    getServer().getScheduler().cancelTasks(plugin)

  • 确定任务是否需要主线程(使用
    runTaskTimer
    ),或者是否属于 I/O(使用
    runTaskTimerAsynchronously
    )
  • 保存
    BukkitTask
    引用,以便在
    onDisable()
    中取消任务
  • 在
    onDisable()
    中取消所有任务,或使用
    getServer().getScheduler().cancelTasks(plugin)

Build, Validate, and Run

构建、验证和运行

  1. Build the plugin JAR:
    bash
    ./gradlew build
    # Output: build/libs/my-plugin-1.0.0-SNAPSHOT.jar
  2. Run the bundled validator to catch config and layout errors:
    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
    The validator requires Node and includes its own YAML parser for descriptor checks.
  3. Fix any reported errors and re-run until clean.
  4. Deploy: copy the built JAR to
    server/plugins/
    and restart the Paper server. If the real project already applies a Paper dev-server plugin such as
    xyz.jpenilla.run-paper
    , use that project's documented dev task instead of assuming
    ./gradlew runServer
    exists.
The validator checks:
  • active
    plugin.yml
    or
    paper-plugin.yml
    required keys (
    name
    ,
    version
    ,
    main
    ,
    api-version
    ) and repo-supported current
    26.<release>
    or legacy
    1.21
    / positive
    1.21.<patch>
    values, with warnings for versions newer than the documented examples
  • cross-descriptor metadata consistency when both descriptors are present;
    paper-plugin.yml
    is selected as active
  • Active main class path exists and extends
    JavaPlugin
  • actual server
    /reload
    anti-patterns such as
    Bukkit.reload()
    or dispatching the server reload command

  1. 构建插件 JAR:
    bash
    ./gradlew build
    # Output: build/libs/my-plugin-1.0.0-SNAPSHOT.jar
  2. 运行内置验证器以捕获配置和布局错误:
    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
    验证器要求安装 Node,并内置用于检查描述文件的 YAML 解析器。
  3. 修复报告中的所有错误,然后重新运行,直至检查结果干净。
  4. 部署:将构建好的 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()
    或调度服务器重载命令

References

参考资料