package net.minestom.server.tag; import net.minestom.server.utils.PropertyUtils; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.UnknownNullability; import org.jglrxavpok.hephaistos.nbt.NBT; import org.jglrxavpok.hephaistos.nbt.NBTCompound; import org.jglrxavpok.hephaistos.nbt.NBTCompoundLike; import org.jglrxavpok.hephaistos.nbt.NBTType; import org.jglrxavpok.hephaistos.nbt.mutable.MutableNBTCompound; import java.lang.invoke.VarHandle; import java.util.function.UnaryOperator; final class TagHandlerImpl implements TagHandler { private static final boolean CACHE_ENABLE = PropertyUtils.getBoolean("minestom.tag-handler-cache", true); static final Serializers.Entry NODE_SERIALIZER = new Serializers.Entry<>(NBTType.TAG_Compound, entries -> fromCompound(entries).root, Node::compound, true); private final Node root; private volatile Node copy; TagHandlerImpl(Node root) { this.root = root; } TagHandlerImpl() { this.root = new Node(); } static TagHandlerImpl fromCompound(NBTCompoundLike compoundLike) { final NBTCompound compound = compoundLike.toCompound(); TagHandlerImpl handler = new TagHandlerImpl(); TagNbtSeparator.separate(compound, entry -> handler.setTag(entry.tag(), entry.value())); handler.root.compound = compound; return handler; } @Override public @UnknownNullability T getTag(@NotNull Tag tag) { VarHandle.fullFence(); return root.getTag(tag); } @Override public void setTag(@NotNull Tag tag, @Nullable T value) { // Handle view tags if (tag.isView()) { synchronized (this) { Node syncNode = traversePathWrite(root, tag, value != null); if (syncNode != null) { syncNode.updateContent(value != null ? (NBTCompound) tag.entry.write(value) : NBTCompound.EMPTY); syncNode.invalidate(); } } 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> entries = node.entries; if (value != null) { Entry previous = entries.get(tagIndex); if (previous != null && previous.tag().shareValue(tag)) { previous.updateValue(tag.copyValue(value)); } else { synchronized (this) { node = traversePathWrite(root, tag, true); node.entries.put(tagIndex, valueToEntry(node, tag, value)); } } } else { synchronized (this) { node = traversePathWrite(root, tag, false); if (node != null) node.entries.remove(tagIndex); } } node.invalidate(); } @Override public void updateTag(@NotNull Tag tag, @NotNull UnaryOperator<@UnknownNullability T> value) { updateTag0(tag, value, false); } @Override public @UnknownNullability T updateAndGetTag(@NotNull Tag tag, @NotNull UnaryOperator<@UnknownNullability T> value) { return updateTag0(tag, value, false); } @Override public @UnknownNullability T getAndUpdateTag(@NotNull Tag tag, @NotNull UnaryOperator<@UnknownNullability T> value) { return updateTag0(tag, value, true); } private synchronized T updateTag0(@NotNull Tag tag, @NotNull UnaryOperator value, boolean returnPrevious) { final int tagIndex = tag.index; final Node node = traversePathWrite(root, tag, true); StaticIntMap> 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 public @NotNull TagReadable readableCopy() { Node copy = this.copy; if (copy == null) { synchronized (this) { this.copy = copy = root.copy(null); } } 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 public @NotNull NBTCompound asCompound() { VarHandle.fullFence(); return root.compound(); } 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) { final Tag.PathEntry[] paths = tag.path; if (paths == null) return root; Node local = root; for (Tag.PathEntry path : paths) { final int pathIndex = path.index(); final Entry entry = local.entries.get(pathIndex); if (entry != null && entry.tag.entry.isPath()) { // Existing path, continue navigating final Node tmp = (Node) entry.value; assert tmp.parent == local : "Path parent is invalid: " + tmp.parent + " != " + local; local = tmp; } else { 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. // Slow path is taken if the entry comes from a Structure tag, requiring conversion from NBT Node tmp = local; local = new Node(tmp); if (synEntry != null && synEntry.nbt() instanceof NBTCompound compound) { local.updateContent(compound); } tmp.entries.put(pathIndex, Entry.makePathEntry(path.name(), local)); } } } return local; } private Entry valueToEntry(Node parent, Tag tag, @NotNull T value) { if (value instanceof NBT nbt) { if (nbt instanceof NBTCompound compound) { final TagHandlerImpl handler = fromCompound(compound); return Entry.makePathEntry(tag, new Node(parent, handler.root.entries)); } else { final var nbtEntry = TagNbtSeparator.separateSingle(tag.getKey(), nbt); return new Entry<>(nbtEntry.tag(), nbtEntry.value()); } } else { return new Entry<>(tag, tag.copyValue(value)); } } final class Node implements TagReadable { final Node parent; final StaticIntMap> entries; NBTCompound compound; public Node(Node parent, StaticIntMap> entries) { this.parent = parent; this.entries = entries; } Node(Node parent) { this(parent, new StaticIntMap.Array<>()); } Node() { this(null); } @Override public @UnknownNullability T getTag(@NotNull Tag tag) { final Node node = traversePathRead(this, tag); if (node == null) return tag.createDefault(); // Must be a path-able entry, but not present if (tag.isView()) return tag.read(node.compound()); final StaticIntMap> entries = node.entries; final Entry entry = entries.get(tag.index); if (entry == null) return tag.createDefault(); // Not present 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.nbt(); final Serializers.Entry serializerEntry = tag.entry; final NBTType type = serializerEntry.nbtType(); return type == null || type == nbt.getID() ? serializerEntry.read(nbt) : tag.createDefault(); } void updateContent(@NotNull NBTCompoundLike compoundLike) { final NBTCompound compound = compoundLike.toCompound(); final TagHandlerImpl converted = fromCompound(compound); this.entries.updateContent(converted.root.entries); this.compound = compound; } NBTCompound compound() { NBTCompound compound; if (!CACHE_ENABLE || (compound = this.compound) == null) { MutableNBTCompound tmp = new MutableNBTCompound(); this.entries.forValues(entry -> { final Tag tag = entry.tag(); 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> 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 { nbt = entry.nbt(); } tmp.put(tag.getKey(), nbt); entries.put(tag.index, valueToEntry(result, tag, value)); }); if (tmp.isEmpty() && parent != null) return null; // Empty child node result.compound = tmp.toCompound(); return result; } void invalidate() { Node tmp = this; do tmp.compound = null; while ((tmp = tmp.parent) != null); TagHandlerImpl.this.copy = null; } } private static final class Entry { private final Tag tag; T value; NBT nbt; Entry(Tag tag, T value) { this.tag = tag; this.value = value; } 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 tag() { return tag; } public T value() { return value; } public NBT nbt() { if (tag.entry.isPath()) return ((Node) value).compound(); NBT nbt = this.nbt; if (nbt == null) this.nbt = nbt = tag.entry.write(value); return nbt; } public void updateValue(T value) { assert !tag.entry.isPath(); this.value = value; this.nbt = null; } Node toNode() { if (tag.entry.isPath()) return (Node) value; if (nbt() instanceof NBTCompound compound) { // 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 return null; } } }