<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/scripts/pretty-feed-v3.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:h="http://www.w3.org/TR/html4/"><channel><title>双葉橘</title><description>双葉橘的个人网站</description><link>https://syju.org</link><item><title>Minecraft服务器插件开发从入门到入门</title><link>https://syju.org/blog/mcpugin</link><guid isPermaLink="true">https://syju.org/blog/mcpugin</guid><description>Minecraft插件开发教程</description><pubDate>Wed, 19 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;astro-pure/user&apos;
import { LinkPreview } from &apos;astro-pure/advanced&apos;&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;h3&gt;关于本篇文章&lt;/h3&gt;
&lt;p&gt;本篇文章并非优秀的mc插件编写教程，比起教程，更像是我学习插件编写的笔记。由于我自身水平有限，文章内容难免出现问题，如果读者对文章内容有疑问或者建议，可以通过邮箱或者其它方式联系我，我在此感激不尽！&lt;/p&gt;
&lt;p&gt;本篇文章虽然以&lt;a href=&quot;https://papermc.io/&quot;&gt;paper&lt;/a&gt;为例，但是文章的大部分内容还是有关&lt;a href=&quot;https://hub.spigotmc.org/javadocs/spigot/&quot;&gt;bukkit API&lt;/a&gt;的，因此本篇文章也可作为&lt;a href=&quot;https://www.spigotmc.org/&quot;&gt;spigot&lt;/a&gt;插件甚至是原生的&lt;a href=&quot;https://dev.bukkit.org/&quot;&gt;bukkit&lt;/a&gt;插件的教程。&lt;/p&gt;
&lt;h3&gt;关于bukkit和bukkit API&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://dev.bukkit.org/&quot;&gt;bukkit&lt;/a&gt;是一款免费、开源的软件，旨在为minecraft服务器提供拓展的方法。&lt;/p&gt;
&lt;p&gt;bukkit修改了minecraft的底层代码，在代码中添加了许多的钩子（hook），提供了丰富的&lt;a href=&quot;https://hub.spigotmc.org/javadocs/spigot/&quot;&gt;bukkit API&lt;/a&gt;，让开发者能够制作各种各样的插件（plugin），从而拓展minecraft游戏内容。&lt;/p&gt;
&lt;h3&gt;关于paper&lt;/h3&gt;
&lt;p&gt;对于minecraft服务器，使用原生的bukkit并不是一个好的选择，大多数minecraft服务器选择使用bukkit的分支&lt;a href=&quot;https://www.spigotmc.org/&quot;&gt;spigot&lt;/a&gt;的分支&lt;a href=&quot;https://papermc.io/&quot;&gt;paper&lt;/a&gt;（对，就是bukkit的分支的分支）。幸运的是，spigot与paper几乎完美地兼容了bukkit插件。&lt;/p&gt;
&lt;h3&gt;开发环境&lt;/h3&gt;
&lt;p&gt;为了方便开发，本篇文章所使用的开发的游戏环境为paper-1.21.4，开发工具是&lt;a href=&quot;https://www.jetbrains.com/idea/&quot;&gt;IntelliJ IDEA&lt;/a&gt;和IntelliJ IDEA的插件&lt;a href=&quot;https://plugins.jetbrains.com/plugin/8327-minecraft-development&quot;&gt;Minecraft Development&lt;/a&gt;（这个真的强力推荐）。&lt;/p&gt;
&lt;h3&gt;关于本文档的示例插件&lt;/h3&gt;
&lt;p&gt;项目文件已上传至&lt;a href=&quot;https://github.com/f0xea/paperdemo&quot;&gt;github&lt;/a&gt;。因为使用到了Paper独有的内容（该部分会特别说明），所以该插件仅能在Paper上运行。&lt;/p&gt;
&lt;h2&gt;第一个paper项目&lt;/h2&gt;
&lt;p&gt;根据&lt;a href=&quot;https://docs.papermc.io/paper/dev/project-setup&quot;&gt;paper官方文档&lt;/a&gt;，我们尝试创建一个paper项目。&lt;/p&gt;
&lt;h3&gt;创建paper项目&lt;/h3&gt;
&lt;p&gt;打开IntelliJ IDEA，点击&lt;code&gt;新建项目&lt;/code&gt;，然后找到一个名为&lt;code&gt;minecraft&lt;/code&gt;的&lt;code&gt;生成器&lt;/code&gt;，项目名称填写对应的项目名称，例如&lt;code&gt;paperdemo&lt;/code&gt;。其它选项依次如下设置。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Groups&lt;/code&gt;：选择&lt;code&gt;Plugin&lt;/code&gt;（其它选项依次是模组、代理端插件）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Templates&lt;/code&gt;：选择&lt;code&gt;Paper&lt;/code&gt;（选择其它项应该也是可以的）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;构建系统&lt;/code&gt;：推荐选择&lt;code&gt;Gradle&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Language&lt;/code&gt;：选择&lt;code&gt;Java&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Minecraft版本&lt;/code&gt;：按实际开发需求进行选择，一般而言，bukkit插件对版本的判定极为宽松，旧版本的插件可能仍然能在新版本中运行。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Plugin名称&lt;/code&gt;：按实际情况填写，推荐与项目一致。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;主类&lt;/code&gt;：带上包名。包名的前面部分通常是一个域名的转置，例如&lt;code&gt;net.minecraft&lt;/code&gt;。Java不会判断你是否拥有这个域名，因此你可以填写&lt;code&gt;com.example.paperdemo.Paperdemo&lt;/code&gt;，但请注意不要填写&lt;code&gt;net.minecraft&lt;/code&gt;、&lt;code&gt;org.bukkit&lt;/code&gt;等你可能会使用的名称作为域名部分。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;许可证&lt;/code&gt;：软件的许可证，例如 &lt;em&gt;MIT&lt;/em&gt;、&lt;em&gt;GPL 3.0&lt;/em&gt;等，根据自己实际需要选择。示例项目不需要考虑特别多，随便选一个就行。&lt;/li&gt;
&lt;li&gt;其它选项依据自身情况填写。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;之后，点击&lt;code&gt;创建&lt;/code&gt;后稍等片刻即可创建项目。&lt;/p&gt;
&lt;h3&gt;项目结构&lt;/h3&gt;
&lt;p&gt;如果你的构建系统使用的是&lt;code&gt;gradle&lt;/code&gt;，那么你的paper项目结构会是这样的（假如你构建过了，或者创建了git仓库，可能会有其它文件）。&lt;/p&gt;
&lt;p&gt;在这里我的插件所使用的包名是&lt;code&gt;top.tachibana.paperdemo&lt;/code&gt;，这是一个真实存在的域名（是我的）。如果你没有自己的域名，可以使用&lt;code&gt;com.example.paperdemo&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-directory&quot;&gt;paperdemo
├── build.gradle.kts
├── settings.gradle.kts
├── src
    └── main
        ├── java
            └── top
                └── tachibana
                    ├── paperdemo
                        ├── Paperdemo.java
                        └── ...
                    └── ...
        └── resources
            └── plugin.yml
└── ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们将会把源文件放在&lt;code&gt;java&lt;/code&gt;文件夹下，资源文件放在&lt;code&gt;resources&lt;/code&gt;文件夹下。&lt;/p&gt;
&lt;p&gt;值得注意的是，&lt;code&gt;Paperdemo.java&lt;/code&gt;是主类。我们将会在这里定义一系列方法，它们决定了插件初始化或启用时的一些行为，或者是注册事件侦听器等重要的行为。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;plugin.yml&lt;/code&gt;储存了关于插件的详细信息，我将在&lt;a href=&quot;#pluginyml&quot;&gt;下一节&lt;/a&gt;详细介绍&lt;code&gt;plugin.yml&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;plugin.yml&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;plugin.yml&lt;/code&gt;的示例文件如下。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yml&quot;&gt;# at /src/main/resources/plugin.yml
name: paperdemo                              #插件的名字
version: &apos;1.0-SNAPSHOT&apos;                      #插件的版本
main: top.tachibana.paperdemo.Paperdemo      #插件的主类
api-version: &apos;1.21.4&apos;                        #API的版本，如果游戏版本低于API版本，插件将不会加载
description: An example paper plugin         #插件描述
author: Futaba_Tachibana                     #作者，也就是你啦
#authors: [Futaba_Tachibana, Dblv_Tangerine] #插件有多个作者时使用
website: https://syju.org                    #插件的网站，可以是宣传页面，也可以是github的项目网站
contributors: [PaperMC, SpigotMC, Bukkit]    #贡献者
prefix: DemoPlugin                           #插件的前缀，这将会显示在日志中
load: STARTUP                                #用来告诉服务器何时加载该插件，可以是STARTUP或POSTWORLD（默认值）
depend: [Vault, WorldEdit]                   #插件的依赖，当且仅当前置插件全部被找到时你的插件才能被加载
#                                            #如果不填写该字段的值你的插件随时都能加载
softdepend: [Vault, WorldEdit]               #软依赖，当服务器没有找到softdepend的插件时，你的插件也能被加载
loadbefore: [Vault, FactionsUUID]            #在加载这些插件之前应该先加载你的插件
provides: [Vault]                            #告诉服务器你能提供这个插件的功能
#                                            #假如另一个插件依赖vault，那么只需安装你的插件而不必要安装Vault
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;权限和命令也被定义在&lt;code&gt;plugin.yml&lt;/code&gt;。例如，示例插件有两个权限节点，它们分别是&lt;code&gt;permission.node&lt;/code&gt;和&lt;code&gt;another.permission.node&lt;/code&gt;，&lt;code&gt;permission.node&lt;/code&gt;默认情况下只有op才有，它有一个子节点&lt;code&gt;child&lt;/code&gt;，而&lt;code&gt;another.permission.node&lt;/code&gt;默认情况下非op玩家才有。那么我们应该这样填写配置文件。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yml&quot;&gt;# at /src/main/resources/plugin.yml
permissions :
  permission.node:
    description: &quot;This is a permission node&quot;
    default: op
    children:
      permission.node.child: true
  another.permission.node:
    description: &quot;This is another permission node&quot;
    default: not op
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;权限节点的值为&lt;code&gt;op&lt;/code&gt;/&lt;code&gt;not op&lt;/code&gt;或&lt;code&gt;true&lt;/code&gt;/&lt;code&gt;false&lt;/code&gt;。如果子节点被设置为&lt;code&gt;true&lt;/code&gt;，那么它的实际值将继承自父节点。可以通过添加&lt;code&gt;default-permission: true&lt;/code&gt;的赋值来将权限节点的默认值（用于那些未指定&lt;code&gt;default&lt;/code&gt;值的权限节点）设置为&lt;code&gt;true&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果示例插件使用到了&lt;code&gt;/demo&lt;/code&gt;（或者&lt;code&gt;/cmd&lt;/code&gt;）命令，而这个命令需要拥有&lt;code&gt;permission.node&lt;/code&gt;节点，那么我们可以这样填写配置文件。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yml&quot;&gt;# at /src/main/resources/plugin.yml
commands:
  demo:
    description: &quot;一个示例指令&quot;
    aliases: [cmd, command]
    usage: |
     实现一个指令，有print, suicide, spawn, msg四个子命令
     /demo print 打印插件的名字
     /demo suicide 杀死执行指令的玩家
     /demo spawn 召唤一头猪
     /demo msg join 输出\&quot;xxx加入了游戏\&quot;的消息
     /demo msg leave 输出\&quot;xxx离开了游戏\&quot;的消息
    permission: permission.node
    permission-message: &quot;You do not have permission to use this command&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;permission-message&lt;/code&gt;是该玩家没有权限时使用该命令的消息。如上，默认情况下，&lt;code&gt;/demo&lt;/code&gt;命令只有op才能使用。&lt;/p&gt;
&lt;h3&gt;主类&lt;/h3&gt;
&lt;p&gt;在主类中，我们可以定义以下方法。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/Paperdemo.java
package top.tachibana.paperdemo;

import org.bukkit.plugin.java.JavaPlugin;

public final class Paperdemo extends JavaPlugin {
    @Override
    public void onLoad(){
        //当插件初始化时，下面的代码将被执行
    }
    @Override
    public void onEnable() {
        //当插件加载时，下面的代码将被执行
        //通常用这个方法来注册一系列内容
    }
    @Override
    public void onDisable() {
        //当插件被禁用时，下面的代码将被执行
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;构建项目&lt;/h3&gt;
&lt;p&gt;在IntelliJ IDEA右上角找到编辑配置并点击。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://syju.org/_astro/%E7%BC%96%E8%BE%91%E9%85%8D%E7%BD%AE.ZxFrj0Vp_Z2rbE6E.webp&quot; alt=&quot;编辑配置&quot;&gt;&lt;/p&gt;
&lt;p&gt;点击左上角的&lt;code&gt;+&lt;/code&gt;并且点击&lt;code&gt;Gradle&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://syju.org/_astro/%E6%B7%BB%E5%8A%A0%E6%96%B0%E9%85%8D%E7%BD%AE.3rNZal3X_1t2fqy.webp&quot; alt=&quot;添加新配置&quot; title=&quot;添加新配置&quot;&gt;&lt;/p&gt;
&lt;p&gt;运行参数填写&lt;code&gt;build&lt;/code&gt;，点击确定即可。之后只要点击右上角的绿色小三角或按住&lt;code&gt;shift+F10&lt;/code&gt;即可构建项目。构建完后的可执行文件将会在路径&lt;code&gt;/build/libs/&lt;/code&gt;下。&lt;/p&gt;
&lt;h3&gt;调试插件&lt;/h3&gt;
&lt;p&gt;将测试服务器的路径放到项目文件夹下，比方说你可以在项目文件夹下创建一个叫做&lt;code&gt;test&lt;/code&gt;的文件夹，然后将服务器的所有文件都拖至该文件夹。&lt;/p&gt;
&lt;p&gt;点击左上角的&lt;code&gt;+&lt;/code&gt;之后在添加新配置中选择&lt;code&gt;JAR 应用程序&lt;/code&gt;，配置按照下图填写。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://syju.org/_astro/%E8%B0%83%E8%AF%95.CBLJMZ-4_ZpR4B.webp&quot; alt=&quot;调试&quot; title=&quot;调试&quot;&gt;&lt;/p&gt;
&lt;p&gt;之后选择该配置点击&lt;strong&gt;调试&lt;/strong&gt;即可。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://syju.org/_astro/%E8%B0%83%E8%AF%952.DRxu_ghi_ZG9yxA.webp&quot; alt=&quot;调试2&quot; title=&quot;调试2&quot;&gt;&lt;/p&gt;
&lt;p&gt;至此，调试环境已经搭建好了，你现在可以为你的插件打上断点了。&lt;/p&gt;
&lt;h2&gt;事件&lt;/h2&gt;
&lt;p&gt;在minecraft游戏进程中，每当游戏发生特定的事件（event），如玩家加入游戏、实体被杀死等等等等（它们分别会触发事件&lt;code&gt;org.bukkit.event.player.PlayerJoinEvent&lt;/code&gt;和事件&lt;code&gt;org.bukkit.event.entity.EntityDeathEvent&lt;/code&gt;）。我们可以通过创建事件监听器（Event Listener）来设计当特定的事件发生时，游戏的运行方式，这可以实现很多有意思的事情。&lt;/p&gt;
&lt;h3&gt;事件监听器&lt;/h3&gt;
&lt;p&gt;为了创建事件监听器，我们需要定义一个类，它实现了&lt;code&gt;org.bukkit.event.Listener&lt;/code&gt;接口。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/PaperdemoListener.java
package top.tachibana.paperdemo;

import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerBucketFillEvent;

public final class PaperdemoListener implements Listener {
    @EventHandler
    public void onBucket(PlayerBucketFillEvent event){
        //当事件PlayerBucketFillEvent触发时
        //这个方法的代码将会被执行
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们在&lt;code&gt;/src/main/java/top/tachibana/paperdemo/&lt;/code&gt;中新建了一个类&lt;code&gt;paperdemoListener&lt;/code&gt;，它有一个名为&lt;code&gt;onBucket()&lt;/code&gt;的方法，这个方法有注解&lt;code&gt;@EventHandler&lt;/code&gt;，因此它是一个事件监听器。它的参数类型是&lt;code&gt;PlayerBucketFillEvent&lt;/code&gt;，查阅bukkit API文档的&lt;a href=&quot;https://hub.spigotmc.org/javadocs/bukkit/org/bukkit/event/player/PlayerBucketFillEvent.html&quot;&gt;相关内容&lt;/a&gt;，我们可以找到关于这个事件的详细信息。例如，文档中是这样描述该事件的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Called when a player fills a bucket&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;因此我们了解到，当玩家使用桶装液体时，该事件会被触发。当事件触发时，方法&lt;code&gt;onBucket()&lt;/code&gt;的代码将会被执行。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/PaperdemoListener.java
package top.tachibana.paperdemo;

import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerBucketFillEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.Material;

public final class PaperdemoListener implements Listener {
    @EventHandler
    public void onBucket(PlayerBucketFillEvent event){
        if(event.getBlock().getType() == Material.LAVA){
            ItemStack water_bucket = new ItemStack(Material.WATER_BUCKET);
            event.setItemStack(water_bucket);
        } else if (event.getBlock().getType() == Material.WATER) {
            ItemStack lava_bucket = new ItemStack(Material.LAVA_BUCKET);
            event.setItemStack(lava_bucket);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述代码中，我调用了事件对象&lt;code&gt;event&lt;/code&gt;的方法&lt;code&gt;getBlock()&lt;/code&gt;和&lt;code&gt;setItemStack()&lt;/code&gt;，前者获取了用桶互动的方块类型，后者设置了事件结束后玩家手上获取到的物品堆栈（ItemStack，物品堆栈即为“占用一个格子的物品组”，它的构造器接受的参数有Material的枚举常量和物品的数量，如果物品的数量未指定则为&lt;strong&gt;1&lt;/strong&gt;），而正常情况下应该是对应液体的桶。在上例中，我做了一个这样的改变：如果玩家尝试用桶去装水，将会获得熔岩桶；如果玩家尝试用桶去装熔岩，将会获得水桶。&lt;/p&gt;
&lt;p&gt;编写完事件监听器后，不要忘记注册事件监听器。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/Paperdemo.java
package top.tachibana.paperdemo;

import org.bukkit.plugin.java.JavaPlugin;

public final class Paperdemo extends JavaPlugin {
    @Override
    public void onEnable() {
        //注册事件
        this.getServer().getPluginManager().registerEvents(new PaperdemoListener(), this);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还记得之前提到过的主类中重写的方法&lt;code&gt;onEnable()&lt;/code&gt;吗，我们正是通过该方法来注册事件监听器。&lt;code&gt;registerEvents()&lt;/code&gt;有两个参数，即我们创建事件监听器的类的引用，以及插件的主类的引用。&lt;/p&gt;
&lt;h3&gt;事件类型&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Bukkit API&lt;/strong&gt;提供了多样的事件类型，它们都在包&lt;code&gt;org.bukkit.event&lt;/code&gt;下，我们查阅Bukkit API文档，可以看到有关此包的详细信息。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Related Packages&lt;/em&gt;的内容为：&lt;/p&gt;
&lt;p&gt;| Package | Description |
| ------- | ----------- |
| org.bukkit | The root package of the Bukkit API, contains generalized API classes. |
| org.bukkit.event.block | Events relating to when a block is changed or interacts with the world. |
| org.bukkit.event.enchantment | Events triggered from an enchantment table. |
| org.bukkit.event.entity | Events relating to entities, excluding some directly referencing some more specific entity types. |
| org.bukkit.event.hanging | Events relating to entities that hang. |
| org.bukkit.event.inventory | Events relating to inventory manipulation. |
| org.bukkit.event.player | Events relating to players. |
| org.bukkit.event.raid | Events related to raids. |
| org.bukkit.event.server | Events relating to programmatic state changes on the server. |
| org.bukkit.event.vehicle | Events relating to vehicular entities. |
| org.bukkit.event.weather | Events relating to weather. |
| org.bukkit.event.world | Events triggered by various world states or changes. |&lt;/p&gt;
&lt;p&gt;bukkit将游戏中的各种事件全部分进了&lt;code&gt;org.bukkit.event&lt;/code&gt;的子包，如果我们想查找玩家扔出物品的事件，那么不难猜测，既然这个事件是与玩家有关的，它应该在&lt;code&gt;player&lt;/code&gt;下被定义，我们点进&lt;code&gt;org.bukkit.event.player&lt;/code&gt;，再用浏览器搜索关键词&lt;em&gt;drop&lt;/em&gt;或者&lt;em&gt;throw&lt;/em&gt;，便可找到这么一个类：&lt;code&gt;org.bukkit.event.player.PlayerDropItemEvent&lt;/code&gt;，它的描述如下：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Thrown when a player drops an item from their inventory&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;完美符合要求！再继续查阅，我们可以发现该类实现了一个&lt;code&gt;getItemDrop()&lt;/code&gt;，可以用它来获取玩家丢弃的物品，它的返回值类型&lt;code&gt;Item&lt;/code&gt;记录了掉落物（众所周知掉落物是实体，所以这个接口理所应当地继承自&lt;code&gt;Entity&lt;/code&gt;，虽然其实是&lt;code&gt;Entity.Spigot&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;于是，我们便可以编写对应的事件监听器了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/PaperdemoListener.java
package top.tachibana.paperdemo;

import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerBucketFillEvent;
import org.bukkit.event.player.PlayerDropItemEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.Material;

public final class PaperdemoListener implements Listener {
    @EventHandler
    public void onBucket(PlayerBucketFillEvent event){
        if(event.getBlock().getType() == Material.LAVA){
            ItemStack water_bucket = new ItemStack(Material.WATER_BUCKET);
            event.setItemStack(water_bucket);
        } else if (event.getBlock().getType() == Material.WATER) {
            ItemStack lava_bucket = new ItemStack(Material.LAVA_BUCKET);
            event.setItemStack(lava_bucket);
        }
    }
    @EventHandler
    public void onDrop(PlayerDropItemEvent event){
        //事件触发时将会被执行。
        //尽情发挥你的想象力编写代码！
    }  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如上，我们编写了这个项目的第二个事件监听器。&lt;/p&gt;
&lt;h3&gt;自定义事件&lt;/h3&gt;
&lt;p&gt;通过新建一个继承抽象类&lt;code&gt;Event&lt;/code&gt;的类，实现其中的一些方法，我们便可自定义自己的事件。下面的例子所展示的事件会在玩家切换快捷栏的槽位让钻石变成手持物品后触发。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/PlayerDiamondHeld.java
package top.tachibana.paperdemo;

import org.bukkit.entity.Player;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;

public class PlayerDiamondHeld extends Event implements Cancellable {
    private final static HandlerList HANDLER_LIST = new HandlerList();
    private final Player player;
    // 构造器
    public PlayerDiamondHeld(Player who){
        this.player = who;
    }
    // 属性
    public Player getPlayer() {
        return player;
    }
    // 只有实现了方法getHandlerList()的事件才能被监听
    // 注意这个方法是静态的
    public static HandlerList getHandlerList(){
        return HANDLER_LIST;
    }
    // 注意这个方法是动态的
    @Override
    public HandlerList getHandlers(){
        return HANDLER_LIST;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为我们新建的类继承了抽象类&lt;code&gt;Event&lt;/code&gt;，所以后续如果要实例化这个类的话，必须实现它的抽象方法&lt;code&gt;getHandlers()&lt;/code&gt;。并且，如果你想让你的事件能被监听，还要实现&lt;strong&gt;静态&lt;/strong&gt;方法&lt;code&gt;getHandlerList()&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;为了让我们的事件能被取消，我们还需实现接口&lt;code&gt;Cancellable&lt;/code&gt;，并且实现它的全部方法。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/PlayerDiamondHeld.java
package top.tachibana.paperdemo;

import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;

public class PlayerDiamondHeld extends Event implements Cancellable {
    private final static HandlerList HANDLER_LIST = new HandlerList();
    private final Player player;
    private boolean cancelled;
    // 构造器
    public PlayerDiamondHeld(Player who){
        this.player = who;
    }
    // 属性
    public Player getPlayer() {
        return player;
    }
    // 只有实现了方法getHandlerList()的事件才能被监听
    // 注意这个方法是静态的
    public static HandlerList getHandlerList(){
        return HANDLER_LIST;
    }
    // 注意这个方法是动态的
    @Override
    public HandlerList getHandlers(){
        return HANDLER_LIST;
    }
    // 为了实现Cancellable 需要定义下面的两个方法
    // cancelled的访问属性
    @Override
    public boolean isCancelled(){
        return this.cancelled;
    }
    // cancelled的修改属性
    @Override
    public void setCancelled(boolean cancelled){
        this.cancelled = cancelled;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上便是我们的自定义事件类的全部代码了。&lt;/p&gt;
&lt;p&gt;我们还需要一个方法处理我们的事件。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/PaperdemoListener.java
package top.tachibana.paperdemo;

import org.bukkit.entity.Player;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
//import ...

public final class PaperdemoListener implements Listener {
    // ...
    public void callPlayerDiamondHeld(Player who){
        PlayerDiamondHeld event = new PlayerDiamondHeld(who);
        // call事件
        if(event.callEvent()) {
            event.getPlayer().sendMessage(Component.text(&quot;PlayerDiamondHeld事件被触发&quot;, NamedTextColor.YELLOW));
        }
        // 与下面的写法等价
//        event.callEvent();
//        if(!event.isCancelled()) {
//            event.getPlayer().sendMessage(Component.text(&quot;PlayerDiamondHeld事件被触发&quot;, NamedTextColor.YELLOW));
//        }
    }
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当方法被调用时，事件也会被触发。值得注意的是，事件将会在自定义类&lt;code&gt;PlayerDiamondHeld&lt;/code&gt;的实例的成员方法&lt;code&gt;callEvent()&lt;/code&gt;被调用时触发，此时对应的事件监听器也会在此刻运行，这个方法的返回值是事件是否被取消。&lt;/p&gt;
&lt;p&gt;以上，我们便可以尝试编写我们自己的事件监听器，去监听我们自己的事件（~~别看有点绕，其实就是这样的~~）。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/PaperdemoListener.java
package top.tachibana.paperdemo;

import org.bukkit.entity.Player;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.inventory.ItemStack;
//import ...

public final class PaperdemoListener implements Listener {
    // ...
    // 我们的自定义事件可被监听 也可被取消
    @EventHandler
    public void onPlayerDiamondHeld(PlayerDiamondHeld event){
        ItemStack offHand = event.getPlayer().getInventory().getItemInOffHand();
        if(offHand.getType() == Material.NETHERITE_INGOT){
            Player player = event.getPlayer();
            player.sendMessage(Component.text(&quot;拿了钻石还拿下界合金锭？什么好事都让你占了。&quot;, NamedTextColor.RED));
            player.playSound(player, Sound.ENTITY_LIGHTNING_BOLT_THUNDER, 1, 1);
            event.setCancelled(true);
        }
    }
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意&lt;code&gt;player&lt;/code&gt;的成员方法&lt;code&gt;playSound()&lt;/code&gt;，作用是播放声音，第一个参数可以是一个&lt;code&gt;Location&lt;/code&gt;对象，也可以是一个&lt;code&gt;Entity&lt;/code&gt;对象（表示该实体的位置），第二个参数是&lt;code&gt;Sound&lt;/code&gt;的枚举常量，第三个参数是音量大小，类型是&lt;code&gt;float&lt;/code&gt;，第四个参数表示音调的高低，类型也是&lt;code&gt;float&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;此外就是方法&lt;code&gt;getInventory()&lt;/code&gt;，这个我将会在&lt;a href=&quot;#%E7%89%A9%E5%93%81%E6%A0%8F&quot;&gt;物品栏&lt;/a&gt;章节中讲述。&lt;/p&gt;
&lt;p&gt;最后我们适时地触发这个事件。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/PaperdemoListener.java
package top.tachibana.paperdemo;

import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.inventory.ItemStack;
import org.bukkit.event.player.*;
//import ...

public final class PaperdemoListener implements Listener {
    // ...
    @EventHandler
    public  void onHeld(PlayerItemHeldEvent event){
        // 用于call我们的自定义事件
        ItemStack newItem = event.getPlayer().getInventory().getItem(event.getNewSlot());
        if(newItem != null &amp;#x26;&amp;#x26; newItem.getType() == Material.DIAMOND){
            callPlayerDiamondHeld(event.getPlayer());
        }
    }
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，我们便可以让我们的事件能被触发。一般地，当玩家切换了快捷栏槽位后手持钻石，事件就会被触发，我们就能看到事件触发后输出的文字；若此时的副手若是下界合金锭，会触发事件监听器，随后事件会被事件监听器取消，我们也就看不到事件触发后输出的文字了。&lt;/p&gt;
&lt;h2&gt;指令&lt;/h2&gt;
&lt;p&gt;对于 Brigadier API 的教程，可以看我的文章：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/blog/brigadier&quot;&gt;Minecraft Brigadier API详解————以Fabric为例&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;还记得我们在&lt;code&gt;plugin.yml&lt;/code&gt;定义的指令吗？&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yml&quot;&gt;# at /src/main/resources/plugin.yml
commands:
  demo:
    description: &quot;一个示例指令&quot;
    aliases: [cmd, command]
    usage: |
     实现一个指令，有print, suicide, spawn, msg四个子命令
     /demo print 打印插件的名字
     /demo suicide 杀死执行指令的玩家
     /demo spawn 召唤一头猪
     /demo msg join 输出\&quot;xxx加入了游戏\&quot;的消息
     /demo msg leave 输出\&quot;xxx离开了游戏\&quot;的消息
    permission: permission.node
    permission-message: &quot;You do not have permission to use this command&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;/demo&lt;/code&gt;现在不起作用，因为我们从未定义指令调用后的行为。为了确保指令能够正常执行，我们先创建一个该指令的类。&lt;/p&gt;
&lt;h3&gt;指令类&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/PaperdemoCommand.java
package top.tachibana.paperdemo;

import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;

public class PaperdemoCommand implements CommandExecutor{
    @Override
    public boolean onCommand(
            @NotNull CommandSender sender, 
            @NotNull Command command, 
            @NotNull String label, 
            @NotNull String[] args){
        //指令的逻辑代码
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们创建的类&lt;code&gt;PaperdemoCommand&lt;/code&gt;实现了&lt;code&gt;CommandExecutor&lt;/code&gt;接口，于是我们可以通过这个类处理相关的指令。为了设置指令执行时的逻辑，我们要在类中重写&lt;code&gt;CommandExecutor&lt;/code&gt;的方法&lt;code&gt;onCommand()&lt;/code&gt;，这个方法的返回值类型是&lt;code&gt;boolean&lt;/code&gt;，当指令正常运行时应当返回&lt;code&gt;true&lt;/code&gt;，而当指令参数有误或其它情况无法运行时应当返回&lt;code&gt;false&lt;/code&gt;，此时游戏会向发送者返回&lt;code&gt;usage&lt;/code&gt;的内容（我们已在&lt;code&gt;plugin.yml&lt;/code&gt;中定义）。根据Bukkit API文档中的&lt;a href=&quot;https://hub.spigotmc.org/javadocs/spigot/org/bukkit/command/CommandExecutor.html&quot;&gt;描述&lt;/a&gt;，这个方法的四个参数分别是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;sender - Source of the command&lt;/p&gt;
&lt;p&gt;command - Command which was executed&lt;/p&gt;
&lt;p&gt;label - Alias of the command which was used&lt;/p&gt;
&lt;p&gt;args - Passed command arguments&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们尝试按照指令的用法来实现该指令。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/PaperdemoCommand.java
package top.tachibana.paperdemo;

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.entity.EntityType;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;

public class PaperdemoCommand implements CommandExecutor, TabCompleter {
    // 实现一个指令，有print, suicide, spawn, msg四个子命令
    // /demo print 打印插件的名字
    // /demo suicide 杀死执行指令的玩家
    // /demo spawn 召唤一头猪
    // /demo msg join 输出&quot;xxx加入了游戏&quot;的消息
    // /demo msg leave 输出&quot;xxx离开了游戏&quot;的消息
    @Override
    public boolean onCommand(
            @NotNull CommandSender sender,
            @NotNull Command command,
            @NotNull String label,
            @NotNull String[] args){
        if(args.length == 0) return false;
        switch (args[0]){
            case &quot;print&quot;:
                sender.sendMessage(Component.text(&quot;插件名字: &quot;).append(Component.text(Paperdemo.getInstance().toString(), NamedTextColor.YELLOW)));
                return true;
            case &quot;suicide&quot;:
                if(sender instanceof Player player){
                    player.setHealth(0);
                    player.sendMessage(Component.text(&quot;已杀死&quot;, NamedTextColor.RED).append(Component.text(sender.getName(), NamedTextColor.YELLOW)));
                    return true;
                }
                else{
                    return false;
                }
            case &quot;spawn&quot;:
                if(sender instanceof Player player){
                    player.getWorld().spawnEntity(player.getLocation(), EntityType.PIG);
                    return true;
                }
                else{
                    return false;
                }
            case &quot;msg&quot;:
                if(args.length == 1){
                    sender.sendMessage(Component.text(&quot;这是一条消息&quot;, NamedTextColor.YELLOW));
                    return true;
                }
                switch (args[1]){
                    case &quot;join&quot;:
                        sender.sendMessage(Component.text(sender.getName() + &quot;加入了游戏&quot;, NamedTextColor.YELLOW));
                        return true;
                    case &quot;leave&quot;:
                        sender.sendMessage(Component.text(sender.getName() + &quot;离开了游戏&quot;, NamedTextColor.YELLOW));
                        return true;
                    default:
                        return false;
                }
            default:
                return false;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这里，我们通过&lt;code&gt;switch case&lt;/code&gt;语句处理了各种参数的逻辑，这是一种常见的方法，当然你也可以使用&lt;code&gt;if else&lt;/code&gt;。除此之外，我们还用到了对象&lt;code&gt;sender&lt;/code&gt;的成员方法&lt;code&gt;sendMessage()&lt;/code&gt;和&lt;code&gt;getName()&lt;/code&gt;，前者是向命令的执行者发送消息，后者是获取命令的执行者的名字（如果是控制台执行即为&lt;code&gt;CONSOLE&lt;/code&gt;）。注意到我们还使用了模式匹配的&lt;code&gt;instanceof&lt;/code&gt;，这是因为&lt;code&gt;sender&lt;/code&gt;只实现了&lt;code&gt;CommandSender&lt;/code&gt;接口，不一定是&lt;code&gt;Player&lt;/code&gt;的示例，我们如果想调用&lt;code&gt;Player&lt;/code&gt;的成员方法必须要先用关键字&lt;code&gt;instanceof&lt;/code&gt;，使用模式匹配（Java16以上）的写法可以让代码更加简洁。&lt;/p&gt;
&lt;p&gt;我们还调用了实体（类&lt;code&gt;Entity&lt;/code&gt;）的一些方法，以及使用消息组件（&lt;code&gt;Component&lt;/code&gt;）美化了输出。有关&lt;a href=&quot;#%E5%AE%9E%E4%BD%93&quot;&gt;实体&lt;/a&gt;和&lt;a href=&quot;#%E6%B6%88%E6%81%AF%E7%BB%84%E4%BB%B6&quot;&gt;消息组件&lt;/a&gt;的内容我将会在之后的章节中讲解。&lt;/p&gt;
&lt;p&gt;我们还调用了&lt;code&gt;Paperdemo&lt;/code&gt;的静态方法&lt;code&gt;getInstance()&lt;/code&gt;，它能返回主类的唯一实例，也就是插件本身。关于这个方法如何去实现我会在对应章节讲解。&lt;/p&gt;
&lt;h3&gt;注册指令&lt;/h3&gt;
&lt;p&gt;不知道你有没有从刚刚的示例中发现一个问题：我们从未正式说明过这个类是&lt;code&gt;/demo&lt;/code&gt;这一个命令的实现。bukkit当然猜不出这个类是属于哪一个指令的，因此，我们还需要注册指令。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/Paperdemo.java
package top.tachibana.paperdemo;

import org.bukkit.plugin.java.JavaPlugin;

public final class Paperdemo extends JavaPlugin {
    @Override
    public void onEnable() {
        //注册事件
        this.getServer().getPluginManager().registerEvents(new PaperdemoListener(), this);
        //注册指令
        this.getCommand(&quot;demo&quot;).setExecutor(new PaperdemoCommand());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;构建后重载插件，新的指令便能被使用了。&lt;/p&gt;
&lt;h3&gt;自动补全&lt;/h3&gt;
&lt;p&gt;光有这些代码仍然不够，在游戏内测试后我们不难发现一个问题：该命令的参数不能自动补全，玩家不能方便地使用我们的命令。为了实现自动补全的功能，我们还需要定义一个类。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/PaperdemoCommand.java
package top.tachibana.paperdemo;

import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
//import ...
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;

public class PaperdemoCommand implements CommandExecutor, TabCompleter{
    // ...
    @Override
    public boolean onCommand(
            @NotNull CommandSender sender, 
            @NotNull Command command, 
            @NotNull String label, 
            @NotNull String[] args){
        // ...
    }
    @Override
    public @Nullable List&amp;#x3C;String&gt; onTabComplete(
            @NotNull CommandSender sender,
            @NotNull Command command,
            @NotNull String label,
            @NotNull String[] args){
        //在这里实现自动补全
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;请注意相比于之前的代码，我们的类还额外实现了一个接口&lt;code&gt;TabCompleter&lt;/code&gt;。为了实现该接口我们定义了一个叫做&lt;code&gt;onTabComplete()&lt;/code&gt;的方法，它的返回值类型是&lt;code&gt;List&amp;#x3C;String&gt;&lt;/code&gt;，并且有&lt;code&gt;@Nullable&lt;/code&gt;注解，这个返回值便代表着自动补全的内容，而它的参数与我们先前定义的方法&lt;code&gt;onCommand()&lt;/code&gt;一模一样。&lt;/p&gt;
&lt;p&gt;以下是一种简单的实现。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/PaperdemoCommand.java
package top.tachibana.paperdemo;

import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
//import ...
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.stream.Collectors;

public class PaperdemoCommand implements CommandExecutor, TabCompleter{
    private final List&amp;#x3C;String&gt; sub_command = List.of(&quot;print&quot;, &quot;suicide&quot;, &quot;spawn&quot;, &quot;msg&quot;);
    private final List&amp;#x3C;String&gt; sub_command_msg = List.of(&quot;join&quot;, &quot;leave&quot;);
    // ...
    @Override
    public boolean onCommand(
            @NotNull CommandSender sender, 
            @NotNull Command command, 
            @NotNull String label, 
            @NotNull String[] args){
        // ...
    }
    @Override
    public @Nullable List&amp;#x3C;String&gt; onTabComplete(
            @NotNull CommandSender sender,
            @NotNull Command command,
            @NotNull String label,
            @NotNull String[] args){
        switch (args.length) {
            case 1:
                if (args[0].isEmpty()) return sub_command;
                else return sub_command.stream().filter(s -&gt; s.startsWith(args[0])).collect(Collectors.toList());
            case 2:
                if (args[0].equals(&quot;msg&quot;)) {
                    if (args[1].isEmpty()) return sub_command_msg;
                    else
                        return sub_command_msg.stream().filter(s -&gt; s.startsWith(args[1])).collect(Collectors.toList());
                }
                else return null;
            default:
                return null;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;实体&lt;/h2&gt;
&lt;p&gt;本章的内容较为简单，主要介绍包&lt;code&gt;org.bukkit.entity&lt;/code&gt;中的一部分类和方法。&lt;/p&gt;
&lt;h3&gt;实体接口&lt;/h3&gt;
&lt;p&gt;一切实体都直接或者间接地继承了接口&lt;code&gt;Entity&lt;/code&gt;。接口&lt;code&gt;Entity&lt;/code&gt;实现了一些基本的方法。&lt;/p&gt;
&lt;p&gt;下面的代码演示了实体（&lt;code&gt;Player&lt;/code&gt;继承自实体，因此也属于实体）的传送功能。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/PaperdemoListener.java
package top.tachibana.paperdemo;

import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.player.*;
import org.bukkit.inventory.ItemStack;

import java.util.Collection;
import java.util.concurrent.ThreadLocalRandom;

public final class PaperdemoListener implements Listener {
    // ...
    @EventHandler
    public void onItemUse(PlayerInteractEvent event){
        // 左键 传送至附近的随机一个实体
        if(event.getItem() != null &amp;#x26;&amp;#x26; event.getItem().getType() == Material.STICK){
            Player player = event.getPlayer();
            if(event.getAction() == Action.LEFT_CLICK_AIR){
                Collection&amp;#x3C;Entity&gt; entities = player.getLocation().getNearbyEntities(32, 32, 32);
                if(!entities.isEmpty()){
                    Entity entity = entities.stream().skip(ThreadLocalRandom.current().nextInt(entities.size())).findFirst().get();
                    player.teleport(entity);
                    player.sendMessage(Component.text(&quot;已传送至 &quot; + entity.getName(), NamedTextColor.YELLOW));
                }
                else{
                    player.sendMessage(Component.text(&quot;你附近暂无实体&quot;, NamedTextColor.YELLOW));
                }
            }
            // 右键 传送至床的位置
            else if(event.getAction() == Action.RIGHT_CLICK_AIR){
                Location respawn = player.getRespawnLocation();
                if(respawn != null){
                    player.teleport(respawn);
                    player.sendMessage(Component.text(&quot;已传送至重生点&quot;, NamedTextColor.YELLOW));
                }
                else{
                    player.sendMessage(Component.text(&quot;你的床或重生锚被破坏&quot;, NamedTextColor.RED));
                }
            }
        }
    }
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我演示了方法&lt;code&gt;Entity.teloport&lt;/code&gt;的两种用法，一种接受一个&lt;code&gt;Location&lt;/code&gt;的实例，即传送至那个位置，另一种接受另一个&lt;code&gt;Entity&lt;/code&gt;的实例，即传送至那个实体的位置。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Entity&lt;/code&gt;中有丰富的功能被实现，由于篇幅限制本篇文章无法一一列举，你可以阅览&lt;a href=&quot;https://hub.spigotmc.org/javadocs/spigot/org/bukkit/entity/Entity.html&quot;&gt;相关文档&lt;/a&gt;了解更多。&lt;/p&gt;
&lt;h3&gt;玩家接口&lt;/h3&gt;
&lt;p&gt;对于方法&lt;code&gt;sendMessage()&lt;/code&gt;，或许你之前已经见过，它是接口&lt;code&gt;Player&lt;/code&gt;的一个方法。&lt;/p&gt;
&lt;p&gt;在上一个例子中，我们也用到了方法&lt;code&gt;sendMessage()&lt;/code&gt;。它有许多重载，我们用的是&lt;code&gt;Adventure API&lt;/code&gt;提供的接口&lt;code&gt;Component&lt;/code&gt;来渲染消息。&lt;/p&gt;
&lt;p&gt;让我们再认识一个方法，&lt;code&gt;setGameMode()&lt;/code&gt;可以改变玩家的游戏模式。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/PaperdemoListener.java
package top.tachibana.paperdemo;

import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.*;
//import ...

public final class PaperdemoListener implements Listener {
    // ...
    @EventHandler
    public void onGamemodeChange(PlayerGameModeChangeEvent event){
        if(event.getNewGameMode() == GameMode.ADVENTURE){
            event.setCancelled(true);
            event.getPlayer().setGameMode(GameMode.SPECTATOR);
        }
    }
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;GameMode&lt;/code&gt;是一个枚举常量，表示游戏模式。接口&lt;code&gt;Player&lt;/code&gt;（其实是它的父接口&lt;code&gt;HumanEntity&lt;/code&gt;）定义了方法&lt;code&gt;setGameMode()&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;其它实体&lt;/h3&gt;
&lt;p&gt;各种生物、盔甲架、物品展示框、掉落物等都属于实体，也有自己的接口。由于篇幅限制，我没法一一演示。如果你想了解具体的内容，可以阅读Bukkit API的Javadoc的&lt;a href=&quot;https://hub.spigotmc.org/javadocs/spigot/org/bukkit/entity/package-summary.html&quot;&gt;有关包&lt;code&gt;Entity&lt;/code&gt;的部分&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;消息组件&lt;/h2&gt;
&lt;p&gt;你可能想让插件输出的消息不再单调，有着丰富的颜色、不同的风格，甚至还能定义点击消息的行为。或许，你可以尝试使用消息组件。本章将介绍&lt;code&gt;Adventure API&lt;/code&gt;和&lt;code&gt;BungeeCord chat API&lt;/code&gt;，你可以只学习其中一种，本篇文章在其它地方所用到的消息组件均由&lt;code&gt;Adventure API&lt;/code&gt;提供。&lt;/p&gt;
&lt;h3&gt;使用Adventure API&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://jd.advntr.dev/api/4.19.0/&quot;&gt;Adventure API&lt;/a&gt;提供了丰富的方式让我们渲染消息。下面我将用之前实现的&lt;code&gt;/demo msg&lt;/code&gt;命令来做文章，演示如何使用&lt;code&gt;Adventure API&lt;/code&gt;的相关内容。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/PaperdemoCommand.java
package top.tachibana.paperdemo;

import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
//import ...
import org.jetbrains.annotations.NotNull;

public class PaperdemoCommand implements CommandExecutor, TabCompleter {
    // 实现一个指令，有print, suicide, spawn, msg四个子命令
    // ...
    @Override
    public boolean onCommand(
            @NotNull CommandSender sender,
            @NotNull Command command,
            @NotNull String label,
            @NotNull String[] args){
        if(args.length == 0) return false;
        switch (args[0]){
            // ...
            case &quot;msg&quot;:
                if(args.length == 1){
                    // 使用Adventure API
                    final Component text1 = Component
                            .text(&quot;这是一条消息，&quot;, NamedTextColor.GREEN, TextDecoration.ITALIC)
                            .append(Component.text(&quot;这是第二句话&quot;, TextColor.color(0x9966FF), TextDecoration.OBFUSCATED))
                            .append(Component.text(&quot;点我复制链接&quot;, NamedTextColor.YELLOW, TextDecoration.BOLD)
                                    .clickEvent(ClickEvent.copyToClipboard(&quot;https://syju.org/posts/mcplugin&quot;)))
                            .append(Component.text(&quot;点我打开链接&quot;, NamedTextColor.AQUA, TextDecoration.UNDERLINED)
                                    .clickEvent(ClickEvent.openUrl(&quot;https://github.com/f0xea/paperdemo&quot;)))
                            .append(Component.text(&quot;指针拖动到我上面试试&quot;, NamedTextColor.GREEN, TextDecoration.STRIKETHROUGH)
                                    .hoverEvent(HoverEvent.showText(Component.text(&quot;你真拖啊&quot;, NamedTextColor.DARK_RED, TextDecoration.STRIKETHROUGH))));
                    sender.sendMessage(text1);
                    return true;
                }
                // ...
            default:
                return false;
        }
    }
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用效果如下。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://syju.org/_astro/text1.GJHE_MYe_ZqiekS.webp&quot; alt=&quot;text1&quot; title=&quot;text1&quot;&gt;&lt;/p&gt;
&lt;p&gt;关于&lt;code&gt;Adventure API&lt;/code&gt;的具体教程可以查看&lt;a href=&quot;https://docs.advntr.dev/text.html&quot;&gt;官方文档&lt;/a&gt;。&lt;/p&gt;
&lt;h3&gt;使用MiniMessage&lt;/h3&gt;
&lt;p&gt;使用&lt;a href=&quot;https://jd.advntr.dev/text-minimessage/4.19.0/&quot;&gt;MiniMessage&lt;/a&gt;可以&lt;strong&gt;更加方便&lt;/strong&gt;地格式化输出。&lt;/p&gt;
&lt;p&gt;MiniMessage的语法格式有点像HTML，具体请查看&lt;a href=&quot;https://docs.advntr.dev/minimessage/format.html#&quot;&gt;官方文档&lt;/a&gt;。下面我简单演示了MiniMessage的用法。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/PaperdemoCommand.java
package top.tachibana.paperdemo;

import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
//import ...
import org.jetbrains.annotations.NotNull;

public class PaperdemoCommand implements CommandExecutor, TabCompleter {
    // 实现一个指令，有print, suicide, spawn, msg四个子命令
    // ...
    @Override
    public boolean onCommand(
            @NotNull CommandSender sender,
            @NotNull Command command,
            @NotNull String label,
            @NotNull String[] args){
        if(args.length == 0) return false;
        switch (args[0]){
            // ...
            case &quot;msg&quot;:
                if(args.length == 1){
                    // 使用Adventure API
                    //...
                    final Component text2 = MiniMessage.miniMessage().deserialize(
                            &quot;使用&amp;#x3C;blue&gt;&amp;#x3C;click:open_url:\&quot;https://jd.advntr.dev/text-minimessage/4.19.0/\&quot;&gt;MiniMessage&amp;#x3C;/click&gt;&amp;#x3C;/blue&gt;可以&amp;#x3C;bold&gt;更加方便&amp;#x3C;/bold&gt;地格式化输出。\n&quot; +
                                    &quot;MiniMessage的语法格式有点像HTML，具体请查看官方文档。下面我简单演示了MiniMessage的用法。\n&quot; +
                                    &quot;&amp;#x3C;rainbow&gt;Rainbow是彩虹，彩虹是Rainbow&amp;#x3C;/rainbow&gt;&quot;
                    );
                    sender.sendMessage(text1);
                    sender.sendMessage(text2);
                    return true;
                }
                // ...
            default:
                return false;
        }
    }
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;效果如下。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://syju.org/_astro/text2.D1crZmX5_2kt93k.webp&quot; alt=&quot;text2&quot; title=&quot;text2&quot;&gt;&lt;/p&gt;
&lt;p&gt;你也可以使用接口&lt;code&gt;CommandSender&lt;/code&gt;的方法&lt;code&gt;sendRichMessage()&lt;/code&gt;。以上代码等价于，&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/PaperdemoCommand.java
package top.tachibana.paperdemo;

//import ...
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
//import ...
import org.jetbrains.annotations.NotNull;

public class PaperdemoCommand implements CommandExecutor, TabCompleter {
    // 实现一个指令，有print, suicide, spawn, msg四个子命令
    // ...
    @Override
    public boolean onCommand(
            @NotNull CommandSender sender,
            @NotNull Command command,
            @NotNull String label,
            @NotNull String[] args){
        if(args.length == 0) return false;
        switch (args[0]){
            // ...
            case &quot;msg&quot;:
                if(args.length == 1){
                    // 使用Adventure API
                    //...
                    sender.sendMessage(text1);
                    sender.sendMessage(text2);
                    sender.sendRichMessage(
                            &quot;使用&amp;#x3C;blue&gt;&amp;#x3C;click:open_url:\&quot;https://jd.advntr.dev/text-minimessage/4.19.0/\&quot;&gt;MiniMessage&amp;#x3C;/click&gt;&amp;#x3C;/blue&gt;可以&amp;#x3C;bold&gt;更加方便&amp;#x3C;/bold&gt;地格式化输出。\n&quot; +
                                    &quot;MiniMessage的语法格式有点像HTML，具体请查看官方文档。下面我简单演示了MiniMessage的用法。\n&quot; +
                                    &quot;&amp;#x3C;rainbow&gt;Rainbow是彩虹，彩虹是Rainbow&amp;#x3C;/rainbow&gt;&quot;
                    );
                    return true;
                }
                // ...
            default:
                return false;
        }
    }
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;使用BungeeCord chat API&lt;/h3&gt;
&lt;p&gt;除了&lt;code&gt;Adventure API&lt;/code&gt;，Bungee提供的&lt;a href=&quot;https://javadoc.io/doc/net.md-5/bungeecord-chat/1.16-R0.3/overview-summary.html&quot;&gt;BungeeCord chat API&lt;/a&gt;也能实现消息渲染。&lt;/p&gt;
&lt;p&gt;因为刚刚介绍的&lt;code&gt;Adventure API&lt;/code&gt;和&lt;code&gt;BungeeCord chat API&lt;/code&gt;有许多相同的标识符，所以尽量不要将混用。为此，我新建了一个指令&lt;code&gt;/msg&lt;/code&gt;，用于打印&lt;code&gt;BungeeCord chat API&lt;/code&gt;渲染的消息。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/MsgCommand.java
package top.tachibana.paperdemo;

import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.ClickEvent;
import net.md_5.bungee.api.chat.TextComponent;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;

public class MsgCommand implements CommandExecutor {
    @Override
    public boolean onCommand(
            @NotNull CommandSender sender,
            @NotNull Command command,
            @NotNull String label,
            @NotNull String[] args){
        // 使用BungeeCord chat API
        TextComponent text1 = new TextComponent(&quot;这是&quot;);
        TextComponent text2 = new TextComponent(&quot;一条消息\n&quot;);
        TextComponent text3 = new TextComponent(&quot;这是&quot;);
        TextComponent text4 = new TextComponent(&quot;作者的博客链接&quot;);

        text2.setColor(ChatColor.DARK_RED);
        text3.setBold(true);
        text3.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, &quot;https://syju.org/&quot;));

        sender.sendMessage(text1, text2, text3, text4);
        return true;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;物品栏&lt;/h2&gt;
&lt;p&gt;通过&lt;code&gt;Bukkit API&lt;/code&gt;提供的丰富的有关物品栏的类和方法，我们可以实现许多功能，比如菜单、原地打开末影箱等。&lt;/p&gt;
&lt;h3&gt;打开物品栏&lt;/h3&gt;
&lt;p&gt;我们演示这么一个功能：当玩家按下 Shift + F 时打开末影箱。&lt;code&gt;Bukkit API&lt;/code&gt;没有提供玩家按下键盘上的哪个键触发的事件（因为这个完全由客户端决定），但是我们可以监听玩家交换主手和副手的事件，如果此时玩家是下蹲状态，那么便可以运行打开末影箱的代码。于是，我们这么编写事件监听器。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/PaperdemoListener.java
package top.tachibana.paperdemo;

import org.bukkit.entity.*;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.*;
//import ...

public final class PaperdemoListener implements Listener {
    // ...
    @EventHandler
    public void onSwapHand(PlayerSwapHandItemsEvent event){
        HumanEntity player = event.getPlayer();
        if(player.isSneaking()){
            event.setCancelled(true);
            player.openInventory(player.getEnderChest());
        }
    }
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;openInventory()&lt;/code&gt;是对象&lt;code&gt;player&lt;/code&gt;的一个成员方法，用于打开玩家的背包，它的唯一参数是一个&lt;code&gt;org.bukkit.inventory.Iventory&lt;/code&gt;的实例，&lt;code&gt;player.getEnderChest()&lt;/code&gt;则返回了对应玩家的末影箱实例。&lt;/p&gt;
&lt;h3&gt;创建物品栏&lt;/h3&gt;
&lt;p&gt;考虑下面的代码，它与上面的代码相似，但是只会在蹲下是丢东西触发，它能打开一个自定义的菜单。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/PaperdemoListener.java
package top.tachibana.paperdemo;

import org.bukkit.entity.*;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.*;
//import ...

public final class PaperdemoListener implements Listener {
    // ...
    @EventHandler
    public void onDropItem(PlayerDropItemEvent event){
        if(event.getPlayer().isSneaking()){
            event.setCancelled(true);
            event.getPlayer().openInventory(Paperdemo.getMenu());
        }
    }
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;菜单呢？我们通过&lt;code&gt;Server&lt;/code&gt;实例的成员方法&lt;code&gt;createInventory()&lt;/code&gt;创建。为了方便演示，我将这个代码写在了主类，然后通过&lt;code&gt;Paperdemo.getMenu()&lt;/code&gt;获取这个菜单。&lt;/p&gt;
&lt;p&gt;我们先写下private static变量&lt;code&gt;menu&lt;/code&gt;的getter，这是面向对象编程中的常规操作&lt;strong&gt;封包&lt;/strong&gt;（Encapsulation）。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/Paperdemo.java
package top.tachibana.paperdemo;

import org.bukkit.inventory.Inventory;
import org.bukkit.plugin.java.JavaPlugin;
// import ...

public final class Paperdemo extends JavaPlugin {
    // ...
    private static Inventory menu;
    public static Inventory getMenu() {
        return menu;
    }
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后编写一个方法用于创建菜单的物品栏，这里是&lt;code&gt;loadMenu()&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/Paperdemo.java
package top.tachibana.paperdemo;

import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.Inventory;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.plugin.java.JavaPlugin;
// import ...

public final class Paperdemo extends JavaPlugin {
    // ...
    private ItemStack createItemStack(Material material, Component displayName){
        ItemStack itemStack = new ItemStack(material);
        itemStack.editMeta((meta) -&gt; {
            meta.displayName(displayName);
        });
        return itemStack;
    }
    private void loadMenu(){
        Paperdemo.menu = instance.getServer().createInventory(
                null, InventoryType.CHEST, Component.text(&quot;插件菜单&quot;, NamedTextColor.DARK_PURPLE)
        );
        menu.addItem(
                createItemStack(Material.GRASS_BLOCK, Component.text(&quot;创造模式&quot;, NamedTextColor.GREEN)),
                createItemStack(Material.IRON_SWORD, Component.text(&quot;生存模式&quot;, NamedTextColor.WHITE)),
                createItemStack(Material.MAP, Component.text(&quot;冒险模式&quot;, NamedTextColor.DARK_RED)),
                createItemStack(Material.ENDER_EYE, Component.text(&quot;旁观模式&quot;, NamedTextColor.BLUE)),
                createItemStack(Material.BARRIER, Component.text(&quot;退出菜单&quot;, NamedTextColor.RED))
        );
    }
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的内容有点多，我会慢慢讲。注意方法&lt;code&gt;loadMenu()&lt;/code&gt;，&lt;code&gt;instance&lt;/code&gt;是一个主类的private变量，表示对象的实例，因为我们在主类直接编写这个方法，所以可以直接调用这个对象。&lt;/p&gt;
&lt;p&gt;我们通过对象&lt;code&gt;Server&lt;/code&gt;的成员方法&lt;code&gt;createInventory()&lt;/code&gt;创建物品栏.第一个参数是物品栏的主人（类型是接口&lt;code&gt;InventoryHolder&lt;/code&gt;，不只是玩家，也可以是村民或者其它能打开物品栏的实体），第二个参数是枚举常量&lt;code&gt;InventoryType&lt;/code&gt;的值（&lt;a href=&quot;#%E7%89%A9%E5%93%81%E6%A0%8F&quot;&gt;刚刚&lt;/a&gt;已经介绍过了）,第三个参数是可选的，它的类型是消息组件&lt;code&gt;Component&lt;/code&gt;，表示物品栏的标题，会显示在GUI的左上角。&lt;/p&gt;
&lt;p&gt;我们再来看&lt;code&gt;menu&lt;/code&gt;的成员方法&lt;code&gt;addItem()&lt;/code&gt;，它的参数为若干个&lt;code&gt;ItemStack&lt;/code&gt;的对象，这个函数会按照你的传参顺序将物品依次填入这个物品栏。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;createItemStack()&lt;/code&gt;是一个自定义方法，它接受一个枚举常量&lt;code&gt;Material&lt;/code&gt;和消息组件&lt;code&gt;Component&lt;/code&gt;，用于创建一个指定名字的物品堆栈，这样我们才知道每个物品有什么功能。&lt;/p&gt;
&lt;p&gt;这里介绍一下&lt;code&gt;ItemStack&lt;/code&gt;的实例的成员方法&lt;code&gt;editMeta()&lt;/code&gt;，它用于修改物品堆栈的元数据，我们可以通过这个方法来修改物品显示的名字。它接受的参数是&lt;code&gt;Consumer&amp;#x3C;? super ItemMeta&gt;&lt;/code&gt;，我们可以传入一个lambda函数，它的参数&lt;code&gt;meta&lt;/code&gt;是一个&lt;code&gt;ItemMeta&lt;/code&gt;类型的对象，然后我们调用其成员方法&lt;code&gt;displayName()&lt;/code&gt;（参数是一个消息组件&lt;code&gt;Component&lt;/code&gt;）便可修改它的显示名。&lt;/p&gt;
&lt;p&gt;接下来，让我们在方法&lt;code&gt;onEnable&lt;/code&gt;中调用这个方法，用来创建我们的菜单的物品栏。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/Paperdemo.java
package top.tachibana.paperdemo;

import org.bukkit.plugin.java.JavaPlugin;
// import ...

public final class Paperdemo extends JavaPlugin {
    // ...
    public void onEnable() {
        // ...
        // 加载插件菜单
        loadMenu();
        // ...
    }
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;效果如下，&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://syju.org/_astro/%E6%8F%92%E4%BB%B6%E8%8F%9C%E5%8D%95.Ce2RkZmM_1MPlT6.webp&quot; alt=&quot;插件菜单&quot; title=&quot;插件菜单&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://syju.org/_astro/%E6%8F%92%E4%BB%B6%E8%8F%9C%E5%8D%952.CIkjB1Tm_9iuyf.webp&quot; alt=&quot;插件菜单2&quot; title=&quot;插件菜单2&quot;&gt;&lt;/p&gt;
&lt;h3&gt;一个菜单的例子&lt;/h3&gt;
&lt;p&gt;我们刚才分明是在创建一个花里胡哨的箱子，它没有菜单的功能，甚至玩家还能把物品从中拿出来！别担心，通过事件监听器，我们可以实现一个真正的菜单。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/PaperdemoListener.java
package top.tachibana.paperdemo;

import org.bukkit.entity.*;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.Inventory;
import org.bukkit.event.inventory.InventoryClickEvent;
//import ...

public final class PaperdemoListener implements Listener {
    // ...
    @EventHandler
    public void onInventoryClick(InventoryClickEvent event){
        if(event.getClickedInventory() == Paperdemo.getMenu()){
            Inventory menu = Paperdemo.getMenu();
            HumanEntity player = event.getWhoClicked();
            event.setCancelled(true);
            ItemStack item = menu.getItem(event.getSlot());
            if(item == null){
                return;
            }
            switch (item.getType()){
                case GRASS_BLOCK -&gt; player.setGameMode(GameMode.CREATIVE);
                case IRON_SWORD -&gt; player.setGameMode(GameMode.SURVIVAL);
                case MAP -&gt; player.setGameMode(GameMode.ADVENTURE);
                case ENDER_EYE -&gt; player.setGameMode(GameMode.SPECTATOR);
                case BARRIER -&gt; player.closeInventory();
            }
        }
    }
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们通过监听玩家点击物品栏的物品槽来实现菜单，当玩家点击菜单的物品时，先取消这个事件（这样无论如何玩家都无法拿出物品栏的物品啦），然后通过&lt;code&gt;switch case&lt;/code&gt;判断玩家的操作，最后执行对应的内容。&lt;/p&gt;
&lt;p&gt;前四个选项都是改变玩家的游戏模式，方法&lt;code&gt;setGameMode()&lt;/code&gt;我们在&lt;a href=&quot;#%E7%8E%A9%E5%AE%B6%E6%8E%A5%E5%8F%A3&quot;&gt;玩家接口&lt;/a&gt;已经讲过。&lt;/p&gt;
&lt;p&gt;第五个选项是关闭当前菜单，我们通过接口&lt;code&gt;HumanEntity&lt;/code&gt;的方法&lt;code&gt;closeInventory()&lt;/code&gt;实现。&lt;/p&gt;
&lt;p&gt;现在，我们完成了真正意义上的菜单，它可以很方便的改变游戏模式（能有&lt;code&gt;F3+F4&lt;/code&gt;方便吗？）。&lt;/p&gt;
&lt;h2&gt;配方&lt;/h2&gt;
&lt;p&gt;基于Bukkit API，我们可以为游戏添加合成、熔炉、高炉、烟熏炉、锻造台、营火（~~多少人不知道营火其实也有配方~~）、切石机、交易配方。添加配方所使用到的类和方法都在包&lt;code&gt;org.bukkit.inventory&lt;/code&gt;下。我们先来看最常用的合成配方。&lt;/p&gt;
&lt;h3&gt;合成&lt;/h3&gt;
&lt;p&gt;根据&lt;a href=&quot;https://zh.minecraft.wiki/w/%E9%85%8D%E6%96%B9#%E5%90%88%E6%88%90%E9%85%8D%E6%96%B9&quot;&gt;mcwiki&lt;/a&gt;的相关内容，合成配方主要分为有序配方（Shaped Recipe）和无序配方（Shapeless Recipe）。其它类型的配方难以也不建议用插件实现，本节内容只演示最常见的有序配方和无序配方。&lt;/p&gt;
&lt;h4&gt;无序配方&lt;/h4&gt;
&lt;p&gt;相较于有序配方，无序配方较为简单，无序配方&lt;strong&gt;不关心&lt;/strong&gt;物品的摆放顺序，只关心有多少个物品参与合成。例如，下界合金锭只需要四个金锭和四个下界合金碎片合成，你可以将八个材料摆成任意形状。假设我们想让一个烈焰棒和两个末影珍珠合成两个末影之眼。&lt;/p&gt;
&lt;p&gt;为了创建一个无序合成的配方，我们只需要在主类的方法&lt;code&gt;onEnable()&lt;/code&gt;添加以下代码。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 烈焰棒 + 2 * 末影珍珠 合成 2 * 末影之眼
ShapelessRecipe blazeRodToEnderEye = new ShapelessRecipe(
        new NamespacedKey(this, &quot;blaze_rod_to_ender_eye&quot;),
        new ItemStack(Material.ENDER_EYE, 2))
        .addIngredient(Material.BLAZE_ROD)
        .addIngredient(2, Material.ENDER_PEARL);
this.getServer().addRecipe(blazeRodToEnderEye);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个代码虽然只有两行（准确来说是两条语句），但是理解起来并不简单。我们&lt;code&gt;new&lt;/code&gt;了一个&lt;code&gt;ShapelessRecipe&lt;/code&gt;的对象，它的构造器有两个参数，第一个参数是类&lt;code&gt;NamespacedKey&lt;/code&gt;的实例（这为我们的配方创建ID，它的构造器也有两个参数，分别是插件的实例和配方的ID），第二个参数是一个&lt;code&gt;ItemStack&lt;/code&gt;的实例（老面孔了，你应该熟悉吧），代表着合成后的产物。&lt;/p&gt;
&lt;p&gt;我们调用这个类的方法&lt;code&gt;addIngredient()&lt;/code&gt;添加合成配方的物品，它的参数是&lt;code&gt;Material&lt;/code&gt;的枚举常量。因为原材料是一个烈焰棒和两个末影珍珠，所以我们调用了两次&lt;code&gt;addIngredient()&lt;/code&gt;。注意，&lt;code&gt;addIngredient()&lt;/code&gt;的返回值是你调用的对象的引用，因此我们便可以这种方式编写代码。&lt;/p&gt;
&lt;p&gt;第二条语句我们调用了方法&lt;code&gt;addRecipe()&lt;/code&gt;注册了这个配方。&lt;/p&gt;
&lt;h4&gt;有序配方&lt;/h4&gt;
&lt;p&gt;当我们合成木镐时，一定要将木板排成一行，如果没有有序配方，工作台无法区分木镐与木斧。在这一节中我们将创建一个有序配方。&lt;/p&gt;
&lt;p&gt;众所周知1.9之后原版无法合成附魔金苹果，我想利用插件复刻老版本的合成配方。与无序合成一样，我们需要先在主类的方法&lt;code&gt;onEnable()&lt;/code&gt;创建一个配方类。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 附魔金苹果的合成配方
// | B | B | B |
// | B | A | B |
// | B | B | B |
// B: 金块
// A: 苹果
ShapedRecipe enchantedGoldenApple = new ShapedRecipe(
        new NamespacedKey(Paperdemo.getInstance(), &quot;craft_enchanted_golden_apple&quot;),
        new ItemStack(Material.ENCHANTED_GOLDEN_APPLE))
        .shape(&quot;BBB&quot;, &quot;BAB&quot;, &quot;BBB&quot;)
        .setIngredient(&apos;A&apos;, Material.APPLE)
        .setIngredient(&apos;B&apos;, Material.GOLD_BLOCK);
this.getServer().addRecipe(enchantedGoldenApple);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有序合成的类的构造器与无序合成的一样，在此不多赘述。如果你写过数据包，那么你一定对这种写法很熟悉。这个类的方法&lt;code&gt;shape()&lt;/code&gt;的三个参数分别表示合成的形状的每一&lt;strong&gt;行&lt;/strong&gt;，它由一些字符构成，不同的字符代表不同的材料，我们把它叫做键（key），例如在这里， &lt;em&gt;B&lt;/em&gt; 代表金块（Block of Gold）， &lt;em&gt;A&lt;/em&gt; 代表苹果（Apple）。方法&lt;code&gt;setIngredient()&lt;/code&gt;则将不同的键与材料连接在了一起，它的两个参数分别是键（注意是&lt;code&gt;char&lt;/code&gt;类型而不是&lt;code&gt;String&lt;/code&gt;）和材料类型（注意是&lt;code&gt;Material&lt;/code&gt;类型而不是&lt;code&gt;ItemStack&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://syju.org/_astro/%E9%99%84%E9%AD%94%E9%87%91%E8%8B%B9%E6%9E%9C.Dq9ZsV_1_Z785wF.webp&quot; alt=&quot;附魔金苹果&quot; title=&quot;附魔金苹果&quot;&gt;&lt;/p&gt;
&lt;h3&gt;优化项目结构&lt;/h3&gt;
&lt;p&gt;将配方相关的代码直接放在方法&lt;code&gt;onEnable()&lt;/code&gt;并不是一个好选择。为了提高代码的可读性，我们期望的是，有一个专门的类实现这些配方，我们仅需要在方法&lt;code&gt;onEnable()&lt;/code&gt;内注册这些配方。&lt;/p&gt;
&lt;p&gt;我们新建了一个叫做&lt;code&gt;PaperdemoRecipe&lt;/code&gt;的类，它提供了一系列的静态方法，能返回这些配方的对象的列表。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/PaperdemoRecipe.java
package top.tachibana.paperdemo;

import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.inventory.*;

import java.util.List;

public class PaperdemoRecipe {
    public static List&amp;#x3C;ShapedRecipe&gt; getShapedRecipe(){
        // 附魔金苹果的合成配方
        // | B | B | B |
        // | B | A | B |
        // | B | B | B |
        // B: 金块
        // A: 苹果
        ShapedRecipe enchantedGoldenApple = new ShapedRecipe(
                new NamespacedKey(Paperdemo.getInstance(), &quot;craft_enchanted_golden_apple&quot;),
                new ItemStack(Material.ENCHANTED_GOLDEN_APPLE))
                .shape(&quot;BBB&quot;, &quot;BAB&quot;, &quot;BBB&quot;)
                .setIngredient(&apos;A&apos;, Material.APPLE)
                .setIngredient(&apos;B&apos;, Material.GOLD_BLOCK);
        return List.of(
                enchantedGoldenApple
        );
    }
    public static List&amp;#x3C;ShapelessRecipe&gt; getShapelessRecipe() {
        // 烈焰棒 + 2 * 末影珍珠 合成 2 * 末影之眼
        ShapelessRecipe blazeRodToEnderEye = new ShapelessRecipe(
                new NamespacedKey(Paperdemo.getInstance(), &quot;blaze_rod_to_ender_eye&quot;),
                new ItemStack(Material.ENDER_EYE, 2))
                .addIngredient(Material.BLAZE_ROD)
                .addIngredient(2, Material.ENDER_PEARL);
        return List.of(
                blazeRodToEnderEye,
        );
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在&lt;code&gt;onEnable()&lt;/code&gt;中用&lt;code&gt;forEach()&lt;/code&gt;作用于&lt;code&gt;getShapedRecipe()&lt;/code&gt;和&lt;code&gt;getShapelessRecipe&lt;/code&gt;返回的列表的每一个元素。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/Paperdemo.java
// ...
    @Override
    public void onEnable() {
        // ...
        PaperdemoRecipe.getShapedRecipe().forEach(this.getServer()::addRecipe);
        PaperdemoRecipe.getShapelessRecipe().forEach(this.getServer()::addRecipe);
        // ...
    }
// ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么不选择直接让&lt;code&gt;PaperdemoRecipe&lt;/code&gt;的静态方法返回某个配方类的实例，而是返回一个列表再使用&lt;code&gt;forEach()&lt;/code&gt;遍历呢？这是因为当你以后想要为你的插件添加该种类的配方时，你便无需再次注册配方，且这种写法更加简洁。&lt;/p&gt;
&lt;h3&gt;多种类的输入&lt;/h3&gt;
&lt;p&gt;部分物品合成配方的不仅仅支持一种材料类型，例如箱子，可以由任意种类的木板环绕一圈合成，难道我们需要为每种木板都创建一个配方么？其实可以借助接口&lt;code&gt;RecipeChoice&lt;/code&gt;来简单地实现我们的需求。&lt;/p&gt;
&lt;p&gt;mc中的铜块暴露在空气中会逐渐被氧化，一旦铜块被氧化，就无法像铜块那样分解成铜锭。如果我想要改变这一特性，正常来说我需要添加三种合成配方（如果算上涂蜡的话是六种）。但是借助接口&lt;code&gt;RecipeChoice&lt;/code&gt;，我们只需要创建一种配方。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/PaperdemoRecipe.java
package top.tachibana.paperdemo;

import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.inventory.*;

import java.util.List;

public class PaperdemoRecipe {
    // ...
    public static List&amp;#x3C;ShapelessRecipe&gt; getShapelessRecipe() {
        // ...
        // 所有类型的铜块都能分解成9 * 铜锭
        RecipeChoice.MaterialChoice choice = new RecipeChoice.MaterialChoice(
                List.of(
                        //Material.COPPER_BLOCK, 原版存在，无需添加
                        Material.EXPOSED_COPPER,
                        Material.WEATHERED_COPPER,
                        Material.OXIDIZED_COPPER)
        );
        ShapelessRecipe copperBlockToIngot = new ShapelessRecipe(
                new NamespacedKey(Paperdemo.getInstance(), &quot;copper_block_to_ingot&quot;),
                new ItemStack(Material.COPPER_INGOT, 9)
        ).addIngredient(choice);
        return List.of(
                blazeRodToEnderEye,
                copperBlockToIngot
        );
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我先创建了一个&lt;code&gt;MaterialChoice&lt;/code&gt;的对象，它实现了接口&lt;code&gt;RecipeChoice&lt;/code&gt;，它的构造器是一个&lt;code&gt;Material&lt;/code&gt;枚举常量的列表，我们将分解成铜锭的三种氧化过了的铜块添加到这个列表中（你也可以将涂蜡的铜块也加入进去）。对于添加配方所调用的&lt;code&gt;addIngredient()&lt;/code&gt;、&lt;code&gt;setIngredient()&lt;/code&gt;等，基本都有对应的重载方法，能接受实现了接口&lt;code&gt;RecipeChoice&lt;/code&gt;的对象，因此我们可以将&lt;code&gt;choice&lt;/code&gt;当作一个&lt;code&gt;Material&lt;/code&gt;的枚举常量传入。&lt;/p&gt;
&lt;p&gt;得益于上一节我们做的努力，现在我们添加新的无序合成配方时不需要再此在方法&lt;code&gt;onEnable()&lt;/code&gt;中注册了，只需要在方法&lt;code&gt;getShapelessRecipe()&lt;/code&gt;返回的列表中添加新的配方对象。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://syju.org/_astro/%E6%96%91%E9%A9%B3%E7%9A%84%E9%93%9C%E5%9D%97.DqJVUdaa_25HDv5.webp&quot; alt=&quot;斑驳的铜块&quot; title=&quot;斑驳的铜块&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://syju.org/_astro/%E9%94%88%E8%9A%80%E7%9A%84%E9%93%9C%E5%9D%97.C1BQ0n6m_16sOCq.webp&quot; alt=&quot;锈蚀的铜块&quot; title=&quot;锈蚀的铜块&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://syju.org/_astro/%E6%B0%A7%E5%8C%96%E7%9A%84%E9%93%9C%E5%9D%97.CSc0GjYX_19EGPe.webp&quot; alt=&quot;氧化的铜块&quot; title=&quot;氧化的铜块&quot;&gt;&lt;/p&gt;
&lt;h3&gt;煅烧&lt;/h3&gt;
&lt;p&gt;本节将讲述如何添加熔炉、高炉、烟熏炉、营火、切石机的配方，前四种配方的添加方式完全一致，切石机的配方相比其它配方构造器的参数少了经验值和处理时间。它们对应的类分别是&lt;code&gt;FurnaceRecipe&lt;/code&gt;、&lt;code&gt;BlastingRecipe&lt;/code&gt;、&lt;code&gt;SmokingRecipe&lt;/code&gt;、&lt;code&gt;CampfireRecipe&lt;/code&gt;、&lt;code&gt;StonecuttingRecipe&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;下面用高炉来举例子，我们将创建一个配方，能将锁链煅烧成三个铁粒。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/PaperdemoRecipe.java
package top.tachibana.paperdemo;

import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.inventory.*;

import java.util.List;

public class PaperdemoRecipe {
    // ...
    public static List&amp;#x3C;BlastingRecipe&gt; getBlastingRecipe(){
        // 铁链煅烧成 3 * 铁粒
        BlastingRecipe chainToIronNugget = new BlastingRecipe(
                new NamespacedKey(Paperdemo.getInstance(), &quot;chain_to_iron_nugget&quot;),
                new ItemStack(Material.IRON_NUGGET, 3),
                Material.CHAIN,
                0.2f,
                100
        );
        return List.of(
                chainToIronNugget
        );
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在方法&lt;code&gt;onEnable()&lt;/code&gt;中添加这样的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/Paperdemo.java
// ...
    @Override
    public void onEnable() {
        // ...
        PaperdemoRecipe.getBlastingRecipe().forEach(this.getServer()::addRecipe);
        // ...
    }
// ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;BlastingRecipe&lt;/code&gt;的构造器的第一个参数和第二个参数基本和之前的配方类相同，第三个参数是烧炼的原材料，是一个&lt;code&gt;Material&lt;/code&gt;的枚举常量，第四个参数是烧炼一次所获得的经验值，是一个&lt;code&gt;float&lt;/code&gt;类型的浮点数，第五个参数是烧炼所消耗的时间，单位是游戏刻（tick）。&lt;/p&gt;
&lt;p&gt;值得一提的是，如果你创建的是切石机配方，即&lt;code&gt;StonecuttingRecipe&lt;/code&gt;，则不需要提供烧炼获得的经验值和烧炼所需的时间。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://syju.org/_astro/%E9%94%81%E9%93%BE.CKOpMcCt_Z17Xtob.webp&quot; alt=&quot;&amp;#x22;锁链&amp;#x22;&quot; title=&quot;锁链&quot;&gt;&lt;/p&gt;
&lt;h3&gt;交易&lt;/h3&gt;
&lt;p&gt;通过实例化类&lt;code&gt;MerchantRecipe&lt;/code&gt;来创建交易配方。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/PaperdemoRecipe.java
package top.tachibana.paperdemo;

import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.inventory.*;

import java.util.List;

public class PaperdemoRecipe {
    // ...
    public static List&amp;#x3C;MerchantRecipe&gt; getMerchantRecipe(){
        // 2 * 绿宝石 交易钻石
        MerchantRecipe merchantDiamond = new MerchantRecipe(
                new ItemStack(Material.DIAMOND),
                10
        );
        merchantDiamond.setIngredients(List.of(new ItemStack(Material.EMERALD, 2)));
        return List.of(
                merchantDiamond
        );
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;类&lt;code&gt;MerchantRecipe&lt;/code&gt;的构造器的第一个参数是交易获取的物品，也就是村民卖的东西，第二个参数是交易的最大次数，交易达到次数后会停止交易直到村民补货。&lt;/p&gt;
&lt;p&gt;我们通过方法&lt;code&gt;setIngredients()&lt;/code&gt;设置交易的物品，这个方法的参数&lt;strong&gt;长度不超过2&lt;/strong&gt;的类型为&lt;code&gt;List&amp;#x3C;ItemStack&gt;&lt;/code&gt;的变量。&lt;/p&gt;
&lt;p&gt;按照之前的方式添加配方你会发现你的配方似乎不生效。这是因为不同的村民有特定的配方。为了给指定的村民添加我们的配方，我写了一个简单的事件监听器（都看到这了不会不知道事件监听器是啥吧），当玩家用钻石对着职业为工具匠的村民右键时，村民的配方将会变成我们设置的配方。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/PaperdemoListener.java
package top.tachibana.paperdemo;

import org.bukkit.Material;
import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerInteractEntityEvent;
//import ...

public final class PaperdemoListener implements Listener {
    // ...
    @EventHandler
    public void onRightClick(PlayerInteractEntityEvent event){
        if(event.getPlayer().getInventory().getItemInMainHand().getType() == Material.DIAMOND){
            if(event.getRightClicked() instanceof Villager villager){
                if(villager.getProfession() == Villager.Profession.TOOLSMITH){
                    villager.setRecipes(PaperdemoRecipe.getMerchantRecipe());
                }
            }
        }
    }
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://syju.org/_astro/%E4%BA%A4%E6%98%93.J-2qxVle_23faQ0.webp&quot; alt=&quot;交易&quot; title=&quot;交易&quot;&gt;&lt;/p&gt;
&lt;h3&gt;锻造&lt;/h3&gt;
&lt;p&gt;添加锻造配方用到的对象与煅烧大同小异。我们先来看对应的类的构造器的类型声明。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;SmithingTransformRecipe(
        NamespacedKey key, 
        ItemStack result, 
        RecipeChoice template, 
        RecipeChoice base, 
        RecipeChoice addition){
            // ...
        }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以及&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;SmithingTrimRecipe(
        NamespacedKey key, 
        RecipeChoice template, 
        RecipeChoice base, 
        RecipeChoice addition){
            // ...
        }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;前者是锻造升级配方，也叫锻造转化（Smithing Transform）配方；后者是盔甲纹饰（Smithing Trim）配方。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://syju.org/_astro/%E9%94%BB%E9%80%A0.CegW2qMf_Z24yEMF.webp&quot; alt=&quot;&amp;#x22;锻造&amp;#x22;&quot; title=&quot;锻造&quot;&gt;&lt;/p&gt;
&lt;p&gt;以上图为例，形式参数&lt;code&gt;key&lt;/code&gt;是配方的ID，&lt;code&gt;template&lt;/code&gt;即为下界升级模板的槽位，&lt;code&gt;base&lt;/code&gt;即为钻石剑的槽位，&lt;code&gt;addition&lt;/code&gt;即为下界合金锭的槽位，&lt;code&gt;result&lt;/code&gt;即为下界合金剑的槽位，这在盔甲纹饰配方中不可用。值得注意的是，除了&lt;code&gt;key&lt;/code&gt;的类型是&lt;code&gt;NamespacedKey&lt;/code&gt;和&lt;code&gt;result&lt;/code&gt;的类型是&lt;code&gt;ItemStack&lt;/code&gt;以外，其余形式参数的类型均为&lt;code&gt;RecipeChoice&lt;/code&gt;，该接口在&lt;a href=&quot;#%E5%A4%9A%E7%A7%8D%E7%B1%BB%E7%9A%84%E8%BE%93%E5%85%A5&quot;&gt;之前&lt;/a&gt;已介绍过。&lt;/p&gt;
&lt;p&gt;按照之前说明过的添加其它配方的方式来添加锻造配方即可。记得要注册配方。&lt;/p&gt;
&lt;h2&gt;任务调度&lt;/h2&gt;
&lt;h3&gt;游戏刻&lt;/h3&gt;
&lt;p&gt;什么是刻？以下是&lt;a href=&quot;https://zh.minecraft.wiki/w/%E5%88%BB&quot;&gt;mcwiki&lt;/a&gt;对刻的解释。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;几乎所有的游戏（包括Minecraft）都由一个大的程序循环驱动，游戏内的计算遵照循环执行，按照固定的顺序依次被调用。当程序执行了一次循环，这个程序就进行了一次滴答（Tick），而计量滴答次数的单位就是刻（Tick）。在大多数情况下，刻不仅可以代表滴答的次数，也可以代表滴答本身。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如无特殊情况，本篇文章的刻均指&lt;strong&gt;游戏刻&lt;/strong&gt;，即游戏主进程的每一次循环。在mc中除了游戏刻以外，还有红石刻的概念，在此我们不会讨论。一般地，我们有&lt;/p&gt;
&lt;p&gt;$$
1 tick = 50 milliseconds
$$&lt;/p&gt;
&lt;p&gt;$$
20 ticks = 1 second
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TPS&lt;/strong&gt;（即Ticks per Second）的意思是每秒钟游戏运行多少刻，一般是20；&lt;strong&gt;MSPT&lt;/strong&gt;（即Milliseconds per Tick）的意思是每一刻需要运行多长时间，一般为50。&lt;/p&gt;
&lt;p&gt;在游戏中我们可以按 F3 + 2 查看当前的TPS。&lt;/p&gt;
&lt;h3&gt;创建一个任务&lt;/h3&gt;
&lt;p&gt;在我们编写插件的时候，可能会设计复杂的运行逻辑，可能有些代码并不会在事件触发后立即执行，又或者有些代码运行速度较慢，不宜占用游戏主线程。包&lt;code&gt;org.bukkit.scheduler&lt;/code&gt;提供了多样的类和方法，能实现以上的需求。&lt;/p&gt;
&lt;h4&gt;通过BukkitRunnable&lt;/h4&gt;
&lt;p&gt;类&lt;code&gt;BukkitRunnable&lt;/code&gt;实现了Java接口&lt;code&gt;Runnable&lt;/code&gt;但没实现方法&lt;code&gt;run()&lt;/code&gt;，因此实例化该类必须实现方法&lt;code&gt;run()&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/PaperdemoListener.java
package top.tachibana.paperdemo;

import org.bukkit.Material;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerDropItemEvent;
import org.bukkit.scheduler.BukkitRunnable;
//import ...

public final class PaperdemoListener implements Listener {
    // ...
    @EventHandler
    public void onDropItem(PlayerDropItemEvent event){
        // 当玩家丢出下界合金锭 输出消息并延迟两秒杀死该玩家 随后取消事件
        if(event.getItemDrop().getItemStack().getType() == Material.NETHERITE_INGOT){
            new BukkitRunnable(){
                public void run(){
                    event.getPlayer().sendMessage(Component.text(&quot;如此宝贵，你竟敢随意丢弃！&quot;, NamedTextColor.YELLOW));
                    event.getPlayer().sendMessage(Component.text(&quot;你将会获得惩罚&quot;, NamedTextColor.RED));
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    event.getPlayer().setHealth(0);
                }
            }.runTask(Paperdemo.getInstance());
            event.setCancelled(true);
        }
    }
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们实例化了一个继承了类&lt;code&gt;BukkitRunnable&lt;/code&gt;的匿名类，并实现了方法&lt;code&gt;run()&lt;/code&gt;。我们没有把这个对象赋值给某一个变量，而是直接调用了其方法&lt;code&gt;runTask()&lt;/code&gt;，这样游戏便会运行这个任务。&lt;/p&gt;
&lt;p&gt;构建后重载插件，玩家便无法丢出下界合金锭（投掷器不受影响），并且会在2s后被杀死。&lt;/p&gt;
&lt;h4&gt;通过BukkitScheduler&lt;/h4&gt;
&lt;p&gt;另一种写法是使用&lt;code&gt;BukkitScheduler.runTask()&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/PaperdemoListener.java
package top.tachibana.paperdemo;

import org.bukkit.Material;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerDropItemEvent;
import org.bukkit.scheduler.BukkitScheduler;
//import ...

public final class PaperdemoListener implements Listener {
    // ...
    @EventHandler
    public void onDropItem(PlayerDropItemEvent event){
        // 当玩家丢出下界合金锭 输出消息并延迟两秒杀死该玩家 随后取消事件
        if(event.getItemDrop().getItemStack().getType() == Material.NETHERITE_INGOT){
            BukkitScheduler scheduler = Paperdemo.getInstance().getServer().getScheduler();
            scheduler.runTask(Paperdemo.getInstance(), () -&gt; {
                    event.getPlayer().sendMessage(Component.text(&quot;如此宝贵，你竟敢随意丢弃！&quot;, NamedTextColor.YELLOW));
                    event.getPlayer().sendMessage(Component.text(&quot;你将会获得惩罚&quot;, NamedTextColor.RED));
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    event.getPlayer().setHealth(0);
            });
            event.setCancelled(true);
        }
    }
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先通过&lt;code&gt;server&lt;/code&gt;对象的成员方法&lt;code&gt;getScheduler()&lt;/code&gt;获得&lt;code&gt;BukkitScheduler&lt;/code&gt;实例，再运行它的成员方法&lt;code&gt;runTask()&lt;/code&gt;。注意方法&lt;code&gt;runTask()&lt;/code&gt;有两个参数，分别是插件的实例和一个&lt;code&gt;Runnable&lt;/code&gt;对象（也可以是&lt;code&gt;Consumer&amp;#x3C;? super BukkitTask&gt;&lt;/code&gt;对象），这里为了代码的简洁性我选择了Lambda表达式，你也可以使用&lt;code&gt;Runnable&lt;/code&gt;对象（就像上一节中那个匿名类的实例），不过这么做似乎有点蠢蠢的。&lt;/p&gt;
&lt;h3&gt;异步执行任务&lt;/h3&gt;
&lt;p&gt;刚刚的代码有一个问题，就是在线程睡眠2s的时候整个游戏也睡眠了2s（你可以使用高频红石看看是否是这种情况）。这是我们插件开发者不愿看到的——看似是对触发事件的玩家的惩罚，实际上是对全服务器的人的惩罚。这是因为当我们调用&lt;code&gt;runTask()&lt;/code&gt;的时候，相关的代码会在整个游戏的主线程中运行，方法&lt;code&gt;Thread.sleep()&lt;/code&gt;便让游戏暂停运行了2s。解决方式其实很简单，只需要把方法&lt;code&gt;runTask()&lt;/code&gt;改用&lt;code&gt;runTaskAsynchronously()&lt;/code&gt;即可。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/PaperdemoListener.java
package top.tachibana.paperdemo;

import org.bukkit.Material;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerDropItemEvent;
import org.bukkit.scheduler.BukkitScheduler;
//import ...

public final class PaperdemoListener implements Listener {
    // ...
    @EventHandler
    public void onDropItem(PlayerDropItemEvent event){
        // 当玩家丢出下界合金锭 输出消息并延迟两秒杀死该玩家 随后取消事件
        if(event.getItemDrop().getItemStack().getType() == Material.NETHERITE_INGOT){
            BukkitScheduler scheduler = Paperdemo.getInstance().getServer().getScheduler();
            scheduler.runTaskAsynchronously(Paperdemo.getInstance(), () -&gt; {
                    event.getPlayer().sendMessage(Component.text(&quot;如此宝贵，你竟敢随意丢弃！&quot;, NamedTextColor.YELLOW));
                    event.getPlayer().sendMessage(Component.text(&quot;你将会获得惩罚&quot;, NamedTextColor.RED));
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    event.getPlayer().setHealth(0);
            });
            event.setCancelled(true);
        }
    }
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用方法&lt;code&gt;runTaskAsynchronously()&lt;/code&gt;，任务将会以异步的方式执行，即，任务将会在主线程以外的另一个线程运行，当玩家丢出下界合金锭的时候，游戏也不会暂停了。&lt;/p&gt;
&lt;p&gt;然而，刚刚的示例也有一点问题。每当事件触发任务被执行时，服务器后台总会发送以下的警告。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-log&quot;&gt;[23:09:14] [Craft Scheduler Thread - 5 - paperdemo/WARN]: [DemoPlugin] Plugin paperdemo v1.0-SNAPSHOT generated an exception while executing task 12
java.lang.IllegalStateException: PlayerDeathEvent may only be triggered synchronously.
	at io.papermc.paper.plugin.manager.PaperEventManager.callEvent(PaperEventManager.java:42) ~[paper-1.21.4.jar:1.21.4-130-a392d47]
	at io.papermc.paper.plugin.manager.PaperPluginManagerImpl.callEvent(PaperPluginManagerImpl.java:131) ~[paper-1.21.4.jar:1.21.4-130-a392d47]
	at org.bukkit.plugin.SimplePluginManager.callEvent(SimplePluginManager.java:628) ~[paper-api-1.21.4-R0.1-SNAPSHOT.jar:?]
	at org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerDeathEvent(CraftEventFactory.java:1028) ~[paper-1.21.4.jar:1.21.4-130-a392d47]
	at net.minecraft.server.level.ServerPlayer.die(ServerPlayer.java:1088) ~[paper-1.21.4.jar:1.21.4-130-a392d47]
	at org.bukkit.craftbukkit.entity.CraftLivingEntity.setHealth(CraftLivingEntity.java:127) ~[paper-1.21.4.jar:1.21.4-130-a392d47]
	at paperdemo-1.0-SNAPSHOT.jar/top.tachibana.paperdemo.PaperdemoListener.lambda$onDropItem$0(PaperdemoListener.java:45) ~[paperdemo-1.0-SNAPSHOT.jar:?]
	at org.bukkit.craftbukkit.scheduler.CraftTask.run(CraftTask.java:78) ~[paper-1.21.4.jar:1.21.4-130-a392d47]
	at org.bukkit.craftbukkit.scheduler.CraftAsyncTask.run(CraftAsyncTask.java:57) ~[paper-1.21.4.jar:1.21.4-130-a392d47]
	at com.destroystokyo.paper.ServerSchedulerReportingWrapper.run(ServerSchedulerReportingWrapper.java:22) ~[paper-1.21.4.jar:?]
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) ~[?:?]
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) ~[?:?]
	at java.base/java.lang.Thread.run(Thread.java:1570) ~[?:?]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这表明，事件&lt;code&gt;PlayerDeathEvent&lt;/code&gt;不能异步触发。我们查阅Bukkit API的&lt;a href=&quot;https://hub.spigotmc.org/javadocs/spigot/org/bukkit/scheduler/BukkitScheduler.html&quot;&gt;相关文档&lt;/a&gt;可以找到这么一句话。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Asynchronous tasks should never access any API in Bukkit. Great care should be taken to assure the thread-safety of asynchronous tasks.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们可以换一种方式实现这种效果。&lt;/p&gt;
&lt;h3&gt;延迟执行任务&lt;/h3&gt;
&lt;p&gt;将代码改写成：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/PaperdemoListener.java
package top.tachibana.paperdemo;

import org.bukkit.Material;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerDropItemEvent;
import org.bukkit.scheduler.BukkitScheduler;
//import ...

public final class PaperdemoListener implements Listener {
    // ...
    @EventHandler
    public void onDropItem(PlayerDropItemEvent event){
        // 当玩家丢出下界合金锭 输出消息并延迟两秒杀死该玩家 随后取消事件
        if(event.getItemDrop().getItemStack().getType() == Material.NETHERITE_INGOT){
            BukkitScheduler scheduler = Paperdemo.getInstance().getServer().getScheduler();
            event.getPlayer().sendMessage(Component.text(&quot;如此宝贵，你竟敢随意丢弃！&quot;, NamedTextColor.YELLOW));
            event.getPlayer().sendMessage(Component.text(&quot;你将会获得惩罚&quot;, NamedTextColor.RED));
            scheduler.runTaskLater(Paperdemo.getInstance(), () -&gt; {
                    event.getPlayer().setHealth(0);
            }, 40);
            event.setCancelled(true);
        }
    }
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们同样可以实现这样的效果，并且不会异步地触发事件&lt;code&gt;PlayerDeathEvent&lt;/code&gt;。我们使用了&lt;code&gt;BukkitScheduler&lt;/code&gt;的示例的方法&lt;code&gt;runTaskLater()&lt;/code&gt;，与&lt;code&gt;runTask()&lt;/code&gt;和&lt;code&gt;runTaskAsynchronously()&lt;/code&gt;相比，&lt;code&gt;runTaskLater()&lt;/code&gt;多了一个参数，即延迟的事件，单位是游戏刻。&lt;/p&gt;
&lt;p&gt;同理，也有方法&lt;code&gt;runTaskLaterAsynchronously()&lt;/code&gt;可以使用。&lt;/p&gt;
&lt;h3&gt;周期执行任务&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;BukkitScheduler&lt;/code&gt;提供了方法&lt;code&gt;runTaskTimer()&lt;/code&gt;，可以定期执行任务。或者你可以用&lt;code&gt;BukkitRunnable&lt;/code&gt;的示例的方法&lt;code&gt;runTaskTimer()&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/PaperdemoListener.java
package top.tachibana.paperdemo;

import org.bukkit.Material;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.*;
import org.bukkit.scheduler.BukkitScheduler;
//import ...

public final class PaperdemoListener implements Listener {
    // ...
    @EventHandler
    public void onMove(PlayerMoveEvent event){
        BukkitRunnable damage = new BukkitRunnable(){
            public void run(){
                event.getPlayer().damage(0.5);
                if(event.getPlayer().getLocation().clone().add(0, -1, 0).getBlock().getType() != Material.BLUE_ICE){
                    this.cancel();
                }
            }
        };
        if(event.getTo().clone().add(0, -1, 0).getBlock().getType() == Material.BLUE_ICE){
            damage.runTaskTimer(Paperdemo.getInstance(), 0, 20);
        }
    }
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过&lt;code&gt;runTaskTimer()&lt;/code&gt;，我们可以让站在蓝冰上的玩家持续受到伤害。该方法的前两个参数分别是插件实例和延迟，延迟与&lt;code&gt;runTaskLater()&lt;/code&gt;一样单位是游戏刻，最后一个参数是执行的周期，单位同样是游戏刻。&lt;/p&gt;
&lt;p&gt;同样地，&lt;code&gt;runTaskTimer()&lt;/code&gt;也有异步版本&lt;code&gt;runTaskTimerAsynchronously()&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;配置&lt;/h2&gt;
&lt;h3&gt;关于YAML&lt;/h3&gt;
&lt;p&gt;什么是YAML？以下介绍来自于&lt;a href=&quot;https://zh.wikipedia.org/wiki/YAML&quot;&gt;维基百科&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;YAML是一个可读性高，用来表达数据序列化的格式。YAML参考了其他多种语言，包括：C语言、Python、Perl，并从XML、电子邮件的数据格式中获得灵感。目前已经有数种编程语言或脚本语言支持（或者说解析）这种语言。&lt;/p&gt;
&lt;p&gt;YAML是&quot;YAML Ain&apos;t a Markup Language&quot;（YAML不是一种标记语言）的递归缩写。在开发的这种语言时，YAML的意思其实是：&quot;Yet Another Markup Language&quot;（仍是一种标记语言），但为了强调这种语言以数据为中心，而不是以标记语言为重点，而用反向缩略语重命名。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;总而言之，YAML并不是一种编程语言，而是类似于HTML的标记语言。YAML对应的文件拓展名是&lt;code&gt;yml&lt;/code&gt;，通常被用来存储一些数据。&lt;code&gt;Bukkit API&lt;/code&gt;提供了一些API支持插件通过YAML来储存和使用它们的配置。本章内容将介绍如何使用它们。&lt;/p&gt;
&lt;h3&gt;创建config.yml&lt;/h3&gt;
&lt;p&gt;我们在&lt;code&gt;resources&lt;/code&gt;文件夹下创建一个&lt;code&gt;config.yml&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yml&quot;&gt;# at /src/main/resources/config.yml
words:
  word1: &quot;hello&quot;
  word2: &quot;Futaba&quot;
num: 114514
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是我们插件的默认配置文件，当插件目录（&lt;code&gt;plugins&lt;/code&gt;目录下以插件名字为名的文件夹）下没有&lt;code&gt;config.yml&lt;/code&gt;时，将会将这个文件拷贝到插件目录下。当然，这个行为插件加载器并不会自己进行，我们要在&lt;code&gt;onEnable()&lt;/code&gt;中执行以下代码来完成插件文件的拷贝。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/Paperdemo.java
package top.tachibana.paperdemo;

import org.bukkit.plugin.java.JavaPlugin;

public final class Paperdemo extends JavaPlugin {
    // ...
    @Override
    public void onEnable() {
        // ...
        // 插件配置
        this.saveDefaultConfig();
    }
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，当我们重载插件后，&lt;code&gt;config.yml&lt;/code&gt;会自动生成，且内容与我们放在&lt;code&gt;resources&lt;/code&gt;文件夹下的一致。&lt;/p&gt;
&lt;h3&gt;加载和获取config.yml&lt;/h3&gt;
&lt;p&gt;为了节省代码量，我们创建一个新类，获取配置文件的字段，然后提供这些字段的getter。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/Config.java
package top.tachibana.paperdemo;

import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.FileConfiguration;

import java.io.File;
import java.io.IOException;

public class Config {
    private static FileConfiguration config = Paperdemo.getInstance().getConfig();
    private static int num = config.getInt(&quot;num&quot;);
    private static String str1 = config.getString(&quot;words.word1&quot;);
    private static String str2 = config.getString(&quot;words.word2&quot;);
    public static void reload(){
        File file = new File(Paperdemo.getInstance().getDataFolder(), &quot;config.yml&quot;);
        try {
            config.load(file);
        } catch (IOException | InvalidConfigurationException e) {
            throw new RuntimeException(e);
        }
        Config.num = config.getInt(&quot;num&quot;);
        Config.str1 = config.getString(&quot;words.word1&quot;);
        Config.str2 = config.getString(&quot;words.word2&quot;);
    }
    public static int getNum() {
        return num;
    }
    public static String getStr1() {
        return str1;
    }
    public static String getStr2() {
        return str2;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;别看代码量很多，其实大多都是声明变量和定义方法。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;config&lt;/code&gt;的类型是&lt;code&gt;FileConfiguration&lt;/code&gt;，这个对象储存了配置文件，我们可以通过&lt;code&gt;config&lt;/code&gt;的成员方法获取和修改配置。通过&lt;code&gt;config()&lt;/code&gt;的成员方法&lt;code&gt;getInt&lt;/code&gt;和&lt;code&gt;getString&lt;/code&gt;，我们获取到了&lt;code&gt;num&lt;/code&gt;，&lt;code&gt;str1&lt;/code&gt;和&lt;code&gt;str2&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;静态方法&lt;code&gt;reload()&lt;/code&gt;用于重新加载配置文件，在这里我们使用了&lt;code&gt;config&lt;/code&gt;的成员方法&lt;code&gt;load()&lt;/code&gt;，它接受一个&lt;code&gt;File&lt;/code&gt;或&lt;code&gt;Reader&lt;/code&gt;对象，用于指定读取的文件。&lt;/p&gt;
&lt;p&gt;再写一个简单的指令获取方法的内容。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/ConfigCommand.java
package top.tachibana.paperdemo;

import net.kyori.adventure.text.Component;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.List;
import java.util.stream.Collectors;

public class ConfigCommand implements CommandExecutor, TabCompleter {
    private final List&amp;#x3C;String&gt; ARGS = List.of(&quot;reload&quot;, &quot;num&quot;, &quot;str1&quot;, &quot;str2&quot;);
    @Override
    public boolean onCommand(
            @NotNull CommandSender sender,
            @NotNull Command command,
            @NotNull String label,
            @NotNull String[] args){
        if(args.length == 0){
            sender.sendMessage(Component.text(&quot;不是这样用的&quot;));
            return true;
        }
        switch (args[0]){
            case &quot;reload&quot;:
                Config.reload();
                sender.sendMessage(Component.text(&quot;已成功重载插件&quot;));
                break;
            case &quot;num&quot;:
                sender.sendMessage(Component.text(&quot;num的值是&quot; + Config.getNum()));
                break;
            case &quot;str1&quot;:
                sender.sendMessage(Component.text(&quot;str1的值是&quot; + Config.getStr1()));
                break;
            case &quot;str2&quot;:
                sender.sendMessage(Component.text(&quot;str2的值是&quot; + Config.getStr2()));
                break;
            default:
                sender.sendMessage(Component.text(&quot;不是这样用的&quot;));
                break;
        }
        return true;
    }
    @Override
    public @Nullable List&amp;#x3C;String&gt; onTabComplete(
            @NotNull CommandSender sender,
            @NotNull Command command,
            @NotNull String label,
            @NotNull String[] args){
        if(args.length == 1) {
            if (args[0].isEmpty()) return ARGS;
            else return ARGS.stream().filter(s -&gt; s.startsWith(args[0])).collect(Collectors.toList());
        }
        return null;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;效果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://syju.org/_astro/%E8%8E%B7%E5%8F%96config.C47wVnyq_1eUEq1.webp&quot; alt=&quot;获取config&quot; title=&quot;获取config&quot;&gt;&lt;/p&gt;
&lt;h3&gt;多文件配置&lt;/h3&gt;
&lt;p&gt;许多知名插件都不止一个配置文件，例如&lt;em&gt;EssentialsX&lt;/em&gt;，&lt;em&gt;Residence&lt;/em&gt;。我们能不能让我们的插件能从&lt;code&gt;custom.yml&lt;/code&gt;中读取文件，做到我们读取&lt;code&gt;config.yml&lt;/code&gt;那样呢？当然可以。&lt;/p&gt;
&lt;p&gt;我们现在&lt;code&gt;resources&lt;/code&gt;文件夹下创建一个&lt;code&gt;custom.yml&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yml&quot;&gt;# at /src/main/resources/custom.yml

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它现在没有东西，后续我将会演示修改它。&lt;/p&gt;
&lt;p&gt;现在，让我们在主类中写下这样的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/Paperdemo.java
package top.tachibana.paperdemo;

import org.bukkit.plugin.java.JavaPlugin;

import java.io.File;

public final class Paperdemo extends JavaPlugin {
    private static FileConfiguration custom;
    // ...
    @Override
    public void onEnable() {
        // ...
        // 插件配置
        this.saveDefaultConfig();
        this.saveResource(&quot;custom.yml&quot;, false);
        File file = new File(this.getDataFolder(), &quot;custom.yml&quot;);
        Paperdemo.custom = YamlConfiguration.loadConfiguration(file);
    }
    // ...
    public static FileConfiguration getCustom(){
        return Paperdemo.custom;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;方法&lt;code&gt;saveResource()&lt;/code&gt;用于将文件夹&lt;code&gt;resources&lt;/code&gt;下的任意文件内容拷贝到插件目录下，第一个参数是文件的名字，第二个参数是是否启用覆盖，如果是，则每次启动插件时文件内容都会被重置。&lt;/p&gt;
&lt;p&gt;之后，我们尝试从文件中加载自定义的配置文件。插件实例的成员方法&lt;code&gt;getDataFolder()&lt;/code&gt;用于获取插件目录。&lt;code&gt;YamlConfiguration&lt;/code&gt;的静态方法&lt;code&gt;loadConfiguration()&lt;/code&gt;用于加载插件，返回一个&lt;code&gt;FileConfiguration&lt;/code&gt;的实例。我们设置了&lt;code&gt;custom&lt;/code&gt;的getter，这可以让我们日后获取&lt;code&gt;custom.yml&lt;/code&gt;的内容。&lt;/p&gt;
&lt;h3&gt;修改配置文件&lt;/h3&gt;
&lt;p&gt;接下来我将演示如何修改配置文件并存储至原配置文件中。这在用户填写一些无效的字段时可以使配置文件的字段重新合法，也可以实现在游戏中修改配置，并将其保存至配置文件中以便下次使用。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/Paperdemo.java
package top.tachibana.paperdemo;

import org.bukkit.plugin.java.JavaPlugin;

import java.io.File;
import java.io.IOException;

public final class Paperdemo extends JavaPlugin {
    private static FileConfiguration custom;
    // ...
    public static void saveCustom() throws IOException {
        File file = new File(Paperdemo.getInstance().getDataFolder(), &quot;custom.yml&quot;);
        Paperdemo.custom.save(file);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/ConfigCommand.java
package top.tachibana.paperdemo;

import net.kyori.adventure.text.Component;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.List;
import java.util.stream.Collectors;

public class ConfigCommand implements CommandExecutor, TabCompleter {
    private final List&amp;#x3C;String&gt; ARGS = List.of(&quot;reload&quot;, &quot;edit&quot;, &quot;save&quot;, &quot;num&quot;, &quot;str1&quot;, &quot;str2&quot;);
    @Override
    public boolean onCommand(
            @NotNull CommandSender sender,
            @NotNull Command command,
            @NotNull String label,
            @NotNull String[] args){
        if(args.length == 0){
            sender.sendMessage(Component.text(&quot;不是这样用的&quot;));
            return true;
        }
        switch (args[0]){
            case &quot;edit&quot;:
                if(args.length == 1 || args.length == 2){
                    sender.sendMessage(Component.text(&quot;不是这样用的&quot;));
                    return true;
                }
                else{
                    Paperdemo.getCustom().set(args[1], args[2]);
                    sender.sendMessage(Component.text(&quot;已将 &quot; + args[1] + &quot; 设置为 &quot; + args[2]));
                }
                break;
            case &quot;save&quot;:
                try {
                    Paperdemo.saveCustom();
                    sender.sendMessage(Component.text(&quot;已保存&quot;));
                } catch (IOException e) {
                    sender.sendMessage(Component.text(&quot;保存失败&quot;));
                }
                break;
            // ...
            default:
                sender.sendMessage(Component.text(&quot;不是这样用的&quot;));
                break;
        }
        return true;
    }
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;自定义方法&lt;code&gt;saveCustom&lt;/code&gt;用于保存配置文件。&lt;code&gt;FileConfiguration&lt;/code&gt;的实例&lt;code&gt;custom&lt;/code&gt;的成员方法&lt;code&gt;save()&lt;/code&gt;用于保存配置至指定的路径，它的参数是一个&lt;code&gt;File&lt;/code&gt;的实例。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;custom&lt;/code&gt;的成员方法&lt;code&gt;set()&lt;/code&gt;可以设置指定字段的值。第一个参数是&lt;code&gt;String&lt;/code&gt;类型的变量，表示&lt;code&gt;key&lt;/code&gt;，第二个参数是&lt;code&gt;Object&lt;/code&gt;类型的变量（可以是&lt;code&gt;String&lt;/code&gt;、&lt;code&gt;int&lt;/code&gt;或其它类型）。&lt;/p&gt;
&lt;p&gt;效果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://syju.org/_astro/%E4%BF%AE%E6%94%B9%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6%E6%BC%94%E7%A4%BA.BGFrj8VH_1xOm2E.webp&quot; alt=&quot;修改配置文件演示&quot; title=&quot;修改配置文件演示&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://syju.org/_astro/custom.yml%E5%86%85%E5%AE%B9.CVx4k7DQ_Z188aYi.webp&quot; alt=&quot;custom.yml内容&quot; title=&quot;custom.yml内容&quot;&gt;&lt;/p&gt;
&lt;h2&gt;杂项&lt;/h2&gt;
&lt;p&gt;感谢你能看到这里！本章的内容较为琐碎，你可以按照需求进行学习。&lt;/p&gt;
&lt;h3&gt;如何获取插件实例&lt;/h3&gt;
&lt;p&gt;在编写插件时，有时候需要获取插件实例的引用（就是在主类的方法&lt;code&gt;onEnable()&lt;/code&gt;反复调用的&lt;code&gt;this&lt;/code&gt;），下面介绍两种方式获取插件实例的引用。&lt;/p&gt;
&lt;h4&gt;通过private static成员字段&lt;/h4&gt;
&lt;p&gt;这是本插件的做法。因为插件主类只会实例化一次，因此我们可以通过一个static的对象存储唯一实例的引用，然后通过方法&lt;code&gt;onEnable()&lt;/code&gt;初始化。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/Paperdemo.java
package top.tachibana.paperdemo;

import org.bukkit.plugin.java.JavaPlugin;

public final class Paperdemo extends JavaPlugin {
    private static Paperdemo instance;
    public static Paperdemo getInstance(){
        return Paperdemo.instance;
    }
    @Override
    public void onEnable() {
        instance = this;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;通过PluginManager&lt;/h4&gt;
&lt;p&gt;使用接口&lt;code&gt;PluginManager&lt;/code&gt;（我们在注册事件监听器时曾经用过）不但可以获取到自己的插件的实例，还可以获取到别的插件的实例（前提是已加载）。&lt;code&gt;getPlugin()&lt;/code&gt;的唯一参数时一个字符串，表示插件的名字。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// at /src/main/java/top/tachibana/paperdemo/Paperdemo.java
package top.tachibana.paperdemo;

import org.bukkit.plugin.java.JavaPlugin;

public final class Paperdemo extends JavaPlugin {
    public static Paperdemo getInstance(){
        //也可以这么写
        return (Paperdemo)Bukkit.getPluginManager().getPlugin(&quot;Paperdemo&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;一些有用的外部网站&lt;/h3&gt;
&lt;p&gt;这些网站可能对你学习插件编写有帮助，建议收藏。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/f0xea/paperdemo&quot;&gt;本项目的github仓库&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://hub.spigotmc.org/javadocs/spigot/&quot;&gt;Bukkit API（spigotmc.org提供）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://jd.papermc.io/paper/1.21.4/&quot;&gt;Bukkit API（papermc.io提供，多了Paper的专属API）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.papermc.io/paper&quot;&gt;Paper官方文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zh.minecraft.wiki/&quot;&gt;Minecraft Wiki&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.jetbrains.com/plugin/8327-minecraft-development&quot;&gt;Minecraft Development插件&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Minecraft服务器配置向客户端发送资源包</title><link>https://syju.org/blog/resourcepacks</link><guid isPermaLink="true">https://syju.org/blog/resourcepacks</guid><description>到底会不会让玩家更方便不清楚，但是腐竹可麻烦了</description><pubDate>Sun, 23 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;~~心系玩家的~~腐竹发现，用小游戏或者地图存档开服的时候，经常需要安装地图作者指定的资源包（仅安装在客户端的资源包一般是材质包，下文都称之为资源包）。对于部分玩家来说，安装资源包轻而易举，但是有些玩家可能觉得麻烦。&lt;/p&gt;
&lt;p&gt;既然资源包可以在游戏启动后甚至是进入世界后加载，如果能实现进入服务器的时候，服务器告诉客户端应该去哪里下载资源包，然后客户端下载好自动安装，那不是很方便吗？&lt;/p&gt;
&lt;p&gt;幸运的是，不需要任何模组和插件，你就可以实现这个效果。&lt;/p&gt;
&lt;h2&gt;了解配置文件&lt;/h2&gt;
&lt;p&gt;你一定知道Minecraft服务器有个叫做&lt;code&gt;server.properties&lt;/code&gt;的文件，仔细观察（别真观察了，直接使用&lt;code&gt;Ctrl+F&lt;/code&gt;或者&lt;code&gt;Command+F&lt;/code&gt;查找关键字&lt;em&gt;res&lt;/em&gt;吧）这个配置文件，你会发现有四个（实际上是五个）跟资源包有关的键，接下来我会讲解如何正确填写这四个键的值。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-properties&quot;&gt;require-resource-pack=false
resource-pack=
resource-pack-id=
resource-pack-prompt=
resource-pack-sha1=
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;require-resource-pack&lt;/code&gt;的值仅能为布尔类型的，表示是否强制玩家下载这个资源包，其实就算你设置成&lt;code&gt;true&lt;/code&gt;，玩家在下载资源包失败的情况下也能进入游戏。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;resource-pack&lt;/code&gt;的值是资源包的 URI ，但是我试过了，不能使用&lt;code&gt;file://&lt;/code&gt;之类的链接来指向服务器本地的一个文件，疑似只能使用HTTP和HTTPS。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;resource-pack-id&lt;/code&gt;，一般来说服务器资源包的 UUID 值都是随机的，用于客户端标识资源包，但是你也可以通过这个键指定设置一个资源包的 UUID ，自动生成和指定 UUID 没有多少区别，所以我不建议你修改这个键的值。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;resource-pack-prompt&lt;/code&gt;的值为首次进入服务器向玩家提示的内容，格式为&lt;a href=&quot;https://zh.minecraft.wiki/w/%E6%96%87%E6%9C%AC%E7%BB%84%E4%BB%B6?variant=zh-cn&quot;&gt;原始JSON文本&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;resource-pack-sha1&lt;/code&gt;，资源包的 SHA-1 值，必须为小写的十六进制数字。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;让服务器为客户端提供资源包&lt;/h2&gt;
&lt;p&gt;将&lt;code&gt;require-resource-pack&lt;/code&gt;设置为&lt;code&gt;true&lt;/code&gt;即可。&lt;/p&gt;
&lt;h2&gt;为资源包指定 URI&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;resource-pack&lt;/code&gt;的值是资源包的 URI ，这个资源包是需要提供在互联网上供人下载的，我们可以通过以下几种方式创建资源包的 URI 。&lt;/p&gt;
&lt;h3&gt;使用地图作者直接提供的链接&lt;/h3&gt;
&lt;p&gt;假设地图作者将客户端资源包单独发布，可以把 TA 提供的链接直接用作这个资源包的 URI 。&lt;/p&gt;
&lt;p&gt;如果客户端资源包和地图一起打包发布，或者这个作者使用了~~万恶的~~百度网盘、夸克网盘之类的东西，那就不能用这个方法了。&lt;/p&gt;
&lt;h3&gt;使用 Nginx&lt;/h3&gt;
&lt;p&gt;配置文件如下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nginx&quot;&gt;server {
    # 如果使用 HTTPS ，这里应该监听443端口然后提供 SSL 证书
    listen 80;
    server_name files.example.com;

    location / {
        alias /var/files/;

        # 防止用户以../访问文件
        if ($request_uri ~* (\.\.|%2e%2e|%u002e%u002e)) {
            return 403;
        }
        # 配置黑名单
        location ~ /\. {
            return 403;
        }
        location ~ ^/.*\.(bak|conf|sql)$ {
            return 403;
        }

        # 以下配置内容都跟浏览器相关，不配置也不影响客户端下载资源包
        autoindex on;
        autoindex_exact_size off;
        autoindex_localtime on;
        location ~* \.(html)$ {
            add_header Content-Disposition &quot;inline&quot;;
        }
        location ~* \.(!html)$ {
            # 强制浏览器下载
            add_header Content-Disposition &quot;attachment&quot;; 
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后将你要提供的资源包（例如&lt;code&gt;example.zip&lt;/code&gt;）放在&lt;code&gt;/var/files&lt;/code&gt;下面，使用&lt;code&gt;http://files.example.com/example.zip&lt;/code&gt;作为这个资源包的 URI 链接。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/var/files&lt;/code&gt;可以改成其它的路径。&lt;/p&gt;
&lt;h3&gt;使用 Github&lt;/h3&gt;
&lt;p&gt;先创建一个 Github 仓库，注意将仓库设置为 &lt;strong&gt;public&lt;/strong&gt; ，上传你要提供的资源包文件在这个仓库里，然后点击这个文件，因为&lt;code&gt;zip&lt;/code&gt;是一个二进制文件，这里不会预览它的内容，右键内容页右上角的 &lt;strong&gt;Raw&lt;/strong&gt; ，点击&lt;strong&gt;复制链接&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://syju.org/_astro/raw.CVQU_qv__29yPQs.webp&quot; alt=&quot;Raw&quot; title=&quot;Raw&quot;&gt;&lt;/p&gt;
&lt;p&gt;你将会得到形如&lt;code&gt;https://github.com/f0xea/resourcepack/raw/refs/heads/main/example.zip&lt;/code&gt;的链接，这就是资源包的URI。&lt;/p&gt;
&lt;h3&gt;使用网盘&lt;/h3&gt;
&lt;p&gt;如果某个网盘提供了直链下载功能，那么你可以自己将文件上传到这个网盘上，并将直链用作这个资源包的 URI 。&lt;/p&gt;
&lt;h2&gt;为资源包设置 SHA-1 值&lt;/h2&gt;
&lt;p&gt;为了保障安全，应该为&lt;code&gt;resource-pack-sha1&lt;/code&gt;提供资源包的 SHA-1 值。尽管该内容留空也没影响，但我还是推荐将这里填上资源包的 SHA-1 值。&lt;/p&gt;
&lt;p&gt;计算文件的 SHA-1 值不会占用太多时间，仅仅需要使用&lt;code&gt;sha1sum&lt;/code&gt;命令。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;futaba@Tachibana:~$ sha1sum example.zip
68e691bfe6329c9f6f61748b0f17a7e197bc6a51  example.zip
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将输出结果的前面这一串十六进制数字（不是我这里的一串数字，不同文件的 SHA-1 值不一样）填入&lt;code&gt;resource-pack-sha1&lt;/code&gt;即可。&lt;/p&gt;
&lt;h2&gt;常见问题&lt;/h2&gt;
&lt;h3&gt;Q: 客户端无法下载资源包&lt;/h3&gt;
&lt;p&gt;资源包下载是客户端的行为，其内容会记录在客户端的 log 中，请让玩家发送客户端日志以解决问题。&lt;/p&gt;
&lt;h3&gt;Q: 使用 Nginx 解析 URI 始终无法访问&lt;/h3&gt;
&lt;p&gt;如果你的服务器位于中国大陆，请先进行备案，否则无法使用服务器的 80 和 443 端口。&lt;/p&gt;
&lt;p&gt;你也可以不通过 80 端口使用 HTTP 传输资源包（但是无法使用 HTTPS 协议）。&lt;/p&gt;
&lt;p&gt;禁止使用该技术在互联网上提供非法内容。&lt;/p&gt;
&lt;h2&gt;相关网站&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://zh.minecraft.wiki/w/%E6%9C%8D%E5%8A%A1%E7%AB%AF%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6%E6%A0%BC%E5%BC%8F#Java%E7%89%88_3&quot;&gt;Minecraft Wiki: 服务端配置文件格式&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://zh.minecraft.wiki/w/%E6%96%87%E6%9C%AC%E7%BB%84%E4%BB%B6&quot;&gt;Minecraft Wiki: 文本组件&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://nginx.org/&quot;&gt;Nginx&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/&quot;&gt;Github&lt;/a&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Minecraft Brigadier API详解————以Fabric为例</title><link>https://syju.org/blog/brigadier</link><guid isPermaLink="true">https://syju.org/blog/brigadier</guid><description>带你了解Brigadier API</description><pubDate>Sun, 07 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;h2&gt;Brigadier API&lt;/h2&gt;
&lt;p&gt;Brigadier 是一个由 Mojang 为 Minecraft: Java Edition 开发的开源指令解析和调度工具，你可以在 &lt;a href=&quot;https://github.com/Mojang/brigadier&quot;&gt;GitHub&lt;/a&gt; 上找到这个项目的源代码。&lt;/p&gt;
&lt;p&gt;Fabric, Forge, Paper 等模组加载器或服务器核心提供了对 Brigadier API 的支持，使开发者能够轻松地创建和管理自定义指令，包括注册新的指令、添加参数、实现自动补全等功能。&lt;/p&gt;
&lt;p&gt;本篇文章将以 Fabric 为例，介绍如何使用 Brigadier API 来创建和管理 Minecraft 指令。&lt;/p&gt;
&lt;h2&gt;Commands&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Command&lt;/code&gt; 是一个函数式接口，它接受一个泛型参数 &lt;code&gt;CommandContext&amp;#x3C;S&gt;&lt;/code&gt; ，并返回一个表示指令执行结果的整数值，并且可能会抛出 &lt;code&gt;CommandSyntaxException &lt;/code&gt; 异常。我们通常使用 Lambda 表达式或者方法引用作为 &lt;code&gt;Command&lt;/code&gt; 的实现。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Command&amp;#x3C;ServerCommandSource&gt; command = context -&gt; {
    // 指令的具体实现逻辑
    // 当指令成功执行的时候，一般会返回 1 
    return 0;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;注册指令&lt;/h2&gt;
&lt;p&gt;在了解了 &lt;code&gt;Command&lt;/code&gt; 接口之后，我们还需要了解注册指令的方式。在 Fabric 中，我们使用 ModInitializer#onInitialize 方法来执行注册指令的逻辑，我们可以将其写在模组主类中，但是我更建议你将其写在一个单独的类中。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class ModCommand implements ModInitializer {
    // ...
    @Override
    public void onInitialize() {
        CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -&gt; {
            dispatcher.register(
                    Commands.literal(&quot;testcommand&quot;).executes(command)
            );
            // 可以在这里注册其它的指令
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里 &lt;code&gt;executes()&lt;/code&gt; 传入的参数 &lt;code&gt;command&lt;/code&gt; 是我们上一节创建的 &lt;code&gt;Command&lt;/code&gt; 对象哦。通常，我们直接传入一个 Lambda 表达式或者一个方法引用。&lt;/p&gt;
&lt;p&gt;我们重新实现一下 &lt;code&gt;command&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Command&amp;#x3C;ServerCommandSource&gt; command = context -&gt; {
    // 指令的具体实现逻辑
    context.getSource().sendSuccess(() -&gt; Component.literal(&quot;Called /testcommand&quot;), false);
    return 1;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后不要忘记在资源文件下的 &lt;code&gt;fabric.mod.json&lt;/code&gt; 中配置模组的入口点。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;&quot;entrypoints&quot;: {
    // ...
    &quot;main&quot;: [
        &quot;org.f14a.fabricdemo.Fabricdemo&quot;,
        &quot;org.f14a.fabricdemo.command.ModCommand&quot;
    ]
}, 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在运行游戏，当 &lt;code&gt;/testcommand&lt;/code&gt; 被调用时，&lt;code&gt;command&lt;/code&gt; 将会向执行指令的玩家（或服务器控制台）发送一条消息。请注意，&lt;code&gt;sendSuccess()&lt;/code&gt; 的第一个参数是一个 &lt;code&gt;Supplier&amp;#x3C;Component&gt;&lt;/code&gt;， 需要提供一个 &lt;strong&gt;Lambda&lt;/strong&gt; 表达式，第二参数的含义是是否向所有 op 发送指令的执行结果，应该根据指令的具体情况来决定。&lt;/p&gt;
&lt;p&gt;现在让我们把指令实现写的稍微复杂一点。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class ModCommand implements ModInitializer {
    private static int executeDedicatedServer(CommandContext&amp;#x3C;CommandSourceStack&gt; context) {
        context.getSource().sendSuccess(() -&gt; Component.literal(&quot;You are running a dedicated server. &quot;), false);
        return 1;
    }
    private static int executeClientServer(CommandContext&amp;#x3C;CommandSourceStack&gt; context) {
        context.getSource().sendSuccess(() -&gt; Component.literal(&quot;You are running a client integrated server. &quot;), false);
        return 1;
    }
    // ...
    @Override
    public void onInitialize() {
        CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -&gt; {
            dispatcher.register(
                    // Differentiate between dedicated server and client integrated server.
                    if(environment.includeDedicated) {
                        dispatcher.register(Commands.literal(&quot;whoami&quot;).executes(ModCommand::executeDedicatedServer));
                    }
                    else {
                        dispatcher.register(Commands.literal(&quot;whoami&quot;).executes(ModCommand::executeClientServer));
                    }
            )
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是 &lt;a href=&quot;https://docs.fabricmc.net/develop/commands/basics#registration-environment&quot;&gt;Fabric 文档&lt;/a&gt; 中对参数 &lt;code&gt;environment&lt;/code&gt; 的演示代码（我稍微改过了一下），&lt;code&gt;includeDedicated&lt;/code&gt; 和 &lt;code&gt;includeIntegrated&lt;/code&gt; 分别在游戏运行在专用服务器（也就是单独启动的游戏服务端）和集成在客户端中的服务端时值为 &lt;code&gt;true&lt;/code&gt; ，然后就没有别的用途了。&lt;/p&gt;
&lt;p&gt;这里我们使用方法引用传入 &lt;code&gt;executes()&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在实际使用中，我们有时会设计一些只有 op 才能使用的指令，这类指令对游戏的影响通常比较大。我们可以使用 &lt;code&gt;requires()&lt;/code&gt; 进行权限检测。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class ModCommand implements ModInitializer {
    // ...
    @Override
    public void onInitialize() {
        CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -&gt; {
            // Display the words if the user has specific permission.
            dispatcher.register(Commands.literal(&quot;amianop&quot;).requires(source -&gt; source.hasPermission(1)).executes(context -&gt; {
                context.getSource().sendSuccess(() -&gt; Component.literal(&quot;You are an OP&quot;), false);
                return 1;
            }));
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;requires()&lt;/code&gt; 接受一个 &lt;code&gt;Predicate&amp;#x3C;S&gt;&lt;/code&gt;，当且仅当 &lt;code&gt;Predicate&amp;#x3C;S&gt;&lt;/code&gt; 在某种情况下返回 &lt;code&gt;true&lt;/code&gt; 时，指令才能被执行，并且会出现在自动补全的列表中。在这里我们使用 &lt;code&gt;hasPermission()&lt;/code&gt; 来判断执行者是否有对应的权限。&lt;/p&gt;
&lt;p&gt;当然，你可以为这个 &lt;code&gt;Predicate&amp;#x3C;S&gt;&lt;/code&gt; 设计独特的逻辑，比如小游戏中未加入任何队伍的玩家才能使用加入队伍的指令。&lt;/p&gt;
&lt;p&gt;另外注意到我们这次为 &lt;code&gt;executes()&lt;/code&gt; 传入的是一个 Lambda 表达式，具体是直接传入 &lt;code&gt;Command&amp;#x3C;ServerCommandSource&gt;&lt;/code&gt; 的变量，还是 Lambda 表达式，还是方法引用，应该根据你的自身喜好和具体实现来决定，比如如果指令实现逻辑复杂的话，使用 Lambda 表达式就不太合适了。&lt;/p&gt;
&lt;h2&gt;为指令添加子指令&lt;/h2&gt;
&lt;p&gt;为指令添加子指令，我们只需使用 &lt;code&gt;then()&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class ModCommand implements ModInitializer {
    // ...
    @Override
    public void onInitialize() {
        CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -&gt; {
            // All time say the words.
            dispatcher.register(Commands.literal(&quot;testcommand&quot;).executes(context -&gt; {
                // sendSuccess(Supplier&amp;#x3C;Component&gt; message, boolean broadcastToOps)
                context.getSource().sendSuccess(() -&gt; Component.literal(&quot;Called /testcommand&quot;), false);
                // Throw an exception when something goes wrong.
                //throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().create();
                // Return 1 when everything is ok.
                return 1;
            }).then(Commands.literal(&quot;subcommand&quot;).executes(context -&gt; {
                    context.getSource().sendSuccess(() -&gt; Component.literal(&quot;Called /testcommand subcommand&quot;), false);
                    return 1;
                    }))
            );
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果觉得括号太多了，看起来很复杂，可以将代码复制到编辑器里面。&lt;/p&gt;
&lt;p&gt;以下为简化版本。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class ModCommand implements ModInitializer {
    // ...
    @Override
    public void onInitialize() {
        CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -&gt; {
            // All time say the words.
            dispatcher.register(
                    Commands.literal(&quot;testcommand&quot;).executes(/*something*/)
                    .then(
                            Commands.literal(&quot;subcommand&quot;).executes(/*something*/)
                    )
            );
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发现了吗，&lt;code&gt;then()&lt;/code&gt; 的参数实际上是一个新的 &lt;code&gt;Commands.literal()&lt;/code&gt; ，而这个新的对象，同样可以使用 &lt;code&gt;then()&lt;/code&gt; 继续套娃，得到一个树状的指令结构。&lt;/p&gt;
&lt;p&gt;读者在此一定要分清楚， &lt;code&gt;then()&lt;/code&gt; 是哪个对象调用的，就意味着传入的参数表示的指令是这个对象的子节点。即，区分&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;A.then(B).then(C).then(D); // A -&gt; B
                           //   -&gt; C
                           //   -&gt; D
A.then(B.then(C).then(D)); // A -&gt; B -&gt; C
                           //        -&gt; D
A.then(B).then(C.then(D)); // A -&gt; B
                           //   -&gt; C -&gt; D
A.then(B.then(C.then(D))); // A -&gt; B -&gt; C -&gt; D 链式结构
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之间的区别。&lt;/p&gt;
&lt;p&gt;在设计比较复杂的指令结构时，最好经常换行缩进，或者写清楚注释。否则代码会难以阅读。另外，如果你觉得这种方式比较丑陋，&lt;a href=&quot;#%E6%9B%B4%E7%BE%8E%E8%A7%82%E7%9A%84%E6%B3%A8%E5%86%8C%E6%8C%87%E4%BB%A4%E5%86%99%E6%B3%95&quot;&gt;后面&lt;/a&gt;我会补充一种比较美观（个人认为）的写法。&lt;/p&gt;
&lt;h2&gt;为指令添加参数&lt;/h2&gt;
&lt;p&gt;在了解如何构造树状结构的指令后，我们还可以进一步让指令功能更完善，比如，就像原版的大部分指令一样，我们可以为指令添加参数。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class ModCommand implements ModInitializer {
    // ...
    @Override
    public void onInitialize() {
        CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -&gt; {
            // Command with arguments
            dispatcher.register(Commands.literal(&quot;command_with_args&quot;)
                    .then(Commands.argument(&quot;value&quot;, IntegerArgumentType.integer(0)).executes(context -&gt; {
                        int value = IntegerArgumentType.getInteger(context, &quot;value&quot;);
                        context.getSource().sendSuccess(() -&gt; Component.literal(&quot;You call the command with argument: %d&quot;.formatted(value)), false);
                        return 1;
                    }))
            );
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们不再使用 &lt;code&gt;literal()&lt;/code&gt; 来创建节点，而是使用 &lt;code&gt;argument()&lt;/code&gt; 来创建一个节点，它接受两个参数，第一个参数是参数的名称（如果没有自动补全，它会显示在文字框上面），第二个参数是参数的类型，一个 &lt;code&gt;ArgumentType&amp;#x3C;T&gt;&lt;/code&gt; 的对象。&lt;code&gt;integer()&lt;/code&gt; 的唯一参数指定了参数的最小值， &lt;code&gt;integer()&lt;/code&gt; 也有无参数和接受两个参数的重载版本，分别表示无范围限制和指定范围。&lt;/p&gt;
&lt;p&gt;下面是一个更复杂的例子，它为这个指令添加了多个参数。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class ModCommand implements ModInitializer {
    // ...
    @Override
    public void onInitialize() {
        CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -&gt; {
            // Command with arguments
            dispatcher.register(Commands.literal(&quot;command_with_args&quot;)
                    .then(Commands.argument(&quot;value&quot;, IntegerArgumentType.integer(0)).executes(context -&gt; {
                        int value = IntegerArgumentType.getInteger(context, &quot;value&quot;);
                        context.getSource().sendSuccess(() -&gt; Component.literal(&quot;You call the command with argument: %d&quot;.formatted(value)), false);
                        return 1;
                    }).then(Commands.argument(&quot;player&quot;, StringArgumentType.string()).executes(context -&gt; {
                        int value = IntegerArgumentType.getInteger(context, &quot;value&quot;);
                        String player = StringArgumentType.getString(context, &quot;player&quot;);
                        context.getSource().sendSuccess(() -&gt; Component.literal(&quot;%s call the command with argument: %d&quot;.formatted(player, value)), false);
                        return 1;
                    })))
            );
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;值得注意的是，我们不必为 &lt;code&gt;execute()&lt;/code&gt; 传入只有一个参数的方法引用，上述代码中两次调用 &lt;code&gt;executes()&lt;/code&gt; 的 Lambda 表达式代码重复了很多，我们可以这么写:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class ModCommand implements ModInitializer {
    // Actually it cannot be used to Commands#executes because of incompatible method signature.
    private static int executeWithParameters(CommandContext&amp;#x3C;CommandSourceStack&gt; context, String player, int value) {
        context.getSource().sendSuccess(() -&gt; Component.literal(&quot;%s call the command with argument: %d&quot;.formatted(player, value)), false);
        return 1;
    }
    // ...
    @Override
    public void onInitialize() {
        CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -&gt; {
            // Equals to
            dispatcher.register(Commands.literal(&quot;command_with_args_&quot;)
                    .then(Commands.argument(&quot;value&quot;, IntegerArgumentType.integer(0)).executes(context -&gt;
                            ModCommand.executeWithParameters(context,
                                    context.getSource().getDisplayName().getString(),
                                    IntegerArgumentType.getInteger(context, &quot;value&quot;))
                    ).then(Commands.argument(&quot;player&quot;, StringArgumentType.string()).executes(context -&gt;
                            ModCommand.executeWithParameters(context,
                                    StringArgumentType.getString(context, &quot;player&quot;),
                                    IntegerArgumentType.getInteger(context, &quot;value&quot;))
                    )))
            );
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样写的好处是，我们将指令的实现逻辑集中在一个方法中，避免了代码重复，提高了代码的可维护性。&lt;/p&gt;
&lt;h2&gt;自定义参数类型&lt;/h2&gt;
&lt;p&gt;下面我将演示如何创建一个自定义的参数类型。&lt;/p&gt;
&lt;p&gt;所有的参数类型都需要实现 &lt;code&gt;ArgumentType&amp;#x3C;T&gt;&lt;/code&gt; 接口，所以我们第一步一定是创建一个类并实现这个接口。&lt;/p&gt;
&lt;p&gt;这个接口有四个方法，但是我们只需要实现 &lt;code&gt;parse()&lt;/code&gt; 方法就可以了。&lt;code&gt;parse()&lt;/code&gt; 方法接受一个 &lt;code&gt;StringReader&lt;/code&gt; 对象作为参数，并返回一个类型为 &lt;code&gt;T&lt;/code&gt; 的对象（这里的 &lt;code&gt;T&lt;/code&gt; 是 &lt;code&gt;BlockPos&lt;/code&gt;）。我们需要从 &lt;code&gt;StringReader&lt;/code&gt; 中读取输入的字符串，并将其解析为我们想要的类型。&lt;/p&gt;
&lt;p&gt;我们尝试创建一个参数类型，它包含一个方块的位置，即三个整数。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class BlockPosArgumentType implements ArgumentType&amp;#x3C;BlockPos&gt; {
    @Override
    public BlockPos parse(StringReader stringReader) throws CommandSyntaxException {
        try {
            int x = stringReader.readInt();
            stringReader.expect(&apos; &apos;);
            int y = stringReader.readInt();
            stringReader.expect(&apos; &apos;);
            int z = stringReader.readInt();
            return new BlockPos(x, y, z);
        }
        catch (Exception e) {
            throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherParseException().create(&quot;Failed to parse BlockPos argument %s.&quot;);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在我们的 &lt;code&gt;parse()&lt;/code&gt; 方法中，我们尝试从 &lt;code&gt;StringReader&lt;/code&gt; 中读取三个整数，并将它们封装在一个 &lt;code&gt;BlockPos&lt;/code&gt; 对象中返回。并且，我们尝试使用 &lt;code&gt;try-catch&lt;/code&gt; 来保证健壮性。无论什么情况我们都需要用一个 &lt;code&gt;try-catch&lt;/code&gt; 来处理可能出现的异常。&lt;/p&gt;
&lt;p&gt;通过 &lt;code&gt;ArgumentTypeRegistry#registerArgumentType&lt;/code&gt; 注册这个参数类型后就可以使用了，在这里我们选择在注册所有的指令之前注册参数类型。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class ModCommand implements ModInitializer {
    private static int executeBlockPosArgument(CommandContext&amp;#x3C;CommandSourceStack&gt; context) {
        BlockPos blockPos = context.getArgument(&quot;blockpos&quot;, BlockPos.class);
        String blockName = context.getSource().getLevel().getBlockState(blockPos).getBlock().getName().getString();
        context.getSource().sendSuccess(() -&gt; Component.literal(&quot;You provided %s at %s&quot;.formatted(blockName, blockPos.toShortString())), false);
        return 1;
    }
    // ...
    @Override
    public void onInitialize() {
        // Register custom argument type
        ArgumentTypeRegistry.registerArgumentType(
                ResourceLocation.fromNamespaceAndPath(Fabricdemo.MOD_ID, &quot;block_pos&quot;),
                BlockPosArgumentType.class,
                SingletonArgumentInfo.contextFree(BlockPosArgumentType::new)
        );
        CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -&gt; {
            // Get a BlockPos argument
            dispatcher.register(Commands.literal(&quot;whatisthis&quot;)
                    .then(Commands.argument(&quot;blockpos&quot;, new BlockPosArgumentType())
                            // suggest()方法用于提供自动补全，下一节会详细介绍
                            .suggests(new BlockPosSuggestionProvider())
                            .executes(ModCommand::executeBlockPosArgument))
            );
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;构造我们自定义的参数类型 &lt;code&gt;BlockPosArgumentType&lt;/code&gt; 的实例后传入 &lt;code&gt;argument()&lt;/code&gt; 方法中，就可以使用这个参数类型了。&lt;/p&gt;
&lt;p&gt;上述代码还使用了一个叫做 &lt;code&gt;suggests()&lt;/code&gt; 的方法，这个方法接受一个 &lt;code&gt;SuggestionProvider&amp;#x3C;S&gt;&lt;/code&gt; 用于为参数实现自动补全，下一节我会介绍如何实现这个接口。&lt;/p&gt;
&lt;h2&gt;为参数实现自动补全&lt;/h2&gt;
&lt;p&gt;光实现了参数类型还不够，我们还需要为参数实现自动补全功能。Brigadier 提供了 &lt;code&gt;SuggestionProvider&amp;#x3C;S&gt;&lt;/code&gt; 接口来实现这个功能。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class BlockPosSuggestionProvider implements SuggestionProvider&amp;#x3C;CommandSourceStack&gt; {
    @Override
    public CompletableFuture&amp;#x3C;Suggestions&gt; getSuggestions(CommandContext&amp;#x3C;CommandSourceStack&gt; context, SuggestionsBuilder builder) throws CommandSyntaxException {
        CommandSourceStack source = context.getSource();
        if (source.getPlayer() != null){
            builder.suggest(&quot;%d %d %d&quot;.formatted(
                    source.getPlayer().blockPosition().getX(),
                    source.getPlayer().blockPosition().getY(),
                    source.getPlayer().blockPosition().getZ()
            ));
        }
        return builder.buildFuture();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;S&lt;/code&gt; 一般是一个 &lt;code&gt;CommandSourceStack&lt;/code&gt; 对象，我们从第一个参数中获得 &lt;code&gt;context&lt;/code&gt; （他的类型&lt;code&gt;CommandContext&amp;#x3C;CommandSourceStack&gt;&lt;/code&gt;是不是有点眼熟？）,以及一个 &lt;code&gt;SuggestionsBuilder&lt;/code&gt; 对象。我们使用 &lt;code&gt;SuggestionsBuilder#suggest()&lt;/code&gt; 来添加建议，这个方法可以多次调用，即为自动补全添加多条建议。最后调用 &lt;code&gt;buildFuture()&lt;/code&gt; 方法来返回一个 &lt;code&gt;CompletableFuture&amp;#x3C;Suggestions&gt;&lt;/code&gt; 对象。&lt;/p&gt;
&lt;p&gt;对于注册的指令，我们只需要在添加参数时调用 &lt;code&gt;suggests()&lt;/code&gt; 方法，并传入我们实现的 &lt;code&gt;SuggestionProvider&lt;/code&gt; 对象即可。&lt;/p&gt;
&lt;p&gt;这是最常见的实现方式。但其实 &lt;code&gt;ArgumentType&amp;#x3C;BlockPos&gt;&lt;/code&gt; 也有提供自动补全的方法 &lt;code&gt;listSuggestions()&lt;/code&gt;，我们重写其方法按理来说也能实现自动补全的功能，然而这个方法的签名是&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;default &amp;#x3C;S&gt; CompletableFuture&amp;#x3C;Suggestions&gt; listSuggestions(CommandContext&amp;#x3C;S&gt; context, SuggestionsBuilder builder) {
    return Suggestions.empty();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可见它并没有指定 &lt;code&gt;S&lt;/code&gt; 的类型，所以我们有时无法访问 &lt;code&gt;CommandSourceStack&lt;/code&gt; 的实例，可见这种方法会受到很多限制。&lt;/p&gt;
&lt;p&gt;事实上，传入这个方法的 &lt;code&gt;S&lt;/code&gt; 是一个客户端类 &lt;code&gt;ClientSuggestionProvider&lt;/code&gt;，我们可以通过反射来访问。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    @Override
    public &amp;#x3C;S&gt; CompletableFuture&amp;#x3C;Suggestions&gt; listSuggestions(CommandContext&amp;#x3C;S&gt; context, SuggestionsBuilder builder) {
        if (context.getSource() instanceof CommandSourceStack source){
            if (source.getPlayer() != null){
                builder.suggest(&quot;%d %d %d&quot;.formatted(
                        source.getPlayer().blockPosition().getX(),
                        source.getPlayer().blockPosition().getY(),
                        source.getPlayer().blockPosition().getZ()
                ));
            }
        }
        else {
            try {
                Class&amp;#x3C;?&gt; cls = Class.forName(&quot;net.minecraft.client.multiplayer.ClientSuggestionProvider&quot;);
                if (cls.isInstance(context.getSource())) {
                    Object playerObj = cls.getMethod(&quot;getPlayer&quot;).invoke(context.getSource());
                    if (playerObj instanceof Player player) {
                        builder.suggest(&quot;%d %d %d&quot;.formatted(
                                player.blockPosition().getX(),
                                player.blockPosition().getY(),
                                player.blockPosition().getZ()
                        ));
                    }
                }

            } catch (ClassNotFoundException | InvocationTargetException | IllegalAccessException |
                     NoSuchMethodException ignored) {
                ;
            }
        }
        return builder.buildFuture();
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样做我们可以把自动补全的逻辑实现在自定义参数类型中，从而免去了调用 &lt;code&gt;suggests()&lt;/code&gt; 方法。&lt;/p&gt;
&lt;h2&gt;在客户端注册指令&lt;/h2&gt;
&lt;p&gt;需要指出的是，上述代码的所有类都是加载在服务端的，一般情况下注册指令都是由服务端完成的。但是 Fabric 也为客户端模组提供了在客户端注册指令的方式，通过在将指令实现在客户端上，可以避免当服务端没有安装模组时，客户端模组无法使用这个指令的问题。&lt;/p&gt;
&lt;p&gt;与之前类似，为了实现在客户端注册指令，我们要在模组的客户端入口点中注册指令。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class ClientModCommand implements ClientModInitializer {
    // ...
    @Override
    public void onInitialize() {
        ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -&gt; {
            dispatcher.register(
                    ClientCommandManager.literal(&quot;clienttestcommand&quot;).executes( context -&gt; {
		                context.getSource().sendFeedback(Component.literal(&quot;Called /clienttestcommand&quot;));
                        return 1;
                    })
            );
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意的是，指令注册的方式与之前有相对细微的区别，请仔细阅读代码，以区别先前的实现。&lt;/p&gt;
&lt;p&gt;我们调用的事件是 &lt;code&gt;ClientCommandRegistrationCallback.EVENT&lt;/code&gt;，并且传入的 Lambda 表达式的参数中没有 &lt;code&gt;environment&lt;/code&gt;，因为客户端模组只能运行在集成服务器环境中。此外，我们使用 &lt;code&gt;ClientCommandManager&lt;/code&gt; 来创建指令节点。最后，向执行者发送消息时，我们使用 &lt;code&gt;sendFeedback()&lt;/code&gt; 方法，它接受一个 &lt;code&gt;Component&lt;/code&gt; 对象而不是先前的 &lt;code&gt;Supplier&amp;#x3C;Component&gt;&lt;/code&gt;，而且也不会广播给其它 op。&lt;/p&gt;
&lt;p&gt;不要忘记在 &lt;code&gt;fabric.mod.json&lt;/code&gt; 中注册客户端入口点。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;&quot;entrypoints&quot;: {
    // ...
    &quot;client&quot;: [
        &quot;org.f14a.fabricdemo.client.FabricdemoClient&quot;,
        &quot;org.f14a.fabricdemo.command.ClientModCommand&quot;
    ]
}, 
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;更美观的注册指令写法&lt;/h2&gt;
&lt;p&gt;当指令结构比较复杂时，使用 &lt;code&gt;then()&lt;/code&gt; 方法嵌套的写法会显得非常臃肿，不易阅读。为了让代码更美观，我们可以先创建各个节点的变量，然后再使用 &lt;code&gt;then()&lt;/code&gt; 方法构建树状结构，最后将根节点注册到指令调度器中。&lt;/p&gt;
&lt;p&gt;在这里我将演示如何使用这种方式来注册一个比较复杂的指令结构。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class ModCommand implements ModInitializer {
    private static int executePlain(CommandContext&amp;#x3C;CommandSourceStack&gt; context) {
        context.getSource().sendSuccess(() -&gt; Component.literal(&quot;Executed command. &quot;), false);
        return 1;
    }
    @Override
    public void onInitialize() {
        CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -&gt; {
            // Easy-to-read ver
            LiteralArgumentBuilder&amp;#x3C;CommandSourceStack&gt; atree = Commands.literal(&quot;atree&quot;);
            LiteralArgumentBuilder&amp;#x3C;CommandSourceStack&gt; atree_sub1 = Commands.literal(&quot;sub1&quot;);
            LiteralArgumentBuilder&amp;#x3C;CommandSourceStack&gt; atree_sub2 = Commands.literal(&quot;sub2&quot;);
            LiteralArgumentBuilder&amp;#x3C;CommandSourceStack&gt; atree_sub1_leaf1 = Commands.literal(&quot;leaf1&quot;);
            LiteralArgumentBuilder&amp;#x3C;CommandSourceStack&gt; atree_sub1_leaf2 = Commands.literal(&quot;leaf2&quot;);
            LiteralArgumentBuilder&amp;#x3C;CommandSourceStack&gt; atree_sub2_leaf1 = Commands.literal(&quot;leaf1&quot;);
            LiteralArgumentBuilder&amp;#x3C;CommandSourceStack&gt; atree_sub2_node = Commands.literal(&quot;node&quot;);
            LiteralArgumentBuilder&amp;#x3C;CommandSourceStack&gt; atree_sub2_node_leaf1 = Commands.literal(&quot;leaf1&quot;);
            LiteralArgumentBuilder&amp;#x3C;CommandSourceStack&gt; atree_sub2_node_leaf2 = Commands.literal(&quot;leaf2&quot;);
            List&amp;#x3C;LiteralArgumentBuilder&amp;#x3C;CommandSourceStack&gt;&gt; commands = List.of(
                    atree,
                    atree_sub1,
                    atree_sub2,
                    atree_sub1_leaf1,
                    atree_sub1_leaf2,
                    atree_sub2_leaf1,
                    atree_sub2_node,
                    atree_sub2_node_leaf1,
                    atree_sub2_node_leaf2
            );
            // Realize commands.
            commands.forEach(cmd -&gt; cmd.executes(ModCommand::executePlain));
            // Build the tree structure.
            atree.then(
                    atree_sub1.then(
                            atree_sub1_leaf1
                    ).then(
                            atree_sub1_leaf2
                    )
            ).then(
                    atree_sub2.then(
                            atree_sub1_leaf1
                    ).then(
                            atree_sub2_node.then(
                                    atree_sub2_node_leaf1
                            ).then(
                                    atree_sub2_node_leaf2
                            )
                    )
            );
            // Finally register the root command.
            dispatcher.register(atree);
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你觉得这种写法更美观的话，可以尝试使用它来注册指令。一般情况下，如果你合理使用缩进，使用嵌套的写法也不会太混乱。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item></channel></rss>