抛弃 Bukkit API,拥抱 Paper API
在两三年前,如果你是一个正在打算开发 Minecraft 服务端插件的开发者,对于选择 API 标准这一方面,你可能会毫不犹豫地选择 Bukkit API:这是一个历史悠久、兼容性强、用户基数多的 Minecraft 服务端插件 API,你没有任何理由不选择它。但是在几年后的现在,你可能会选择另一个异军突起的标准 —— Paper API。
这时候有些开发者就要跳出来问了:Paper API 不本就是 Bukkit API 的一个超集吗,这两者有什么可比性吗?但实际上,Paper 在去年(2024)年底已经和 Spigot 发生了硬分叉(hard-fork
),这意味着两者之间不再存在继承关系,因此,开发者们需要在二者之间做一个抉择。而对于一个离开 Bukkit 开发多年,需要重新回到这里进行开发的我来说,在进行了简单调研后,我便毋庸置疑的选择了使用 Paper API 进行开发。
paperweight-userdev:一站式的开发环境以及 NMS 访问支持
paperweight 是 Paper 自定义构建工具的名称。paperweight-userdev Gradle 插件是该工具的一部分,用于在开发过程中访问内部代码(也称为 NMS)。
如果你是一名 Forge 开发者,那你可能听说过 ForgeGradle,其可以根据一定的重映射(remapping)表将 Minecraft 源代码反混淆为人类友好的源代码,并帮你在构建模组时将使用到 Minecraft 源代码的源代码重新混淆回原来的样子,以支持在 Minecraft 客户端上的运行。很多年以来,Bukkit API 开发者们并不能很好的在插件开发中使用 Minecraft 源代码(net.minecraft.server
包,简称 NMS;当然这里要说的是和 Forge 不同,Bukkit 因为提供了很好的上层封装,因此并不鼓励这种使用方式,所以可能刻意没有提供很好的工具),即使后续 SpigotMC 推出了 BuildTools 这样的工具来辅助你访问 NMS,但是整个开发链路并不完整,也没有很好的脚手架。paperweight-userdev 解决了这个问题。
和 ForgeGradle 一样,在安装 Gradle 插件并配置依赖后,你便得到了一个反混淆后的服务器核心依赖,其中包括了所有的 NMS 代码,你可以在你的 IDE 中丝滑的访问和使用这些代码,并且配置断点来进行调试。你只需要使用 NMS,其他的,交给 paperweight 即可。
Data Component API:比 ItemStack 更好的 Data Component 支持
Minecraft 在 1.20.5 引入了 Data Component,取代了已经存在了十几个版本的 NBT 数据标签,Data Component 旨在通过模块化、数据驱动的模式,为 Minecraft 引入全新的可自定义性。例如,可以通过为仙人掌添加 consumable
和 food
组件让其成为可以食用的食物(客户端也可以显示对应的动画),或者,通过设置 max_stack_size
组件控制一类物品的最大堆叠数。这在以前是绝对做不到的。
Bukkit API 为 ItemStack 提供了有限的 Data Component 支持,但并不完善,对于很多 Component,ItemStack 并不支持进行设置。Paper 的 Data Component API 提供了一套很方便的 API 来帮助你存取这些数据。
拿 Paper API 文档上的一个例子来说,你可以创建一个拿在手上是钻石,穿在身上是下界合金,而且当你穿戴时还会发出自定义音效的头盔:
ItemStack helmet = ItemStack.of(Material.DIAMOND_HELMET);
// Get the equippable component for this item, and make it a builder.
// Note: Not all types have .toBuilder() methods
// This is the prototype value of the diamond helmet.
Equippable.Builder builder = helmet.getData(DataComponentTypes.EQUIPPABLE).toBuilder();
// Make the helmet look like netherite
// We get the prototype equippable value from NETHERITE_HELMET
builder.assetId(Material.NETHERITE_HELMET.getDefaultData(DataComponentTypes.EQUIPPABLE).assetId());
// And give it a spooky sound when putting it on
builder.equipSound(SoundEventKeys.ENTITY_GHAST_HURT);
// Set our new item
helmet.setData(DataComponentTypes.EQUIPPABLE, builder);
Command API:来自 Brigadier,超越 Brigadier
Bukkit API 的命令系统一直饱受诟病,尤其是自 Minecraft 1.13 引入 Brigadier 以后,要在 Bukkit 上实现优雅且完备的命令补全几乎不可能;更不用说 Brigadier 提供的指令树功能到了 Bukkit 这里完全不受支持。
Paper API 则直接引入了一整套 Brigadier 的指令系统,并做了一些适配其自身的优化。无论如何,现在你都可以快乐地使用 Brigadier 来创建复杂的指令了:
LiteralArgumentBuilder root = Commands.literal("nationcraft")
.requires(sender -> sender.getSender().hasPermission("nationcraft.admin"))
.then(
Commands.literal("reload").executes(this::reload)
)
.then(
Commands.literal("nation").then(
Commands.argument("nation", new NationArgumentType())
.then(Commands.literal("destroy").executes(this::nationDestroy))
.then(Commands.literal("fall").executes(this::nationFall))
.then(Commands.literal("add-member").then(Commands.argument("player", ArgumentTypes.player()).executes(this::nationAddMember)))
.then(Commands.literal("remove-member").then(Commands.argument("player", ArgumentTypes.player()).executes(this::nationRemoveMember)))
.then(Commands.literal("add-land").then(Commands.argument("world", ArgumentTypes.world()).then(Commands.argument("x", IntegerArgumentType.integer()).then(Commands.argument("z", IntegerArgumentType.integer()).executes(this::nationAddLand)))))
.then(Commands.literal("remove-land").then(Commands.argument("world", ArgumentTypes.world()).then(Commands.argument("x", IntegerArgumentType.integer()).then(Commands.argument("z", IntegerArgumentType.integer()).executes(this::nationRemoveLand)))))
.then(Commands.literal("set-name")).then(Commands.argument("newName", StringArgumentType.string()).executes(this::nationSetName))
.then(Commands.literal("set-color").then(Commands.argument("newColor", new ColorArgumentType()).executes(this::nationSetColor)))
.then(Commands.literal("set-pantheon").then(Commands.argument("newPantheon", new PantheonArgumentType()).executes(this::nationSetPantheon)))
.then(Commands.literal("cancel-process").then(Commands.argument("world", ArgumentTypes.world()).then(Commands.argument("x", IntegerArgumentType.integer()).then(Commands.argument("z", IntegerArgumentType.integer()).executes(this::nationCancelProcess)))))
.then(Commands.literal("info").executes(this::nationInfo))
)
)
.then(
Commands.literal("human").then(
Commands.argument("player", ArgumentTypes.player())
.then(Commands.literal("join-nation").then(Commands.argument("nation", new NationArgumentType()).executes(this::humanJoinNation)))
.then(Commands.literal("quit-nation").executes(this::humanQuitNation))
)
)
.then(
Commands.literal("create-nation").then(Commands.argument("name", ArgumentTypes.component()).executes(this::createNation))
);
plugin.getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, commands -> commands.registrar().register(root.build()));
更好的 PersistentDataContainer:也许我们可以做到更多...
长久以来,Bukkit API 并不支持存取自定义的 NBT 数据,而 PersistentDataContainer 作为上述问题的解决方案实则已经存在许久,但是其依然存在一些问题,例如我们无法在玩家不在线的情况下存取玩家的 PersistentDataContainer 数据。当然这个问题主要是因为 Bukkit API 从底层设计上就无法支持离线存储数据导致的。但是 Paper API 还是提供了 PersistentDataContainerView
,为部分对象(例如 OfflinePlayer
)提供有限的只读支持),虽说不够完美,但在一定情况下也满足了一些需求。
Adventure API:创建文本组件的终极选择
早在 Paper 未与 Spigot 硬分叉之前,前者就引入了 Adventure API 作为文本组件的支持,并且逐渐弃用原有的纯文本或是 BungeeChat 支持。Bukkit 生态在继纯文本、BungeeChat API 后,迎来了又一个抽象支持。
Adventure API 几乎支持所有需要填写文字的地方,从聊天框到 Bossbar,Title 到 Actionbar,甚至 Paper 还专门为 Adventure 做了一个支持 Adventure Component 的 Logger,以帮助你在控制台上显示格式化的数据。
Adventure 并不是一个 Paper-only 的库,相反,它支持几乎所有 Minecraft 开发平台,但在 Paper 上使用时,会更方便一些。