复活Bukkit的PDC(1)

前言

Bukkit在11400扩展了NMS的NBT存储,使其能够非常方便的存取来自第三方的各种类型(Int, Double, Long, String, 各种Array,以及更后期的Boolean和List)的持久化数据到游戏内的各种载体(生物,物品,方块,甚至区块和世界),此“接口”对应Bukkit的org.bukkit.persistence.* ,对应实现为CraftBukkit的org.bukkit.craftbukkit.persistence.CraftXXX,由于它只是扩展了NMS的原有功能,所以在原则上,将其带回更早的版本是可行的,本文章将以11202(NMS 1_12_R1)探究如何将Bukkit的PDC Backport到更早期的版本

更多有关PDC的介绍,请见海螺先生在2019年的博客文章:Bukkit 持久化数据存储 | IzzelAliz's Blog

注:

1.术语Bukkit:MC第三方服务端软件,为原版MC服务端扩展了极多的外部开发接口,由Bukkit团队开发,因直接分发反混淆后的MC服务端原代码被Mojang 以侵权 DMCA Takedown,目前社区由SpigotMC接手其开发,采用BuildTools分发反混淆后的MC服务端源代码以及Bukkit的Fork: Spigot

2.术语CraftBukkit:Bukkit接口的实现,由Bukkit团队开发,目前由SpigotMC接手

3.术语BuildData:Bukkit采用和维护的MC反混淆方案,与MCP(Forge采用)、Yarn(Fabric采用)相同,旨在以人为臆测的方式反混淆MC这个商业混淆软件中的代码,目前由SpigotMC接手

4.术语NMS:net.minecraft.server包名的缩写,是BuildData在反混淆MC服务端源码时假想的MC服务端代码结构,包含了Mojang为MC服务端编写的所有底层代码(1.16.5以前),CraftBukkit通过对它的修改来得到接口方法,最终提供给Bukkit

5.关于版本:上方和下方对于MC版本的声明使用不加”.“的形式,如11202代表1.12.2,对NMS的声明使用以”_“分隔的后两部分,如12R1代表1.12.2的NMS

准备工作

  • 11202的MC服务端源码:BuildTools获取org.spigotmc.minecraftserver 后在本地部署任意Spigot Fork源码即可得到,推荐使用Paper,部署方式为使用git ./paper p(1.16.5及以前)
  • 11400的MC服务端源码:如上
  • 11605的MC服务端源码:如上,因为PDC的部分功能在11400更后期的版本加入,故需要更高版本的服务端源码

从接口入手

我们首先从11400外部可见的Bukkit PDC接口org.bukkit.persistence.* 开始,显然,这些接口应该不加改动地被移植到我们的11200服务端的API模块下,所以目前我们获得了如下几个接口:

java 复制代码
public interface PersistentDataAdapterContext
public interface PersistentDataContainer
public interface PersistentDataHolder
public interface PersistentDataType<T, Z>

顾名思义,它们功能依次为:

  • 为PDC提供上下文的接口,主要方法为创建新容器PersistentDataContainer newPersistentDataContainer()
  • PDC主体的接口,包含一系列类型字段的getter, setter等
  • PDC的载体的接口,包含从游戏内的各种载体(见前言)获取PDC的方法PersistentDataContainer getPersistentDataContainer() ,它会在下文多次出现
  • PDC存取数据的类型接口:泛型T为实际存储在NBT中的类型,泛型Z为PDC向用户提供的接口类型

接着实现接口

根据Bukkit的惯例,一个在Bukkit中的接口XXX,它的实现一般为CraftBukkit同包名下的CraftXXX,然后CraftXXX会适配NMS内的相应方法完成操作,果不其然,在Server模块的org.craftbukkit.persistence 包下,我们找到了

java 复制代码
public final class CraftPersistentDataAdapterContext implements PersistentDataAdapterContext
public final class CraftPersistentDataContainer implements PersistentDataContainer
public final class CraftPersistentDataTypeRegistry

顾名思义,它们的功能也应为:

  • 为PDC提供上下文的实现
  • 为PDC提供主体功能的实现
  • PDC的类型注册表

解决第一个问题

如果你也尝试了上述操作,我们就会发现,我们所遇到的第一个问题就是出现在CraftPersistentDataTypeRegistry 中的报错,它们出现在

java 复制代码
    /**
     * Creates a suitable adapter instance for the primitive class type
     *
     * @param type the type to create an adapter for
     * @param <T> the generic type of that class
     *
     * @return the created adapter instance
     *
     * @throws IllegalArgumentException if no suitable tag type adapter for this
     * type was found
     */
private <T> TagAdapter createAdapter(Class<T> type)

这个方法中,由Bukkit的注释我们可知此方法的主要作用为针对NMS中的不同NBT类型提供对应的适配器,出错的位置为找不到NMS NBT类型中的对应方法。

这种错误在我们进行跨版本NMS兼容时经常会见到,其中大部分的错误是由于BuildData在不同版本对NMS字段的反混淆名称不同,或在更早期的版本中暂未提供方法的反混淆名导致的

错误出现的位置为每个return语句的最后一个字段

return createAdapter(XXX.class, NBTTagXXX.class, NBTTagXXX::new, NBTTagByte::方法)
阅读源码知,此处创建的类型适配器参数为

  • Bukkit提供的接口类型,对应第一个字段

  • NMS对应此类型的类,对应第二个字段

  • 将Bukkit提供的接口类型的数据转换为NMS对应类型的数据的方法,在11400和11202中均由NMS NBT类的默认单参构造函数实现

  • 将NMS对应类型数据转换为Bukkit接口类型数据的方法,也即是出问题的地方

既然出现问题的是”将NMS对应类型数据转换为Bukkit接口类型数据的方法“,我们应推断这个NBT类内方法应是”返回对应此类存储数据类型“的方法,如NBTTagByte内的返回byte的方法

同时查看11400和11202的对应NMS NBT源码,我们发现11200

  • NBTTagByte中的public byte g()
  • NBTTagShort中的public short f()
  • NBTTagInt中的public int e()
  • NBTTagLong中的public long d()
  • NBTTagFloat中的public float i()
  • NBTTagDouble中的public double asDouble() (这个方法BuildData就提供了反混淆名,但不知为何其他方法没有提供)
  • NBTTagString中的public String c_()
    均符合我们的要求,这些方法可直接修改至实现的源码中

对于Array数据类型,转换数据类型的方法类似,但11200的NMS NBT类中均缺少了返回存储数据长度的size()方法,所以我们需要手动添加回size() 方法,返回data.length

至此,我们解决了Bukkit和NMS数据类型的转换问题

分析具体实现

根据前言,我们知PDC提供了对游戏内各种载体持久数据的存取接口,那么这些载体的接口,肯定实现了PersistentDataHolder 接口,查找11400中此接口的实现,我们发现它有300多个实现,几乎涵盖了游戏中所有与持久数据有关的内容,但归根到底,实现了PersistentDataHolder 的类只有4个,它们分别是

  • CraftEntity:对应生物
  • CraftMetaItem:对应物品ItemStack中的ItemMeta
  • TileState接口
    • 实现类CraftBlockEntityState:对应方块
      其中TileState接口比较特殊,
java 复制代码
/**
 * Represents a block state that also hosts a tile entity at the given location.
 *
 * This interface alone is merely a marker that does not provide any data.
 *
 * Data about the tile entities is provided by the respective interface for each
 * tile entity type.
 *
 * After modifying the data provided by a TileState, {@link #update()} needs to
 * be called to store the data.
 */
public interface TileState extends BlockState, PersistentDataHolder

它下方还有十几个实现接口,每个接口又由对应的CraftXXX实现,但我们发现CraftXXX无一例外实现了CraftBlockEntityState ,因此我们只需改造这十几个实现接口,使它们继承TileState ,并在CraftBlockEntityState 中实现我们的具体操作,这样在这些接口调用PersistentDataContainer getPersistentDataContainer() 时,它就会找到我们在CraftBlockEntityState 中的实现

待我们改造完这十几个接口,使它们继承TileState 后,这些接口所代表的方块便拥有了访问PDC的能力

未完待续

在本篇文章中,我们复活了PDC的顶层接口,在接下来我们会详细探讨CraftBlockEntityState ,CraftEntity ,CraftMetaItem 的具体实现,因为Bukkit服务端的结构为

Bukkit(提供顶层接口)-> CraftBukkit(提供Bukkit接口的实现)-> NMS中CraftBukkit改造的方法

所以要具体实现这三个类的操作,我们将不可避免地接触到Minecraft服务端的底层代码(就比如上面的NBTTagByte等),而它们中的大多数在早期版本是没有反混淆名的,因此其复杂程度值得我们再开一篇文章单独分析

另外,在更高版本(如11605以上),PDC的功能拓展了许多(类型、存储载体),这些将在以后我们将11400的PDC Backport的部分讲解完成后作为补充

游客

全部评论 (0)

暂无评论,快来抢沙发吧~