diff --git a/patches/server/1046-Serialize-ItemMeta-to-SNBT-to-losslessly-save-ItemSt.patch b/patches/server/1046-Serialize-ItemMeta-to-SNBT-to-losslessly-save-ItemSt.patch new file mode 100644 index 000000000..0fd3ab383 --- /dev/null +++ b/patches/server/1046-Serialize-ItemMeta-to-SNBT-to-losslessly-save-ItemSt.patch @@ -0,0 +1,194 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jake Potrebic +Date: Sun, 28 Apr 2024 12:42:16 -0700 +Subject: [PATCH] Serialize ItemMeta to SNBT to losslessly save ItemStacks + + +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaBlockState.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaBlockState.java +index aee276c844b9efc3c16b3f728ef237707011958d..2ce2701abd2556405ef9659e2651785c23fccd43 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaBlockState.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaBlockState.java +@@ -266,6 +266,13 @@ public class CraftMetaBlockState extends CraftMetaItem implements BlockStateMeta + } + } + ++ // Paper start - serialize to SNBT ++ @Override ++ ImmutableMap.Builder modernSerialize(final ImmutableMap.Builder builder) { ++ return builder.put("blockMaterial", this.material.name()); ++ } ++ // Paper end - serialize to SNBT ++ + @Override + ImmutableMap.Builder serialize(ImmutableMap.Builder builder) { + super.serialize(builder); +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java +index c2517ad00b6efba47e792a46e591038d79cb3a82..b691bb08d79bd1827ad47338f4ba048ed219f939 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java +@@ -1619,11 +1619,17 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta { + + @Override + public final Map serialize() { ++ if (true) return PaperMetaSerialization.serialize(this); // Paper - serialize to SNBT + ImmutableMap.Builder map = ImmutableMap.builder(); + map.put(SerializableMeta.TYPE_FIELD, SerializableMeta.classMap.get(this.getClass())); + this.serialize(map); + return map.build(); + } ++ // Paper start ++ ImmutableMap.Builder modernSerialize(final ImmutableMap.Builder builder) { ++ return builder; ++ } ++ // Paper end + + @Overridden + ImmutableMap.Builder serialize(ImmutableMap.Builder builder) { +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/PaperMetaSerialization.java b/src/main/java/org/bukkit/craftbukkit/inventory/PaperMetaSerialization.java +new file mode 100644 +index 0000000000000000000000000000000000000000..1e02d04ba90b2cdfdb9bdf9467d965425a7eb99d +--- /dev/null ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/PaperMetaSerialization.java +@@ -0,0 +1,118 @@ ++package org.bukkit.craftbukkit.inventory; ++ ++import com.google.common.collect.ImmutableMap; ++import com.mojang.brigadier.StringReader; ++import java.lang.reflect.Constructor; ++import java.lang.reflect.InvocationTargetException; ++import java.util.Map; ++import java.util.Objects; ++import java.util.Set; ++import net.minecraft.SharedConstants; ++import net.minecraft.core.component.DataComponentPatch; ++import net.minecraft.nbt.NbtOps; ++import net.minecraft.nbt.SnbtPrinterTagVisitor; ++import net.minecraft.nbt.Tag; ++import net.minecraft.nbt.TagParser; ++import net.minecraft.resources.RegistryOps; ++import org.bukkit.Material; ++import org.bukkit.craftbukkit.CraftRegistry; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.checkerframework.framework.qual.DefaultQualifier; ++ ++@DefaultQualifier(NonNull.class) ++public final class PaperMetaSerialization { ++ ++ @FunctionalInterface ++ interface MetaCreator { ++ CraftMetaItem create(DataComponentPatch patch, Material material) throws Throwable; ++ } ++ ++ static final Map CONSTRUCTOR_MAP; ++ static final Map> CLASS_MAP; ++ static { ++ final ImmutableMap.Builder builder = ImmutableMap.builder(); ++ final ImmutableMap.Builder> classBuilder = ImmutableMap.builder(); ++ for (final Map.Entry, String> entry : SerializableMeta.classMap.entrySet()) { ++ classBuilder.put(entry.getValue(), entry.getKey()); ++ @Nullable MetaCreator creator = null; ++ for (final Constructor ctor : entry.getKey().getDeclaredConstructors()) { ++ if (entry.getKey().equals(CraftMetaBlockState.class)) { ++ creator = (dataComponentPatch, material) -> { ++ return new CraftMetaBlockState(dataComponentPatch, material, null); ++ }; ++ continue; ++ } ++ final Class[] paramTypes = ctor.getParameterTypes(); ++ if (paramTypes.length != 2 && paramTypes.length != 3) { ++ continue; ++ } ++ if (!paramTypes[0].equals(DataComponentPatch.class) || !paramTypes[1].equals(Set.class)) { ++ continue; ++ } ++ creator = (dataComponentPatch, material) -> { ++ try { ++ return (CraftMetaItem) ctor.newInstance(dataComponentPatch, null); ++ } catch (final InstantiationException | IllegalAccessException e) { ++ throw new AssertionError(e); ++ } catch (final InvocationTargetException e) { ++ throw e.getCause(); ++ } ++ }; ++ } ++ if (creator == null) { ++ throw new AssertionError("No suitable constructor found for " + entry.getKey()); ++ } ++ builder.put(entry.getValue(), creator); ++ } ++ CONSTRUCTOR_MAP = builder.build(); ++ CLASS_MAP = classBuilder.build(); ++ if (CONSTRUCTOR_MAP.size() != SerializableMeta.constructorMap.size()) { ++ throw new AssertionError("Mismatched constructor map size"); ++ } ++ } ++ static final String PAPER_SNBT_TYPE = "PAPER_SNBT"; ++ ++ static final String SNBT_FIELD = "snbt"; ++ static final String SUBTYPE_FIELD = "meta-subtype"; ++ static final String VERSION_FIELD = "_version"; ++ static Map serialize(final CraftMetaItem meta) { ++ final CraftMetaItem.Applicator applicator = new CraftMetaItem.Applicator() {}; ++ meta.applyToItem(applicator); ++ final RegistryOps ops = CraftRegistry.getMinecraftRegistry().createSerializationContext(NbtOps.INSTANCE); ++ final Tag tag = DataComponentPatch.CODEC.encodeStart(ops, applicator.build()).getOrThrow(); ++ final ImmutableMap.Builder builder = ImmutableMap.builder(); ++ builder.put(SNBT_FIELD, new SnbtPrinterTagVisitor().visit(tag)); ++ builder.put(VERSION_FIELD, SharedConstants.getCurrentVersion().getDataVersion().getVersion()); ++ builder.put(SerializableMeta.TYPE_FIELD, PAPER_SNBT_TYPE); ++ builder.put(SUBTYPE_FIELD, Objects.requireNonNull(SerializableMeta.classMap.get(meta.getClass()))); ++ return meta.modernSerialize(builder).build(); ++ } ++ ++ static CraftMetaItem deserialize(final Map map) throws Throwable { ++ final String subtype = SerializableMeta.getString(map, SUBTYPE_FIELD, false); ++ final MetaCreator creator = Objects.requireNonNull(CONSTRUCTOR_MAP.get(subtype)); ++ final Class metaClass = Objects.requireNonNull(CLASS_MAP.get(subtype)); ++ final int version = SerializableMeta.getObject(Integer.class, map, VERSION_FIELD, false); ++ // TODO - handle versioning ++ final String snbt = SerializableMeta.getString(map, SNBT_FIELD, false); ++ final RegistryOps ops = CraftRegistry.getMinecraftRegistry().createSerializationContext(NbtOps.INSTANCE); ++ final TagParser parser = new TagParser(new StringReader(snbt)); ++ final DataComponentPatch patch = DataComponentPatch.CODEC.parse(ops, parser.readValue()).getOrThrow(); ++ if (metaClass.equals(CraftMetaBlockState.class)) { ++ final String matName = SerializableMeta.getString(map, "blockMaterial", true); ++ final @Nullable Material m; ++ if (matName != null) { ++ m = Material.getMaterial(matName); ++ } else { ++ m = Material.AIR; ++ } ++ return creator.create(patch, m != null ? m : Material.AIR); ++ } else { ++ return creator.create(patch, Material.AIR); // only CraftMetaBlockState uses the Material ++ } ++ } ++ ++ private PaperMetaSerialization() { ++ } ++} +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/SerializableMeta.java b/src/main/java/org/bukkit/craftbukkit/inventory/SerializableMeta.java +index a86eb660d8f523cb99a0b668ef1130535d50ce1c..0901f566a9aea8349237a0284629a69fd086b8f3 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/SerializableMeta.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/SerializableMeta.java +@@ -65,6 +65,11 @@ public final class SerializableMeta implements ConfigurationSerializable { + Preconditions.checkArgument(map != null, "Cannot deserialize null map"); + + String type = SerializableMeta.getString(map, SerializableMeta.TYPE_FIELD, false); ++ // Paper start - serialize to SNBT ++ if (type.equals(PaperMetaSerialization.PAPER_SNBT_TYPE)) { ++ return PaperMetaSerialization.deserialize(map); ++ } ++ // Paper end - serialize to SNBT + Constructor constructor = SerializableMeta.constructorMap.get(type); + + if (constructor == null) { +@@ -96,6 +101,7 @@ public final class SerializableMeta implements ConfigurationSerializable { + return value != null && value; + } + ++ @org.jetbrains.annotations.Contract("_, _, _, false -> !null") // Paper + public static T getObject(Class clazz, Map map, Object field, boolean nullable) { + final Object object = map.get(field); + diff --git a/test-plugin/src/main/java/io/papermc/testplugin/TestPlugin.java b/test-plugin/src/main/java/io/papermc/testplugin/TestPlugin.java index 4e68423bb..d3fb93a60 100644 --- a/test-plugin/src/main/java/io/papermc/testplugin/TestPlugin.java +++ b/test-plugin/src/main/java/io/papermc/testplugin/TestPlugin.java @@ -1,6 +1,16 @@ package io.papermc.testplugin; +import io.papermc.paper.event.player.ChatEvent; +import org.bukkit.NamespacedKey; +import org.bukkit.block.BlockState; +import org.bukkit.block.TileState; +import org.bukkit.configuration.InvalidConfigurationException; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.BlockStateMeta; +import org.bukkit.persistence.PersistentDataType; import org.bukkit.plugin.java.JavaPlugin; public final class TestPlugin extends JavaPlugin implements Listener { @@ -9,4 +19,95 @@ public final class TestPlugin extends JavaPlugin implements Listener { public void onEnable() { this.getServer().getPluginManager().registerEvents(this, this); } + + @EventHandler + public void onEvent(ChatEvent event) throws InvalidConfigurationException { + final ItemStack inHand = event.getPlayer().getInventory().getItemInMainHand(); + if (true) this.testEquals(inHand); + if (true) return; + final YamlConfiguration config = new YamlConfiguration(); + config.set("item", inHand); + System.out.println(config.saveToString()); + // config.loadFromString(OLD); + // final ItemStack fromConfig = config.getSerializable("item", ItemStack.class); + // final YamlConfiguration modern = new YamlConfiguration(); + // modern.loadFromString(MODERN); + // final ItemStack fromModern = modern.getSerializable("item", ItemStack.class); + // System.out.println(fromConfig); + // System.out.println(inHand.equals(fromConfig)); + // System.out.println(fromConfig.equals(fromModern)); + // config.set("item", inHand); + // System.out.println(config.saveToString()); + } + + void testEquals(ItemStack inHand) throws InvalidConfigurationException { + final YamlConfiguration old = new YamlConfiguration(); + old.loadFromString(OLD); + final YamlConfiguration neww = new YamlConfiguration(); + neww.loadFromString(MODERN); + final ItemStack fromOld = old.getSerializable("item", ItemStack.class); + final ItemStack fromNew = neww.getSerializable("item", ItemStack.class); + System.out.println("fromOld = inHand: " + fromOld.equals(inHand)); + System.out.println("fromNew = inHand: " + fromNew.equals(inHand)); + System.out.println("fromOld = fromNew: " + fromOld.equals(fromNew)); + System.out.println("inHand = fromOld: " + inHand.equals(fromOld)); + System.out.println("inHand = fromNew: " + inHand.equals(fromNew)); + } + + static final String MODERN = """ + item: + /e ==: org.bukkit.inventory.ItemStack + v: 3837 + type: SHULKER_BOX + meta: + ==: ItemMeta + snbt: |- + { + "minecraft:block_entity_data": { + id: "minecraft:shulker_box", + x: 0, + y: 0, + z: 0 + }, + "minecraft:container": [ + { + item: { + components: { + "minecraft:stored_enchantments": { + levels: { + "minecraft:projectile_protection": 3 + } + } + }, + count: 1, + id: "minecraft:enchanted_book" + }, + slot: 0 + }, + { + item: { + components: { + "!minecraft:tool": {} + }, + count: 1, + id: "minecraft:diamond_pickaxe" + }, + slot: 1 + } + ] + } + _version: 3837 + meta-type: PAPER_SNBT + meta-subtype: TILE_ENTITY + blockMaterial: SHULKER_BOX"""; + static final String OLD = """ + item: + ==: org.bukkit.inventory.ItemStack + v: 3837 + type: SHULKER_BOX + meta: + ==: ItemMeta + meta-type: TILE_ENTITY + internal: H4sIAAAAAAAA/22OwW7CMAyG3XZFWw/jhDQOCO01dkTiwHncqzQ1NDSxq9agsqfHZauKJnLxr+TL5z8DyOB949nWWxIn1705JhD9gJ5XiF0Ji+AIbWsO8tVVZ19jmxfcK3MdGJ39MN8g3QmGLtMcJ5BaPpNojiJ4+fYsuiWzHBomJFFq9WAVbrHMkWxlSMLv+8zjBX2XwHoCm5ZPaMV5zDXKEJnuHcauHxP8p1NxwVzDk0rRv0rzz+m3MPtRupyuS2cCU5k3ztamR5XdABHDfI5AAQAA + blockMaterial: SHULKER_BOX"""; }