diff --git a/src/main/java/net/minestom/server/tag/Serializers.java b/src/main/java/net/minestom/server/tag/Serializers.java new file mode 100644 index 000000000..defbd44be --- /dev/null +++ b/src/main/java/net/minestom/server/tag/Serializers.java @@ -0,0 +1,38 @@ +package net.minestom.server.tag; + +import net.minestom.server.item.ItemStack; +import org.jglrxavpok.hephaistos.nbt.*; + +import java.util.function.Function; + +final class Serializers { + static final Entry VOID = new Entry<>(null, null); + + static final Entry BYTE = new Entry<>(NBTByte::getValue, NBT::Byte); + static final Entry SHORT = new Entry<>(NBTShort::getValue, NBT::Short); + static final Entry INT = new Entry<>(NBTInt::getValue, NBT::Int); + static final Entry LONG = new Entry<>(NBTLong::getValue, NBT::Long); + static final Entry FLOAT = new Entry<>(NBTFloat::getValue, NBT::Float); + static final Entry DOUBLE = new Entry<>(NBTDouble::getValue, NBT::Double); + static final Entry STRING = new Entry<>(NBTString::getValue, NBT::String); + static final Entry NBT_ENTRY = new Entry<>(Function.identity(), Function.identity()); + + static final Entry ITEM = new Entry<>(ItemStack::fromItemNBT, ItemStack::toItemNBT); + + static Entry fromTagSerializer(TagSerializer serializer) { + return new Serializers.Entry<>( + (NBTCompound compound) -> { + if (compound.isEmpty()) return null; + return serializer.read(TagHandler.fromCompound(compound)); + }, + (value) -> { + if (value == null) return NBTCompound.EMPTY; + TagHandler handler = TagHandler.newHandler(); + serializer.write(handler, value); + return handler.asCompound(); + }); + } + + record Entry(Function read, Function write) { + } +} diff --git a/src/main/java/net/minestom/server/tag/Tag.java b/src/main/java/net/minestom/server/tag/Tag.java index b7bbd4103..5cd3ab7cf 100644 --- a/src/main/java/net/minestom/server/tag/Tag.java +++ b/src/main/java/net/minestom/server/tag/Tag.java @@ -6,7 +6,10 @@ import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.jglrxavpok.hephaistos.nbt.*; +import org.jglrxavpok.hephaistos.nbt.NBT; +import org.jglrxavpok.hephaistos.nbt.NBTCompoundLike; +import org.jglrxavpok.hephaistos.nbt.NBTList; +import org.jglrxavpok.hephaistos.nbt.NBTType; import org.jglrxavpok.hephaistos.nbt.mutable.MutableNBTCompound; import java.util.List; @@ -30,40 +33,43 @@ public class Tag { final int index; private final String key; - final Function readFunction; - final Function writeFunction; + final Serializers.Entry entry; private final Supplier defaultValue; - final Function originalRead; + final Function readComparator; // Optional properties final PathEntry[] path; final UnaryOperator copy; final int listScope; Tag(int index, String key, - Function originalRead, - Function readFunction, Function writeFunction, + Function readComparator, + Serializers.Entry entry, Supplier defaultValue, PathEntry[] path, UnaryOperator copy, int listScope) { assert index == INDEX_MAP.get(key); this.index = index; this.key = key; - this.originalRead = originalRead; - this.readFunction = readFunction; - this.writeFunction = writeFunction; + this.readComparator = readComparator; + this.entry = entry; this.defaultValue = defaultValue; this.path = path; this.copy = copy; this.listScope = listScope; } - static Tag tag(@NotNull String key, - @NotNull Function readFunction, - @NotNull Function writeFunction) { - return new Tag<>(INDEX_MAP.get(key), key, readFunction, - (Function) readFunction, (Function) writeFunction, + static Tag tag(@NotNull String key, @NotNull Serializers.Entry entry) { + return new Tag<>(INDEX_MAP.get(key), key, entry.read(), (Serializers.Entry) entry, null, null, null, 0); } + static Tag fromSerializer(@NotNull String key, @NotNull TagSerializer serializer) { + if (serializer instanceof TagRecord.Serializer recordSerializer) { + // Allow fast retrieval + return tag(key, recordSerializer.serializerEntry); + } + return tag(key, Serializers.fromTagSerializer(serializer)); + } + /** * Returns the key used to navigate inside the holder nbt. * @@ -75,7 +81,7 @@ public class Tag { @Contract(value = "_ -> new", pure = true) public Tag defaultValue(@NotNull Supplier defaultValue) { - return new Tag<>(index, key, originalRead, readFunction, writeFunction, defaultValue, path, copy, listScope); + return new Tag<>(index, key, readComparator, entry, defaultValue, path, copy, listScope); } @Contract(value = "_ -> new", pure = true) @@ -86,15 +92,14 @@ public class Tag { @Contract(value = "_, _ -> new", pure = true) public Tag map(@NotNull Function readMap, @NotNull Function writeMap) { - return new Tag<>(index, key, - readMap, - // Read - readFunction.andThen(t -> { - if (t == null) return null; - return readMap.apply(t); - }), - // Write - writeMap.andThen(writeFunction), + var entry = this.entry; + final Function readFunction = entry.read().andThen(t -> { + if (t == null) return null; + return readMap.apply(t); + }); + final Function writeFunction = writeMap.andThen(entry.write()); + return new Tag<>(index, key, readMap, + new Serializers.Entry<>(readFunction, writeFunction), // Default value () -> readMap.apply(createDefault()), path, null, listScope); @@ -103,7 +108,10 @@ public class Tag { @ApiStatus.Experimental @Contract(value = "-> new", pure = true) public Tag> list() { - return new Tag<>(index, key, originalRead, + var entry = this.entry; + var readFunction = entry.read(); + var writeFunction = entry.write(); + var listEntry = new Serializers.Entry, NBT>( read -> { var list = (NBTList) read; final int size = list.getSize(); @@ -131,35 +139,34 @@ public class Tag { array[i] = nbt; } return NBT.List(type, List.of(array)); - }, null, path, - copy != null ? ts -> { - final int size = ts.size(); - T[] array = (T[]) new Object[size]; - boolean shallowCopy = true; - for (int i = 0; i < size; i++) { - final T t = ts.get(i); - final T copy = this.copy.apply(t); - if (shallowCopy && copy != t) shallowCopy = false; - array[i] = copy; - } - return shallowCopy ? List.copyOf(ts) : List.of(array); - } : List::copyOf, listScope + 1); + }); + UnaryOperator> co = this.copy != null ? ts -> { + final int size = ts.size(); + T[] array = (T[]) new Object[size]; + boolean shallowCopy = true; + for (int i = 0; i < size; i++) { + final T t = ts.get(i); + final T copy = this.copy.apply(t); + if (shallowCopy && copy != t) shallowCopy = false; + array[i] = copy; + } + return shallowCopy ? List.copyOf(ts) : List.of(array); + } : List::copyOf; + return new Tag<>(index, key, readComparator, listEntry, null, path, co, listScope + 1); } @ApiStatus.Experimental @Contract(value = "_ -> new", pure = true) public Tag path(@NotNull String @Nullable ... path) { if (path == null || path.length == 0) { - return new Tag<>(index, key, originalRead, - readFunction, writeFunction, defaultValue, null, copy, listScope); + return new Tag<>(index, key, readComparator, entry, defaultValue, null, copy, listScope); } - PathEntry[] entries = new PathEntry[path.length]; + PathEntry[] pathEntries = new PathEntry[path.length]; for (int i = 0; i < path.length; i++) { var name = path[i]; - entries[i] = new PathEntry(name, INDEX_MAP.get(name)); + pathEntries[i] = new PathEntry(name, INDEX_MAP.get(name)); } - return new Tag<>(index, key, originalRead, - readFunction, writeFunction, defaultValue, entries, copy, listScope); + return new Tag<>(index, key, readComparator, entry, defaultValue, pathEntries, copy, listScope); } public @Nullable T read(@NotNull NBTCompoundLike nbt) { @@ -167,7 +174,7 @@ public class Tag { final NBT readable = key.isEmpty() ? nbt.toCompound() : nbt.get(key); final T result; try { - if (readable == null || (result = readFunction.apply(readable)) == null) + if (readable == null || (result = entry.read().apply(readable)) == null) return createDefault(); return result; } catch (ClassCastException e) { @@ -183,7 +190,7 @@ public class Tag { public void write(@NotNull MutableNBTCompound nbtCompound, @Nullable T value) { final String key = this.key; if (value != null) { - final NBT nbt = writeFunction.apply(value); + final NBT nbt = entry.write().apply(value); if (key.isEmpty()) nbtCompound.copyFrom((NBTCompoundLike) nbt); else nbtCompound.set(key, nbt); } else { @@ -198,41 +205,43 @@ public class Tag { } final boolean shareValue(@NotNull Tag other) { - // Verify if these 2 tags can share the same cached value - // Key/Default value/Path are ignored - return this == other || (this.originalRead == other.originalRead && this.listScope == other.listScope); + if (this == other) return true; + // Tags are not strictly the same, compare readers + if (this.listScope != other.listScope) + return false; + return this.readComparator == other.readComparator; } public static @NotNull Tag Byte(@NotNull String key) { - return tag(key, NBTByte::getValue, NBT::Byte); + return tag(key, Serializers.BYTE); } public static @NotNull Tag Short(@NotNull String key) { - return tag(key, NBTShort::getValue, NBT::Short); + return tag(key, Serializers.SHORT); } public static @NotNull Tag Integer(@NotNull String key) { - return tag(key, NBTInt::getValue, NBT::Int); + return tag(key, Serializers.INT); } public static @NotNull Tag Long(@NotNull String key) { - return tag(key, NBTLong::getValue, NBT::Long); + return tag(key, Serializers.LONG); } public static @NotNull Tag Float(@NotNull String key) { - return tag(key, NBTFloat::getValue, NBT::Float); + return tag(key, Serializers.FLOAT); } public static @NotNull Tag Double(@NotNull String key) { - return tag(key, NBTDouble::getValue, NBT::Double); + return tag(key, Serializers.DOUBLE); } public static @NotNull Tag String(@NotNull String key) { - return tag(key, NBTString::getValue, NBT::String); + return tag(key, Serializers.STRING); } public static @NotNull Tag NBT(@NotNull String key) { - return Tag.tag(key, nbt -> nbt, t -> t); + return tag(key, (Serializers.Entry) Serializers.NBT_ENTRY); } /** @@ -244,26 +253,20 @@ public class Tag { * @return the created tag */ public static @NotNull Tag Structure(@NotNull String key, @NotNull TagSerializer serializer) { - return tag(key, - (NBTCompound compound) -> serializer.read(TagHandler.fromCompound(compound)), - (value) -> { - TagHandler handler = TagHandler.newHandler(); - serializer.write(handler, value); - return handler.asCompound(); - }); + return fromSerializer(key, serializer); + } + + @ApiStatus.Experimental + public static @NotNull Tag Structure(@NotNull String key, @NotNull Class type) { + assert type.isRecord(); + return fromSerializer(key, TagRecord.serializer(type)); } public static @NotNull Tag View(@NotNull TagSerializer serializer) { - return tag("", - (NBTCompound compound) -> serializer.read(TagHandler.fromCompound(compound)), - (value) -> { - TagHandler handler = TagHandler.newHandler(); - serializer.write(handler, value); - return handler.asCompound(); - }); + return Structure("", serializer); } public static @NotNull Tag ItemStack(@NotNull String key) { - return tag(key, ItemStack::fromItemNBT, ItemStack::toItemNBT); + return tag(key, Serializers.ITEM); } } diff --git a/src/main/java/net/minestom/server/tag/TagHandlerImpl.java b/src/main/java/net/minestom/server/tag/TagHandlerImpl.java index 330a0000f..8791e74b3 100644 --- a/src/main/java/net/minestom/server/tag/TagHandlerImpl.java +++ b/src/main/java/net/minestom/server/tag/TagHandlerImpl.java @@ -51,7 +51,7 @@ final class TagHandlerImpl implements TagHandler { if (value == null) return; // Empty path, create a new handler local = new TagHandlerImpl(); - entries[pathIndex] = new Entry<>(Tag.tag(path.name(), null, null), local); + entries[pathIndex] = new Entry(Tag.tag(path.name(), Serializers.VOID), local); } else if (entry.value instanceof TagHandlerImpl handler) { // Existing path, continue navigating local = handler; @@ -161,7 +161,7 @@ final class TagHandlerImpl implements TagHandler { NBT updatedNbt() { NBT nbt = this.nbt; - if (nbt == null) this.nbt = nbt = tag.writeFunction.apply(value); + if (nbt == null) this.nbt = nbt = tag.entry.write().apply(value); return nbt; } } @@ -210,7 +210,7 @@ final class TagHandlerImpl implements TagHandler { // Value must be parsed from nbt if the tag is different final NBT nbt = entry.updatedNbt(); try { - return tag.readFunction.apply(nbt); + return tag.entry.read().apply(nbt); } catch (ClassCastException e) { return tag.createDefault(); } diff --git a/src/main/java/net/minestom/server/tag/TagRecord.java b/src/main/java/net/minestom/server/tag/TagRecord.java new file mode 100644 index 000000000..4fbfa9973 --- /dev/null +++ b/src/main/java/net/minestom/server/tag/TagRecord.java @@ -0,0 +1,108 @@ +package net.minestom.server.tag; + +import net.minestom.server.item.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jglrxavpok.hephaistos.nbt.NBT; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.RecordComponent; +import java.util.Arrays; +import java.util.Map; +import java.util.function.Function; + +import static java.util.Map.entry; + +final class TagRecord { + static final Map, Function>> SUPPORTED_TYPES = Map.ofEntries( + entry(Byte.class, Tag::Byte), entry(byte.class, Tag::Byte), + entry(Short.class, Tag::Short), entry(short.class, Tag::Short), + entry(Integer.class, Tag::Integer), entry(int.class, Tag::Integer), + entry(Long.class, Tag::Long), entry(long.class, Tag::Long), + entry(Float.class, Tag::Float), entry(float.class, Tag::Float), + entry(Double.class, Tag::Double), entry(double.class, Tag::Double), + entry(String.class, Tag::String), + + entry(ItemStack.class, Tag::ItemStack)); + + static final ClassValue> serializers = new ClassValue<>() { + @Override + protected Serializer computeValue(Class type) { + assert type.isRecord(); + final RecordComponent[] components = type.getRecordComponents(); + final Entry[] entries = Arrays.stream(components) + .map(recordComponent -> { + final String componentName = recordComponent.getName(); + final Class componentType = recordComponent.getType(); + final Tag tag; + if (componentType.isRecord()) { + tag = Tag.Structure(componentName, serializers.get(componentType)); + } else if (NBT.class.isAssignableFrom(componentType)) { + tag = Tag.NBT(componentName); + } else { + final var fun = SUPPORTED_TYPES.get(componentType); + if (fun == null) + throw new IllegalArgumentException("Unsupported type: " + componentType); + tag = fun.apply(componentName); + } + return new Entry(recordComponent, (Tag) tag); + }).toArray(Entry[]::new); + Constructor constructor; + try { + constructor = type.getDeclaredConstructor(Arrays.stream(components).map(RecordComponent::getType).toArray(Class[]::new)); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + return new Serializer<>(Constructor.class.cast(constructor), entries); + } + }; + + public static @NotNull Serializer serializer(@NotNull Class type) { + //noinspection unchecked + return (Serializer) serializers.get(type); + } + + static final class Serializer implements TagSerializer { + Constructor constructor; + Entry[] entries; + Serializers.Entry serializerEntry; + + Serializer(Constructor constructor, Entry[] entries) { + this.constructor = constructor; + this.entries = entries; + this.serializerEntry = Serializers.fromTagSerializer(this); + } + + @Override + public @Nullable T read(@NotNull TagReadable reader) { + Object[] components = new Object[entries.length]; + for (int i = 0; i < components.length; i++) { + final Entry entry = entries[i]; + Object component = reader.getTag(entry.tag); + if (component == null) return null; + components[i] = component; + } + try { + return constructor.newInstance(components); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + @Override + public void write(@NotNull TagWritable writer, @NotNull T value) { + try { + for (Entry entry : entries) { + final Object component = entry.component.getAccessor().invoke(value); + writer.setTag(entry.tag, component); + } + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + } + + record Entry(RecordComponent component, Tag tag) { + } +} diff --git a/src/main/java/net/minestom/server/tag/TagSerializer.java b/src/main/java/net/minestom/server/tag/TagSerializer.java index 36f0f08c2..5220a717f 100644 --- a/src/main/java/net/minestom/server/tag/TagSerializer.java +++ b/src/main/java/net/minestom/server/tag/TagSerializer.java @@ -22,7 +22,7 @@ public interface TagSerializer { * Writes the custom tag to a {@link TagWritable}. * * @param writer the writer - * @param value the value to serialize, null to remove + * @param value the value to serialize */ - void write(@NotNull TagWritable writer, @Nullable T value); + void write(@NotNull TagWritable writer, @NotNull T value); } diff --git a/src/test/java/net/minestom/server/tag/TagEqualityTest.java b/src/test/java/net/minestom/server/tag/TagEqualityTest.java index e755414c1..116e76ed8 100644 --- a/src/test/java/net/minestom/server/tag/TagEqualityTest.java +++ b/src/test/java/net/minestom/server/tag/TagEqualityTest.java @@ -1,5 +1,6 @@ package net.minestom.server.tag; +import net.minestom.server.coordinate.Vec; import org.junit.jupiter.api.Test; import java.util.function.Function; @@ -96,4 +97,19 @@ public class TagEqualityTest { assertFalse(tag.shareValue(tag2)); assertFalse(tag.list().shareValue(tag2.list())); } + + @Test + public void recordStructure() { + var tag = Tag.Structure("test", Vec.class); + var tag2 = Tag.Structure("test", Vec.class); + assertTrue(tag.shareValue(tag2)); + } + + @Test + public void recordStructureList() { + var tag = Tag.Structure("test", Vec.class).list(); + var tag2 = Tag.Structure("test", Vec.class).list(); + assertTrue(tag.shareValue(tag2)); + assertTrue(tag.list().shareValue(tag2.list())); + } } diff --git a/src/test/java/net/minestom/server/tag/TagPathTest.java b/src/test/java/net/minestom/server/tag/TagPathTest.java index 55bbd95e9..6237fc9e4 100644 --- a/src/test/java/net/minestom/server/tag/TagPathTest.java +++ b/src/test/java/net/minestom/server/tag/TagPathTest.java @@ -223,8 +223,8 @@ public class TagPathTest { } @Override - public void write(@NotNull TagWritable writer, @Nullable Entry value) { - writer.setTag(VALUE_TAG, value != null ? value.value : null); + public void write(@NotNull TagWritable writer, @NotNull Entry value) { + writer.setTag(VALUE_TAG, value.value); } }); diff --git a/src/test/java/net/minestom/server/tag/TagRecordTest.java b/src/test/java/net/minestom/server/tag/TagRecordTest.java new file mode 100644 index 000000000..2a10f4e1c --- /dev/null +++ b/src/test/java/net/minestom/server/tag/TagRecordTest.java @@ -0,0 +1,91 @@ +package net.minestom.server.tag; + +import net.minestom.server.coordinate.Vec; +import net.minestom.server.entity.Entity; +import net.minestom.server.item.ItemStack; +import org.jglrxavpok.hephaistos.nbt.NBT; +import org.jglrxavpok.hephaistos.nbt.NBTCompound; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +import static net.minestom.server.api.TestUtils.assertEqualsSNBT; +import static org.junit.jupiter.api.Assertions.*; + +public class TagRecordTest { + + @Test + public void basic() { + var handler = TagHandler.newHandler(); + var tag = Tag.Structure("vec", Vec.class); + var vec = new Vec(1, 2, 3); + assertNull(handler.getTag(tag)); + handler.setTag(tag, vec); + assertEquals(vec, handler.getTag(tag)); + } + + @Test + public void basicSerializer() { + var handler = TagHandler.newHandler(); + var serializer = TagRecord.serializer(Vec.class); + serializer.write(handler, new Vec(1, 2, 3)); + assertEquals(new Vec(1, 2, 3), serializer.read(handler)); + } + + @Test + public void basicSnbt() { + var handler = TagHandler.newHandler(); + var tag = Tag.Structure("vec", Vec.class); + var vec = new Vec(1, 2, 3); + handler.setTag(tag, vec); + assertEqualsSNBT(""" + { + "vec": { + "x":1D, + "y":2D, + "z":3D + } + } + """, handler.asCompound()); + handler.removeTag(tag); + assertEqualsSNBT("{}", handler.asCompound()); + } + + @Test + public void nbtSerializer() { + record CompoundRecord(NBTCompound compound) { + } + var test = new CompoundRecord(NBT.Compound(Map.of("key", NBT.String("value")))); + var handler = TagHandler.newHandler(); + var serializer = TagRecord.serializer(CompoundRecord.class); + serializer.write(handler, test); + assertEquals(test, serializer.read(handler)); + } + + @Test + public void unsupportedList() { + record Test(List list) { + } + assertThrows(IllegalArgumentException.class, () -> Tag.Structure("test", Test.class)); + } + + @Test + public void unsupportedArray() { + record Test(Object[] array) { + } + assertThrows(IllegalArgumentException.class, () -> Tag.Structure("test", Test.class)); + } + + @Test + public void forceRecord() { + assertThrows(Throwable.class, () -> Tag.Structure("entity", Class.class.cast(Entity.class))); + } + + @Test + public void invalidItem() { + // ItemStack cannot become a record due to `ItemStack#toItemNBT` being serialized differently, and independently of + // the item record components + assertThrows(Throwable.class, () -> Tag.Structure("item", Class.class.cast(ItemStack.class))); + } +} diff --git a/src/test/java/net/minestom/server/tag/TagStructureTest.java b/src/test/java/net/minestom/server/tag/TagStructureTest.java index e874fad9a..6746d83b7 100644 --- a/src/test/java/net/minestom/server/tag/TagStructureTest.java +++ b/src/test/java/net/minestom/server/tag/TagStructureTest.java @@ -19,12 +19,8 @@ public class TagStructureTest { } @Override - public void write(@NotNull TagWritable writer, @Nullable Entry value) { - if (value != null) { - writer.setTag(VALUE_TAG, value.value); - } else { - writer.removeTag(VALUE_TAG); - } + public void write(@NotNull TagWritable writer, @NotNull Entry value) { + writer.setTag(VALUE_TAG, value.value); } }); @@ -38,12 +34,8 @@ public class TagStructureTest { } @Override - public void write(@NotNull TagWritable writer, @Nullable Entry value) { - if (value != null) { - writer.setTag(VALUE_TAG, value.value); - } else { - writer.removeTag(VALUE_TAG); - } + public void write(@NotNull TagWritable writer, @NotNull Entry value) { + writer.setTag(VALUE_TAG, value.value); } }); diff --git a/src/test/java/net/minestom/server/tag/TagViewTest.java b/src/test/java/net/minestom/server/tag/TagViewTest.java index add73233f..bf8558ff8 100644 --- a/src/test/java/net/minestom/server/tag/TagViewTest.java +++ b/src/test/java/net/minestom/server/tag/TagViewTest.java @@ -19,12 +19,8 @@ public class TagViewTest { } @Override - public void write(@NotNull TagWritable writer, @Nullable Entry value) { - if (value != null) { - writer.setTag(VALUE_TAG, value.value); - } else { - writer.removeTag(VALUE_TAG); - } + public void write(@NotNull TagWritable writer, @NotNull Entry value) { + writer.setTag(VALUE_TAG, value.value); } });