本帖最后由 Neige 于 2023-9-30 21:35 编辑
一、前言
2023-3-31 23:22:09 我在编程开发版发布了一个帖子 [插件开发讨论] 获取NBT时仅获取原NBT的副本的意义是什么
现在,我认为我可以对这个问题做出一些有意义的回答了,故在此分享,供诸位参考。
二、背景
要深入理解相关内容,我们需要了解社区通常是如何编辑物品 NBT 的,所以我们要先进行长篇幅的相关介绍。
2.1 第三方NBT库
众所周知,Bukkit 中与 ItemStack 及 ItemMeta 相关的 API 中,没有给出"获取、编辑、设置 NBT "的方法。
因此,社区出现了一些与 NBT 有关的前置库,帮助插件开发者更便捷地进行相关操作。
比如著名的 Item-NBT-API,又或者 TabooLib 中的 NBT 相关内容
以 TabooLib 为例:
预准备:
编写一些类似于 NMS 中 NBT 的类(例如命名为 ItemTag、ItemTagList、ItemTagData)
获取 NBT:
调用 CraftItemStack.asNMSCopy 方法,将传入的 org.bukkit.inventory.ItemStack 实例转化为 NMS 中 ItemStack 的克隆调用 ItemStack 下的 getTag 方法,获取物品 NBT,又或者说 NBTTagCompound整体遍历这个 NBTTagCompound,将其转换为自己的对应 NBTTagCompound 的类(在 TabooLib 中为 ItemTag)返回这个类似深复制的 NBT 克隆,即 ItemTag
修改 NBT:
直接操作这个 ItemTag
设置 NBT:
整体遍历这个 ItemTag,将其转换为 NBTTagCompound调用 CraftItemStack.asNMSCopy 方法获取待操作的 ItemStack 的 NMS 形式克隆调用 setTag 方法给这个 NMS 的 ItemStack 设置 NBT调用 CraftItemStack.asBukkitCopy 方法将这个 NMS 的 ItemStack 转换为 org.bukkit.inventory.ItemStack 形式的克隆通过 org.bukkit.inventory.ItemStack#getItemMeta 获取这个克隆的 ItemMeta 副本把这个 ItemMeta 副本设置到目标物品上
Item-NBT-API 的实现方式与 TabooLib 有很大差异,但与 TabooLib 有一个共同点:在不停地克隆。
asNMSCopy asBukkitCopy getItemMeta clone
如果你尝试过直接调用 NMS 进行 NBT 获取-修改-设置,你就会发现,这与这类 NBT 库有很大的的性能差距。
此时应该产生一个疑问:获取克隆的意义在哪里?我不克隆会发生什么?
2.2 直接调用 NMS - 较无脑形式
比较无脑的的流程如下:
获取 NBT:
调用 CraftItemStack.asNMSCopy 方法获取待操作的 ItemStack 的 NMS 形式克隆调用 ItemStack 下的 getTag 方法,获取物品 NBT,即 NBTTagCompound
修改 NBT:
直接对 NBTTagCompound 进行操作
设置 NBT:
调用 CraftItemStack.asNMSCopy 方法获取待操作的 ItemStack 的 NMS 形式克隆调用 setTag 方法给这个 NMS 的 ItemStack 设置 NBT调用 CraftItemStack.asCraftMirror 方法给这个 NMS 的 ItemStack 套壳,转化为 org.bukkit.inventory.ItemStack 的子类 CraftItemStack通过 CraftItemStack.getItemMeta 根据 NBT 生成 ItemMeta把这个 ItemMeta 设置到目标物品上
仔细观察就会发现,这个过程似乎克隆的也不少,调用了两次 asNMSCopy。
那么有没有少进行克隆,或者不进行克隆的方式呢?
有,但需要分情况讨论。
2.3 直接调用 NMS - 优化形式
我们需要知道:很多情况下,我们通过 BukkitAPI 得到的 org.bukkit.inventory.ItemStack 其实是它的子类 org.bukkit.craftbukkit.版本号.inventory.CraftItemStack。
CraftItemStack 是对 NMS 中 ItemStack 的套壳,它的 handle 字段就是对应的 NMS ItemStack:
data/myattachment/forum/202309/30/174427y252355ugulz3qlb.png
也就是说,对于 CraftItemStack,我们可以通过以下方式操作 NBT:
获取 NBT:
通过 ItemStack handle 字段获取 NMS 形式的 ItemStack调用 setTag 获取物品 NBT,即 NBTTagCompound
修改 NBT:
直接对 NBTTagCompound 进行操作
设置 NBT:
不需要设置 NBT
为什么不需要设置 NBT 呢?因为 getTag 获取的不是副本,我们对 getTag 获取的 NBTTagCompound 的所有修改操作都会即时反应到对应物品上。
而对于 org.bukkit.inventory.ItemStack,也存在一些更好的操作方式
ItemStack 下有一个私有字段 meta,我们可以通过反射获取它,这样我们就跳过了 getItemMeta 对 ItemMeta 的克隆。
而众所周知,ItemMeta 是一个接口,我们获取的 ItemMeta 肯定是它的实现,即 org.bukkit.craftbukkit.版本号.inventory.CraftMetaItem。
CraftMetaItem 下有一个 default 方法 applyToItem,向其中传入 NBTTagCompound,可以将 ItemMeta 中的所有内容转换进传入的 NBTTagCompound。
data/myattachment/forum/202309/30/183253wxicqmyuuabcmzu9.png
由于某些原因,答应我,获取之后调用一下 clone。后面你会知道为什么的
由此,我们可以得出以下流程:
获取 NBT:
通过 ItemStack 私有 meta 字段获取 ItemStack 的 ItemMetaNBTTagCompound nbt = new NBTTagCompound()itemMeta.applyToItem(nbt)nbt = nbt.clone()
修改 NBT:
直接对 NBTTagCompound 进行操作
设置 NBT:
通过反射将 ItemStack 私有字段 meta 设置为 null调用 CraftItemStack.asCraftCopy 获取 CraftItemStack 形式的物品克隆通过 craftItemStack.handle.setTag(nbt) 给 CraftItemStack 形式的物品克隆设置 NBTbukkitItemStack.meta = craftItemStack.getItemMeta()
因为我们最终要根据已有的 NBTTagCompound 生成对应的 ItemMeta,所以待设置的 ItemStack 他当前的 ItemMeta 实际上毛用没用。
所以在设置 NBT 的过程中,我们先将 ItemStack 私有字段 meta 设置为 null,再调用 CraftItemStack.asCraftCopy。
这样我们实质上是根据 Material 和 Count 创建了一个空的 CraftItemStack。
然后我们扒壳获取里面的 handle,再给 handle 设置 NBT,然后调用getItemMeta,根据 NBT 生成 ItemMeta。
那么对于 org.bukkit.inventory.ItemStack,这样操作就是优化的极限了吗?
不是。
2.4 直接调用 NMS - 只读
如果你只希望读取“自定义 NBT”,并且不需要修改,那么我们可以通过一种光速的方式,在不进行任何克隆的情况下进行 NBT 读取。
关键就在 CraftMetaItem 的 unhandledTags 字段,unhandledTags 在 spigot 中是一个 HashMap,在 paper 中是一个 TreeMap。
unhandledTags 中存储了所有“自定义NBT”,像是物品名、物品描述、物品附魔这类物品属性,属于已处理NBT,而像是第三方插件比如 MMOItems 的 MMOITEMS_TYPE 这类 NBT,存储于 unhandledTags 之中。
而一般情况下,我们也只存在读取这些“自定义NBT”的需求,因此操作流程如下:
获取 NBT:
通过 ItemStack 私有 meta 字段获取 ItemStack 的 ItemMeta通过 CraftMetaItem 的 unhandledTags 字段获取“自定义NBT”
而通过以上两种方式获取的内容,但最好不要修改它们。
而“为什么不要修改它们”将引出本帖的题目:“NBT库获取ItemStack的NBT副本的意义”
三、一个巨大的问题
假定现有一个 org.bukkit.inventory.ItemStack 实例,其中包含一对自定义NBT"test: 100.0d"。
data/myattachment/forum/202309/30/200703r24bz2421w9b9dbx.png
请查看以下代码:ItemStack = CraftItemStack.asBukkitCopy(Bukkit.getPlayer("Neige").getInventory().getItemInMainHand().handle);
Field metaField = ItemStack.class.getDeclaredField("meta");
metaField.setAccessible(true);
ItemMeta itemMeta = metaField.get(itemStack);
Field unhandledTagsField = itemMeta.getClass().getDeclaredField("unhandledTags");
unhandledTagsField.setAccessible(true);
Map unhandledTags = unhandledTagsField.get(itemMeta);
System.out.println(unhandledTags.get("test"))
复制代码后台将输出“100.0d”
data/myattachment/forum/202309/30/200728bbgdsd1iezz55i51.png
而如果对 unhandledTags 进行一些修改,它也会切实应用到对应的 ItemStack 上。
一切看起来没啥问题,对吧?
那么,请看以下代码:Map unhandledTags = unhandledTagsField.get(itemMeta);
ItemStack itemClone = itemStack.clone();
Map cloneTags = unhandledTagsField.get(metaField.get(itemClone));
System.out.println(unhandledTags.get("test1"))
System.out.println(cloneTags.get("test1"))
unhandledTags.put("test1", new net.minecraft.server.v1_12_R1.NBTTagDouble(100.0))
System.out.println(unhandledTags.get("test1"))
System.out.println(cloneTags.get("test1"))复制代码后台返回值如下:
data/myattachment/forum/202309/30/201923dabuwxhbuwwuixyi.png
为什么我们对 itemStack 的操作,反映到了 itemClone 上?
3.1 ItemStack#clone 返回的真的是物品克隆吗
ItemStack#clone
    @NotNull
    @Override
    public ItemStack clone() {
        try {
            ItemStack itemStack = (ItemStack) super.clone();
            if (this.meta != null) {
                itemStack.meta = this.meta.clone();
            }
            if (this.data != null) {
                itemStack.data = this.data.clone();
            }
            return itemStack;
        } catch (CloneNotSupportedException e) {
            throw new Error(e);
        }
    }复制代码CraftMetaItem#clone
    @Overridden
    public CraftMetaItem clone() {
        try {
            CraftMetaItem clone = (CraftMetaItem)super.clone();
            if (this.lore != null) {
                clone.lore = new ArrayList(this.lore);
            }
            clone.customModelData = this.customModelData;
            clone.blockData = this.blockData;
            if (this.enchantments != null) {
                clone.enchantments = new CraftMetaItem.EnchantmentMap(this.enchantments);
            }
            if (this.hasAttributeModifiers()) {
                clone.attributeModifiers = LinkedHashMultimap.create(this.attributeModifiers);
            }
            clone.persistentDataContainer = new CraftPersistentDataContainer(this.persistentDataContainer.getRaw(), DATA_TYPE_REGISTRY);
            clone.hideFlag = this.hideFlag;
            clone.unbreakable = this.unbreakable;
            clone.damage = this.damage;
            clone.version = this.version;
            if (this.placeableKeys != null) {
                clone.placeableKeys = Sets.newHashSet(this.placeableKeys);
            }
            if (this.destroyableKeys != null) {
                clone.destroyableKeys = Sets.newHashSet(this.destroyableKeys);
            }
            return clone;
        } catch (CloneNotSupportedException var2) {
            throw new Error(var2);
        }
    }复制代码你发现了什么?
ItemStack 的克隆实质上是创建一个新的 ItemStack 然后将 新 ItemStack 的 meta 替换为 meta.clone()
而 CraftMetaItem,也就是 ItemMeta 的 clone,竟然没有处理 unhandledTags。
也就是说,对于 org.bukkit.inventory.ItemStack,无论你怎么 clone,它底下的 unhandledTags 都是同一个 Map,你修改这个 Map 就会导致一揽子 ItemStack 同时发生改变。
3.2 CraftItemStack#clone 安全吗
结论:安全
CraftItemStack#clone
    public CraftItemStack clone() {
        CraftItemStack itemStack = (CraftItemStack)super.clone();
        if (this.handle != null) {
            itemStack.handle = this.handle.copy();
        }
        return itemStack;
    }复制代码NMS ItemStack#copy    public ItemStack copy() {
        return this.copy(false);
    }
    public ItemStack copy(boolean originalItem) {
        if (!originalItem && this.isEmpty()) {
            return EMPTY;
        } else {
            ItemStack itemstack = new ItemStack(originalItem ? this.item : this.getItem(), this.count);
            itemstack.setPopTime(this.getPopTime());
            if (this.tag != null) {
                itemstack.tag = this.tag.copy();
            }
            return itemstack;
        }
    }复制代码CompoundTag#copy    public CompoundTag copy() {
        Object2ObjectOpenHashMap ret = new Object2ObjectOpenHashMap(this.tags.size(), 0.8F);
        Object iterator = this.tags instanceof Object2ObjectOpenHashMap ? ((Object2ObjectOpenHashMap)this.tags).object2ObjectEntrySet().fastIterator() : this.tags.entrySet().iterator();
        while(((Iterator)iterator).hasNext()) {
            Entry entry = (Entry)((Iterator)iterator).next();
            ret.put((String)entry.getKey(), ((Tag)entry.getValue()).copy());
        }
        return new CompoundTag(ret);
    }复制代码可以看到,CraftItemStack是个壳子,实质上是调用 handle 的 copy 方法把包在里面的 NMS ItemStack 克隆一下。
而 NMS ItemStack 的克隆,关键在于 CompoundTag,也就是 NBT 的克隆。
而 CompoundTag 的克隆,是实打实的深复制。
因此,经过 clone 的 CraftItemStack 之间不会产生 NBT 穿透。
3.3 asBukkitCopy 和 asCraftCopy 会产生 NBT 穿透吗
结论:asBukkitCopy 不会穿透,asCraftCopy 会穿透
以 PlayerItemConsumeEvent 为例:
LivingEntity#completeUsingItem    protected void completeUsingItem() {
        if (!this.level().isClientSide || this.isUsingItem()) {
            InteractionHand enumhand = this.getUsedItemHand();
            if (!this.useItem.equals(this.getItemInHand(enumhand))) {
                this.releaseUsingItem();
            } else if (!this.useItem.isEmpty() && this.isUsingItem()) {
                this.startUsingItem(this.getUsedItemHand(), true);
                this.triggerItemUseEffects(this.useItem, 16);
                PlayerItemConsumeEvent event = null;
                ItemStack itemstack;
                if (this instanceof ServerPlayer) {
                    org.bukkit.inventory.ItemStack craftItem = CraftItemStack.asBukkitCopy(this.useItem);
                    org.bukkit.inventory.EquipmentSlot hand = CraftEquipmentSlot.getHand(enumhand);
                    event = new PlayerItemConsumeEvent((org.bukkit.entity.Player)this.getBukkitEntity(), craftItem, hand);
                    this.level().getCraftServer().getPluginManager().callEvent(event);
                    if (event.isCancelled()) {
                        this.stopUsingItem();
                        ((ServerPlayer)this).getBukkitEntity().updateInventory();
                        ((ServerPlayer)this).getBukkitEntity().updateScaledHealth();
                        return;
                    }
                    itemstack = craftItem.equals(event.getItem()) ? this.useItem.finishUsingItem(this.level(), this) : CraftItemStack.asNMSCopy(event.getItem()).finishUsingItem(this.level(), this);
                } else {
                    itemstack = this.useItem.finishUsingItem(this.level(), this);
                }
                if (event != null && event.getReplacement() != null) {
                    itemstack = CraftItemStack.asNMSCopy(event.getReplacement());
                }
                if (itemstack != this.useItem) {
                    this.setItemInHand(enumhand, itemstack);
                }
                this.stopUsingItem();
                if (this instanceof ServerPlayer) {
                    ((ServerPlayer)this).getBukkitEntity().updateInventory();
                }
            }
        }
    }复制代码可以看到,PlayerItemConsumeEvent 中的 ItemStack 是通过 CraftItemStack.asBukkitCopy 获取的 org.bukkit.inventory.ItemStack。
asBukkitCopy 的过程实质上是根据 NMS 的 ItemStack 的 Item 获取对应的 Material
然后创建一个基础的 org.bukkit.inventory.ItemStack
然后通过 NMS 的 ItemStack 调用 getItemMeta,将产生的 ItemMeta 设置给 org.bukkit.inventory.ItemStack
通过 NMS 的 ItemStack 生成 ItemMeta 的过程实质上是一个 new CraftMetaItem(itemStack.getTag()) 的过程,相关代码为        Set keys = tag.getAllKeys();
        Iterator var11 = keys.iterator();
        while(var11.hasNext()) {
            String key = (String)var11.next();
            if (!getHandledTags().contains(key)) {
                this.unhandledTags.put(key, tag.get(key).copy());
            }
        }复制代码这个过程中所有 NBT 都是 copy()
因此,通过 unhandledTags 修改生成的 ItemStack 是无法对原先的 NMS 的 ItemStack 产生影响的,所以 asBukkitCopy 的原料和产物之间不会发生穿透
而如果我传入一个 org.bukkit.inventory.ItemStack,将其 asCraftCopy,实质上是传入了 org.bukkit.inventory.ItemStack 的材质,数量,以及通过 getItemMeta 获取的 ItemMeta 克隆
但是众所周知,ItemMeta 的克隆对于不克隆 unhandledTags
所以二者所有顶层 NBT 的值是同一个对象,如果物品中包含嵌套结构,比如ItemsAdder:
  id: test复制代码我们通过“ItemsAdder”这个key获取到“id: test”这个 NBTTagCompound 以后,如果对它进行修改,修改的结果将同时对 asCraftCopy 的原料和产物产生影响,发生 NBT 穿透。
而如果在此基础上对 asCraftCopy 获取的 ItemStack 再进行一次clone
由于此次 clone 实质上是新建一个 CraftItemStack 然后把当前 handle 的 clone 塞进去
handle 是 NMS 的 ItemStack,而 NMS 的 ItemStack 的 clone 是对 tag 的 clone,而 tag 的 clone 是深复制
nbt穿透将被阻断
由此得出结论,asCraftCopy无法阻断 NBT 穿透,需要在 asCraftCopy 后进行一步 clone 进一步阻断 NBT 传递。
四、结论
如果直接通过 craftItemStack.handle.getTag 并对NBT进行操作,假设这个 craftItemStack 是通过 asCraftCopy 的产物,那么它所有 NBTTagCompound 类型的自定义 NBT 都会与他的模板发生联动,改一个变两个,发生“NBT 穿透”如果通过 itemStack.meta.unhandledTags 获取物品自定义 NBT 后对其进行修改,那所有 itemStack.clone() 和 CraftItemStack.asCraftCopy(itemStack) 都会同步变化,改一个动一窝
五、建议
CraftMetaItem#applyToItem 在处理 unhandledTags 时用的不是 NBT 的 clone,而是本体。因此通过这种操作获取的 NBT 其中自定义 NBT 部分不能直接修改,直接修改会导致像 unhandledTags 一样的 NBT 穿透现象,建议获取后 clone,然后进行后续操作如果通过 itemStack.meta.unhandledTags 获取的自定义 NBT 仅可用于读取,切勿修改