Generate Tag from record type (#883)

This commit is contained in:
TheMode 2022-04-07 11:05:11 +02:00 committed by GitHub
parent b6ba6901ed
commit bbd9e58d35
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 341 additions and 97 deletions

View File

@ -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, NBTByte> BYTE = new Entry<>(NBTByte::getValue, NBT::Byte);
static final Entry<Short, NBTShort> SHORT = new Entry<>(NBTShort::getValue, NBT::Short);
static final Entry<Integer, NBTInt> INT = new Entry<>(NBTInt::getValue, NBT::Int);
static final Entry<Long, NBTLong> LONG = new Entry<>(NBTLong::getValue, NBT::Long);
static final Entry<Float, NBTFloat> FLOAT = new Entry<>(NBTFloat::getValue, NBT::Float);
static final Entry<Double, NBTDouble> DOUBLE = new Entry<>(NBTDouble::getValue, NBT::Double);
static final Entry<String, NBTString> STRING = new Entry<>(NBTString::getValue, NBT::String);
static final Entry<NBT, NBT> NBT_ENTRY = new Entry<>(Function.identity(), Function.identity());
static final Entry<ItemStack, NBTCompound> ITEM = new Entry<>(ItemStack::fromItemNBT, ItemStack::toItemNBT);
static <T> Entry<T,?> fromTagSerializer(TagSerializer<T> 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<T, N extends NBT>(Function<N, T> read, Function<T, N> write) {
}
}

View File

@ -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<T> {
final int index;
private final String key;
final Function<NBT, T> readFunction;
final Function<T, NBT> writeFunction;
final Serializers.Entry<T, NBT> entry;
private final Supplier<T> defaultValue;
final Function<?, ?> originalRead;
final Function<?, ?> readComparator;
// Optional properties
final PathEntry[] path;
final UnaryOperator<T> copy;
final int listScope;
Tag(int index, String key,
Function<?, ?> originalRead,
Function<NBT, T> readFunction, Function<T, NBT> writeFunction,
Function<?, ?> readComparator,
Serializers.Entry<T, NBT> entry,
Supplier<T> defaultValue, PathEntry[] path, UnaryOperator<T> 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 <T, N extends NBT> Tag<T> tag(@NotNull String key,
@NotNull Function<N, T> readFunction,
@NotNull Function<T, N> writeFunction) {
return new Tag<>(INDEX_MAP.get(key), key, readFunction,
(Function<NBT, T>) readFunction, (Function<T, NBT>) writeFunction,
static <T, N extends NBT> Tag<T> tag(@NotNull String key, @NotNull Serializers.Entry<T, N> entry) {
return new Tag<>(INDEX_MAP.get(key), key, entry.read(), (Serializers.Entry<T, NBT>) entry,
null, null, null, 0);
}
static <T> Tag<T> fromSerializer(@NotNull String key, @NotNull TagSerializer<T> 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<T> {
@Contract(value = "_ -> new", pure = true)
public Tag<T> defaultValue(@NotNull Supplier<T> 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<T> {
@Contract(value = "_, _ -> new", pure = true)
public <R> Tag<R> map(@NotNull Function<T, R> readMap,
@NotNull Function<R, T> 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<NBT, R> readFunction = entry.read().andThen(t -> {
if (t == null) return null;
return readMap.apply(t);
});
final Function<R, NBT> 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<T> {
@ApiStatus.Experimental
@Contract(value = "-> new", pure = true)
public Tag<List<T>> list() {
return new Tag<>(index, key, originalRead,
var entry = this.entry;
var readFunction = entry.read();
var writeFunction = entry.write();
var listEntry = new Serializers.Entry<List<T>, NBT>(
read -> {
var list = (NBTList<?>) read;
final int size = list.getSize();
@ -131,35 +139,34 @@ public class Tag<T> {
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<List<T>> 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<T> 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<T> {
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<T> {
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<T> {
}
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> Byte(@NotNull String key) {
return tag(key, NBTByte::getValue, NBT::Byte);
return tag(key, Serializers.BYTE);
}
public static @NotNull Tag<Short> Short(@NotNull String key) {
return tag(key, NBTShort::getValue, NBT::Short);
return tag(key, Serializers.SHORT);
}
public static @NotNull Tag<Integer> Integer(@NotNull String key) {
return tag(key, NBTInt::getValue, NBT::Int);
return tag(key, Serializers.INT);
}
public static @NotNull Tag<Long> Long(@NotNull String key) {
return tag(key, NBTLong::getValue, NBT::Long);
return tag(key, Serializers.LONG);
}
public static @NotNull Tag<Float> Float(@NotNull String key) {
return tag(key, NBTFloat::getValue, NBT::Float);
return tag(key, Serializers.FLOAT);
}
public static @NotNull Tag<Double> Double(@NotNull String key) {
return tag(key, NBTDouble::getValue, NBT::Double);
return tag(key, Serializers.DOUBLE);
}
public static @NotNull Tag<String> String(@NotNull String key) {
return tag(key, NBTString::getValue, NBT::String);
return tag(key, Serializers.STRING);
}
public static <T extends NBT> @NotNull Tag<T> NBT(@NotNull String key) {
return Tag.<T, T>tag(key, nbt -> nbt, t -> t);
return tag(key, (Serializers.Entry<T, ? extends NBT>) Serializers.NBT_ENTRY);
}
/**
@ -244,26 +253,20 @@ public class Tag<T> {
* @return the created tag
*/
public static <T> @NotNull Tag<T> Structure(@NotNull String key, @NotNull TagSerializer<T> 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 <T extends Record> @NotNull Tag<T> Structure(@NotNull String key, @NotNull Class<T> type) {
assert type.isRecord();
return fromSerializer(key, TagRecord.serializer(type));
}
public static <T> @NotNull Tag<T> View(@NotNull TagSerializer<T> 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> ItemStack(@NotNull String key) {
return tag(key, ItemStack::fromItemNBT, ItemStack::toItemNBT);
return tag(key, Serializers.ITEM);
}
}

View File

@ -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();
}

View File

@ -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<Class<?>, Function<String, Tag<?>>> 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<Serializer<? extends Record>> serializers = new ClassValue<>() {
@Override
protected Serializer<? extends Record> 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<Object>) 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 <T extends Record> @NotNull Serializer<T> serializer(@NotNull Class<T> type) {
//noinspection unchecked
return (Serializer<T>) serializers.get(type);
}
static final class Serializer<T extends Record> implements TagSerializer<T> {
Constructor<T> constructor;
Entry[] entries;
Serializers.Entry<T, ?> serializerEntry;
Serializer(Constructor<T> 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<Object> tag) {
}
}

View File

@ -22,7 +22,7 @@ public interface TagSerializer<T> {
* 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);
}

View File

@ -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()));
}
}

View File

@ -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);
}
});

View File

@ -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<Object> 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)));
}
}

View File

@ -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);
}
});

View File

@ -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);
}
});