Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Serialize ItemMeta to SNBT to losslessly save ItemStacks #10609

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Jake Potrebic <jake.m.potrebic@gmail.com>
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<String, Object> modernSerialize(final ImmutableMap.Builder<String, Object> builder) {
+ return builder.put("blockMaterial", this.material.name());
Copy link
Contributor

@masmc05 masmc05 Apr 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't be it better to use https://jd.papermc.io/paper/1.20/org/bukkit/Registry.html#MATERIAL (or nms registry) to get the namespaced name instead of enum name?
(Including if we believe rumours that with those updates items and blocks are going to be data driven, using the name from registry may be better for long term)

+ }
+ // Paper end - serialize to SNBT
+
@Override
ImmutableMap.Builder<String, Object> serialize(ImmutableMap.Builder<String, Object> 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<String, Object> serialize() {
+ if (true) return PaperMetaSerialization.serialize(this); // Paper - serialize to SNBT
ImmutableMap.Builder<String, Object> map = ImmutableMap.builder();
map.put(SerializableMeta.TYPE_FIELD, SerializableMeta.classMap.get(this.getClass()));
this.serialize(map);
return map.build();
}
+ // Paper start
+ ImmutableMap.Builder<String, Object> modernSerialize(final ImmutableMap.Builder<String, Object> builder) {
+ return builder;
+ }
+ // Paper end

@Overridden
ImmutableMap.Builder<String, Object> serialize(ImmutableMap.Builder<String, Object> 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<String, MetaCreator> CONSTRUCTOR_MAP;
+ static final Map<String, Class<? extends CraftMetaItem>> CLASS_MAP;
+ static {
+ final ImmutableMap.Builder<String, MetaCreator> builder = ImmutableMap.builder();
+ final ImmutableMap.Builder<String, Class<? extends CraftMetaItem>> classBuilder = ImmutableMap.builder();
+ for (final Map.Entry<Class<? extends CraftMetaItem>, 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<String, Object> serialize(final CraftMetaItem meta) {
+ final CraftMetaItem.Applicator applicator = new CraftMetaItem.Applicator() {};
+ meta.applyToItem(applicator);
+ final RegistryOps<Tag> ops = CraftRegistry.getMinecraftRegistry().createSerializationContext(NbtOps.INSTANCE);
+ final Tag tag = DataComponentPatch.CODEC.encodeStart(ops, applicator.build()).getOrThrow();
+ final ImmutableMap.Builder<String, Object> 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<String, Object> map) throws Throwable {
+ final String subtype = SerializableMeta.getString(map, SUBTYPE_FIELD, false);
+ final MetaCreator creator = Objects.requireNonNull(CONSTRUCTOR_MAP.get(subtype));
+ final Class<? extends CraftMetaItem> 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<Tag> 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<? extends CraftMetaItem> 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> T getObject(Class<T> clazz, Map<?, ?> map, Object field, boolean nullable) {
final Object object = map.get(field);

101 changes: 101 additions & 0 deletions test-plugin/src/main/java/io/papermc/testplugin/TestPlugin.java
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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""";
}