thumbnail
抛弃 Bukkit API,拥抱 Paper API
由 AI 生成的文章摘要
本文探讨了从Bukkit API转向Paper API的优势。博主指出,虽然Bukkit API曾长期是Minecraft插件开发的首选,但随着Paper与Spigot的硬分叉,Paper API已成为更优选择。文章重点介绍了Paper API的几大优势:paperweight-userdev提供完整的NMS开发支持;Data Component API比传统ItemStack更强大;Command API直接整合Brigadier指令系统;PersistentDataContainer功能更完善;以及Adventure API作为文本组件的终极解决方案。博主通过具体代码示例展示了这些API的实用性,认为Paper API为现代Minecraft插件开发提供了更先进、更完善的技术支持。

抛弃 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 即可。

image-20250618164933499

Data Component API:比 ItemStack 更好的 Data Component 支持

Minecraft 在 1.20.5 引入了 Data Component,取代了已经存在了十几个版本的 NBT 数据标签,Data Component 旨在通过模块化、数据驱动的模式,为 Minecraft 引入全新的可自定义性。例如,可以通过为仙人掌添加 consumablefood 组件让其成为可以食用的食物(客户端也可以显示对应的动画),或者,通过设置 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 上使用时,会更方便一些。

扫码关注 HikariLan's Blog 微信公众号,及时获取最新博文!


微信公众号图片
暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇