mirror of
https://github.com/Minestom/Minestom.git
synced 2024-12-28 20:18:10 +01:00
Improve tag performance + concurrency tests (#1165)
This commit is contained in:
parent
1d469ca6a6
commit
6ecede145e
@ -0,0 +1,50 @@
|
|||||||
|
package net.minestom.server.tag;
|
||||||
|
|
||||||
|
import org.openjdk.jcstress.annotations.*;
|
||||||
|
import org.openjdk.jcstress.infra.results.LL_Result;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.openjdk.jcstress.annotations.Expect.ACCEPTABLE;
|
||||||
|
|
||||||
|
@JCStressTest
|
||||||
|
@Outcome(id = "1, 198", expect = ACCEPTABLE)
|
||||||
|
@Outcome(id = "1, 99", expect = ACCEPTABLE)
|
||||||
|
@Outcome(id = "2, 198", expect = ACCEPTABLE)
|
||||||
|
@Outcome(id = "2, 99", expect = ACCEPTABLE)
|
||||||
|
@State
|
||||||
|
public class TagPathRehashTest {
|
||||||
|
private static final int MAX_SIZE = 500;
|
||||||
|
private static final List<Tag<Integer>> TAGS;
|
||||||
|
|
||||||
|
static {
|
||||||
|
List<Tag<Integer>> tags = new ArrayList<>();
|
||||||
|
for (int i = 0; i < MAX_SIZE; i++) {
|
||||||
|
tags.add(Tag.Integer("key" + i).path("path"));
|
||||||
|
}
|
||||||
|
TAGS = List.copyOf(tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final TagHandler handler = TagHandler.newHandler();
|
||||||
|
|
||||||
|
@Actor
|
||||||
|
public void actor1() {
|
||||||
|
for (int i = 0; i < MAX_SIZE; i++) {
|
||||||
|
handler.setTag(TAGS.get(i), i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Actor
|
||||||
|
public void actor2() {
|
||||||
|
for (int i = 0; i < MAX_SIZE; i++) {
|
||||||
|
handler.setTag(TAGS.get(i), i * 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Arbiter
|
||||||
|
public void arbiter(LL_Result r) {
|
||||||
|
r.r1 = handler.getTag(TAGS.get(1));
|
||||||
|
r.r2 = handler.getTag(TAGS.get(99));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
package net.minestom.server.tag;
|
||||||
|
|
||||||
|
import org.openjdk.jcstress.annotations.*;
|
||||||
|
import org.openjdk.jcstress.infra.results.LL_Result;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.openjdk.jcstress.annotations.Expect.ACCEPTABLE;
|
||||||
|
|
||||||
|
@JCStressTest
|
||||||
|
@Outcome(id = "1, 198", expect = ACCEPTABLE)
|
||||||
|
@Outcome(id = "1, 99", expect = ACCEPTABLE)
|
||||||
|
@Outcome(id = "2, 198", expect = ACCEPTABLE)
|
||||||
|
@Outcome(id = "2, 99", expect = ACCEPTABLE)
|
||||||
|
@State
|
||||||
|
public class TagRehashTest {
|
||||||
|
private static final int MAX_SIZE = 500;
|
||||||
|
private static final List<Tag<Integer>> TAGS;
|
||||||
|
|
||||||
|
static {
|
||||||
|
List<Tag<Integer>> tags = new ArrayList<>();
|
||||||
|
for (int i = 0; i < MAX_SIZE; i++) {
|
||||||
|
tags.add(Tag.Integer("key" + i));
|
||||||
|
}
|
||||||
|
TAGS = List.copyOf(tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final TagHandler handler = TagHandler.newHandler();
|
||||||
|
|
||||||
|
@Actor
|
||||||
|
public void actor1() {
|
||||||
|
for (int i = 0; i < MAX_SIZE; i++) {
|
||||||
|
handler.setTag(TAGS.get(i), i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Actor
|
||||||
|
public void actor2() {
|
||||||
|
for (int i = 0; i < MAX_SIZE; i++) {
|
||||||
|
handler.setTag(TAGS.get(i), i * 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Arbiter
|
||||||
|
public void arbiter(LL_Result r) {
|
||||||
|
r.r1 = handler.getTag(TAGS.get(1));
|
||||||
|
r.r2 = handler.getTag(TAGS.get(99));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,57 @@
|
|||||||
|
package net.minestom.server.tag;
|
||||||
|
|
||||||
|
import org.openjdk.jcstress.annotations.*;
|
||||||
|
import org.openjdk.jcstress.infra.results.L_Result;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.openjdk.jcstress.annotations.Expect.ACCEPTABLE;
|
||||||
|
|
||||||
|
@JCStressTest
|
||||||
|
@Outcome(id = "2000", expect = ACCEPTABLE)
|
||||||
|
@State
|
||||||
|
public class TagUpdatePathRehashTest {
|
||||||
|
private static final Tag<Integer> TAG = Tag.Integer("key").path("path").defaultValue(0);
|
||||||
|
|
||||||
|
private static final int MAX_SIZE = 500;
|
||||||
|
private static final List<Tag<Integer>> TAGS;
|
||||||
|
|
||||||
|
static {
|
||||||
|
List<Tag<Integer>> tags = new ArrayList<>();
|
||||||
|
for (int i = 0; i < MAX_SIZE; i++) {
|
||||||
|
tags.add(Tag.Integer("key" + i).path("path"));
|
||||||
|
}
|
||||||
|
TAGS = List.copyOf(tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final TagHandler handler = TagHandler.newHandler();
|
||||||
|
|
||||||
|
@Actor
|
||||||
|
public void actor1() {
|
||||||
|
for (int i = 0; i < 1000; i++) {
|
||||||
|
handler.updateTag(TAG, integer -> integer + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Actor
|
||||||
|
public void actor2() {
|
||||||
|
for (int i = 0; i < 1000; i++) {
|
||||||
|
handler.updateTag(TAG, integer -> integer + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Actor
|
||||||
|
public void actor3() {
|
||||||
|
// May be able to disturb actor1/2
|
||||||
|
for (int i = 0; i < MAX_SIZE; i++) {
|
||||||
|
handler.setTag(TAGS.get(i), i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Arbiter
|
||||||
|
public void arbiter(L_Result r) {
|
||||||
|
r.r1 = handler.getTag(TAG);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,36 +1,33 @@
|
|||||||
package net.minestom.server.tag;
|
package net.minestom.server.tag;
|
||||||
|
|
||||||
import org.openjdk.jcstress.annotations.Actor;
|
import org.openjdk.jcstress.annotations.*;
|
||||||
import org.openjdk.jcstress.annotations.JCStressTest;
|
|
||||||
import org.openjdk.jcstress.annotations.Outcome;
|
|
||||||
import org.openjdk.jcstress.annotations.State;
|
|
||||||
import org.openjdk.jcstress.infra.results.L_Result;
|
import org.openjdk.jcstress.infra.results.L_Result;
|
||||||
|
|
||||||
import static org.openjdk.jcstress.annotations.Expect.ACCEPTABLE;
|
import static org.openjdk.jcstress.annotations.Expect.ACCEPTABLE;
|
||||||
|
|
||||||
@JCStressTest
|
@JCStressTest
|
||||||
@Outcome(id = "null", expect = ACCEPTABLE)
|
@Outcome(id = "2000", expect = ACCEPTABLE)
|
||||||
@Outcome(id = "3", expect = ACCEPTABLE)
|
|
||||||
@Outcome(id = "4", expect = ACCEPTABLE)
|
|
||||||
@Outcome(id = "10", expect = ACCEPTABLE)
|
|
||||||
@Outcome(id = "11", expect = ACCEPTABLE)
|
|
||||||
@State
|
@State
|
||||||
public class TagUpdateTest {
|
public class TagUpdateTest {
|
||||||
private static final Tag<Integer> TAG = Tag.Integer("key");
|
private static final Tag<Integer> TAG = Tag.Integer("key").defaultValue(0);
|
||||||
|
|
||||||
private final TagHandler handler = TagHandler.newHandler();
|
private final TagHandler handler = TagHandler.newHandler();
|
||||||
|
|
||||||
@Actor
|
@Actor
|
||||||
public void actor1() {
|
public void actor1() {
|
||||||
handler.updateAndGetTag(TAG, integer -> integer == null ? 3 : integer + 1);
|
for (int i = 0; i < 1000; i++) {
|
||||||
|
handler.updateAndGetTag(TAG, integer -> integer + 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Actor
|
@Actor
|
||||||
public void actor2() {
|
public void actor2() {
|
||||||
handler.updateAndGetTag(TAG, integer -> integer == null ? 10 : integer + 1);
|
for (int i = 0; i < 1000; i++) {
|
||||||
|
handler.updateAndGetTag(TAG, integer -> integer + 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Actor
|
@Arbiter
|
||||||
public void arbiter(L_Result r) {
|
public void arbiter(L_Result r) {
|
||||||
r.r1 = handler.getTag(TAG);
|
r.r1 = handler.getTag(TAG);
|
||||||
}
|
}
|
||||||
|
@ -153,7 +153,7 @@ public sealed interface ItemMeta extends TagReadable, Writeable
|
|||||||
|
|
||||||
@Contract("_ -> this")
|
@Contract("_ -> this")
|
||||||
default @NotNull Builder canPlaceOn(@NotNull Set<@NotNull Block> blocks) {
|
default @NotNull Builder canPlaceOn(@NotNull Set<@NotNull Block> blocks) {
|
||||||
return set(ItemTags.CAN_PLACE_ON, List.copyOf(blocks));
|
return set(ItemTags.CAN_PLACE_ON, blocks.stream().map(block -> Block.fromNamespaceId(block.name())).toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Contract("_ -> this")
|
@Contract("_ -> this")
|
||||||
@ -163,7 +163,7 @@ public sealed interface ItemMeta extends TagReadable, Writeable
|
|||||||
|
|
||||||
@Contract("_ -> this")
|
@Contract("_ -> this")
|
||||||
default @NotNull Builder canDestroy(@NotNull Set<@NotNull Block> blocks) {
|
default @NotNull Builder canDestroy(@NotNull Set<@NotNull Block> blocks) {
|
||||||
return set(ItemTags.CAN_DESTROY, List.copyOf(blocks));
|
return set(ItemTags.CAN_DESTROY, blocks.stream().map(block -> Block.fromNamespaceId(block.name())).toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Contract("_ -> this")
|
@Contract("_ -> this")
|
||||||
|
@ -12,8 +12,6 @@ import java.util.function.Function;
|
|||||||
* Basic serializers for {@link Tag tags}.
|
* Basic serializers for {@link Tag tags}.
|
||||||
*/
|
*/
|
||||||
final class Serializers {
|
final class Serializers {
|
||||||
static final Entry<TagHandlerImpl, NBTCompound> PATH = new Entry<>(NBTType.TAG_Compound, TagHandlerImpl::fromCompound, TagHandlerImpl::asCompound);
|
|
||||||
|
|
||||||
static final Entry<Byte, NBTByte> BYTE = new Entry<>(NBTType.TAG_Byte, NBTByte::getValue, NBT::Byte);
|
static final Entry<Byte, NBTByte> BYTE = new Entry<>(NBTType.TAG_Byte, NBTByte::getValue, NBT::Byte);
|
||||||
static final Entry<Boolean, NBTByte> BOOLEAN = new Entry<>(NBTType.TAG_Byte, NBTByte::asBoolean, NBT::Boolean);
|
static final Entry<Boolean, NBTByte> BOOLEAN = new Entry<>(NBTType.TAG_Byte, NBTByte::asBoolean, NBT::Boolean);
|
||||||
static final Entry<Short, NBTShort> SHORT = new Entry<>(NBTType.TAG_Short, NBTShort::getValue, NBT::Short);
|
static final Entry<Short, NBTShort> SHORT = new Entry<>(NBTType.TAG_Short, NBTShort::getValue, NBT::Short);
|
||||||
@ -44,7 +42,11 @@ final class Serializers {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
record Entry<T, N extends NBT>(NBTType<N> nbtType, Function<N, T> reader, Function<T, N> writer) {
|
record Entry<T, N extends NBT>(NBTType<N> nbtType, Function<N, T> reader, Function<T, N> writer, boolean isPath) {
|
||||||
|
Entry(NBTType<N> nbtType, Function<N, T> reader, Function<T, N> writer) {
|
||||||
|
this(nbtType, reader, writer, false);
|
||||||
|
}
|
||||||
|
|
||||||
T read(N nbt) {
|
T read(N nbt) {
|
||||||
return reader.apply(nbt);
|
return reader.apply(nbt);
|
||||||
}
|
}
|
||||||
|
87
src/main/java/net/minestom/server/tag/StaticIntMap.java
Normal file
87
src/main/java/net/minestom/server/tag/StaticIntMap.java
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
package net.minestom.server.tag;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Range;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
sealed interface StaticIntMap<T> permits StaticIntMap.Array {
|
||||||
|
|
||||||
|
T get(@Range(from = 0, to = Integer.MAX_VALUE) int key);
|
||||||
|
|
||||||
|
void forValues(@NotNull Consumer<T> consumer);
|
||||||
|
|
||||||
|
@NotNull StaticIntMap<T> copy();
|
||||||
|
|
||||||
|
// Methods potentially causing re-hashing
|
||||||
|
|
||||||
|
void put(@Range(from = 0, to = Integer.MAX_VALUE) int key, T value);
|
||||||
|
|
||||||
|
void remove(@Range(from = 0, to = Integer.MAX_VALUE) int key);
|
||||||
|
|
||||||
|
void updateContent(@NotNull StaticIntMap<T> content);
|
||||||
|
|
||||||
|
final class Array<T> implements StaticIntMap<T> {
|
||||||
|
private static final Object[] EMPTY_ARRAY = new Object[0];
|
||||||
|
|
||||||
|
private T[] array;
|
||||||
|
|
||||||
|
public Array(T[] array) {
|
||||||
|
this.array = array;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Array() {
|
||||||
|
//noinspection unchecked
|
||||||
|
this.array = (T[]) EMPTY_ARRAY;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public T get(int key) {
|
||||||
|
final T[] array = this.array;
|
||||||
|
return key < array.length ? array[key] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void forValues(@NotNull Consumer<T> consumer) {
|
||||||
|
final T[] array = this.array;
|
||||||
|
for (T value : array) {
|
||||||
|
if (value != null) consumer.accept(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull StaticIntMap<T> copy() {
|
||||||
|
return new Array<>(array.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void put(int key, T value) {
|
||||||
|
T[] array = this.array;
|
||||||
|
if (key >= array.length) {
|
||||||
|
array = updateArray(Arrays.copyOf(array, key * 2 + 1));
|
||||||
|
}
|
||||||
|
array[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateContent(@NotNull StaticIntMap<T> content) {
|
||||||
|
if (content instanceof StaticIntMap.Array<T> arrayMap) {
|
||||||
|
updateArray(arrayMap.array.clone());
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Invalid content type: " + content.getClass());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void remove(int key) {
|
||||||
|
T[] array = this.array;
|
||||||
|
if (key < array.length) array[key] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
T[] updateArray(T[] result) {
|
||||||
|
this.array = result;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -210,6 +210,11 @@ public class Tag<T> {
|
|||||||
return supplier != null ? supplier.get() : null;
|
return supplier != null ? supplier.get() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final T copyValue(@NotNull T value) {
|
||||||
|
final UnaryOperator<T> copier = copy;
|
||||||
|
return copier != null ? copier.apply(value) : value;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
if (this == o) return true;
|
if (this == o) return true;
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
package net.minestom.server.tag;
|
package net.minestom.server.tag;
|
||||||
|
|
||||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
|
||||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
|
||||||
import net.minestom.server.utils.PropertyUtils;
|
import net.minestom.server.utils.PropertyUtils;
|
||||||
|
import org.jetbrains.annotations.Contract;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
import org.jetbrains.annotations.UnknownNullability;
|
import org.jetbrains.annotations.UnknownNullability;
|
||||||
@ -13,205 +12,227 @@ import org.jglrxavpok.hephaistos.nbt.NBTType;
|
|||||||
import org.jglrxavpok.hephaistos.nbt.mutable.MutableNBTCompound;
|
import org.jglrxavpok.hephaistos.nbt.mutable.MutableNBTCompound;
|
||||||
|
|
||||||
import java.lang.invoke.VarHandle;
|
import java.lang.invoke.VarHandle;
|
||||||
import java.util.function.Supplier;
|
|
||||||
import java.util.function.UnaryOperator;
|
import java.util.function.UnaryOperator;
|
||||||
|
|
||||||
final class TagHandlerImpl implements TagHandler {
|
final class TagHandlerImpl implements TagHandler {
|
||||||
private static final boolean CACHE_ENABLE = PropertyUtils.getBoolean("minestom.tag-handler-cache", true);
|
private static final boolean CACHE_ENABLE = PropertyUtils.getBoolean("minestom.tag-handler-cache", true);
|
||||||
|
static final Serializers.Entry<Node, NBTCompound> NODE_SERIALIZER = new Serializers.Entry<>(NBTType.TAG_Compound, entries -> fromCompound(entries).root, Node::compound, true);
|
||||||
|
|
||||||
private final TagHandlerImpl parent;
|
private final Node root;
|
||||||
private volatile SPMCMap entries;
|
private volatile Node copy;
|
||||||
private Cache cache;
|
|
||||||
|
|
||||||
TagHandlerImpl(TagHandlerImpl parent) {
|
TagHandlerImpl(Node root) {
|
||||||
this.parent = parent;
|
this.root = root;
|
||||||
this.entries = new SPMCMap(this);
|
|
||||||
this.cache = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TagHandlerImpl() {
|
TagHandlerImpl() {
|
||||||
this(null);
|
this.root = new Node();
|
||||||
}
|
}
|
||||||
|
|
||||||
static TagHandlerImpl fromCompound(NBTCompoundLike compoundLike) {
|
static TagHandlerImpl fromCompound(NBTCompoundLike compoundLike) {
|
||||||
final NBTCompound compound = compoundLike.toCompound();
|
final NBTCompound compound = compoundLike.toCompound();
|
||||||
TagHandlerImpl handler = new TagHandlerImpl(null);
|
TagHandlerImpl handler = new TagHandlerImpl();
|
||||||
TagNbtSeparator.separate(compound, entry -> handler.setTag(entry.tag(), entry.value()));
|
TagNbtSeparator.separate(compound, entry -> handler.setTag(entry.tag(), entry.value()));
|
||||||
|
handler.root.compound = compound;
|
||||||
return handler;
|
return handler;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T> @UnknownNullability T getTag(@NotNull Tag<T> tag) {
|
public <T> @UnknownNullability T getTag(@NotNull Tag<T> tag) {
|
||||||
return read(entries, tag, this::asCompound);
|
VarHandle.fullFence();
|
||||||
|
return root.getTag(tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T> void setTag(@NotNull Tag<T> tag, @Nullable T value) {
|
public <T> void setTag(@NotNull Tag<T> tag, @Nullable T value) {
|
||||||
TagHandlerImpl local = this;
|
// Handle view tags
|
||||||
final Tag.PathEntry[] paths = tag.path;
|
if (tag.isView()) {
|
||||||
final boolean present = value != null;
|
|
||||||
final int tagIndex = tag.index;
|
|
||||||
final boolean isView = tag.isView();
|
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
if (paths != null) {
|
Node syncNode = traversePathWrite(root, tag, value != null);
|
||||||
if ((local = traversePathWrite(this, paths, present)) == null)
|
if (syncNode != null) {
|
||||||
return; // Tried to remove an absent tag. Do nothing
|
syncNode.updateContent(value != null ? (NBTCompound) tag.entry.write(value) : NBTCompound.EMPTY);
|
||||||
|
syncNode.invalidate();
|
||||||
}
|
}
|
||||||
SPMCMap entries = local.entries;
|
|
||||||
if (present) {
|
|
||||||
if (!isView) {
|
|
||||||
Entry previous = entries.get(tagIndex);
|
|
||||||
if (previous != null && previous.tag().shareValue(tag)) {
|
|
||||||
previous.updateValue(value);
|
|
||||||
} else {
|
|
||||||
entries.put(tagIndex, valueToEntry(local, tag, value));
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
local.updateContent((NBTCompound) tag.entry.write(value));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Normal tag
|
||||||
|
final int tagIndex = tag.index;
|
||||||
|
VarHandle.fullFence();
|
||||||
|
Node node = traversePathWrite(root, tag, value != null);
|
||||||
|
if (node == null)
|
||||||
|
return; // Tried to remove an absent tag. Do nothing
|
||||||
|
StaticIntMap<Entry<?>> entries = node.entries;
|
||||||
|
if (value != null) {
|
||||||
|
Entry previous = entries.get(tagIndex);
|
||||||
|
if (previous != null && previous.tag().shareValue(tag)) {
|
||||||
|
previous.updateValue(tag.copyValue(value));
|
||||||
} else {
|
} else {
|
||||||
// Remove recursively
|
synchronized (this) {
|
||||||
if (!isView) {
|
node = traversePathWrite(root, tag, true);
|
||||||
if (entries.remove(tagIndex) == null) return;
|
node.entries.put(tagIndex, valueToEntry(node, tag, value));
|
||||||
} else {
|
|
||||||
entries.clear();
|
|
||||||
}
|
}
|
||||||
if (paths != null) {
|
|
||||||
TagHandlerImpl tmp = local;
|
|
||||||
int i = paths.length;
|
|
||||||
do {
|
|
||||||
if (!tmp.entries.isEmpty()) break;
|
|
||||||
tmp = tmp.parent;
|
|
||||||
tmp.entries.remove(paths[--i].index());
|
|
||||||
} while (i > 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
entries.invalidate();
|
|
||||||
assert !local.entries.rehashed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private <T> Entry<?> valueToEntry(TagHandlerImpl parent, Tag<T> tag, @NotNull T value) {
|
|
||||||
if (value instanceof NBT nbt) {
|
|
||||||
if (nbt instanceof NBTCompound compound) {
|
|
||||||
var handler = new TagHandlerImpl(parent);
|
|
||||||
handler.updateContent(compound);
|
|
||||||
return new PathEntry(tag.getKey(), handler);
|
|
||||||
} else {
|
|
||||||
final var nbtEntry = TagNbtSeparator.separateSingle(tag.getKey(), nbt);
|
|
||||||
return new TagEntry<>(nbtEntry.tag(), nbtEntry.value());
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
final UnaryOperator<T> copy = tag.copy;
|
synchronized (this) {
|
||||||
if (copy != null) value = copy.apply(value);
|
node = traversePathWrite(root, tag, false);
|
||||||
return new TagEntry<>(tag, value);
|
if (node != null) node.entries.remove(tagIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
node.invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized <T> void updateTag(@NotNull Tag<T> tag, @NotNull UnaryOperator<@UnknownNullability T> value) {
|
public <T> void updateTag(@NotNull Tag<T> tag, @NotNull UnaryOperator<@UnknownNullability T> value) {
|
||||||
setTag(tag, value.apply(getTag(tag)));
|
updateTag0(tag, value, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized <T> @UnknownNullability T updateAndGetTag(@NotNull Tag<T> tag, @NotNull UnaryOperator<@UnknownNullability T> value) {
|
public <T> @UnknownNullability T updateAndGetTag(@NotNull Tag<T> tag, @NotNull UnaryOperator<@UnknownNullability T> value) {
|
||||||
final T next = value.apply(getTag(tag));
|
return updateTag0(tag, value, false);
|
||||||
setTag(tag, next);
|
|
||||||
return next;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized <T> @UnknownNullability T getAndUpdateTag(@NotNull Tag<T> tag, @NotNull UnaryOperator<@UnknownNullability T> value) {
|
public <T> @UnknownNullability T getAndUpdateTag(@NotNull Tag<T> tag, @NotNull UnaryOperator<@UnknownNullability T> value) {
|
||||||
final T prev = getTag(tag);
|
return updateTag0(tag, value, true);
|
||||||
setTag(tag, value.apply(prev));
|
}
|
||||||
return prev;
|
|
||||||
|
private synchronized <T> T updateTag0(@NotNull Tag<T> tag, @NotNull UnaryOperator<T> value, boolean returnPrevious) {
|
||||||
|
final int tagIndex = tag.index;
|
||||||
|
final Node node = traversePathWrite(root, tag, true);
|
||||||
|
StaticIntMap<Entry<?>> entries = node.entries;
|
||||||
|
|
||||||
|
final Entry previousEntry = entries.get(tagIndex);
|
||||||
|
final T previousValue = previousEntry != null ? (T) previousEntry.value() : tag.createDefault();
|
||||||
|
final T newValue = value.apply(previousValue);
|
||||||
|
if (newValue != null) entries.put(tagIndex, valueToEntry(node, tag, newValue));
|
||||||
|
else entries.remove(tagIndex);
|
||||||
|
|
||||||
|
node.invalidate();
|
||||||
|
return returnPrevious ? previousValue : newValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NotNull TagReadable readableCopy() {
|
public @NotNull TagReadable readableCopy() {
|
||||||
return updatedCache();
|
Node copy = this.copy;
|
||||||
}
|
if (copy == null) {
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NotNull TagHandler copy() {
|
|
||||||
return fromCompound(asCompound());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void updateContent(@NotNull NBTCompoundLike compound) {
|
|
||||||
final TagHandlerImpl converted = fromCompound(compound);
|
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
this.cache = converted.cache;
|
this.copy = copy = root.copy(null);
|
||||||
this.entries = new SPMCMap(this, converted.entries);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized @NotNull TagHandler copy() {
|
||||||
|
return new TagHandlerImpl(root.copy(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void updateContent(@NotNull NBTCompoundLike compound) {
|
||||||
|
this.root.updateContent(compound);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NotNull NBTCompound asCompound() {
|
public @NotNull NBTCompound asCompound() {
|
||||||
return updatedCache().compound;
|
VarHandle.fullFence();
|
||||||
|
return root.compound();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TagHandlerImpl traversePathWrite(TagHandlerImpl root, Tag.PathEntry[] paths,
|
private static Node traversePathRead(Node node, Tag<?> tag) {
|
||||||
|
final Tag.PathEntry[] paths = tag.path;
|
||||||
|
if (paths == null) return node;
|
||||||
|
for (var path : paths) {
|
||||||
|
final Entry<?> entry = node.entries.get(path.index());
|
||||||
|
if (entry == null || (node = entry.toNode()) == null)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Contract("_, _, true -> !null")
|
||||||
|
private Node traversePathWrite(Node root, Tag<?> tag,
|
||||||
boolean present) {
|
boolean present) {
|
||||||
TagHandlerImpl local = root;
|
final Tag.PathEntry[] paths = tag.path;
|
||||||
|
if (paths == null) return root;
|
||||||
|
Node local = root;
|
||||||
for (Tag.PathEntry path : paths) {
|
for (Tag.PathEntry path : paths) {
|
||||||
final int pathIndex = path.index();
|
final int pathIndex = path.index();
|
||||||
final Entry<?> entry = local.entries.get(pathIndex);
|
final Entry<?> entry = local.entries.get(pathIndex);
|
||||||
if (entry instanceof PathEntry pathEntry) {
|
if (entry != null && entry.tag.entry.isPath()) {
|
||||||
// Existing path, continue navigating
|
// Existing path, continue navigating
|
||||||
assert pathEntry.value.parent == local : "Path parent is invalid: " + pathEntry.value.parent + " != " + local;
|
final Node tmp = (Node) entry.value;
|
||||||
local = pathEntry.value;
|
assert tmp.parent == local : "Path parent is invalid: " + tmp.parent + " != " + local;
|
||||||
|
local = tmp;
|
||||||
} else {
|
} else {
|
||||||
if (!present) return null;
|
if (!present) return null;
|
||||||
|
synchronized (this) {
|
||||||
|
var synEntry = local.entries.get(pathIndex);
|
||||||
|
if (synEntry != null && synEntry.tag.entry.isPath()) {
|
||||||
|
// Existing path, continue navigating
|
||||||
|
final Node tmp = (Node) synEntry.value;
|
||||||
|
assert tmp.parent == local : "Path parent is invalid: " + tmp.parent + " != " + local;
|
||||||
|
local = tmp;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Empty path, create a new handler.
|
// Empty path, create a new handler.
|
||||||
// Slow path is taken if the entry comes from a Structure tag, requiring conversion from NBT
|
// Slow path is taken if the entry comes from a Structure tag, requiring conversion from NBT
|
||||||
TagHandlerImpl tmp = local;
|
Node tmp = local;
|
||||||
local = new TagHandlerImpl(tmp);
|
local = new Node(tmp);
|
||||||
if (entry != null && entry.nbt() instanceof NBTCompound compound) {
|
if (synEntry != null && synEntry.nbt() instanceof NBTCompound compound) {
|
||||||
local.updateContent(compound);
|
local.updateContent(compound);
|
||||||
}
|
}
|
||||||
tmp.entries.put(pathIndex, new PathEntry(path.name(), local));
|
tmp.entries.put(pathIndex, Entry.makePathEntry(path.name(), local));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return local;
|
return local;
|
||||||
}
|
}
|
||||||
|
|
||||||
private synchronized Cache updatedCache() {
|
private <T> Entry<?> valueToEntry(Node parent, Tag<T> tag, @NotNull T value) {
|
||||||
Cache cache;
|
if (value instanceof NBT nbt) {
|
||||||
if (!CACHE_ENABLE || (cache = this.cache) == null) {
|
if (nbt instanceof NBTCompound compound) {
|
||||||
final SPMCMap entries = this.entries;
|
final TagHandlerImpl handler = fromCompound(compound);
|
||||||
if (!entries.isEmpty()) {
|
return Entry.makePathEntry(tag, new Node(parent, handler.root.entries));
|
||||||
MutableNBTCompound tmp = new MutableNBTCompound();
|
} else {
|
||||||
for (Entry<?> entry : entries.values()) {
|
final var nbtEntry = TagNbtSeparator.separateSingle(tag.getKey(), nbt);
|
||||||
if (entry != null) tmp.put(entry.tag().getKey(), entry.nbt());
|
return new Entry<>(nbtEntry.tag(), nbtEntry.value());
|
||||||
}
|
}
|
||||||
cache = new Cache(entries.clone(), tmp.toCompound());
|
} else {
|
||||||
} else cache = Cache.EMPTY;
|
return new Entry<>(tag, tag.copyValue(value));
|
||||||
this.cache = cache;
|
|
||||||
}
|
}
|
||||||
return cache;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static <T> T read(Int2ObjectOpenHashMap<Entry<?>> entries, Tag<T> tag,
|
final class Node implements TagReadable {
|
||||||
Supplier<NBTCompound> rootCompoundSupplier) {
|
final Node parent;
|
||||||
final Tag.PathEntry[] paths = tag.path;
|
final StaticIntMap<Entry<?>> entries;
|
||||||
TagHandlerImpl pathHandler = null;
|
NBTCompound compound;
|
||||||
if (paths != null) {
|
|
||||||
if ((pathHandler = traversePathRead(paths, entries)) == null)
|
public Node(Node parent, StaticIntMap<Entry<?>> entries) {
|
||||||
|
this.parent = parent;
|
||||||
|
this.entries = entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
Node(Node parent) {
|
||||||
|
this(parent, new StaticIntMap.Array<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
Node() {
|
||||||
|
this(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> @UnknownNullability T getTag(@NotNull Tag<T> tag) {
|
||||||
|
final Node node = traversePathRead(this, tag);
|
||||||
|
if (node == null)
|
||||||
return tag.createDefault(); // Must be a path-able entry, but not present
|
return tag.createDefault(); // Must be a path-able entry, but not present
|
||||||
entries = pathHandler.entries;
|
if (tag.isView()) return tag.read(node.compound());
|
||||||
}
|
|
||||||
|
|
||||||
if (tag.isView()) {
|
final StaticIntMap<Entry<?>> entries = node.entries;
|
||||||
return tag.read(pathHandler != null ?
|
final Entry<?> entry = entries.get(tag.index);
|
||||||
pathHandler.asCompound() : rootCompoundSupplier.get());
|
if (entry == null)
|
||||||
}
|
return tag.createDefault(); // Not present
|
||||||
|
|
||||||
final Entry<?> entry;
|
|
||||||
if ((entry = entries.get(tag.index)) == null) {
|
|
||||||
return tag.createDefault();
|
|
||||||
}
|
|
||||||
if (entry.tag().shareValue(tag)) {
|
if (entry.tag().shareValue(tag)) {
|
||||||
// The tag used to write the entry is compatible with the one used to get
|
// The tag used to write the entry is compatible with the one used to get
|
||||||
// return the value directly
|
// return the value directly
|
||||||
@ -225,143 +246,114 @@ final class TagHandlerImpl implements TagHandler {
|
|||||||
return type == null || type == nbt.getID() ? serializerEntry.read(nbt) : tag.createDefault();
|
return type == null || type == nbt.getID() ? serializerEntry.read(nbt) : tag.createDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TagHandlerImpl traversePathRead(Tag.PathEntry[] paths,
|
void updateContent(@NotNull NBTCompoundLike compoundLike) {
|
||||||
Int2ObjectOpenHashMap<Entry<?>> entries) {
|
final NBTCompound compound = compoundLike.toCompound();
|
||||||
assert paths != null && paths.length > 0;
|
final TagHandlerImpl converted = fromCompound(compound);
|
||||||
TagHandlerImpl result = null;
|
this.entries.updateContent(converted.root.entries);
|
||||||
for (var path : paths) {
|
this.compound = compound;
|
||||||
final Entry<?> entry;
|
}
|
||||||
if ((entry = entries.get(path.index())) == null)
|
|
||||||
return null;
|
NBTCompound compound() {
|
||||||
if (entry instanceof PathEntry pathEntry) {
|
NBTCompound compound;
|
||||||
result = pathEntry.value;
|
if (!CACHE_ENABLE || (compound = this.compound) == null) {
|
||||||
} else if (entry.nbt() instanceof NBTCompound compound) {
|
MutableNBTCompound tmp = new MutableNBTCompound();
|
||||||
// Slow path forcing a conversion of the structure to NBTCompound
|
this.entries.forValues(entry -> {
|
||||||
// TODO should the handler be cached inside the entry?
|
final Tag tag = entry.tag();
|
||||||
result = fromCompound(compound);
|
final NBT nbt = entry.nbt();
|
||||||
|
if (!tag.entry.isPath() || !((NBTCompound) nbt).isEmpty()) {
|
||||||
|
tmp.put(tag.getKey(), nbt);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.compound = compound = tmp.toCompound();
|
||||||
|
}
|
||||||
|
return compound;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Contract("null -> !null")
|
||||||
|
Node copy(Node parent) {
|
||||||
|
MutableNBTCompound tmp = new MutableNBTCompound();
|
||||||
|
Node result = new Node(parent, new StaticIntMap.Array<>());
|
||||||
|
StaticIntMap<Entry<?>> entries = result.entries;
|
||||||
|
this.entries.forValues(entry -> {
|
||||||
|
Tag tag = entry.tag;
|
||||||
|
Object value = entry.value;
|
||||||
|
NBT nbt;
|
||||||
|
if (value instanceof Node node) {
|
||||||
|
Node copy = node.copy(result);
|
||||||
|
if (copy == null)
|
||||||
|
return; // Empty node
|
||||||
|
value = copy;
|
||||||
|
nbt = copy.compound;
|
||||||
|
assert nbt != null : "Node copy should also compute the compound";
|
||||||
} else {
|
} else {
|
||||||
// Entry is not path-able
|
nbt = entry.nbt();
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
assert result != null;
|
|
||||||
entries = result.entries;
|
tmp.put(tag.getKey(), nbt);
|
||||||
}
|
entries.put(tag.index, valueToEntry(result, tag, value));
|
||||||
assert result != null;
|
});
|
||||||
|
if (tmp.isEmpty() && parent != null)
|
||||||
|
return null; // Empty child node
|
||||||
|
result.compound = tmp.toCompound();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private record Cache(Int2ObjectOpenHashMap<Entry<?>> entries, NBTCompound compound) implements TagReadable {
|
void invalidate() {
|
||||||
static final Cache EMPTY = new Cache(new Int2ObjectOpenHashMap<>(), NBTCompound.EMPTY);
|
Node tmp = this;
|
||||||
|
do tmp.compound = null;
|
||||||
@Override
|
while ((tmp = tmp.parent) != null);
|
||||||
public <T> @UnknownNullability T getTag(@NotNull Tag<T> tag) {
|
TagHandlerImpl.this.copy = null;
|
||||||
return read(entries, tag, () -> compound);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed interface Entry<T>
|
private static final class Entry<T> {
|
||||||
permits TagEntry, PathEntry {
|
|
||||||
Tag<T> tag();
|
|
||||||
|
|
||||||
T value();
|
|
||||||
|
|
||||||
NBT nbt();
|
|
||||||
|
|
||||||
void updateValue(T value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class TagEntry<T> implements Entry<T> {
|
|
||||||
private final Tag<T> tag;
|
private final Tag<T> tag;
|
||||||
volatile T value;
|
T value;
|
||||||
volatile NBT nbt;
|
NBT nbt;
|
||||||
|
|
||||||
TagEntry(Tag<T> tag, T value) {
|
Entry(Tag<T> tag, T value) {
|
||||||
this.tag = tag;
|
this.tag = tag;
|
||||||
this.value = value;
|
this.value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
static Entry<?> makePathEntry(String path, Node node) {
|
||||||
|
return new Entry<>(Tag.tag(path, NODE_SERIALIZER), node);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Entry<?> makePathEntry(Tag<?> tag, Node node) {
|
||||||
|
return makePathEntry(tag.getKey(), node);
|
||||||
|
}
|
||||||
|
|
||||||
public Tag<T> tag() {
|
public Tag<T> tag() {
|
||||||
return tag;
|
return tag;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public T value() {
|
public T value() {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public NBT nbt() {
|
public NBT nbt() {
|
||||||
|
if (tag.entry.isPath()) return ((Node) value).compound();
|
||||||
NBT nbt = this.nbt;
|
NBT nbt = this.nbt;
|
||||||
if (nbt == null) this.nbt = nbt = tag.entry.write(value);
|
if (nbt == null) this.nbt = nbt = tag.entry.write(value);
|
||||||
return nbt;
|
return nbt;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void updateValue(T value) {
|
public void updateValue(T value) {
|
||||||
|
assert !tag.entry.isPath();
|
||||||
this.value = value;
|
this.value = value;
|
||||||
this.nbt = null;
|
this.nbt = null;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private record PathEntry(Tag<TagHandlerImpl> tag,
|
Node toNode() {
|
||||||
TagHandlerImpl value) implements Entry<TagHandlerImpl> {
|
if (tag.entry.isPath()) return (Node) value;
|
||||||
PathEntry(String key, TagHandlerImpl value) {
|
if (nbt() instanceof NBTCompound compound) {
|
||||||
this(Tag.tag(key, Serializers.PATH), value);
|
// Slow path forcing a conversion of the structure to NBTCompound
|
||||||
|
// TODO should the handler be cached inside the entry?
|
||||||
|
return fromCompound(compound).root;
|
||||||
}
|
}
|
||||||
|
// Entry is not path-able
|
||||||
@Override
|
return null;
|
||||||
public NBTCompound nbt() {
|
|
||||||
return value.asCompound();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void updateValue(TagHandlerImpl value) {
|
|
||||||
throw new UnsupportedOperationException("Cannot update a path entry");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static final class SPMCMap extends Int2ObjectOpenHashMap<Entry<?>> {
|
|
||||||
final TagHandlerImpl handler;
|
|
||||||
volatile boolean rehashed;
|
|
||||||
|
|
||||||
SPMCMap(TagHandlerImpl handler) {
|
|
||||||
super();
|
|
||||||
this.handler = handler;
|
|
||||||
assertState();
|
|
||||||
}
|
|
||||||
|
|
||||||
SPMCMap(TagHandlerImpl handler, Int2ObjectMap<TagHandlerImpl.Entry<?>> m) {
|
|
||||||
super(m.size(), DEFAULT_LOAD_FACTOR);
|
|
||||||
this.handler = handler;
|
|
||||||
assertState();
|
|
||||||
putAll(m);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void rehash(int newSize) {
|
|
||||||
assertState();
|
|
||||||
this.handler.entries = new SPMCMap(handler, this);
|
|
||||||
this.rehashed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SPMCMap clone() {
|
|
||||||
return (SPMCMap) super.clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
void invalidate() {
|
|
||||||
if (!CACHE_ENABLE) return;
|
|
||||||
TagHandlerImpl tmp = handler;
|
|
||||||
do {
|
|
||||||
tmp.cache = null;
|
|
||||||
} while ((tmp = tmp.parent) != null);
|
|
||||||
VarHandle.fullFence();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void assertState() {
|
|
||||||
assert !rehashed;
|
|
||||||
assert handler != null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -71,9 +71,9 @@ final class TagRecord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static final class Serializer<T extends Record> implements TagSerializer<T> {
|
static final class Serializer<T extends Record> implements TagSerializer<T> {
|
||||||
Constructor<T> constructor;
|
final Constructor<T> constructor;
|
||||||
Entry[] entries;
|
final Entry[] entries;
|
||||||
Serializers.Entry<T, NBTCompound> serializerEntry;
|
final Serializers.Entry<T, NBTCompound> serializerEntry;
|
||||||
|
|
||||||
Serializer(Constructor<T> constructor, Entry[] entries) {
|
Serializer(Constructor<T> constructor, Entry[] entries) {
|
||||||
this.constructor = constructor;
|
this.constructor = constructor;
|
||||||
|
62
src/test/java/net/minestom/server/item/ItemBlockTest.java
Normal file
62
src/test/java/net/minestom/server/item/ItemBlockTest.java
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package net.minestom.server.item;
|
||||||
|
|
||||||
|
import net.minestom.server.instance.block.Block;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static net.minestom.server.api.TestUtils.assertEqualsSNBT;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
public class ItemBlockTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void canPlace() {
|
||||||
|
var item = ItemStack.builder(Material.STONE)
|
||||||
|
.meta(builder -> builder.canPlaceOn(Block.STONE))
|
||||||
|
.build();
|
||||||
|
assertTrue(item.meta().getCanPlaceOn().contains(Block.STONE));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void canPlaceNbt() {
|
||||||
|
var item = ItemStack.builder(Material.STONE)
|
||||||
|
.meta(builder -> builder.canPlaceOn(Block.STONE))
|
||||||
|
.build();
|
||||||
|
assertEqualsSNBT("""
|
||||||
|
{"CanPlaceOn":["minecraft:stone"]}
|
||||||
|
""", item.meta().toNBT());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void canPlaceMismatchProperties() {
|
||||||
|
var item = ItemStack.builder(Material.STONE)
|
||||||
|
.meta(builder -> builder.canPlaceOn(Block.SANDSTONE_STAIRS.withProperty("facing", "south")))
|
||||||
|
.build();
|
||||||
|
assertTrue(item.meta().getCanPlaceOn().contains(Block.SANDSTONE_STAIRS));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void canDestroy() {
|
||||||
|
var item = ItemStack.builder(Material.STONE)
|
||||||
|
.meta(builder -> builder.canDestroy(Block.STONE))
|
||||||
|
.build();
|
||||||
|
assertTrue(item.meta().getCanDestroy().contains(Block.STONE));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void canDestroyNbt() {
|
||||||
|
var item = ItemStack.builder(Material.STONE)
|
||||||
|
.meta(builder -> builder.canDestroy(Block.STONE))
|
||||||
|
.build();
|
||||||
|
assertEqualsSNBT("""
|
||||||
|
{"CanDestroy":["minecraft:stone"]}
|
||||||
|
""", item.meta().toNBT());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void canDestroyMismatchProperties() {
|
||||||
|
var item = ItemStack.builder(Material.STONE)
|
||||||
|
.meta(builder -> builder.canDestroy(Block.SANDSTONE_STAIRS.withProperty("facing", "south")))
|
||||||
|
.build();
|
||||||
|
assertTrue(item.meta().getCanDestroy().contains(Block.SANDSTONE_STAIRS));
|
||||||
|
}
|
||||||
|
}
|
55
src/test/java/net/minestom/server/item/ItemDisplayTest.java
Normal file
55
src/test/java/net/minestom/server/item/ItemDisplayTest.java
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package net.minestom.server.item;
|
||||||
|
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
|
||||||
|
import org.jglrxavpok.hephaistos.nbt.NBTString;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
public class ItemDisplayTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void lore() {
|
||||||
|
var item = ItemStack.of(Material.DIAMOND_SWORD);
|
||||||
|
assertEquals(List.of(), item.getLore());
|
||||||
|
assertNull(item.meta().toNBT().get("display"));
|
||||||
|
|
||||||
|
{
|
||||||
|
var lore = List.of(Component.text("Hello"));
|
||||||
|
item = item.withLore(lore);
|
||||||
|
assertEquals(lore, item.getLore());
|
||||||
|
var loreNbt = item.meta().toNBT().getCompound("display").<NBTString>getList("Lore");
|
||||||
|
assertNotNull(loreNbt);
|
||||||
|
assertEquals(1, loreNbt.getSize());
|
||||||
|
assertEquals(lore, loreNbt.asListView().stream().map(line -> GsonComponentSerializer.gson().deserialize(line.getValue())).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var lore = List.of(Component.text("Hello"), Component.text("World"));
|
||||||
|
item = item.withLore(lore);
|
||||||
|
assertEquals(lore, item.getLore());
|
||||||
|
var loreNbt = item.meta().toNBT().getCompound("display").<NBTString>getList("Lore");
|
||||||
|
assertNotNull(loreNbt);
|
||||||
|
assertEquals(2, loreNbt.getSize());
|
||||||
|
assertEquals(lore, loreNbt.asListView().stream().map(line -> GsonComponentSerializer.gson().deserialize(line.getValue())).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var lore = Stream.of("string test").map(Component::text).toList();
|
||||||
|
item = item.withLore(lore);
|
||||||
|
assertEquals(lore, item.getLore());
|
||||||
|
var loreNbt = item.meta().toNBT().getCompound("display").<NBTString>getList("Lore");
|
||||||
|
assertNotNull(loreNbt);
|
||||||
|
assertEquals(1, loreNbt.getSize());
|
||||||
|
assertEquals(lore, loreNbt.asListView().stream().map(line -> GsonComponentSerializer.gson().deserialize(line.getValue())).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that lore can be properly removed without residual (display compound)
|
||||||
|
item = item.withLore(List.of());
|
||||||
|
assertNull(item.meta().toNBT().get("display"));
|
||||||
|
}
|
||||||
|
}
|
42
src/test/java/net/minestom/server/item/ItemEnchantTest.java
Normal file
42
src/test/java/net/minestom/server/item/ItemEnchantTest.java
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package net.minestom.server.item;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
public class ItemEnchantTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void enchant() {
|
||||||
|
var item = ItemStack.of(Material.DIAMOND_SWORD);
|
||||||
|
var enchantments = item.meta().getEnchantmentMap();
|
||||||
|
assertTrue(enchantments.isEmpty(), "items do not have enchantments by default");
|
||||||
|
|
||||||
|
item = item.withMeta(meta -> meta.enchantment(Enchantment.EFFICIENCY, (short) 10));
|
||||||
|
enchantments = item.meta().getEnchantmentMap();
|
||||||
|
assertEquals(enchantments.size(), 1);
|
||||||
|
assertEquals(enchantments.get(Enchantment.EFFICIENCY), (short) 10);
|
||||||
|
|
||||||
|
item = item.withMeta(meta -> meta.enchantment(Enchantment.INFINITY, (short) 5));
|
||||||
|
enchantments = item.meta().getEnchantmentMap();
|
||||||
|
assertEquals(enchantments.size(), 2);
|
||||||
|
assertEquals(enchantments.get(Enchantment.EFFICIENCY), (short) 10);
|
||||||
|
assertEquals(enchantments.get(Enchantment.INFINITY), (short) 5);
|
||||||
|
|
||||||
|
item = item.withMeta(meta -> meta.enchantments(Map.of()));
|
||||||
|
enchantments = item.meta().getEnchantmentMap();
|
||||||
|
assertTrue(enchantments.isEmpty());
|
||||||
|
|
||||||
|
// Ensure that enchantments can still be modified after being emptied
|
||||||
|
item = item.withMeta(meta -> meta.enchantment(Enchantment.EFFICIENCY, (short) 10));
|
||||||
|
enchantments = item.meta().getEnchantmentMap();
|
||||||
|
assertEquals(enchantments.get(Enchantment.EFFICIENCY), (short) 10);
|
||||||
|
|
||||||
|
item = item.withMeta(ItemMeta.Builder::clearEnchantment);
|
||||||
|
enchantments = item.meta().getEnchantmentMap();
|
||||||
|
assertTrue(enchantments.isEmpty());
|
||||||
|
}
|
||||||
|
}
|
@ -2,14 +2,8 @@ package net.minestom.server.item;
|
|||||||
|
|
||||||
import net.kyori.adventure.text.Component;
|
import net.kyori.adventure.text.Component;
|
||||||
import net.kyori.adventure.text.format.NamedTextColor;
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
|
|
||||||
import org.jglrxavpok.hephaistos.nbt.NBTString;
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
public class ItemTest {
|
public class ItemTest {
|
||||||
@ -78,78 +72,6 @@ public class ItemTest {
|
|||||||
assertEquals(createItem(), item, "Items must be equal if created from the same meta nbt");
|
assertEquals(createItem(), item, "Items must be equal if created from the same meta nbt");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testEnchant() {
|
|
||||||
var item = ItemStack.of(Material.DIAMOND_SWORD);
|
|
||||||
var enchantments = item.meta().getEnchantmentMap();
|
|
||||||
assertTrue(enchantments.isEmpty(), "items do not have enchantments by default");
|
|
||||||
|
|
||||||
item = item.withMeta(meta -> meta.enchantment(Enchantment.EFFICIENCY, (short) 10));
|
|
||||||
enchantments = item.meta().getEnchantmentMap();
|
|
||||||
assertEquals(enchantments.size(), 1);
|
|
||||||
assertEquals(enchantments.get(Enchantment.EFFICIENCY), (short) 10);
|
|
||||||
|
|
||||||
item = item.withMeta(meta -> meta.enchantment(Enchantment.INFINITY, (short) 5));
|
|
||||||
enchantments = item.meta().getEnchantmentMap();
|
|
||||||
assertEquals(enchantments.size(), 2);
|
|
||||||
assertEquals(enchantments.get(Enchantment.EFFICIENCY), (short) 10);
|
|
||||||
assertEquals(enchantments.get(Enchantment.INFINITY), (short) 5);
|
|
||||||
|
|
||||||
item = item.withMeta(meta -> meta.enchantments(Map.of()));
|
|
||||||
enchantments = item.meta().getEnchantmentMap();
|
|
||||||
assertTrue(enchantments.isEmpty());
|
|
||||||
|
|
||||||
// Ensure that enchantments can still be modified after being emptied
|
|
||||||
item = item.withMeta(meta -> meta.enchantment(Enchantment.EFFICIENCY, (short) 10));
|
|
||||||
enchantments = item.meta().getEnchantmentMap();
|
|
||||||
assertEquals(enchantments.get(Enchantment.EFFICIENCY), (short) 10);
|
|
||||||
|
|
||||||
item = item.withMeta(ItemMeta.Builder::clearEnchantment);
|
|
||||||
enchantments = item.meta().getEnchantmentMap();
|
|
||||||
assertTrue(enchantments.isEmpty());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testLore() {
|
|
||||||
var item = ItemStack.of(Material.DIAMOND_SWORD);
|
|
||||||
assertEquals(List.of(), item.getLore());
|
|
||||||
assertNull(item.meta().toNBT().get("display"));
|
|
||||||
|
|
||||||
{
|
|
||||||
var lore = List.of(Component.text("Hello"));
|
|
||||||
item = item.withLore(lore);
|
|
||||||
assertEquals(lore, item.getLore());
|
|
||||||
var loreNbt = item.meta().toNBT().getCompound("display").<NBTString>getList("Lore");
|
|
||||||
assertNotNull(loreNbt);
|
|
||||||
assertEquals(1, loreNbt.getSize());
|
|
||||||
assertEquals(lore, loreNbt.asListView().stream().map(line -> GsonComponentSerializer.gson().deserialize(line.getValue())).toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
var lore = List.of(Component.text("Hello"), Component.text("World"));
|
|
||||||
item = item.withLore(lore);
|
|
||||||
assertEquals(lore, item.getLore());
|
|
||||||
var loreNbt = item.meta().toNBT().getCompound("display").<NBTString>getList("Lore");
|
|
||||||
assertNotNull(loreNbt);
|
|
||||||
assertEquals(2, loreNbt.getSize());
|
|
||||||
assertEquals(lore, loreNbt.asListView().stream().map(line -> GsonComponentSerializer.gson().deserialize(line.getValue())).toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
var lore = Stream.of("string test").map(Component::text).toList();
|
|
||||||
item = item.withLore(lore);
|
|
||||||
assertEquals(lore, item.getLore());
|
|
||||||
var loreNbt = item.meta().toNBT().getCompound("display").<NBTString>getList("Lore");
|
|
||||||
assertNotNull(loreNbt);
|
|
||||||
assertEquals(1, loreNbt.getSize());
|
|
||||||
assertEquals(lore, loreNbt.asListView().stream().map(line -> GsonComponentSerializer.gson().deserialize(line.getValue())).toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that lore can be properly removed without residual (display compound)
|
|
||||||
item = item.withLore(List.of());
|
|
||||||
assertNull(item.meta().toNBT().get("display"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testBuilderReuse() {
|
public void testBuilderReuse() {
|
||||||
var builder = ItemStack.builder(Material.DIAMOND);
|
var builder = ItemStack.builder(Material.DIAMOND);
|
||||||
|
@ -2,6 +2,7 @@ package net.minestom.server.tag;
|
|||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static net.minestom.server.api.TestUtils.assertEqualsSNBT;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
|
||||||
@ -23,6 +24,48 @@ public class TagAtomicTest {
|
|||||||
assertEquals(10, handler.getTag(tag));
|
assertEquals(10, handler.getTag(tag));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void updateDefault() {
|
||||||
|
var tag = Tag.Integer("coin").defaultValue(25);
|
||||||
|
var handler = TagHandler.newHandler();
|
||||||
|
handler.updateTag(tag, integer -> {
|
||||||
|
assertEquals(25, integer);
|
||||||
|
return 5;
|
||||||
|
});
|
||||||
|
assertEquals(5, handler.getTag(tag));
|
||||||
|
handler.updateTag(tag, integer -> {
|
||||||
|
assertEquals(5, integer);
|
||||||
|
return 10;
|
||||||
|
});
|
||||||
|
assertEquals(10, handler.getTag(tag));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void updateRemoval() {
|
||||||
|
var tag = Tag.Integer("coin");
|
||||||
|
var handler = TagHandler.newHandler();
|
||||||
|
handler.setTag(tag, 5);
|
||||||
|
handler.updateTag(tag, integer -> {
|
||||||
|
assertEquals(5, integer);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
assertNull(handler.getTag(tag));
|
||||||
|
assertEqualsSNBT("{}", handler.asCompound());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void updateRemovalPath() {
|
||||||
|
var tag = Tag.Integer("coin").path("path");
|
||||||
|
var handler = TagHandler.newHandler();
|
||||||
|
handler.setTag(tag, 5);
|
||||||
|
handler.updateTag(tag, integer -> {
|
||||||
|
assertEquals(5, integer);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
assertNull(handler.getTag(tag));
|
||||||
|
assertEqualsSNBT("{}", handler.asCompound());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void updateAndGet() {
|
public void updateAndGet() {
|
||||||
var tag = Tag.Integer("coin");
|
var tag = Tag.Integer("coin");
|
||||||
|
@ -0,0 +1,63 @@
|
|||||||
|
package net.minestom.server.tag;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static net.minestom.server.api.TestUtils.assertEqualsSNBT;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||||
|
|
||||||
|
public class TagHandlerReadableCopyTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void copyCache() {
|
||||||
|
var tag = Tag.String("key");
|
||||||
|
var handler = TagHandler.newHandler();
|
||||||
|
handler.setTag(tag, "test");
|
||||||
|
|
||||||
|
var copy = handler.readableCopy();
|
||||||
|
assertEquals(handler.getTag(tag), copy.getTag(tag));
|
||||||
|
|
||||||
|
handler.setTag(tag, "test2");
|
||||||
|
assertEquals("test2", handler.getTag(tag));
|
||||||
|
assertEquals("test", copy.getTag(tag));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void copyCachePath() {
|
||||||
|
var tag = Tag.String("key").path("path");
|
||||||
|
var handler = TagHandler.newHandler();
|
||||||
|
handler.setTag(tag, "test");
|
||||||
|
assertEqualsSNBT("""
|
||||||
|
{"path":{"key":"test"}}
|
||||||
|
""", handler.asCompound());
|
||||||
|
|
||||||
|
var copy = handler.readableCopy();
|
||||||
|
handler.setTag(tag, "test2");
|
||||||
|
assertEquals("test2", handler.getTag(tag));
|
||||||
|
assertEquals("test", copy.getTag(tag));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void copyCacheReuse() {
|
||||||
|
var handler = TagHandler.newHandler();
|
||||||
|
handler.setTag(Tag.String("key"), "test");
|
||||||
|
assertSame(handler.readableCopy(), handler.readableCopy());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void copyRehashing() {
|
||||||
|
var tag = Tag.String("key");
|
||||||
|
var handler = TagHandler.newHandler();
|
||||||
|
handler.setTag(tag, "test");
|
||||||
|
var copy = handler.readableCopy();
|
||||||
|
for (int i = 0; i < 1000; i++) {
|
||||||
|
handler.setTag(Tag.Integer("copyRehashing" + i), i);
|
||||||
|
}
|
||||||
|
assertEquals("test", handler.getTag(tag));
|
||||||
|
assertEquals("test", copy.getTag(tag));
|
||||||
|
|
||||||
|
handler.setTag(tag, "test2");
|
||||||
|
assertEquals("test2", handler.getTag(tag));
|
||||||
|
assertEquals("test", copy.getTag(tag));
|
||||||
|
}
|
||||||
|
}
|
@ -62,6 +62,14 @@ public class TagTest {
|
|||||||
assertEquals(mutable.toCompound(), handler.asCompound(), "NBT is not the same");
|
assertEquals(mutable.toCompound(), handler.asCompound(), "NBT is not the same");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void fromNbtCache() {
|
||||||
|
// Ensure that TagHandler#asCompound reuse the same compound used for construction
|
||||||
|
var compound = NBT.Compound(Map.of("key", NBT.Int(5)));
|
||||||
|
var handler = TagHandler.fromCompound(compound);
|
||||||
|
assertSame(compound, handler.asCompound(), "NBT is not the same");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void defaultValue() {
|
public void defaultValue() {
|
||||||
var nullable = Tag.String("key");
|
var nullable = Tag.String("key");
|
||||||
|
Loading…
Reference in New Issue
Block a user