mirror of
https://github.com/PaperMC/Paper.git
synced 2025-03-13 11:18:23 +01:00
#1230: Move unstructured PDC NBT serialisation to SNBT
The initial implementation of the CraftNBTTagConfigSerialiser attempted to represent the entire NBT tree in yaml. While the tree structure itself is nicely represented, the values and their respective types become increasingly difficult to properly store in the context of snakeyml/yml in general. This is mainly due to the fact that nbt offers a lot of different types compared to yaml, specifically the primitive arrays and different floating point values are troublesome as they require parsing via mojang parsers due to their custom format. To build a future proof system for unstructured nbt in spigot yml, this commit moves the entire serialisation fully into SNBT, producing a single string as output rather than a full yml tree. SNBT remains easily readable and editable for server owners, which was one of the main criteria during the initial implementation of the serialiser (preventing the use of bas64ed gzipped nbt bytes), while also completely using mojangs parsing, eliminating any need for custom parsing logic in spigot. Additionally, a string allows for very straight forward handling of legacy data by simply parsing strings as SNBT and maps/yml trees as legacy content, depending on what type snakeyml produces after parsing the yml content, removing the need for a versioning schema. By: Bjarne Koll <lynxplay101@gmail.com>
This commit is contained in:
parent
07002cbfcd
commit
5692b3f59a
6 changed files with 158 additions and 28 deletions
|
@ -534,7 +534,7 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
|
|||
}
|
||||
}
|
||||
|
||||
Map nbtMap = SerializableMeta.getObject(Map.class, map, BUKKIT_CUSTOM_TAG.BUKKIT, true);
|
||||
Object nbtMap = SerializableMeta.getObject(Object.class, map, BUKKIT_CUSTOM_TAG.BUKKIT, true); // We read both legacy maps and potential modern snbt strings here
|
||||
if (nbtMap != null) {
|
||||
this.persistentDataContainer.putAll((NBTTagCompound) CraftNBTTagConfigSerializer.deserialize(nbtMap));
|
||||
}
|
||||
|
|
|
@ -153,7 +153,7 @@ public class CraftPersistentDataContainer implements PersistentDataContainer {
|
|||
return hashCode;
|
||||
}
|
||||
|
||||
public Map<String, Object> serialize() {
|
||||
return (Map<String, Object>) CraftNBTTagConfigSerializer.serialize(toTagCompound());
|
||||
public String serialize() {
|
||||
return CraftNBTTagConfigSerializer.serialize(toTagCompound());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,8 @@ import net.minecraft.nbt.NBTTagDouble;
|
|||
import net.minecraft.nbt.NBTTagInt;
|
||||
import net.minecraft.nbt.NBTTagList;
|
||||
import net.minecraft.nbt.NBTTagString;
|
||||
import net.minecraft.nbt.SnbtPrinterTagVisitor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class CraftNBTTagConfigSerializer {
|
||||
|
||||
|
@ -23,35 +25,29 @@ public class CraftNBTTagConfigSerializer {
|
|||
private static final Pattern DOUBLE = Pattern.compile("[-+]?(?:[0-9]+[.]?|[0-9]*[.][0-9]+)(?:e[-+]?[0-9]+)?d", Pattern.CASE_INSENSITIVE);
|
||||
private static final MojangsonParser MOJANGSON_PARSER = new MojangsonParser(new StringReader(""));
|
||||
|
||||
public static Object serialize(NBTBase base) {
|
||||
if (base instanceof NBTTagCompound) {
|
||||
Map<String, Object> innerMap = new HashMap<>();
|
||||
for (String key : ((NBTTagCompound) base).getAllKeys()) {
|
||||
innerMap.put(key, serialize(((NBTTagCompound) base).get(key)));
|
||||
}
|
||||
|
||||
return innerMap;
|
||||
} else if (base instanceof NBTTagList) {
|
||||
List<Object> baseList = new ArrayList<>();
|
||||
for (int i = 0; i < ((NBTList) base).size(); i++) {
|
||||
baseList.add(serialize((NBTBase) ((NBTList) base).get(i)));
|
||||
}
|
||||
|
||||
return baseList;
|
||||
} else if (base instanceof NBTTagString) {
|
||||
return base.getAsString();
|
||||
} else if (base instanceof NBTTagInt) { // No need to check for doubles, those are covered by the double itself
|
||||
return base.toString() + "i";
|
||||
}
|
||||
|
||||
return base.toString();
|
||||
public static String serialize(@NotNull final NBTBase base) {
|
||||
final SnbtPrinterTagVisitor snbtVisitor = new SnbtPrinterTagVisitor();
|
||||
return snbtVisitor.visit(base);
|
||||
}
|
||||
|
||||
public static NBTBase deserialize(Object object) {
|
||||
public static NBTBase deserialize(final Object object) {
|
||||
// The new logic expects the top level object to be a single string, holding the entire nbt tag as SNBT.
|
||||
if (object instanceof final String snbtString) {
|
||||
try {
|
||||
return MojangsonParser.parseTag(snbtString);
|
||||
} catch (final CommandSyntaxException e) {
|
||||
throw new RuntimeException("Failed to deserialise nbt", e);
|
||||
}
|
||||
} else { // Legacy logic is passed to the internal legacy deserialization that attempts to read the old format that *unsuccessfully* attempted to read/write nbt to a full yml tree.
|
||||
return internalLegacyDeserialization(object);
|
||||
}
|
||||
}
|
||||
|
||||
private static NBTBase internalLegacyDeserialization(@NotNull final Object object) {
|
||||
if (object instanceof Map) {
|
||||
NBTTagCompound compound = new NBTTagCompound();
|
||||
for (Map.Entry<String, Object> entry : ((Map<String, Object>) object).entrySet()) {
|
||||
compound.put(entry.getKey(), deserialize(entry.getValue()));
|
||||
compound.put(entry.getKey(), internalLegacyDeserialization(entry.getValue()));
|
||||
}
|
||||
|
||||
return compound;
|
||||
|
@ -63,7 +59,7 @@ public class CraftNBTTagConfigSerializer {
|
|||
|
||||
NBTTagList tagList = new NBTTagList();
|
||||
for (Object tag : list) {
|
||||
tagList.add(deserialize(tag));
|
||||
tagList.add(internalLegacyDeserialization(tag));
|
||||
}
|
||||
|
||||
return tagList;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package org.bukkit.craftbukkit.inventory;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.lang.reflect.Array;
|
||||
import java.nio.ByteBuffer;
|
||||
|
@ -10,6 +11,7 @@ import net.minecraft.nbt.NBTTagCompound;
|
|||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.configuration.InvalidConfigurationException;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
|
@ -164,6 +166,49 @@ public class PersistentDataContainerTest extends AbstractTestingBase {
|
|||
return itemMeta;
|
||||
}
|
||||
|
||||
/*
|
||||
Test edge cases with strings
|
||||
*/
|
||||
@Test
|
||||
public void testStringEdgeCases() throws IOException, InvalidConfigurationException {
|
||||
final ItemStack stack = new ItemStack(Material.DIAMOND);
|
||||
final ItemMeta meta = stack.getItemMeta();
|
||||
assertNotNull(meta);
|
||||
|
||||
final String arrayLookalike = "[\"UnicornParticle\",\"TotemParticle\",\"AngelParticle\",\"ColorSwitchParticle\"]";
|
||||
final String jsonLookalike = """
|
||||
{
|
||||
"key": 'A value wrapped in single quotes',
|
||||
"other": "A value with normal quotes",
|
||||
"array": ["working", "unit", "tests"]
|
||||
}
|
||||
""";
|
||||
|
||||
final PersistentDataContainer pdc = meta.getPersistentDataContainer();
|
||||
pdc.set(requestKey("string_int"), PersistentDataType.STRING, "5i");
|
||||
pdc.set(requestKey("string_true"), PersistentDataType.STRING, "true");
|
||||
pdc.set(requestKey("string_byte_array"), PersistentDataType.STRING, "[B;-128B]");
|
||||
pdc.set(requestKey("string_array_lookalike"), PersistentDataType.STRING, arrayLookalike);
|
||||
pdc.set(requestKey("string_json_lookalike"), PersistentDataType.STRING, jsonLookalike);
|
||||
|
||||
stack.setItemMeta(meta);
|
||||
|
||||
final YamlConfiguration config = new YamlConfiguration();
|
||||
config.set("test", stack);
|
||||
config.load(new StringReader(config.saveToString())); // Reload config from string
|
||||
|
||||
final ItemStack loadedStack = config.getItemStack("test");
|
||||
assertNotNull(loadedStack);
|
||||
final ItemMeta loadedMeta = loadedStack.getItemMeta();
|
||||
assertNotNull(loadedMeta);
|
||||
|
||||
final PersistentDataContainer loadedPdc = loadedMeta.getPersistentDataContainer();
|
||||
assertEquals("5i", loadedPdc.get(requestKey("string_int"), PersistentDataType.STRING));
|
||||
assertEquals("true", loadedPdc.get(requestKey("string_true"), PersistentDataType.STRING));
|
||||
assertEquals(arrayLookalike, loadedPdc.get(requestKey("string_array_lookalike"), PersistentDataType.STRING));
|
||||
assertEquals(jsonLookalike, loadedPdc.get(requestKey("string_json_lookalike"), PersistentDataType.STRING));
|
||||
}
|
||||
|
||||
/*
|
||||
Test complex object storage
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
package org.bukkit.craftbukkit.legacy;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.craftbukkit.inventory.CraftItemFactory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.persistence.PersistentDataContainer;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import org.bukkit.support.AbstractTestingBase;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class PersistentDataContainerLegacyTest extends AbstractTestingBase {
|
||||
|
||||
@Test
|
||||
public void ensureLegacyParsing() {
|
||||
CraftItemFactory.instance(); // Initialize craft item factory to register craft item meta serializers
|
||||
|
||||
YamlConfiguration legacyConfig = null;
|
||||
try (final InputStream input = getClass().getClassLoader().getResourceAsStream("pdc/legacy_pdc.yml")) {
|
||||
assertNotNull(input, "Legacy pdc yaml was null");
|
||||
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(input))) {
|
||||
legacyConfig = YamlConfiguration.loadConfiguration(reader);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
fail("Failed to find test resource!");
|
||||
}
|
||||
|
||||
assertNotNull(legacyConfig, "Could not fetch legacy config");
|
||||
|
||||
final ItemStack stack = legacyConfig.getItemStack("test");
|
||||
assertNotNull(stack);
|
||||
|
||||
final ItemMeta meta = stack.getItemMeta();
|
||||
assertNotNull(meta);
|
||||
|
||||
final PersistentDataContainer pdc = meta.getPersistentDataContainer();
|
||||
assertEquals(Byte.valueOf(Byte.MAX_VALUE), pdc.get(key("byte"), PersistentDataType.BYTE), "legacy byte was wrong");
|
||||
assertEquals(Short.valueOf(Short.MAX_VALUE), pdc.get(key("short"), PersistentDataType.SHORT), "legacy short was wrong");
|
||||
assertEquals(Integer.valueOf(Integer.MAX_VALUE), pdc.get(key("integer"), PersistentDataType.INTEGER), "legacy integer was wrong");
|
||||
assertEquals(Long.valueOf(Long.MAX_VALUE), pdc.get(key("long"), PersistentDataType.LONG), "legacy long was wrong");
|
||||
assertEquals(Float.valueOf(Float.MAX_VALUE), pdc.get(key("float"), PersistentDataType.FLOAT), "legacy float was wrong");
|
||||
assertEquals(Double.valueOf(Double.MAX_VALUE), pdc.get(key("double"), PersistentDataType.DOUBLE), "legacy double was wrong");
|
||||
assertEquals("stringy", pdc.get(key("string_simple"), PersistentDataType.STRING), "legacy string-simple was wrong");
|
||||
assertEquals("What a fun complex string 🔥", pdc.get(key("string_complex"), PersistentDataType.STRING), "legacy string-complex was wrong");
|
||||
|
||||
assertArrayEquals(new byte[]{Byte.MIN_VALUE}, pdc.get(key("byte_array"), PersistentDataType.BYTE_ARRAY), "legacy byte array was wrong");
|
||||
|
||||
assertArrayEquals(new int[]{Integer.MIN_VALUE}, pdc.get(key("integer_array"), PersistentDataType.INTEGER_ARRAY), "legacy integer array was wrong");
|
||||
|
||||
assertArrayEquals(new long[]{Long.MIN_VALUE}, pdc.get(key("long_array"), PersistentDataType.LONG_ARRAY), "legacy long array was wrong");
|
||||
|
||||
assertEquals("5", pdc.get(key("string_edge_case_number"), PersistentDataType.STRING), "legacy string edge case number");
|
||||
assertEquals("\"Hello world\"", pdc.get(key("string_edge_case_quoted"), PersistentDataType.STRING), "legacy string edge case quotes");
|
||||
}
|
||||
|
||||
private NamespacedKey key(String key) {
|
||||
return new NamespacedKey("test", key);
|
||||
}
|
||||
}
|
23
paper-server/src/test/resources/pdc/legacy_pdc.yml
Normal file
23
paper-server/src/test/resources/pdc/legacy_pdc.yml
Normal file
|
@ -0,0 +1,23 @@
|
|||
test:
|
||||
==: org.bukkit.inventory.ItemStack
|
||||
v: 2584
|
||||
type: NETHER_STAR
|
||||
meta:
|
||||
==: ItemMeta
|
||||
meta-type: UNSPECIFIC
|
||||
PublicBukkitValues:
|
||||
test:string_simple: stringy
|
||||
test:integer: 2147483647i
|
||||
test:long_array: '[L;-9223372036854775808L]'
|
||||
test:byte: 127b
|
||||
test:double: 1.7976931348623157E308d
|
||||
test:short: 32767s
|
||||
test:string_complex: What a fun complex string 🔥
|
||||
test:integer_array: '[I;-2147483648]'
|
||||
test:float: 3.4028235E38f
|
||||
test:byte_array: '[B;-128B]'
|
||||
test:long: 9223372036854775807L
|
||||
test:string_edge_case_number: '5'
|
||||
# Constructed via set(key, STRING, "\"Hello world\"") in legacy
|
||||
test:string_edge_case_quoted: '"Hello world"'
|
||||
|
Loading…
Add table
Reference in a new issue