From 8cd8851498e42b69835d467141a1c3c71a771c59 Mon Sep 17 00:00:00 2001 From: CraftBukkit/Spigot Date: Sat, 6 Jan 2024 16:03:58 +1100 Subject: [PATCH] #1295: Define native persistent data types for lists By: Bjarne Koll --- .../net/minecraft/nbt/NBTTagList.patch | 11 + .../tags/DeprecatedContainerTagType.java | 17 +- .../inventory/tags/DeprecatedItemTagType.java | 19 +- .../CraftPersistentDataContainer.java | 26 +- .../CraftPersistentDataTypeRegistry.java | 385 +++++++++++++----- .../DirtyCraftPersistentDataContainer.java | 5 +- .../PersistentDataContainerTest.java | 194 +++++++-- 7 files changed, 482 insertions(+), 175 deletions(-) create mode 100644 paper-server/nms-patches/net/minecraft/nbt/NBTTagList.patch diff --git a/paper-server/nms-patches/net/minecraft/nbt/NBTTagList.patch b/paper-server/nms-patches/net/minecraft/nbt/NBTTagList.patch new file mode 100644 index 0000000000..0281b565c8 --- /dev/null +++ b/paper-server/nms-patches/net/minecraft/nbt/NBTTagList.patch @@ -0,0 +1,11 @@ +--- a/net/minecraft/nbt/NBTTagList.java ++++ b/net/minecraft/nbt/NBTTagList.java +@@ -145,7 +145,7 @@ + private final List list; + private byte type; + +- NBTTagList(List list, byte b0) { ++ public NBTTagList(List list, byte b0) { // PAIL: package-private -> public + this.list = list; + this.type = b0; + } diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/tags/DeprecatedContainerTagType.java b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/tags/DeprecatedContainerTagType.java index 3df2876b02..8e1c1e9bd6 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/tags/DeprecatedContainerTagType.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/tags/DeprecatedContainerTagType.java @@ -7,27 +7,31 @@ import org.bukkit.inventory.meta.tags.ItemTagType; import org.bukkit.persistence.PersistentDataAdapterContext; import org.bukkit.persistence.PersistentDataContainer; import org.bukkit.persistence.PersistentDataType; +import org.jetbrains.annotations.NotNull; -public final class DeprecatedContainerTagType implements PersistentDataType { +public final class DeprecatedContainerTagType implements PersistentDataType { - private final ItemTagType deprecated; + private final ItemTagType deprecated; - DeprecatedContainerTagType(ItemTagType deprecated) { + DeprecatedContainerTagType(ItemTagType deprecated) { this.deprecated = deprecated; } + @NotNull @Override public Class getPrimitiveType() { return PersistentDataContainer.class; } + @NotNull @Override - public Class getComplexType() { + public Class getComplexType() { return deprecated.getComplexType(); } + @NotNull @Override - public PersistentDataContainer toPrimitive(Z complex, PersistentDataAdapterContext context) { + public PersistentDataContainer toPrimitive(@NotNull C complex, @NotNull PersistentDataAdapterContext context) { CustomItemTagContainer deprecated = this.deprecated.toPrimitive(complex, new DeprecatedItemAdapterContext(context)); Preconditions.checkArgument(deprecated instanceof DeprecatedCustomTagContainer, "Could not wrap deprecated API due to foreign CustomItemTagContainer implementation %s", deprecated.getClass().getSimpleName()); @@ -39,8 +43,9 @@ public final class DeprecatedContainerTagType implements PersistentDataType

implements PersistentDataType { +public final class DeprecatedItemTagType implements PersistentDataType { - private final ItemTagType deprecated; + private final ItemTagType deprecated; - public DeprecatedItemTagType(ItemTagType deprecated) { + public DeprecatedItemTagType(ItemTagType deprecated) { this.deprecated = deprecated; } + @NotNull @Override - public Class getPrimitiveType() { + public Class

getPrimitiveType() { return deprecated.getPrimitiveType(); } + @NotNull @Override - public Class getComplexType() { + public Class getComplexType() { return deprecated.getComplexType(); } + @NotNull @Override - public T toPrimitive(Z complex, PersistentDataAdapterContext context) { + public P toPrimitive(@NotNull C complex, @NotNull PersistentDataAdapterContext context) { return this.deprecated.toPrimitive(complex, new DeprecatedItemAdapterContext(context)); } + @NotNull @Override - public Z fromPrimitive(T primitive, PersistentDataAdapterContext context) { + public C fromPrimitive(@NotNull P primitive, @NotNull PersistentDataAdapterContext context) { return this.deprecated.fromPrimitive(primitive, new DeprecatedItemAdapterContext(context)); } } diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/persistence/CraftPersistentDataContainer.java b/paper-server/src/main/java/org/bukkit/craftbukkit/persistence/CraftPersistentDataContainer.java index 2c26a17147..af31f75463 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/persistence/CraftPersistentDataContainer.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/persistence/CraftPersistentDataContainer.java @@ -14,6 +14,7 @@ import org.bukkit.craftbukkit.util.CraftNBTTagConfigSerializer; import org.bukkit.persistence.PersistentDataAdapterContext; import org.bukkit.persistence.PersistentDataContainer; import org.bukkit.persistence.PersistentDataType; +import org.jetbrains.annotations.NotNull; public class CraftPersistentDataContainer implements PersistentDataContainer { @@ -33,16 +34,16 @@ public class CraftPersistentDataContainer implements PersistentDataContainer { @Override - public void set(NamespacedKey key, PersistentDataType type, Z value) { + public void set(@NotNull NamespacedKey key, @NotNull PersistentDataType type, @NotNull Z value) { Preconditions.checkArgument(key != null, "The NamespacedKey key cannot be null"); Preconditions.checkArgument(type != null, "The provided type cannot be null"); Preconditions.checkArgument(value != null, "The provided value cannot be null"); - this.customDataTags.put(key.toString(), registry.wrap(type.getPrimitiveType(), type.toPrimitive(value, adapterContext))); + this.customDataTags.put(key.toString(), this.registry.wrap(type, type.toPrimitive(value, adapterContext))); } @Override - public boolean has(NamespacedKey key, PersistentDataType type) { + public boolean has(@NotNull NamespacedKey key, @NotNull PersistentDataType type) { Preconditions.checkArgument(key != null, "The NamespacedKey key cannot be null"); Preconditions.checkArgument(type != null, "The provided type cannot be null"); @@ -51,7 +52,7 @@ public class CraftPersistentDataContainer implements PersistentDataContainer { return false; } - return registry.isInstanceOf(type.getPrimitiveType(), value); + return this.registry.isInstanceOf(type, value); } @Override @@ -60,7 +61,7 @@ public class CraftPersistentDataContainer implements PersistentDataContainer { } @Override - public Z get(NamespacedKey key, PersistentDataType type) { + public Z get(@NotNull NamespacedKey key, @NotNull PersistentDataType type) { Preconditions.checkArgument(key != null, "The NamespacedKey key cannot be null"); Preconditions.checkArgument(type != null, "The provided type cannot be null"); @@ -69,15 +70,17 @@ public class CraftPersistentDataContainer implements PersistentDataContainer { return null; } - return type.fromPrimitive(registry.extract(type.getPrimitiveType(), value), adapterContext); + return type.fromPrimitive(this.registry.extract(type, value), adapterContext); } + @NotNull @Override - public Z getOrDefault(NamespacedKey key, PersistentDataType type, Z defaultValue) { - Z z = get(key, type); + public Z getOrDefault(@NotNull NamespacedKey key, @NotNull PersistentDataType type, @NotNull Z defaultValue) { + Z z = this.get(key, type); return z != null ? z : defaultValue; } + @NotNull @Override public Set getKeys() { Set keys = new HashSet<>(); @@ -93,7 +96,7 @@ public class CraftPersistentDataContainer implements PersistentDataContainer { } @Override - public void remove(NamespacedKey key) { + public void remove(@NotNull NamespacedKey key) { Preconditions.checkArgument(key != null, "The NamespacedKey key cannot be null"); this.customDataTags.remove(key.toString()); @@ -104,6 +107,7 @@ public class CraftPersistentDataContainer implements PersistentDataContainer { return this.customDataTags.isEmpty(); } + @NotNull @Override public void copyTo(PersistentDataContainer other, boolean replace) { Preconditions.checkArgument(other != null, "The target container cannot be null"); @@ -127,7 +131,7 @@ public class CraftPersistentDataContainer implements PersistentDataContainer { return false; } - Map myRawMap = getRaw(); + Map myRawMap = this.getRaw(); Map theirRawMap = ((CraftPersistentDataContainer) obj).getRaw(); return Objects.equals(myRawMap, theirRawMap); @@ -160,7 +164,7 @@ public class CraftPersistentDataContainer implements PersistentDataContainer { } public CraftPersistentDataTypeRegistry getDataTagTypeRegistry() { - return registry; + return this.registry; } @Override diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/persistence/CraftPersistentDataTypeRegistry.java b/paper-server/src/main/java/org/bukkit/craftbukkit/persistence/CraftPersistentDataTypeRegistry.java index b261aa2e0e..7fa09654c6 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/persistence/CraftPersistentDataTypeRegistry.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/persistence/CraftPersistentDataTypeRegistry.java @@ -1,11 +1,16 @@ package org.bukkit.craftbukkit.persistence; import com.google.common.base.Preconditions; +import com.google.common.collect.Lists; import com.google.common.primitives.Primitives; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.function.BiFunction; +import java.util.function.BiPredicate; import java.util.function.Function; import net.minecraft.nbt.NBTBase; import net.minecraft.nbt.NBTTagByte; @@ -20,85 +25,110 @@ import net.minecraft.nbt.NBTTagLong; import net.minecraft.nbt.NBTTagLongArray; import net.minecraft.nbt.NBTTagShort; import net.minecraft.nbt.NBTTagString; +import org.bukkit.persistence.ListPersistentDataType; import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; +import org.jetbrains.annotations.NotNull; /** - * This class represents a registry that contains the used adapters for. + * The craft persistent data type registry, at its core, is responsible for the + * conversion process between a {@link PersistentDataType} and a respective + * {@link NBTBase} instance. + *

+ * It does so by creating {@link TagAdapter} instances that are capable of + * mappings the supported "primitive types" of {@link PersistentDataType}s to + * their respective {@link NBTBase} instances. + *

+ * To accomplish this, the class makes heavy use of raw arguments. Their + * validity is enforced by the mapping of class to {@link TagAdapter} + * internally. */ +@SuppressWarnings({"rawtypes", "unchecked"}) public final class CraftPersistentDataTypeRegistry { private final Function CREATE_ADAPTER = this::createAdapter; - private class TagAdapter { - - private final Function builder; - private final Function extractor; - - private final Class primitiveType; - private final Class nbtBaseType; - - public TagAdapter(Class primitiveType, Class nbtBaseType, Function builder, Function extractor) { - this.primitiveType = primitiveType; - this.nbtBaseType = nbtBaseType; - this.builder = builder; - this.extractor = extractor; - } + /** + * A tag adapter is a closely related type to a specific implementation of + * the {@link NBTBase} interface. It exists to convert from and to the + * respective value of a {@link NBTBase} to a "primitive type" for later + * usage in {@link PersistentDataType}. + * + * @param primitiveType the class of the primitive type, e.g. + * {@link String}. + * @param nbtBaseType the class of the tag implementation that is used to + * store this primitive type, e.g {@link NBTTagString}. + * @param nmsTypeByte the byte identifier of the tag as defined by + * {@link NBTBase#getId()}. + * @param builder a bi function that is responsible for mapping a "primitive + * type" and its respective {@link PersistentDataType} to a {@link NBTBase}. + * @param extractor a bi function that is responsible for extracting a + * "primitive type" from a {@link NBTBase} given a + * {@link PersistentDataType}. + * @param matcher a bi predicate that is responsible for computing if the + * passed {@link NBTBase} holds a value that the {@link PersistentDataType} + * can extract. + * @param

the generic type of the primitive the persistent data type + * expects. + * @param the generic type of the concrete {@link NBTBase} + * implementation that the primitive type is mapped into. + */ + private record TagAdapter( + Class

primitiveType, + Class nbtBaseType, + byte nmsTypeByte, + BiFunction, P, T> builder, + BiFunction, T, P> extractor, + BiPredicate, NBTBase> matcher) { /** - * This method will extract the value stored in the tag, according to - * the expected primitive type. + * Extract the primitive value from the {@link NBTBase}. * * @param base the base to extract from - * - * @return the value stored inside of the tag - * + * @return the value stored inside the tag * @throws ClassCastException if the passed base is not an instanced of * the defined base type and therefore is not applicable to the - * extractor function + * extractor function. */ - T extract(NBTBase base) { - Preconditions.checkArgument(nbtBaseType.isInstance(base), "The provided NBTBase was of the type %s. Expected type %s", base.getClass().getSimpleName(), nbtBaseType.getSimpleName()); - return this.extractor.apply(nbtBaseType.cast(base)); + private P extract(final PersistentDataType dataType, final NBTBase base) { + Preconditions.checkArgument(this.nbtBaseType.isInstance(base), "The provided NBTBase was of the type %s. Expected type %s", base.getClass().getSimpleName(), this.nbtBaseType.getSimpleName()); + return this.extractor.apply(dataType, this.nbtBaseType.cast(base)); } /** - * Builds a tag instance wrapping around the provided value object. + * Builds a tag instance wrapping around the provided primitive value. * * @param value the value to store inside the created tag - * * @return the new tag instance - * * @throws ClassCastException if the passed value object is not of the * defined primitive type and therefore is not applicable to the builder - * function + * function. */ - Z build(Object value) { - Preconditions.checkArgument(primitiveType.isInstance(value), "The provided value was of the type %s. Expected type %s", value.getClass().getSimpleName(), primitiveType.getSimpleName()); - return this.builder.apply(primitiveType.cast(value)); + private T build(final PersistentDataType dataType, final Object value) { + Preconditions.checkArgument(this.primitiveType.isInstance(value), "The provided value was of the type %s. Expected type %s", value.getClass().getSimpleName(), this.primitiveType.getSimpleName()); + return this.builder.apply(dataType, this.primitiveType.cast(value)); } /** - * Returns if the tag instance matches the adapters one. - * - * @param base the base to check + * Computes if the provided persistent data type's primitive type is a + * representation of the {@link NBTBase}. * + * @param base the base tag instance to check against * @return if the tag was an instance of the set type */ - boolean isInstance(NBTBase base) { - return this.nbtBaseType.isInstance(base); + private boolean isInstance(final PersistentDataType persistentDataType, final NBTBase base) { + return this.matcher.test(persistentDataType, base); } } private final Map adapters = new HashMap<>(); /** - * Creates a suitable adapter instance for the primitive class type + * Creates a suitable adapter instance for the primitive class type. * * @param type the type to create an adapter for - * @param the generic type of that class - * + * @param the generic type of the primitive type * @return the created adapter instance - * * @throws IllegalArgumentException if no suitable tag type adapter for this * type was found */ @@ -107,109 +137,163 @@ public final class CraftPersistentDataTypeRegistry { type = Primitives.wrap(type); //Make sure we will always "switch" over the wrapper types } - /* - Primitives - */ + // Primitives if (Objects.equals(Byte.class, type)) { - return createAdapter(Byte.class, NBTTagByte.class, NBTTagByte::valueOf, NBTTagByte::getAsByte); + return this.createAdapter( + Byte.class, NBTTagByte.class, NBTBase.TAG_BYTE, + NBTTagByte::valueOf, NBTTagByte::getAsByte + ); } if (Objects.equals(Short.class, type)) { - return createAdapter(Short.class, NBTTagShort.class, NBTTagShort::valueOf, NBTTagShort::getAsShort); + return this.createAdapter( + Short.class, NBTTagShort.class, NBTBase.TAG_SHORT, NBTTagShort::valueOf, NBTTagShort::getAsShort + ); } if (Objects.equals(Integer.class, type)) { - return createAdapter(Integer.class, NBTTagInt.class, NBTTagInt::valueOf, NBTTagInt::getAsInt); + return this.createAdapter( + Integer.class, NBTTagInt.class, NBTBase.TAG_INT, NBTTagInt::valueOf, NBTTagInt::getAsInt + ); } if (Objects.equals(Long.class, type)) { - return createAdapter(Long.class, NBTTagLong.class, NBTTagLong::valueOf, NBTTagLong::getAsLong); + return this.createAdapter( + Long.class, NBTTagLong.class, NBTBase.TAG_LONG, NBTTagLong::valueOf, NBTTagLong::getAsLong + ); } if (Objects.equals(Float.class, type)) { - return createAdapter(Float.class, NBTTagFloat.class, NBTTagFloat::valueOf, NBTTagFloat::getAsFloat); + return this.createAdapter( + Float.class, NBTTagFloat.class, NBTBase.TAG_FLOAT, + NBTTagFloat::valueOf, NBTTagFloat::getAsFloat + ); } if (Objects.equals(Double.class, type)) { - return createAdapter(Double.class, NBTTagDouble.class, NBTTagDouble::valueOf, NBTTagDouble::getAsDouble); + return this.createAdapter( + Double.class, NBTTagDouble.class, NBTBase.TAG_DOUBLE, + NBTTagDouble::valueOf, NBTTagDouble::getAsDouble + ); } - - /* - String - */ if (Objects.equals(String.class, type)) { - return createAdapter(String.class, NBTTagString.class, NBTTagString::valueOf, NBTTagString::getAsString); + return this.createAdapter( + String.class, NBTTagString.class, NBTBase.TAG_STRING, + NBTTagString::valueOf, NBTTagString::getAsString + ); } - /* - Primitive Arrays - */ + // Primitive non-list arrays if (Objects.equals(byte[].class, type)) { - return createAdapter(byte[].class, NBTTagByteArray.class, array -> new NBTTagByteArray(Arrays.copyOf(array, array.length)), n -> Arrays.copyOf(n.getAsByteArray(), n.size())); + return this.createAdapter( + byte[].class, NBTTagByteArray.class, NBTBase.TAG_BYTE_ARRAY, + array -> new NBTTagByteArray(Arrays.copyOf(array, array.length)), + n -> Arrays.copyOf(n.getAsByteArray(), n.size()) + ); } if (Objects.equals(int[].class, type)) { - return createAdapter(int[].class, NBTTagIntArray.class, array -> new NBTTagIntArray(Arrays.copyOf(array, array.length)), n -> Arrays.copyOf(n.getAsIntArray(), n.size())); + return this.createAdapter( + int[].class, NBTTagIntArray.class, NBTBase.TAG_INT_ARRAY, + array -> new NBTTagIntArray(Arrays.copyOf(array, array.length)), + n -> Arrays.copyOf(n.getAsIntArray(), n.size()) + ); } if (Objects.equals(long[].class, type)) { - return createAdapter(long[].class, NBTTagLongArray.class, array -> new NBTTagLongArray(Arrays.copyOf(array, array.length)), n -> Arrays.copyOf(n.getAsLongArray(), n.size())); + return this.createAdapter( + long[].class, NBTTagLongArray.class, NBTBase.TAG_LONG_ARRAY, + array -> new NBTTagLongArray(Arrays.copyOf(array, array.length)), + n -> Arrays.copyOf(n.getAsLongArray(), n.size()) + ); } - /* - Complex Arrays - */ + // Previously "emulated" compound lists, now useless as a proper list type exists. if (Objects.equals(PersistentDataContainer[].class, type)) { - return createAdapter(PersistentDataContainer[].class, NBTTagList.class, + return this.createAdapter( + PersistentDataContainer[].class, NBTTagList.class, NBTBase.TAG_LIST, (containerArray) -> { - NBTTagList list = new NBTTagList(); - for (int i = 0; i < containerArray.length; i++) { - list.add(((CraftPersistentDataContainer) containerArray[i]).toTagCompound()); + final NBTTagList list = new NBTTagList(); + for (final PersistentDataContainer persistentDataContainer : containerArray) { + list.add(((CraftPersistentDataContainer) persistentDataContainer).toTagCompound()); } return list; }, (tag) -> { - PersistentDataContainer[] containerArray = new CraftPersistentDataContainer[tag.size()]; + final PersistentDataContainer[] containerArray = new CraftPersistentDataContainer[tag.size()]; for (int i = 0; i < tag.size(); i++) { - CraftPersistentDataContainer container = new CraftPersistentDataContainer(this); - NBTTagCompound compound = tag.getCompound(i); - for (String key : compound.getAllKeys()) { + final CraftPersistentDataContainer container = new CraftPersistentDataContainer(this); + final NBTTagCompound compound = tag.getCompound(i); + for (final String key : compound.getAllKeys()) { container.put(key, compound.get(key)); } containerArray[i] = container; } return containerArray; + } + ); + } + + // Note that this will map the interface PersistentMetadataContainer directly to the CraftBukkit implementation + // Passing any other instance of this form to the tag type registry will throw a ClassCastException + // as defined in TagAdapter#build. + if (Objects.equals(PersistentDataContainer.class, type)) { + return this.createAdapter( + CraftPersistentDataContainer.class, NBTTagCompound.class, NBTBase.TAG_COMPOUND, + CraftPersistentDataContainer::toTagCompound, + tag -> { + final CraftPersistentDataContainer container = new CraftPersistentDataContainer(this); + for (final String key : tag.getAllKeys()) { + container.put(key, tag.get(key)); + } + return container; }); } - /* - Note that this will map the interface PersistentMetadataContainer directly to the CraftBukkit implementation - Passing any other instance of this form to the tag type registry will throw a ClassCastException as defined in TagAdapter#build - */ - if (Objects.equals(PersistentDataContainer.class, type)) { - return createAdapter(CraftPersistentDataContainer.class, NBTTagCompound.class, CraftPersistentDataContainer::toTagCompound, tag -> { - CraftPersistentDataContainer container = new CraftPersistentDataContainer(this); - for (String key : tag.getAllKeys()) { - container.put(key, tag.get(key)); - } - return container; - }); + if (Objects.equals(List.class, type)) { + return createAdapter( + List.class, + net.minecraft.nbt.NBTTagList.class, + NBTBase.TAG_LIST, + this::constructList, + this::extractList, + this::matchesListTag + ); } throw new IllegalArgumentException("Could not find a valid TagAdapter implementation for the requested type " + type.getSimpleName()); } - private TagAdapter createAdapter(Class primitiveType, Class nbtBaseType, Function builder, Function extractor) { - return new TagAdapter<>(primitiveType, nbtBaseType, builder, extractor); + // Plain constructor helper method. + private TagAdapter createAdapter( + final Class primitiveType, final Class nbtBaseType, final byte nmsTypeByte, + final Function builder, final Function extractor + ) { + return createAdapter( + primitiveType, + nbtBaseType, + nmsTypeByte, + (type, t) -> builder.apply(t), + (type, z) -> extractor.apply(z), + (type, t) -> nbtBaseType.isInstance(t) + ); + } + + // Plain constructor helper method. + private TagAdapter createAdapter( + final Class primitiveType, final Class nbtBaseType, final byte nmsTypeByte, + final BiFunction, T, Z> builder, + final BiFunction, Z, T> extractor, + final BiPredicate, NBTBase> matcher + ) { + return new TagAdapter<>(primitiveType, nbtBaseType, nmsTypeByte, builder, extractor, matcher); } /** - * Wraps the passed value into a tag instance. + * Wraps the passed primitive value into a tag instance. * * @param type the type of the passed value * @param value the value to be stored in the tag * @param the generic type of the value - * * @return the created tag instance - * * @throws IllegalArgumentException if no suitable tag type adapter for this - * type was found + * type was found. */ - public NBTBase wrap(Class type, T value) { - return this.adapters.computeIfAbsent(type, CREATE_ADAPTER).build(value); + public NBTBase wrap(final PersistentDataType type, final T value) { + return this.getOrCreateAdapter(type).build(type, value); } /** @@ -218,14 +302,29 @@ public final class CraftPersistentDataTypeRegistry { * @param type the type of the primitive value * @param base the base instance to check * @param the generic type of the type - * * @return if the base stores values of the primitive type passed - * * @throws IllegalArgumentException if no suitable tag type adapter for this - * type was found + * type was found. */ - public boolean isInstanceOf(Class type, NBTBase base) { - return this.adapters.computeIfAbsent(type, CREATE_ADAPTER).isInstance(base); + public boolean isInstanceOf(final PersistentDataType type, final NBTBase base) { + return this.getOrCreateAdapter(type).isInstance(type, base); + } + + /** + * Fetches or creates an adapter for the requested persistent data type. + * + * @param type the persistent data type to find or create an adapter for. + * @param the generic type of the primitive type of the persistent data + * type. + * @param the generic type of the complex type of the persistent data + * type. + * @return the tag adapter instance that was found or created. + * @throws IllegalArgumentException if no adapter can be created for the + * persistent data type. + */ + @NotNull + private TagAdapter getOrCreateAdapter(@NotNull final PersistentDataType type) { + return this.adapters.computeIfAbsent(type.getPrimitiveType(), CREATE_ADAPTER); } /** @@ -234,23 +333,95 @@ public final class CraftPersistentDataTypeRegistry { * @param type the type of the value to extract * @param tag the tag to extract the value from * @param the generic type of the value stored inside the tag - * * @return the extracted value - * * @throws IllegalArgumentException if the passed base is not an instanced * of the defined base type and therefore is not applicable to the extractor - * function + * function. * @throws IllegalArgumentException if the found object is not of type - * passed + * passed. * @throws IllegalArgumentException if no suitable tag type adapter for this - * type was found + * type was found. */ - public T extract(Class type, NBTBase tag) throws ClassCastException, IllegalArgumentException { - TagAdapter adapter = this.adapters.computeIfAbsent(type, CREATE_ADAPTER); - Preconditions.checkArgument(adapter.isInstance(tag), "The found tag instance (%s) cannot store %s", tag.getClass().getSimpleName(), type.getSimpleName()); + public T extract(final PersistentDataType type, final NBTBase tag) throws ClassCastException, IllegalArgumentException { + final Class primitiveType = type.getPrimitiveType(); + final TagAdapter adapter = this.getOrCreateAdapter(type); + Preconditions.checkArgument(adapter.isInstance(type, tag), "The found tag instance (%s) cannot store %s", tag.getClass().getSimpleName(), primitiveType.getSimpleName()); - Object foundValue = adapter.extract(tag); - Preconditions.checkArgument(type.isInstance(foundValue), "The found object is of the type %s. Expected type %s", foundValue.getClass().getSimpleName(), type.getSimpleName()); - return type.cast(foundValue); + final Object foundValue = adapter.extract(type, tag); + Preconditions.checkArgument(primitiveType.isInstance(foundValue), "The found object is of the type %s. Expected type %s", foundValue.getClass().getSimpleName(), primitiveType.getSimpleName()); + return primitiveType.cast(foundValue); + } + + /** + * Constructs a {@link NBTTagList} from a {@link List} instance by using the + * passed persistent data type. + * + * @param type the persistent data type of the list. + * @param list the list or primitive values. + * @param

the generic type of the primitive values in the list. + * @return the constructed {@link NBTTagList}. + */ + private > NBTTagList constructList(@NotNull final PersistentDataType type, @NotNull final List

list) { + Preconditions.checkArgument(type instanceof ListPersistentDataType, "The passed list cannot be written to the PDC with a %s (expected a list data type)", type.getClass().getSimpleName()); + final ListPersistentDataType listPersistentDataType = (ListPersistentDataType) type; + + final TagAdapter elementAdapter = this.getOrCreateAdapter(listPersistentDataType.elementType()); + + final List values = Lists.newArrayListWithCapacity(list.size()); + for (final P primitiveValue : list) { + values.add(this.wrap(listPersistentDataType.elementType(), primitiveValue)); + } + + return new NBTTagList(values, elementAdapter.nmsTypeByte()); + } + + /** + * Extracts a {@link List} from a {@link NBTTagList} and a respective + * {@link PersistentDataType}. + * + * @param type the persistent data type of the list. + * @param listTag the list tag to extract the {@link List} from. + * @param

the generic type of the primitive values stored in the + * {@link List}. + * @return the extracted {@link List} instance. + * @throws IllegalArgumentException if the passed {@link PersistentDataType} + * is not a {@link ListPersistentDataType} and can hence not be used to + * extract a {@link List}. + */ + private

List

extractList(@NotNull final PersistentDataType type, + @NotNull final NBTTagList listTag) { + Preconditions.checkArgument(type instanceof ListPersistentDataType, "The found list tag cannot be read with a %s (expected a list data type)", type.getClass().getSimpleName()); + final ListPersistentDataType listPersistentDataType = (ListPersistentDataType) type; + + final List

output = new ObjectArrayList<>(listTag.size()); + for (final NBTBase tag : listTag) { + output.add(this.extract(listPersistentDataType.elementType(), tag)); + } + + return output; + } + + /** + * Computes if the passed {@link NBTBase} is a {@link NBTTagList} and it, + * including its elements, can be read/written via the passed + * {@link PersistentDataType}. + * + * @param type the persistent data type for which to check if the tag + * matches. + * @param tag the tag that is to be checked if it matches the data type. + * @return whether the passed tag can be read/written via the passed type. + */ + private boolean matchesListTag(final PersistentDataType type, final NBTBase tag) { + if ((!(type instanceof final ListPersistentDataType listPersistentDataType))) { + return false; + } + if (!(tag instanceof final NBTTagList listTag)) { + return false; + } + + final byte elementType = listTag.getElementType(); + final TagAdapter elementAdapter = this.getOrCreateAdapter(listPersistentDataType.elementType()); + + return elementAdapter.nmsTypeByte() == elementType; } } diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/persistence/DirtyCraftPersistentDataContainer.java b/paper-server/src/main/java/org/bukkit/craftbukkit/persistence/DirtyCraftPersistentDataContainer.java index 3d967dce2c..fe837dcab7 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/persistence/DirtyCraftPersistentDataContainer.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/persistence/DirtyCraftPersistentDataContainer.java @@ -5,6 +5,7 @@ import net.minecraft.nbt.NBTBase; import net.minecraft.nbt.NBTTagCompound; import org.bukkit.NamespacedKey; import org.bukkit.persistence.PersistentDataType; +import org.jetbrains.annotations.NotNull; /** * A child class of the persistent data container that recalls if it has been @@ -31,13 +32,13 @@ public final class DirtyCraftPersistentDataContainer extends CraftPersistentData } @Override - public void set(NamespacedKey key, PersistentDataType type, Z value) { + public void set(@NotNull NamespacedKey key, @NotNull PersistentDataType type, @NotNull Z value) { super.set(key, type, value); this.dirty(true); } @Override - public void remove(NamespacedKey key) { + public void remove(@NotNull NamespacedKey key) { super.remove(key); this.dirty(true); } diff --git a/paper-server/src/test/java/org/bukkit/craftbukkit/inventory/PersistentDataContainerTest.java b/paper-server/src/test/java/org/bukkit/craftbukkit/inventory/PersistentDataContainerTest.java index 1695789796..f89aa8f7d0 100644 --- a/paper-server/src/test/java/org/bukkit/craftbukkit/inventory/PersistentDataContainerTest.java +++ b/paper-server/src/test/java/org/bukkit/craftbukkit/inventory/PersistentDataContainerTest.java @@ -5,8 +5,12 @@ import java.io.IOException; import java.io.StringReader; import java.lang.reflect.Array; import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.function.BiConsumer; +import java.util.stream.Stream; import net.minecraft.nbt.NBTTagCompound; import org.bukkit.Bukkit; import org.bukkit.Material; @@ -15,12 +19,18 @@ import org.bukkit.configuration.InvalidConfigurationException; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.persistence.ListPersistentDataType; import org.bukkit.persistence.PersistentDataAdapterContext; import org.bukkit.persistence.PersistentDataContainer; import org.bukkit.persistence.PersistentDataType; import org.bukkit.support.AbstractTestingBase; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; public class PersistentDataContainerTest extends AbstractTestingBase { @@ -31,18 +41,14 @@ public class PersistentDataContainerTest extends AbstractTestingBase { VALID_KEY = new NamespacedKey("test", "validkey"); } - /* - Sets a test - */ + // Sets a test @Test public void testSetNoAdapter() { ItemMeta itemMeta = createNewItemMeta(); assertThrows(IllegalArgumentException.class, () -> itemMeta.getPersistentDataContainer().set(VALID_KEY, new PrimitiveTagType<>(boolean.class), true)); } - /* - Contains a tag - */ + // Contains a tag @Test public void testHasNoAdapter() { ItemMeta itemMeta = createNewItemMeta(); @@ -57,9 +63,7 @@ public class PersistentDataContainerTest extends AbstractTestingBase { assertTrue(itemMeta.getPersistentDataContainer().has(VALID_KEY)); } - /* - Getting a tag - */ + // Getting a tag @Test public void testGetNoAdapter() { ItemMeta itemMeta = createNewItemMeta(); @@ -87,11 +91,11 @@ public class PersistentDataContainerTest extends AbstractTestingBase { assertEquals(160L, (long) meta.getPersistentDataContainer().get(namespacedKeyB, PersistentDataType.LONG)); } - private ItemMeta createNewItemMeta() { + private static ItemMeta createNewItemMeta() { return Bukkit.getItemFactory().getItemMeta(Material.DIAMOND_PICKAXE); } - private NamespacedKey requestKey(String keyName) { + private static NamespacedKey requestKey(String keyName) { return new NamespacedKey("test-plugin", keyName.toLowerCase()); } @@ -117,9 +121,7 @@ public class PersistentDataContainerTest extends AbstractTestingBase { assertEquals(container, target); } - /* - Removing a tag - */ + // Removing a tag @Test public void testNBTTagStoring() { CraftMetaItem itemMeta = createComplexItemMeta(); @@ -188,6 +190,12 @@ public class PersistentDataContainerTest extends AbstractTestingBase { itemMeta.getPersistentDataContainer().set(requestKey("custom-string"), PersistentDataType.STRING, "Hello there world"); itemMeta.getPersistentDataContainer().set(requestKey("custom-int"), PersistentDataType.INTEGER, 3); itemMeta.getPersistentDataContainer().set(requestKey("custom-double"), PersistentDataType.DOUBLE, 3.123); + itemMeta.getPersistentDataContainer().set( + requestKey("custom-list-string"), PersistentDataType.LIST.strings(), List.of("first[]", "second{}", "third()") + ); + itemMeta.getPersistentDataContainer().set( + requestKey("custom-list-bytes"), PersistentDataType.LIST.bytes(), List.of((byte) 1, (byte) 2, (byte) 3) + ); PersistentDataContainer innerContainer = itemMeta.getPersistentDataContainer().getAdapterContext().newPersistentDataContainer(); //Add a inner container innerContainer.set(VALID_KEY, PersistentDataType.LONG, 5L); @@ -195,9 +203,7 @@ public class PersistentDataContainerTest extends AbstractTestingBase { return itemMeta; } - /* - Test edge cases with strings - */ + // Test edge cases with strings @Test public void testStringEdgeCases() throws IOException, InvalidConfigurationException { final ItemStack stack = new ItemStack(Material.DIAMOND); @@ -238,9 +244,7 @@ public class PersistentDataContainerTest extends AbstractTestingBase { assertEquals(jsonLookalike, loadedPdc.get(requestKey("string_json_lookalike"), PersistentDataType.STRING)); } - /* - Test complex object storage - */ + // Test complex object storage @Test public void storeUUIDOnItemTest() { ItemMeta itemMeta = createNewItemMeta(); @@ -285,25 +289,29 @@ public class PersistentDataContainerTest extends AbstractTestingBase { class UUIDPersistentDataType implements PersistentDataType { @Override + @NotNull public Class getPrimitiveType() { return byte[].class; } + @NotNull @Override public Class getComplexType() { return UUID.class; } + @NotNull @Override - public byte[] toPrimitive(UUID complex, PersistentDataAdapterContext context) { + public byte[] toPrimitive(@NotNull UUID complex, @NotNull PersistentDataAdapterContext context) { ByteBuffer bb = ByteBuffer.wrap(new byte[16]); bb.putLong(complex.getMostSignificantBits()); bb.putLong(complex.getLeastSignificantBits()); return bb.array(); } + @NotNull @Override - public UUID fromPrimitive(byte[] primitive, PersistentDataAdapterContext context) { + public UUID fromPrimitive(@NotNull byte[] primitive, @NotNull PersistentDataAdapterContext context) { ByteBuffer bb = ByteBuffer.wrap(primitive); long firstLong = bb.getLong(); long secondLong = bb.getLong(); @@ -315,22 +323,22 @@ public class PersistentDataContainerTest extends AbstractTestingBase { public void testPrimitiveCustomTags() { ItemMeta itemMeta = createNewItemMeta(); - testPrimitiveCustomTag(itemMeta, PersistentDataType.BYTE, (byte) 1); - testPrimitiveCustomTag(itemMeta, PersistentDataType.SHORT, (short) 1); - testPrimitiveCustomTag(itemMeta, PersistentDataType.INTEGER, 1); - testPrimitiveCustomTag(itemMeta, PersistentDataType.LONG, 1L); - testPrimitiveCustomTag(itemMeta, PersistentDataType.FLOAT, 1.34F); - testPrimitiveCustomTag(itemMeta, PersistentDataType.DOUBLE, 151.123); + this.testPrimitiveCustomTag(itemMeta, PersistentDataType.BYTE, (byte) 1); + this.testPrimitiveCustomTag(itemMeta, PersistentDataType.SHORT, (short) 1); + this.testPrimitiveCustomTag(itemMeta, PersistentDataType.INTEGER, 1); + this.testPrimitiveCustomTag(itemMeta, PersistentDataType.LONG, 1L); + this.testPrimitiveCustomTag(itemMeta, PersistentDataType.FLOAT, 1.34F); + this.testPrimitiveCustomTag(itemMeta, PersistentDataType.DOUBLE, 151.123); - testPrimitiveCustomTag(itemMeta, PersistentDataType.STRING, "test"); + this.testPrimitiveCustomTag(itemMeta, PersistentDataType.STRING, "test"); - testPrimitiveCustomTag(itemMeta, PersistentDataType.BYTE_ARRAY, new byte[]{ + this.testPrimitiveCustomTag(itemMeta, PersistentDataType.BYTE_ARRAY, new byte[]{ 1, 4, 2, Byte.MAX_VALUE }); - testPrimitiveCustomTag(itemMeta, PersistentDataType.INTEGER_ARRAY, new int[]{ + this.testPrimitiveCustomTag(itemMeta, PersistentDataType.INTEGER_ARRAY, new int[]{ 1, 4, 2, Integer.MAX_VALUE }); - testPrimitiveCustomTag(itemMeta, PersistentDataType.LONG_ARRAY, new long[]{ + this.testPrimitiveCustomTag(itemMeta, PersistentDataType.LONG_ARRAY, new long[]{ 1L, 4L, 2L, Long.MAX_VALUE }); } @@ -356,31 +364,35 @@ public class PersistentDataContainerTest extends AbstractTestingBase { assertFalse(meta.getPersistentDataContainer().has(tagKey, type)); } - class PrimitiveTagType implements PersistentDataType { + class PrimitiveTagType

implements PersistentDataType { - private final Class primitiveType; + private final Class

primitiveType; - PrimitiveTagType(Class primitiveType) { + PrimitiveTagType(Class

primitiveType) { this.primitiveType = primitiveType; } + @NotNull @Override - public Class getPrimitiveType() { - return primitiveType; + public Class

getPrimitiveType() { + return this.primitiveType; } + @NotNull @Override - public Class getComplexType() { - return primitiveType; + public Class

getComplexType() { + return this.primitiveType; } + @NotNull @Override - public T toPrimitive(T complex, PersistentDataAdapterContext context) { + public P toPrimitive(@NotNull P complex, @NotNull PersistentDataAdapterContext context) { return complex; } + @NotNull @Override - public T fromPrimitive(T primitive, PersistentDataAdapterContext context) { + public P fromPrimitive(@NotNull P primitive, @NotNull PersistentDataAdapterContext context) { return primitive; } } @@ -397,7 +409,105 @@ public class PersistentDataContainerTest extends AbstractTestingBase { assertNotSame(container, clonedContainer); assertEquals(container, clonedContainer); - clonedContainer.set(VALID_KEY, PersistentDataType.STRING, "dinnerbone"); + clonedContainer.set(PersistentDataContainerTest.VALID_KEY, PersistentDataType.STRING, "dinnerbone"); assertNotEquals(container, clonedContainer); } + + @ParameterizedTest + @MethodSource("testListTypeArgumentSource") + public void testListType(@NotNull final ListPersistentDataType type, @NotNull final List list, @NotNull final BiConsumer equalsCheck) { + final ItemMeta meta = createNewItemMeta(); + final PersistentDataContainer container = meta.getPersistentDataContainer(); + + container.set(requestKey("list"), type, list); + + final List returnedList = container.get(requestKey("list"), type); + + assertNotNull(returnedList); + assertEquals(list.size(), returnedList.size()); + + for (int i = 0; i < list.size(); i++) { + final T expectedValue = list.get(i); + final T foundValue = returnedList.get(i); + equalsCheck.accept(expectedValue, foundValue); + } + } + + @NotNull + private static Stream testListTypeArgumentSource() { + final PersistentDataContainer first = createNewItemMeta().getPersistentDataContainer(); + final PersistentDataContainer second = first.getAdapterContext().newPersistentDataContainer(); + first.set(requestKey("a"), PersistentDataType.STRING, "hello world"); + second.set(requestKey("b"), PersistentDataType.BOOLEAN, true); + + final BiConsumer objectAssertion = Assertions::assertEquals; + final BiConsumer byteArrayAssertion = Assertions::assertArrayEquals; + final BiConsumer intArrayAssertion = Assertions::assertArrayEquals; + final BiConsumer longArrayAssertion = Assertions::assertArrayEquals; + + return Stream.of( + Arguments.of(PersistentDataType.LIST.bytes(), List.of((byte) 1, (byte) 2, (byte) 3), objectAssertion), + Arguments.of(PersistentDataType.LIST.shorts(), List.of((short) 1, (short) 2, (short) 3), objectAssertion), + Arguments.of(PersistentDataType.LIST.integers(), List.of(1, 2, 3), objectAssertion), + Arguments.of(PersistentDataType.LIST.longs(), List.of(1L, 2L, 3L), objectAssertion), + Arguments.of(PersistentDataType.LIST.floats(), List.of(1F, 2F, 3F), objectAssertion), + Arguments.of(PersistentDataType.LIST.doubles(), List.of(1D, 2D, 3D), objectAssertion), + Arguments.of(PersistentDataType.LIST.booleans(), List.of(true, true, false), objectAssertion), + Arguments.of(PersistentDataType.LIST.strings(), List.of("a", "b", "c"), objectAssertion), + Arguments.of(PersistentDataType.LIST.byteArrays(), List.of(new byte[]{1, 2, 3}, new byte[]{4, 5, 6}), byteArrayAssertion), + Arguments.of(PersistentDataType.LIST.integerArrays(), List.of(new int[]{1, 2, 3}, new int[]{4, 5, 6}), intArrayAssertion), + Arguments.of(PersistentDataType.LIST.longArrays(), List.of(new long[]{1, 2, 3}, new long[]{4, 5, 6}), longArrayAssertion), + Arguments.of(PersistentDataType.LIST.dataContainers(), List.of(first, second), objectAssertion)); + } + + @Test + public void testEmptyListDataMaintainType() { + final ItemMeta meta = createNewItemMeta(); + final PersistentDataContainer container = meta.getPersistentDataContainer(); + + container.set(requestKey("list"), PersistentDataType.LIST.strings(), List.of()); + + assertTrue(container.has(requestKey("list"), PersistentDataType.LIST.strings())); + assertFalse(container.has(requestKey("list"), PersistentDataType.LIST.bytes())); + } + + // This is a horrific marriage of tag container array "primitive" types the API offered and the new list types. + // We are essentially testing if these two play nice as tag container array was an emulated primitive type + // that used lists under the hood, hence this is testing the extra handling of TAG_CONTAINER_ARRAY in combination + // with lists. Plain lists in lists are tested above. + // + // Little faith is to be had when it comes to abominations constructed by plugin developers, this test ensures + // even this disgrace of a combination functions in PDCs. + @Test + public void testListOfListViaContainerArray() { + final ListPersistentDataType listPersistentDataType = PersistentDataType.LIST.listTypeFrom(PersistentDataType.TAG_CONTAINER_ARRAY); + + final ItemMeta meta = createNewItemMeta(); + final PersistentDataContainer container = meta.getPersistentDataContainer(); + final PersistentDataAdapterContext adapterContext = container.getAdapterContext(); + + final PersistentDataContainer first = adapterContext.newPersistentDataContainer(); + first.set(requestKey("a"), PersistentDataType.STRING, "hi"); + + final PersistentDataContainer second = adapterContext.newPersistentDataContainer(); + second.set(requestKey("a"), PersistentDataType.INTEGER, 2); + + final List listOfArrays = new ArrayList<>(); + listOfArrays.add(new PersistentDataContainer[]{first, second}); + + container.set(requestKey("containerListList"), listPersistentDataType, listOfArrays); + + assertTrue(container.has(requestKey("containerListList"), listPersistentDataType)); + + final List containerListList = container.get(requestKey("containerListList"), listPersistentDataType); + + assertNotNull(containerListList); + assertEquals(1, containerListList.size()); + + final PersistentDataContainer[] arrayOfPDC = containerListList.get(0); + assertEquals(2, arrayOfPDC.length); + + assertEquals("hi", arrayOfPDC[0].get(requestKey("a"), PersistentDataType.STRING)); + assertEquals(2, arrayOfPDC[1].get(requestKey("a"), PersistentDataType.INTEGER)); + } }