欢迎光临散文网 会员登陆 & 注册

从零开始的Minecraft - Nbt 序列化库开发:添加功能:LocateAt注解

2023-02-16 15:04 作者:疑似叉叉星来的鹩八哥  | 我要投稿

项目开篇:项目介绍 & 当前开发成果总结

上一篇:从零开始的Minecraft - Nbt 序列化库开发:当上层接口变成“野接口”时会发生什么

项目github地址:https://github.com/Cmyna/mnbt-in-development-

摘要:本篇将简单介绍两个功能的开发:自定义注解LocateAt,修改已有的Tag对象(OverrideTag)。并且简单说些细碎的改动,比如名字改动什么的。

前置知识:本项目相关的上层接口的理解,java/kotlin的自定义注解相关内容

先简单说下其他方面的一些改动:首先Serdes更名为Codec,名字参考的是这篇网站上的讨论: https://english.stackexchange.com/questions/76549/a-word-that-describes-both-encoding-and-decoding。同样的原本Serdes下的两个方法serialize,deserialize也被更名为encode和decode。

其次,在文章 从零开始的Minecraft - Nbt序列化库开发:嵌套类型Nbt开发(1) - 顶层接口改动 中,我对CompoundTag的要求有误解,Nbt CompoundTag有要求其中的所有子Tag名字不能重复,因此表示CompoundTag的java Tag对象被改为Tag<Map<String, Tag<out Any>>> 而不是原来的Tag<Collection>。

然后是关于上一篇“野接口”,因为TagConverter/Codec在处理嵌套Tag的子Tag时是交给代理处理,这个过程不可避免的涉及到intent的接口增删/修改。为了保持intent的健壮性,从Codec/TagConverter传给代理proxy的intent都通过动态代理创建,动态代理将继承传入intent的所有接口,只增删/修改覆盖部分需要的接口和其方法。

然后是一个应用类Mnbt,主要是提供之前实现的各种功能接口的汇总。类似于Gson中的Gson类,用户可以通过这个类调用该库实现的所有功能。可以查看仓库 api子项目中com.myna.mnbt.Mnbt的实现

自定义注解LocateAt:

这个注解是对现有从类似于 javaBean 对象创建Tag,或者从Tag转换为 javaBean对象的一个补充,主要目标是提供重映射Class/Field到对应的nbt结构这一功能。在我的设想中,其功能类似于Gson的SerializeName注解,或者是JackJson的@JsonProperty注解;可以对类中的成员映射到另一个名字的Tag,例如以下的类

但不仅只是重映射一个名字,我还希望注解可以映射更复杂的nbt结构,直接文字描述会比较复杂,我用下面一张图作为例子以辅助解释我所期望的映射方式:


LocateAt映射方式例子

图中棕色和黄色的Tag都是Compound Tag,而绿色则可以是任意类型的Tag。箭头指示了Nbt 结构,或者是类和其成员到Nbt结构的映射关系,根 Compound Tag指的是最上层调用 TagConverter.createTag 中返回的Tag对象,或者是最上层TagConverter.toValue方法传入的Tag对象

比如将右边Foo类的对象实例转成一个Tag对象时,如果没有LocateAt注解,那么它只会被转成一个特定名字的根CompoundTag, 然后下面带三个子Tag,以其成员变量名作为子Tag的名字。但是如果使用了LocateAt注解,那么他可以不是这种结构,容载Foo对象数据的CompoundTag被映射到了 根CompoundTag->CompoundTag 1->CompoundTag 2,而其成员变量也被映射到了Compound Tag 2下的子nbt结构,如图所示,成员1被映射到CompoundTag 2下的 Compound Tag3 -> Tag 1,成员2 被映射到 CompoundTag 2下的 Tag2,成员3 则被映射到 Compound Tag 2 下的Compound Tag4 -> Tag 3。

或者在使用LocateAt注解后从根CompoundTag转成Foo对象时,TagConverter会查找 根CompoundTag->Compound Tag 1 -> Compound Tag 2 并从这里开始转换,转换Foo中的 成员1变量时,会查找 Compound Tag 2-> Compound Tag 3 -> Tag 1,并将其尝试转成成员1变量类型;诸如此类。

在项目中,我是这么声明LocateAt注解的:

这是一个kotlin注解,理论上也可以被用于java代码中,其中有两个变量:toTagPath,fromTagPath,分别为createTag和toValue两个方法时映射的nbt结构,fromTagPath有默认值,所以使用者可以不显示声明,当使用默认值“”(也就是空字符串)时,期望TagConverter使用toTagPath作为替代。

该注解可以用在两种类型:CLASS和FIELD,也就是可以注解在类或者成员变量上。

NbtPath 格式:

这里我一直没有说明注解中String变量的格式。在上面代码和更上面的图中都是一种类url字符串形式。这里定义NbtPath的格式:开头以"mnbt://"表示绝对路径,或者以"./"表示相对路径。

路径主体是以'/'分割的字符串,除了被"/"分割最末尾的子字符串外,路径中任何中间字符串所代表的都是一个嵌套类型的Nbt Tag对象(例如list tag或Compound tag)

如果Tag对象名字为空(例如ListTag下的子Tag),在路径中以"#"开头,后面带上数字则表明为其处于ListTag中的索引位置

LocateAt结构抽象:

首先是对LocateAt定义的Nbt子结构进行抽象。因为LocateAt既可以用于注解类也可以注解类中字段(成员变量),所以我们额外定义一个抽象: DataEntryTag,表示这是一个类中所有字段(成员变量)的数据入口Tag,如下图所示:

类上注解所映射的结构

注解于类上的LocateAt中路径变量表示的就是这么一个结构,注意 根 CompoundTag并不包含在路径中,如果注解路径不为空(包含多个有效Tag名字),那么路径最后一段表示的即为DataEntryTag,否则根CompoundTag为DataEntryTag。例如上面的TestClass注解例子,名为“compound tag 2”的Tag为DataEntryTag

从DataEntryTag到字段对应的Tag如下图:

字段上注解所映射的结构

和注解在类上代表的nbt结构类似,注解在字段上的LocateAt路径,如果路径不为空,取路径最后一段作为字段对应的Tag对象名字,否则Tag对象名字为类中字段名字。

在ReflectiveConverter中添加相关功能:

由此NbtPath字符串格式和LocateAt注解的定义,我们可以开始为这个需求添加具体实现。由于处理POJO类的TagConverter由ReflectiveConverter负责,所以功能实现也集中在ReflectiveConverter中。具体功能已经在github仓库gradle子项目api的com.myna.mnbt.converter.ReflectiveConverter中实现。由于整段功能代码非常长且复杂,所以我就不在此贴具体代码,而是简要概括思路 & 伪代码。

首先是TagConverter.toValue部分,这部分相对简单,首先以toValue函数传入的Tag作为根Tag,根据是否有LocateAt注解找到DataEntryTag(当然如果没有注解或注解为空,则toValue传入的Tag参数即为DataEntryTag),然后再从DataEntryTag开始,根据类字段注解(或没有注解,则是类字段名称)找到字段对应的Tag,再交给proxy转成对应的值,伪代码如下:

然后是createTag函数部分的修改。这里有个特殊情况,即createTag时可能存在多个被LocateAt注解的类/字段,而他们注解的路径可能会部分重叠,例如:

我们可能希望在转成Tag时,这些重叠的路径声明不会影响到最终的Tag对象生成,例如Foo中字段i和j会被放在同一个tag1->tag2的CompoundTag结构中,再比如Bar.bytearr字段生成的Tag会被放在foo->class tag1->tag1->byte array中,和foo.i共享一段相同的nbt结构(foo->class tag1->tag1->tag2->int tag)。

但是很明显,目前的ReflectiveConverter难以支持这种功能实现,因为现在处理嵌套类型Tag采用的代理模式,让被代理的TagConverter无法知道代理者的信息,例如处理Foo对象时TagConverter并不知道它的上级Bar有个字段和其共享部分相同的nbt结构。处理Foo对象的TagConverter会自行创建相同的Nbt结构,如果程序之前是先处理Bar的bytearr字段,那么新结构不可避免地会和原结构冲突。

一种解决这个问题的方法是传递已经创建的nbt结构信息。举个例子,我们模拟在转化Bar对象到Tag对象过程中,ReflectiveConverter先处理bytearr字段再处理foo字段,那么ReflectiveConverter可以先创建一个根节点,然后把bytearr字段转成的nbt子结构填入其中,然后再把这棵nbt树的信息传给代理proxy,下一层的ReflectiveConverter拿到这段结构信息,则已经知道class tag1->tag1这一段结构已经建立,那么在需要这段结构时直接从传入的信息取出对应Tag即可,而避免创建新的相同nbt结构。对于ReflectiveConverter,这段过程可以描述成一个更通用的代理形式:

  • 尝试获取上文nbt结构信息,如果没有,则创建一个新的nbt结构信息并设置根节点,否则用上文nbt结构对应的Tag作为当前节点

  • 无论是处理类LocateAt注解还是字段LocateAt注解,首先从nbt结构信息获取对应的Tag,只有在已建立的nbt结构没有对应节点时才创建新的Tag,并填入补充nbt结构。

  • 创建子Tag时,将nbt结构信息传给代理proxy,让下层TagConverter也可以获得已创建的nbt结构信息

为此,我声明了一个新的继承自CreatTagIntent的intent:BuiltCompoundSubTree

并且ReflectiveConverter.createTag方法被改写为以下伪代码逻辑:

以上伪代码省略了大量的分支逻辑判断,异常判断,还有一些函数功能细节,具体实现可以取github仓库查看。

看完以上你可能会发现这个过程完全只需要修改到ReflectiveConverter,那如果一个POJO字段要被转换成listTag中的某一个子Tag呢?我在一开始开发这个功能时确实想到了ListTag,并且如上文也声明了在路径中表示ListTag索引的方式,但最终我还是放弃对重映射进listTag元素功能,倒也不是开发这个功能上有多少困难,主要是listTag本身特点导致使用逻辑会变得很复杂。比如将某个类/字段转成listTag特定索引下的一个nbt子结构,那就不得不考虑listTag前面的元素是否为空。二进制数据的list tag要求元素不可为空,但如果转换结果出现部分元素为空的ListTag对象,那势必导致最终ListTag索引发生变动(或者是直接抛出异常)。LocateAt注解重映射的本意是方便用户序列化/反序列化对象,增加灵活性,但TagConverter在处理包含listTag索引的路径时难以保证最终映射结果会到那个索引上。因为这种不可靠性,LocateAt注解分出了ToTagPath和FromTagPath两个参数,并且要求ToTagPath不包含任何nbt listTag格式的索引路径。如果用户真的有将字段映射到一个ListTag索引下的某个nbt子结构这一需求,可以考虑自行实现一个TagConverter接口并注册进Mnbt对象中(就是文章最开头提到的Mnbt应用类,其提供了一个注册TagConverter函数)


从零开始的Minecraft - Nbt 序列化库开发:添加功能:LocateAt注解的评论 (共 条)

分享到微博请遵守国家法律