红石中继站
    • 资源
    • 新帖
    • 版块
    • 热门
    • 登录

    抛弃 Bukkit API,拥抱 Paper API

    编程开发
    2
    4
    201
    正在加载更多帖子
    • 从旧到新
    • 从新到旧
    • 最多赞同
    回复
    登录后回复
    此主题已被删除。只有拥有主题管理权限的用户可以查看。
    • HikariLan贺兰星辰H
      HikariLan贺兰星辰
      最后由 编辑 · 陕西省

      在两三年前,如果你是一个正在打算开发 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.png

      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<CommandSourceStack> 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 上使用时,会更方便一些。

      半栈工程师 | 开源爱好者 | GitHub | 个人主页 | 个人博客

      1 条回复 最后回复 回复 引用
      • PQguanfangP
        PQguanfang
        最后由 编辑 · 安徽省

        拥抱不了一点,不会用。

        我来问几个 Data Component API 有关的问题:

        • BlockData 该怎么用?为什么其 Builder 是空的?
          https://jd.papermc.io/paper/1.21.6/io/papermc/paper/datacomponent/item/BlockItemDataProperties.html
        • CUSTOM_DATA去哪了?
        • 1.21.3 的 CUSTOM_MODEL_DATA 代码是这样的:
          https://jd.papermc.io/paper/1.21.3/io/papermc/paper/datacomponent/item/CustomModelData.html
          1.21.6 的 CUSTOM_MODEL_DATA 代码是这样的:
          https://jd.papermc.io/paper/1.21.6/io/papermc/paper/datacomponent/item/CustomModelData.html
          没有一丝跨版本兼容性的吗?
        • Spigot 的 ToolComponent 提供了 remove 方法:
          https://hub.spigotmc.org/javadocs/spigot/org/bukkit/inventory/meta/components/ToolComponent.html
          DataCompnentAPI 中该如何 remove?
          https://jd.papermc.io/paper/1.21.3/io/papermc/paper/datacomponent/item/Tool.html
        HikariLan贺兰星辰H 1 条回复 最后回复 回复 引用
        • HikariLan贺兰星辰H
          HikariLan贺兰星辰 @游客
          最后由 编辑 · 陕西省

          @PQguanfang 竟然您指出了如此之多的问题,我相信您一定也认真研读了 Paper 发布的 Data components 文档

          其中写道:

          When should I use DataComponents or ItemMeta?
          You would want to use ItemMeta if you:

          Are doing only simple changes to ItemStacks
          Want to keep cross-version compatibility with your plugin
          You would want to use data components if you:

          Want more complicated ItemStack modifications
          Do not care about cross-version compatibility
          Want to access default (prototype) values
          Want to remove components from an ItemStack’s prototype

          如果您还没有看,那么相信其中的内容能解决您的大部分问题。

          至于 CUSTOM_DATA,我相信您可以用 PDC 作为代替。

          最后,希望您日后在发表任何言论的时候都能心平气和,而不是将您的怒气释放在一个小网站的某个小帖子的不知名发布者身上。

          半栈工程师 | 开源爱好者 | GitHub | 个人主页 | 个人博客

          PQguanfangP 1 条回复 最后回复 回复 引用
          • PQguanfangP
            PQguanfang @游客
            最后由 PQguanfang 编辑 · 未知地域

            @HikariLan贺兰星辰

            抱歉,我只是觉得,你的标题

            抛弃 Bukkit API,拥抱 Paper API

            是站不住脚的。

            当然,每个人都有自己的看法,我应该尊重你的看法,是我鲁莽了。

            针对你所说的,我想说,现在 Paper 不同步 ItemMeta,想要用新增的 WeaponComponent等等,你只能选择 Data Component,而它还有很多东西不完善,有时,你只能对同一个物品调用来自两个地方的 API,才能把物品改成你想要的样子

            1 条回复 最后回复 回复 引用
            • 第一个帖子
              最后一个帖子
              "Minecraft" 以及 "我的世界" 为 Mojang Synergies AB 的商标,本站与 Mojang 以及 Microsoft 没有从属关系
              © 2024-2025 红石中继站 版权所有 本站原创图文内容版权属于原创作者,未经许可不得转载
              侵权投诉邮箱:[email protected]
              由 长亭雷池WAF 提供安全检测与防护 由 WAFPRO 提供 SCDN 安全加速
              苏公网安备32050902102328号 苏ICP备2023043601号-8