Tag nbt conversion (#901)

This commit is contained in:
TheMode 2022-04-10 10:01:39 +02:00 committed by GitHub
parent dd26d4ceb8
commit af43c977bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 553 additions and 183 deletions

View File

@ -10,7 +10,9 @@ import net.minestom.server.utils.NBTUtils;
import net.minestom.server.utils.validate.Check;
import org.jetbrains.annotations.*;
import org.jglrxavpok.hephaistos.nbt.NBT;
import org.jglrxavpok.hephaistos.nbt.NBTByte;
import org.jglrxavpok.hephaistos.nbt.NBTCompound;
import org.jglrxavpok.hephaistos.nbt.NBTString;
import java.util.List;
import java.util.Map;
@ -85,9 +87,9 @@ public final class ItemStack implements TagReadable, HoverEventSource<HoverEvent
Check.notNull(material, "Unknown material: {0}", id);
Byte amount = nbtCompound.getByte("Count");
return fromNBT(material,
nbtCompound.getCompound("tag"),
amount == null ? 1 : amount);
if (amount == null) amount = 1;
final NBTCompound tag = nbtCompound.getCompound("tag");
return tag != null ? fromNBT(material, tag, amount) : of(material, amount);
}
@Contract(pure = true)
@ -231,10 +233,11 @@ public final class ItemStack implements TagReadable, HoverEventSource<HoverEvent
*/
@ApiStatus.Experimental
public @NotNull NBTCompound toItemNBT() {
return NBT.Compound(Map.of(
"id", NBT.String(getMaterial().name()),
"Count", NBT.Byte(getAmount()),
"tag", getMeta().toNBT()));
final NBTString material = NBT.String(getMaterial().name());
final NBTByte amount = NBT.Byte(getAmount());
final NBTCompound nbt = getMeta().toNBT();
if (nbt.isEmpty()) return NBT.Compound(Map.of("id", material, "Count", amount));
return NBT.Compound(Map.of("id", material, "Count", amount, "tag", nbt));
}
@Contract(value = "-> new", pure = true)

View File

@ -5,8 +5,11 @@ import org.jglrxavpok.hephaistos.nbt.*;
import java.util.function.Function;
/**
* Basic serializers for {@link Tag tags}.
*/
final class Serializers {
static final Entry<?, ?> VOID = new Entry<>(null, null);
static final Entry<TagHandlerImpl, NBTCompound> PATH = new Entry<>(TagHandlerImpl::fromCompound, TagHandlerImpl::asCompound);
static final Entry<Byte, NBTByte> BYTE = new Entry<>(NBTByte::getValue, NBT::Byte);
static final Entry<Short, NBTShort> SHORT = new Entry<>(NBTShort::getValue, NBT::Short);
@ -19,7 +22,7 @@ final class Serializers {
static final Entry<ItemStack, NBTCompound> ITEM = new Entry<>(ItemStack::fromItemNBT, ItemStack::toItemNBT);
static <T> Entry<T,?> fromTagSerializer(TagSerializer<T> serializer) {
static <T> Entry<T, ?> fromTagSerializer(TagSerializer<T> serializer) {
return new Serializers.Entry<>(
(NBTCompound compound) -> {
if (compound.isEmpty()) return null;

View File

@ -14,6 +14,7 @@ import org.jglrxavpok.hephaistos.nbt.mutable.MutableNBTCompound;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
@ -93,6 +94,8 @@ public class Tag<T> {
@Contract(value = "_, _ -> new", pure = true)
public <R> Tag<R> map(@NotNull Function<T, R> readMap,
@NotNull Function<R, T> writeMap) {
if (entry == Serializers.NBT_ENTRY)
throw new IllegalArgumentException("Cannot create a list from a NBT entry");
var entry = this.entry;
final Function<NBT, R> readFunction = entry.read().andThen(t -> {
if (t == null) return null;
@ -109,6 +112,8 @@ public class Tag<T> {
@ApiStatus.Experimental
@Contract(value = "-> new", pure = true)
public Tag<List<T>> list() {
if (entry == Serializers.NBT_ENTRY)
throw new IllegalArgumentException("Cannot create a list from a NBT entry");
var entry = this.entry;
var readFunction = entry.read();
var writeFunction = entry.write();
@ -219,6 +224,24 @@ public class Tag<T> {
return supplier != null ? supplier.get() : null;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Tag<?> tag)) return false;
return index == tag.index &&
listScope == tag.listScope &&
readComparator.equals(tag.readComparator) &&
Objects.equals(defaultValue, tag.defaultValue) &&
Arrays.equals(path, tag.path) && Objects.equals(copy, tag.copy);
}
@Override
public int hashCode() {
int result = Objects.hash(index, readComparator, defaultValue, copy, listScope);
result = 31 * result + Arrays.hashCode(path);
return result;
}
public static @NotNull Tag<Byte> Byte(@NotNull String key) {
return tag(key, Serializers.BYTE);
}

View File

@ -16,6 +16,16 @@ final class TagHandlerImpl implements TagHandler {
private volatile Entry<?>[] entries = new Entry[0];
private Cache cache;
static TagHandlerImpl fromCompound(NBTCompoundLike compound) {
TagHandlerImpl handler = new TagHandlerImpl();
for (var entry : compound) {
final Tag<NBT> tag = Tag.NBT(entry.getKey());
final NBT nbt = entry.getValue();
handler.setTag(tag, nbt);
}
return handler;
}
@Override
public <T> @UnknownNullability T getTag(@NotNull Tag<T> tag) {
if (tag.isView()) return tag.read(asCompound());
@ -29,11 +39,19 @@ final class TagHandlerImpl implements TagHandler {
tag.writeUnsafe(viewCompound, value);
updateContent(viewCompound);
} else {
if (value != null) {
final UnaryOperator<T> copy = tag.copy;
if (copy != null) value = copy.apply(value);
if (value instanceof NBT nbt) {
synchronized (this) {
write(tag, null);
TagNbtSeparator.separate(tag.getKey(), nbt,
entry -> write(entry.tag(), entry.value()));
}
} else {
if (value != null) {
final UnaryOperator<T> copy = tag.copy;
if (copy != null) value = copy.apply(value);
}
write(tag, value);
}
write(tag, value);
}
}
@ -57,7 +75,6 @@ final class TagHandlerImpl implements TagHandler {
}
public synchronized <T> void write(@NotNull Tag<T> tag, @Nullable T value) {
// Normal write
int tagIndex = tag.index;
TagHandlerImpl local = this;
Entry<?>[] entries = this.entries;
@ -80,12 +97,17 @@ 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(), Serializers.VOID), local);
} else if (entry.value instanceof TagHandlerImpl handler) {
entries[pathIndex] = new PathEntry(path.name(), local);
} else if (entry instanceof PathEntry pathEntry) {
// Existing path, continue navigating
local = handler;
local = pathEntry.value;
} else {
throw new IllegalStateException("Cannot set a path-able tag on a non-path-able entry: " + entry.value.getClass());
// Probably is a Structure entry,
// convert it to nbt to allow mutation (and drop the cached object)
if (value == null) return;
final NBT nbt = entry.updatedNbt();
local = nbt instanceof NBTCompound compound ? fromCompound(compound) : new TagHandlerImpl();
entries[pathIndex] = new PathEntry(path.name(), local);
}
entries = local.entries;
pathHandlers[i] = local;
@ -117,7 +139,7 @@ final class TagHandlerImpl implements TagHandler {
if (value == null) return;
local.entries = entries = Arrays.copyOf(entries, tagIndex + 1);
}
entries[tagIndex] = value != null ? new Entry<>(tag, value) : null;
entries[tagIndex] = value != null ? new TagEntry<>(tag, value) : null;
this.cache = null;
if (pathHandlers != null) {
for (var handler : pathHandlers) handler.cache = null;
@ -127,44 +149,58 @@ final class TagHandlerImpl implements TagHandler {
private synchronized Cache updatedCache() {
Cache cache = this.cache;
if (cache == null) {
Entry<?>[] entries = this.entries;
final Entry<?>[] entries = this.entries;
if (entries.length > 0) {
entries = entries.clone();
MutableNBTCompound tmp = new MutableNBTCompound();
for (Entry<?> entry : entries) {
if (entry == null) continue;
final Tag<?> tag = entry.tag;
final Object value = entry.value;
if (value instanceof TagHandler handler) {
// Path-able entry
tmp.put(tag.getKey(), handler.asCompound());
} else {
tag.writeUnsafe(tmp, value);
}
if (entry != null) tmp.put(entry.tag().getKey(), entry.updatedNbt());
}
cache = !tmp.isEmpty() ? new Cache(entries, tmp.toCompound()) : Cache.EMPTY;
} else {
cache = Cache.EMPTY;
}
cache = !tmp.isEmpty() ? new Cache(entries.clone(), tmp.toCompound()) : Cache.EMPTY;
} else cache = Cache.EMPTY;
this.cache = cache;
}
return cache;
}
private static final class Entry<T> {
final Tag<T> tag;
final T value; // TagHandler type for path-able tags
volatile NBT nbt;
Entry(Tag<T> tag, T value) {
this.tag = tag;
this.value = value;
private static <T> T read(Entry<?>[] entries, Tag<T> tag) {
Entry<?> entry;
final var paths = tag.path;
if (paths != null) {
// Must be a path-able entry
for (var path : paths) {
final int pathIndex = path.index();
if (pathIndex >= entries.length || (entry = entries[pathIndex]) == null) {
return tag.createDefault();
}
if (entry instanceof PathEntry pathEntry) {
entries = pathEntry.value.entries;
} else if (entry.updatedNbt() instanceof NBTCompound compound) {
// Slow path forcing a conversion of the structure to NBTCompound
// TODO should the handler be cached inside the entry?
TagHandlerImpl tmp = fromCompound(compound);
entries = tmp.entries;
} else {
// Entry is not path-able
return tag.createDefault();
}
}
}
NBT updatedNbt() {
NBT nbt = this.nbt;
if (nbt == null) this.nbt = nbt = tag.entry.write().apply(value);
return nbt;
final int index = tag.index;
if (index >= entries.length || (entry = entries[index]) == null) {
return tag.createDefault();
}
if (entry.tag().shareValue(tag)) {
// The tag used to write the entry is compatible with the one used to get
// return the value directly
//noinspection unchecked
return (T) entry.value();
}
// Value must be parsed from nbt if the tag is different
final NBT nbt = entry.updatedNbt();
try {
return tag.entry.read().apply(nbt);
} catch (ClassCastException e) {
return tag.createDefault();
}
}
@ -178,53 +214,51 @@ final class TagHandlerImpl implements TagHandler {
}
}
private static <T> T read(Entry<?>[] entries, Tag<T> tag) {
final int index = tag.index;
Entry<?> entry;
final var paths = tag.path;
if (paths != null) {
// Must be a path-able entry
for (var path : paths) {
final int pathIndex = path.index();
if (pathIndex >= entries.length || (entry = entries[pathIndex]) == null) {
return tag.createDefault();
}
if (entry.value instanceof TagHandlerImpl handler) {
entries = handler.entries;
} else if (entry.updatedNbt() instanceof NBTCompound compound) {
TagHandlerImpl tmp = fromCompound(compound);
entries = tmp.entries;
}
}
private sealed interface Entry<T> permits TagEntry, PathEntry {
Tag<T> tag();
T value();
NBT updatedNbt();
}
private static final class TagEntry<T> implements Entry<T> {
final Tag<T> tag;
final T value;
volatile NBT nbt;
TagEntry(Tag<T> tag, T value) {
this.tag = tag;
this.value = value;
}
if (index >= entries.length || (entry = entries[index]) == null) {
return tag.createDefault();
@Override
public Tag<T> tag() {
return tag;
}
final Object value = entry.value;
if (value instanceof TagHandlerImpl)
throw new IllegalStateException("Cannot read path-able tag " + tag.getKey());
final Tag entryTag = entry.tag;
if (entryTag.shareValue(tag)) {
// Tag is the same, return the value
//noinspection unchecked
return (T) value;
@Override
public T value() {
return value;
}
// Value must be parsed from nbt if the tag is different
final NBT nbt = entry.updatedNbt();
try {
return tag.entry.read().apply(nbt);
} catch (ClassCastException e) {
return tag.createDefault();
@Override
public NBT updatedNbt() {
NBT nbt = this.nbt;
if (nbt == null) this.nbt = nbt = tag.entry.write().apply(value);
return nbt;
}
}
static TagHandlerImpl fromCompound(NBTCompoundLike compound) {
TagHandlerImpl handler = new TagHandlerImpl();
for (var entry : compound) {
final Tag<NBT> tag = Tag.NBT(entry.getKey());
final NBT nbt = entry.getValue();
handler.setTag(tag, nbt);
private record PathEntry(Tag<TagHandlerImpl> tag,
TagHandlerImpl value) implements Entry<TagHandlerImpl> {
PathEntry(String key, TagHandlerImpl value) {
this(Tag.tag(key, Serializers.PATH), value);
}
@Override
public NBT updatedNbt() {
return value.asCompound();
}
return handler;
}
}

View File

@ -0,0 +1,101 @@
package net.minestom.server.tag;
import org.jglrxavpok.hephaistos.nbt.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
import static java.util.Map.entry;
/**
* Handles conversion of {@link NBT} subtypes into one or multiple primitive {@link Tag tags}.
*/
final class TagNbtSeparator {
static final Map<NBTType<?>, Function<String, Tag<?>>> SUPPORTED_TYPES = Map.ofEntries(
entry(NBTType.TAG_Byte, Tag::Byte),
entry(NBTType.TAG_Short, Tag::Short),
entry(NBTType.TAG_Int, Tag::Integer),
entry(NBTType.TAG_Long, Tag::Long),
entry(NBTType.TAG_Float, Tag::Float),
entry(NBTType.TAG_Double, Tag::Double),
entry(NBTType.TAG_String, Tag::String));
static void separate(String key, NBT nbt, Consumer<Entry> consumer) {
convert(new ArrayList<>(), key, nbt, consumer);
}
private static void convert(List<String> path, String key, NBT nbt, Consumer<Entry> consumer) {
if (nbt instanceof NBTByte nbtByte) {
consumer.accept(makeEntry(path, Tag.Byte(key), nbtByte.getValue()));
} else if (nbt instanceof NBTShort nbtShort) {
consumer.accept(makeEntry(path, Tag.Short(key), nbtShort.getValue()));
} else if (nbt instanceof NBTInt nbtInt) {
consumer.accept(makeEntry(path, Tag.Integer(key), nbtInt.getValue()));
} else if (nbt instanceof NBTLong nbtLong) {
consumer.accept(makeEntry(path, Tag.Long(key), nbtLong.getValue()));
} else if (nbt instanceof NBTFloat nbtFloat) {
consumer.accept(makeEntry(path, Tag.Float(key), nbtFloat.getValue()));
} else if (nbt instanceof NBTDouble nbtDouble) {
consumer.accept(makeEntry(path, Tag.Double(key), nbtDouble.getValue()));
} else if (nbt instanceof NBTString nbtString) {
consumer.accept(makeEntry(path, Tag.String(key), nbtString.getValue()));
} else if (nbt instanceof NBTCompound nbtCompound) {
for (var ent : nbtCompound) {
var newPath = new ArrayList<>(path);
newPath.add(key);
convert(newPath, ent.getKey(), ent.getValue(), consumer);
}
} else if (nbt instanceof NBTList<?> nbtList) {
var tagFunction = SUPPORTED_TYPES.get(nbtList.getSubtagType());
if (tagFunction == null) {
// Invalid list subtype, fallback to nbt
consumer.accept(makeEntry(path, Tag.NBT(key), nbt));
} else {
try {
var tag = tagFunction.apply(key).list();
Object[] values = new Object[nbtList.getSize()];
for (int i = 0; i < values.length; i++) {
values[i] = nbtValue(nbtList.get(i));
}
consumer.accept(makeEntry(path, Tag.class.cast(tag), List.of(values)));
} catch (Exception e) {
e.printStackTrace();
consumer.accept(makeEntry(path, Tag.NBT(key), nbt));
}
}
} else {
// TODO array support
consumer.accept(makeEntry(path, Tag.NBT(key), nbt));
}
}
private static <T> Entry<?> makeEntry(List<String> path, Tag<T> tag, T value) {
return new Entry<>(tag.path(path.toArray(String[]::new)), value);
}
record Entry<T>(Tag<T> tag, T value) {
}
private static Object nbtValue(NBT nbt) throws IllegalArgumentException {
if (nbt instanceof NBTByte nbtByte) {
return nbtByte.getValue();
} else if (nbt instanceof NBTShort nbtShort) {
return nbtShort.getValue();
} else if (nbt instanceof NBTInt nbtInt) {
return nbtInt.getValue();
} else if (nbt instanceof NBTLong nbtLong) {
return nbtLong.getValue();
} else if (nbt instanceof NBTFloat nbtFloat) {
return nbtFloat.getValue();
} else if (nbt instanceof NBTDouble nbtDouble) {
return nbtDouble.getValue();
} else if (nbt instanceof NBTString nbtString) {
return nbtString.getValue();
} else {
throw new IllegalArgumentException("Unsupported NBT type: " + nbt.getClass().getName());
}
}
}

View File

@ -1,115 +1,67 @@
package net.minestom.server.tag;
import net.minestom.server.coordinate.Vec;
import org.junit.jupiter.api.Test;
import java.util.function.Function;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Test tags that can share cached values.
*/
public class TagEqualityTest {
record Entry(int value) {
@Test
public void sameType() {
var tag1 = Tag.Integer("key");
var tag2 = Tag.Integer("key");
assertEquals(tag1, tag1);
assertEquals(tag2, tag2);
assertEquals(tag1, tag2);
}
@Test
public void same() {
var tag = Tag.String("test");
assertTrue(tag.shareValue(tag));
public void differentKey() {
var tag1 = Tag.Integer("key1");
var tag2 = Tag.Integer("key2");
assertNotEquals(tag1, tag2);
}
@Test
public void similar() {
var tag = Tag.String("test");
var tag2 = Tag.String("test");
assertTrue(tag.shareValue(tag2));
}
@Test
public void differentDefault() {
var tag = Tag.String("test").defaultValue("test2");
var tag2 = Tag.String("test").defaultValue("test3");
assertTrue(tag.shareValue(tag2));
}
@Test
public void differentType() {
var tag = Tag.String("test");
var tag2 = Tag.Integer("test");
assertFalse(tag.shareValue(tag2));
}
@Test
public void mapSame() {
// Force identical functions
Function<Integer, Entry> t1 = Entry::new;
Function<Entry, Integer> t2 = Entry::value;
var tag = Tag.Integer("key");
var map1 = tag.map(t1, t2);
var map2 = tag.map(t1, t2);
assertTrue(map1.shareValue(map2));
}
@Test
public void mapChild() {
var intTag = Tag.Integer("key");
var tag = intTag.map(Entry::new, Entry::value);
assertFalse(intTag.shareValue(tag));
}
@Test
public void list() {
var tag = Tag.String("test").list();
assertTrue(tag.shareValue(tag));
}
@Test
public void listScope() {
var tag = Tag.String("test");
assertFalse(tag.shareValue(tag.list()));
}
@Test
public void similarList() {
var tag = Tag.String("test").list();
var tag2 = Tag.String("test").list();
assertTrue(tag.shareValue(tag2));
assertTrue(tag.list().shareValue(tag2.list()));
public void sameList() {
var tag1 = Tag.Integer("key").list();
var tag2 = Tag.Integer("key").list();
assertEquals(tag1, tag2);
}
@Test
public void differentList() {
var tag = Tag.String("test").list();
var tag2 = Tag.String("test").list();
assertFalse(tag.shareValue(tag2.list()));
assertFalse(tag.list().shareValue(tag2.list().list()));
var tag1 = Tag.Integer("key").list();
var tag2 = Tag.Integer("key");
assertNotEquals(tag1, tag2);
}
@Test
public void differentListType() {
var tag = Tag.String("test").list();
var tag2 = Tag.Integer("test").list();
assertFalse(tag.shareValue(tag2));
assertFalse(tag.list().shareValue(tag2.list()));
public void unmatchedList() {
var tag1 = Tag.Integer("key").list().list();
var tag2 = Tag.Integer("key").list();
assertNotEquals(tag1, tag2);
}
@Test
public void recordStructure() {
var tag = Tag.Structure("test", Vec.class);
var tag2 = Tag.Structure("test", Vec.class);
assertTrue(tag.shareValue(tag2));
public void samePath() {
var tag1 = Tag.Integer("key").path("path");
var tag2 = Tag.Integer("key").path("path");
assertEquals(tag1, 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()));
public void differentPath() {
var tag1 = Tag.Integer("key").path("path");
var tag2 = Tag.Integer("key").path("path2");
assertNotEquals(tag1, tag2);
}
@Test
public void unmatchedPath() {
var tag1 = Tag.Integer("key").path("path", "path2");
var tag2 = Tag.Integer("key").path("path");
assertNotEquals(tag1, tag2);
}
}

View File

@ -102,8 +102,7 @@ public class TagItemTest {
{
"item": {
"id":"minecraft:diamond",
"Count":1B,
"tag":{}
"Count":1B
}
}
""", handler.asCompound());

View File

@ -0,0 +1,58 @@
package net.minestom.server.tag;
import org.jglrxavpok.hephaistos.nbt.NBT;
import org.jglrxavpok.hephaistos.nbt.NBTType;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class TagNbtSeparatorTest {
@Test
public void primitives() {
assertSeparation(new TagNbtSeparator.Entry<>(Tag.Byte("key"), (byte) 1),
"key", NBT.Byte(1));
assertSeparation(new TagNbtSeparator.Entry<>(Tag.Short("key"), (short) 1),
"key", NBT.Short(1));
assertSeparation(new TagNbtSeparator.Entry<>(Tag.Integer("key"), 1),
"key", NBT.Int(1));
assertSeparation(new TagNbtSeparator.Entry<>(Tag.Long("key"), 1L),
"key", NBT.Long(1));
assertSeparation(new TagNbtSeparator.Entry<>(Tag.Float("key"), 1f),
"key", NBT.Float(1));
assertSeparation(new TagNbtSeparator.Entry<>(Tag.Double("key"), 1d),
"key", NBT.Double(1));
}
@Test
public void compound() {
assertSeparation(new TagNbtSeparator.Entry<>(Tag.Byte("key").path("path"), (byte) 1),
"path", NBT.Compound(Map.of("key", NBT.Byte(1))));
}
@Test
public void list() {
assertSeparation(new TagNbtSeparator.Entry<>(Tag.Integer("key").list(), List.of(1)),
"key", NBT.List(NBTType.TAG_Int, NBT.Int(1)));
}
void assertSeparation(List<TagNbtSeparator.Entry<?>> expected, String key, NBT nbt) {
assertEquals(expected, retrieve(key, nbt));
}
void assertSeparation(TagNbtSeparator.Entry<?> expected, String key, NBT nbt) {
var entries = retrieve(key, nbt);
assertEquals(1, entries.size());
assertEquals(expected, entries.get(0));
}
List<TagNbtSeparator.Entry<?>> retrieve(String key, NBT nbt) {
List<TagNbtSeparator.Entry<?>> entries = new ArrayList<>();
TagNbtSeparator.separate(key, nbt, entries::add);
return entries;
}
}

View File

@ -1,17 +1,29 @@
package net.minestom.server.tag;
import org.jglrxavpok.hephaistos.nbt.NBT;
import org.jglrxavpok.hephaistos.nbt.NBTType;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.*;
/**
* Ensure that NBT tag can be read from other tags properly.
*/
public class TagNbtTest {
@Test
public void invalidListTag() {
assertThrows(IllegalArgumentException.class, () -> Tag.NBT("nbt").list());
}
@Test
public void invalidMapTag() {
assertThrows(IllegalArgumentException.class, () -> Tag.NBT("nbt").map(nbt -> 1, NBT::Int));
}
@Test
public void compoundRead() {
var handler = TagHandler.newHandler();
@ -37,4 +49,50 @@ public class TagNbtTest {
var path = Tag.Integer("key").path("path1", "path2");
assertEquals(5, handler.getTag(path));
}
@Test
public void compoundWrite() {
var handler = TagHandler.newHandler();
var nbtTag = Tag.NBT("path1");
var nbt = NBT.Compound(Map.of("key", NBT.Int(5)));
handler.setTag(nbtTag, nbt);
assertEquals(nbt, handler.getTag(nbtTag));
var path = Tag.Integer("key").path("path1");
handler.setTag(path, 10);
assertEquals(10, handler.getTag(path));
assertEquals(NBT.Compound(Map.of("key", NBT.Int(10))), handler.getTag(nbtTag));
}
@Test
public void rawList() {
var handler = TagHandler.newHandler();
var nbtTag = Tag.NBT("list");
var list = NBT.List(NBTType.TAG_Int, NBT.Int(1));
handler.setTag(nbtTag, list);
assertEquals(list, handler.getTag(nbtTag));
}
@Test
public void listConversion() {
var handler = TagHandler.newHandler();
var nbtTag = Tag.NBT("list");
var listTag = Tag.Integer("list").list();
var list = NBT.List(NBTType.TAG_Int, NBT.Int(1));
handler.setTag(nbtTag, list);
assertEquals(list, handler.getTag(nbtTag));
assertNotSame(list, handler.getTag(nbtTag));
assertEquals(List.of(1), handler.getTag(listTag));
}
@Test
public void rawArray() {
var handler = TagHandler.newHandler();
var nbtTag = Tag.NBT("array");
var array = NBT.IntArray(1, 2, 3);
handler.setTag(nbtTag, array);
assertEquals(array, handler.getTag(nbtTag));
}
}

View File

@ -133,7 +133,14 @@ public class TagPathTest {
}
""", handler.asCompound());
assertThrows(IllegalStateException.class, () -> handler.setTag(tag1, 5));
handler.setTag(tag1, 2);
assertEqualsSNBT("""
{
"key": {
"value":2
}
}
""", handler.asCompound());
}
@Test
@ -142,7 +149,7 @@ public class TagPathTest {
var tag = Tag.Integer("key");
var path = Tag.Integer("value").path("key");
handler.setTag(path, 5);
assertThrows(IllegalStateException.class, () -> handler.getTag(tag));
assertNull(handler.getTag(tag));
}
@Test
@ -254,8 +261,16 @@ public class TagPathTest {
}
""", handler.asCompound());
// Cannot enter a structure from a path tag
assertThrows(IllegalStateException.class, () -> handler.setTag(tag.path("struct"), 5));
handler.setTag(tag.path("struct"), 2);
assertEqualsSNBT("""
{
value:5,
"struct": {
"value":2
}
}
""", handler.asCompound());
assertEquals(new Entry(2), handler.getTag(struct));
}
@Test
@ -269,6 +284,15 @@ public class TagPathTest {
"key":5
}
""", handler.asCompound());
assertThrows(IllegalStateException.class, () -> handler.setTag(path, 5));
handler.setTag(path, 2);
assertEqualsSNBT("""
{
"key": {
"second": {
"value":2
}
}
}
""", handler.asCompound());
}
}

View File

@ -0,0 +1,115 @@
package net.minestom.server.tag;
import net.minestom.server.coordinate.Vec;
import org.junit.jupiter.api.Test;
import java.util.function.Function;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Test tags that can share cached values.
*/
public class TagValueShareTest {
record Entry(int value) {
}
@Test
public void same() {
var tag = Tag.String("test");
assertTrue(tag.shareValue(tag));
}
@Test
public void similar() {
var tag = Tag.String("test");
var tag2 = Tag.String("test");
assertTrue(tag.shareValue(tag2));
}
@Test
public void differentDefault() {
var tag = Tag.String("test").defaultValue("test2");
var tag2 = Tag.String("test").defaultValue("test3");
assertTrue(tag.shareValue(tag2));
}
@Test
public void differentType() {
var tag = Tag.String("test");
var tag2 = Tag.Integer("test");
assertFalse(tag.shareValue(tag2));
}
@Test
public void mapSame() {
// Force identical functions
Function<Integer, Entry> t1 = Entry::new;
Function<Entry, Integer> t2 = Entry::value;
var tag = Tag.Integer("key");
var map1 = tag.map(t1, t2);
var map2 = tag.map(t1, t2);
assertTrue(map1.shareValue(map2));
}
@Test
public void mapChild() {
var intTag = Tag.Integer("key");
var tag = intTag.map(Entry::new, Entry::value);
assertFalse(intTag.shareValue(tag));
}
@Test
public void list() {
var tag = Tag.String("test").list();
assertTrue(tag.shareValue(tag));
}
@Test
public void listScope() {
var tag = Tag.String("test");
assertFalse(tag.shareValue(tag.list()));
}
@Test
public void similarList() {
var tag = Tag.String("test").list();
var tag2 = Tag.String("test").list();
assertTrue(tag.shareValue(tag2));
assertTrue(tag.list().shareValue(tag2.list()));
}
@Test
public void differentList() {
var tag = Tag.String("test").list();
var tag2 = Tag.String("test").list();
assertFalse(tag.shareValue(tag2.list()));
assertFalse(tag.list().shareValue(tag2.list().list()));
}
@Test
public void differentListType() {
var tag = Tag.String("test").list();
var tag2 = Tag.Integer("test").list();
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()));
}
}