美味的曲奇

   基于NBT的ItemStack序列化 [1.8.x-1.18.x]


环境
        Kotlin
        TabooLib
参考原型:
        [1.8.x-1.18.x] [v7.18.2] [Maven] Single Class NBT Editor for items, skulls, mobs, and tile entities!

前言
为什么要使用NMS从NBT序列化ItemStack,而不直接调用 ConfigurationSerializable 接口中的序列化方法?
        1. ConfigurationSerializable 得到的Map中未知NBT会被处理,可读性不佳
        2. 序列化储存时大多数时候需要String,Map需要被二次序列化
        3. ConfigurationSerializable 出的结果无法直接被没有 bukkitAPI 的外部程序反序列化(需要额外处理),而NBT本身易于解析与读取
        4. NBT与原版密切相关,利于兼容

实现思路
        序列化:
                Bukkit-ItemStack -> NMS-ItemStack
                NMS-ItemStack -> NBT
                NBT -> String
        反序列化:
                String -> NBT
                NBT -> NMS-ItemStack
                NMS-ItemStack -> Bukkit-ItemStack

实现辅助
        TabooLib:
                taboolib.module.nms.MinecraftVersion -> 用于获取当前服务器版本
                taboolib.module.nms.nmsClass -> 获取NMS(net.minecraft.server)类
                taboolib.module.nms.obcClass -> 获取OBC(org.bukkit.craftbukkit)类
        其他:
                Reflexes -> 反射工具类 用于优化代码可读性

        对于不基于 TabooLib 的开发(真的有人Kotlin开发不用TabooLib吗!| Doge ),这里有一份简单的方法含义:


具体实现-Reflexes
        Reflexes 类的意义就是通过Kotlin的运算符重载,让反射调用看起来更整洁
      
  1. import java.lang.reflect.Constructor
  2. import java.lang.reflect.Method

  3. @SuppressWarnings("unchecked","shadowed")
  4. internal operator fun <T> Method.invoke(obj: Any? = null, vararg arg: Any?) : T?{
  5.     return this.invoke(obj,*arg) as T?
  6. }

  7. @SuppressWarnings("unchecked")
  8. internal operator fun <T> Constructor<*>.invoke(vararg arg: Any?) : T{
  9.     return this.newInstance(*arg) as T
  10. }
复制代码


具体实现-无反射
//就不用代码块了,一个颜色费眼睛
fun serialize(itemStack: ItemStack):String{
        val nmsItemStack = CraftItemStack.asNMSCopy(itemStack) // Bukkit-ItemStack -> NMS-ItemStack
        val nbt =  nmsItemStack.save(NBTTagCompound()) // NMS-ItemStack -> NBT
        return nbt.toString() // NBT -> String
}
fun deserialize(nbtRaw: String):ItemStack{
        val nbt = MojangsonParser.parse(nbtRaw) // String -> NBT
        // 此处ItemStack为NMS-Itemstack
        // 部分版本为 ItemStack.createStack(nbt)
        val nmsItemStack = ItemStack(nbt) // NBT -> NMS-ItemStack
        return CraftItemStack.asBukkitCopy(nmsItemStack) // NMS-ItemStack -> Bukkit-ItemStack
}
此时的函数没有跨版本的能力,我们需要再做一下修正
具体实现-反射预备
        反射来获取其中用到的方法
      
  1.    private val CraftItemStack_asNMSCopy: Method = obcClass("inventory.CraftItemStack").getMethod("asNMSCopy", ItemStack::class.java)
  2.     private val CraftItemStack_asBukkitCopy: Method = obcClass("inventory.CraftItemStack").getMethod("asBukkitCopy", nmsClass("ItemStack"))
  3.     private val NBTTagCompound_: Constructor<*> = nmsClass("NBTTagCompound").getConstructor()
  4.     private val MojangsonParser_parse = if (MinecraftVersion.major >= 0){
  5.         nmsClass("MojangsonParser").getMethod("a", String::class.java)
  6.     }else{
  7.         nmsClass("MojangsonParser").getMethod("parse", String::class.java)
  8.     }
  9.     private val ItemStack_createStack: Method? = if (MinecraftVersion.major<=2){
  10.         nmsClass("ItemStack").getMethod("createStack", nmsClass("NBTTagCompound"))
  11.     }else if(MinecraftVersion.major>=5){
  12.         nmsClass("ItemStack").getMethod("a", nmsClass("NBTTagCompound"))
  13.     }else{
  14.         null
  15.     }
  16.     private val ItemStack_: Constructor<*>? = try {
  17.         nmsClass("ItemStack").getConstructor()
  18.     }catch (e : NoSuchMethodException){
  19.         null
  20.         // 不必须的
  21.     }
  22.     private val ItemStack_save: Method = if(MinecraftVersion.major >= 10){
  23.         nmsClass("ItemStack").getMethod("b", nmsClass("NBTTagCompound"))
  24.     }else{
  25.         nmsClass("ItemStack").getMethod("save", nmsClass("NBTTagCompound"))
  26.     }
复制代码


具体实现-反射调用
      
  1. fun serialize(itemStack: ItemStack) : String{
  2.         return ItemStack_save(
  3.         CraftItemStack_asNMSCopy(null,itemStack),
  4.         NBTTagCompound_()
  5.     ).toString()
  6. }
  7. fun deserialize(nbtRaw: String):ItemStack{
  8.         val nbt = MojangsonParser_parse(null,nbtRaw)
  9.         return CraftItemStack_asBukkitCopy<ItemStack>(
  10.             null,
  11.             if(MinecraftVersion.major in listOf(3,4)){
  12.                 ItemStack_!!(nbt)
  13.             }else{
  14.                 ItemStack_createStack!!(null,nbt)
  15.             })!!
  16.     }
复制代码

        这里就能看出来 Reflexes 类的作用了 可以让反射的调用更加整齐

到这里,反射NMS序列化ItemStack就完成了
* 这里再贴出来完整的源码


**个人技术与测试环境有限,没法保证100%的正确和200%的效率
如果此帖有问题或有其他的方法 欢迎提出

_zZ白熊Zz_
其实可以直接 `ItemStack#serialize() -> Map<String,Object>` 和 `ItemStack.deserialize(Map<S,O>) -> ItemStack` 然后走 GSON, etc.  

包括 PersistentDataContainer 内的数据也会被序列化出来,大体可读。

美味的曲奇
_zZ白熊Zz_ 发表于 2022-5-21 14:14
其实可以直接 `ItemStack#serialize() -> Map` 和 `ItemStack.deserialize(Map) -> ItemStack` 然后走 GSON ...

不使用ItemStack#serialize() 理由我已经在前言里写了
就最简单的一个情景
NBT序列化可以从单人客户端获取NBT Tag
但是 Map<String,Object> 只有在服务端才能获得

_zZ白熊Zz_
本帖最后由 _zZ白熊Zz_ 于 2022-5-21 17:40 编辑
美味的曲奇 发表于 2022-5-21 14:18
不使用ItemStack#serialize() 理由我已经在前言里写了
就最简单的一个情景
NBT序列化可以从单人 ...

感觉其实可读性什么的都还挺好..

  1. {
  2.         "v": 1,
  3.         "type": "BOW",
  4.         "meta": {
  5.                 "displayName": "§aaaaa",
  6.                 "damage": 0,
  7.                 "repairCost": 0,
  8.                 "enchants": {},
  9.                 "hideFlags": [],
  10.                 "persistentDataContainer": {
  11.                         "context": {},
  12.                         "map": {
  13.                                 "astralflow:internal.item.state": [0, 0, 0, 0, 0, 0, 0, 118, 123, 34, 116, 121, 112, 101, 34, 58, 34, 105, 111, 46, 105, 98, 54, 55, 46, 97, 115, 116, 114, 97, 108, 102, 108, 111, 119, 46, 105, 110, 116, 101, 114, 110, 97, 108, 46, 105, 116, 101, 109, 46, 115, 116, 97, 116, 101, 46, 73, 110, 116, 101, 114, 110, 97, 108, 73, 116, 101, 109, 83, 116, 97, 116, 101, 34, 44, 34, 100, 97, 116, 97, 34, 58, 123, 34, 112, 114, 111, 116, 111, 116, 121, 112, 101, 75, 101, 121, 34, 58, 123, 34, 105, 100, 34, 58, 34, 99, 34, 44, 34, 110, 97, 109, 101, 115, 112, 97, 99, 101, 34, 58, 34, 97, 34, 125, 125, 125]
  14.                         }
  15.                 },
  16.                 "unbreakable": false,
  17.                 "customModelData": 0
  18.         }
  19. }
复制代码


而且他序列化成 Map 其实很快. 至于我这里item.state是 nbt array 那是我自己选的 PDC<?,byte[]> ,如果是用 String 的会更美观一些

byxiaobai
可以直接用yml序列化,可以保存nbt和nbt顺序
  1. /**
  2.      * 序列化itemStack为String
  3.      *
  4.      * @param itemStack 物品
  5.      * [url=home.php?mod=space&uid=491268]@Return[/url] String
  6.      */
  7.     public static String itemStackSerialize(ItemStack itemStack) {
  8.         YamlConfiguration yml = new YamlConfiguration();
  9.         yml.set("item", itemStack);
  10.         return yml.saveToString();
  11.     }

  12.     /**
  13.      * 反序列化String为itemStack
  14.      *
  15.      * @param str 物品str
  16.      * @return ItemStack
  17.      */
  18.     public static ItemStack itemStackDeserialize(String str) {
  19.         YamlConfiguration yml = new YamlConfiguration();
  20.         ItemStack item;
  21.         try {
  22.             yml.loadFromString(str);
  23.             item = yml.getItemStack("item");
  24.         } catch (InvalidConfigurationException ex) {
  25.             item = new ItemStack(Material.AIR, 1);
  26.         }
  27.         return item;
  28.     }
复制代码

曦-执念
做的不错支持