package net.minestom.server.tag; import net.kyori.adventure.text.Component; import net.minestom.server.item.ItemStack; import net.minestom.server.utils.collection.AutoIncrementMap; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jglrxavpok.hephaistos.nbt.NBT; import org.jglrxavpok.hephaistos.nbt.NBTCompoundLike; import org.jglrxavpok.hephaistos.nbt.NBTList; import org.jglrxavpok.hephaistos.nbt.NBTType; import org.jglrxavpok.hephaistos.nbt.mutable.MutableNBTCompound; import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.UUID; import java.util.function.Function; import java.util.function.Supplier; import java.util.function.UnaryOperator; /** * Represents a key to retrieve or change a value. *

* All tags are serializable. * * @param the tag type */ @ApiStatus.NonExtendable public class Tag { private static final AutoIncrementMap INDEX_MAP = new AutoIncrementMap<>(); record PathEntry(String name, int index) { } final int index; private final String key; final Serializers.Entry entry; private final Supplier defaultValue; final Function readComparator; // Optional properties final PathEntry[] path; final UnaryOperator copy; final int listScope; Tag(int index, String key, Function readComparator, Serializers.Entry entry, Supplier defaultValue, PathEntry[] path, UnaryOperator copy, int listScope) { assert index == INDEX_MAP.get(key); this.index = index; this.key = key; this.readComparator = readComparator; this.entry = entry; this.defaultValue = defaultValue; this.path = path; this.copy = copy; this.listScope = listScope; } static Tag tag(@NotNull String key, @NotNull Serializers.Entry entry) { return new Tag<>(INDEX_MAP.get(key), key, entry.reader(), (Serializers.Entry) entry, null, null, null, 0); } static Tag fromSerializer(@NotNull String key, @NotNull TagSerializer serializer) { if (serializer instanceof TagRecord.Serializer recordSerializer) { // Allow fast retrieval //noinspection unchecked return tag(key, recordSerializer.serializerEntry); } return tag(key, Serializers.fromTagSerializer(serializer)); } /** * Returns the key used to navigate inside the holder nbt. * * @return the tag key */ public @NotNull String getKey() { return key; } @Contract(value = "_ -> new", pure = true) public Tag defaultValue(@NotNull Supplier defaultValue) { return new Tag<>(index, key, readComparator, entry, defaultValue, path, copy, listScope); } @Contract(value = "_ -> new", pure = true) public Tag defaultValue(@NotNull T defaultValue) { return defaultValue(() -> defaultValue); } @Contract(value = "_, _ -> new", pure = true) public Tag map(@NotNull Function readMap, @NotNull Function writeMap) { var entry = this.entry; final Function readFunction = entry.reader().andThen(t -> { if (t == null) return null; return readMap.apply(t); }); final Function writeFunction = writeMap.andThen(entry.writer()); return new Tag<>(index, key, readMap, new Serializers.Entry<>(entry.nbtType(), readFunction, writeFunction), // Default value () -> { T defaultValue = createDefault(); if (defaultValue == null) return null; return readMap.apply(defaultValue); }, path, null, listScope); } @ApiStatus.Experimental @Contract(value = "-> new", pure = true) public Tag> list() { var entry = this.entry; var readFunction = entry.reader(); var writeFunction = entry.writer(); var listEntry = new Serializers.Entry, NBTList>( NBTType.TAG_List, read -> { if (read.isEmpty()) return List.of(); return read.asListView().stream().map(readFunction).toList(); }, write -> { if (write.isEmpty()) return NBT.List(NBTType.TAG_String); // String is the default type for lists final List list = write.stream().map(writeFunction).toList(); final NBTType type = list.get(0).getID(); return NBT.List(type, list); }); UnaryOperator> co = this.copy != null ? ts -> { final int size = ts.size(); T[] array = (T[]) new Object[size]; boolean shallowCopy = true; for (int i = 0; i < size; i++) { final T t = ts.get(i); final T copy = this.copy.apply(t); if (shallowCopy && copy != t) shallowCopy = false; array[i] = copy; } return shallowCopy ? List.copyOf(ts) : List.of(array); } : List::copyOf; return new Tag<>(index, key, readComparator, Serializers.Entry.class.cast(listEntry), null, path, co, listScope + 1); } @ApiStatus.Experimental @Contract(value = "_ -> new", pure = true) public Tag path(@NotNull String @Nullable ... path) { if (path == null || path.length == 0) { return new Tag<>(index, key, readComparator, entry, defaultValue, null, copy, listScope); } PathEntry[] pathEntries = new PathEntry[path.length]; for (int i = 0; i < path.length; i++) { final String name = path[i]; if (name == null || name.isEmpty()) throw new IllegalArgumentException("Path must not be empty: " + Arrays.toString(path)); pathEntries[i] = new PathEntry(name, INDEX_MAP.get(name)); } return new Tag<>(index, key, readComparator, entry, defaultValue, pathEntries, copy, listScope); } public @Nullable T read(@NotNull NBTCompoundLike nbt) { final NBT readable = isView() ? nbt.toCompound() : nbt.get(key); final T result; try { if (readable == null || (result = entry.read(readable)) == null) return createDefault(); return result; } catch (ClassCastException e) { return createDefault(); } } public void write(@NotNull MutableNBTCompound nbtCompound, @Nullable T value) { if (value != null) { final NBT nbt = entry.write(value); if (isView()) nbtCompound.copyFrom((NBTCompoundLike) nbt); else nbtCompound.set(key, nbt); } else { if (isView()) nbtCompound.clear(); else nbtCompound.remove(key); } } public void writeUnsafe(@NotNull MutableNBTCompound nbtCompound, @Nullable Object value) { //noinspection unchecked write(nbtCompound, (T) value); } final boolean isView() { return key.isEmpty(); } final boolean shareValue(@NotNull Tag other) { if (this == other) return true; // Tags are not strictly the same, compare readers if (this.listScope != other.listScope) return false; return this.readComparator == other.readComparator; } final T createDefault() { final Supplier supplier = defaultValue; return supplier != null ? supplier.get() : null; } final T copyValue(@NotNull T value) { final UnaryOperator copier = copy; return copier != null ? copier.apply(value) : value; } @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(@NotNull String key) { return tag(key, Serializers.BYTE); } public static @NotNull Tag Boolean(@NotNull String key) { return tag(key, Serializers.BOOLEAN); } public static @NotNull Tag Short(@NotNull String key) { return tag(key, Serializers.SHORT); } public static @NotNull Tag Integer(@NotNull String key) { return tag(key, Serializers.INT); } public static @NotNull Tag Long(@NotNull String key) { return tag(key, Serializers.LONG); } public static @NotNull Tag Float(@NotNull String key) { return tag(key, Serializers.FLOAT); } public static @NotNull Tag Double(@NotNull String key) { return tag(key, Serializers.DOUBLE); } public static @NotNull Tag String(@NotNull String key) { return tag(key, Serializers.STRING); } @ApiStatus.Experimental public static @NotNull Tag UUID(@NotNull String key) { return tag(key, Serializers.UUID); } public static @NotNull Tag ItemStack(@NotNull String key) { return tag(key, Serializers.ITEM); } public static @NotNull Tag Component(@NotNull String key) { return tag(key, Serializers.COMPONENT); } /** * Creates a flexible tag able to read and write any {@link NBT} objects. *

* Specialized tags are recommended if the type is known as conversion will be required both way (read and write). */ public static @NotNull Tag NBT(@NotNull String key) { return tag(key, Serializers.NBT_ENTRY); } /** * Creates a tag containing multiple fields. *

* Those fields cannot be modified from an outside tag. (This is to prevent the backed object from becoming out of sync) * * @param key the tag key * @param serializer the tag serializer * @param the tag type * @return the created tag */ public static @NotNull Tag Structure(@NotNull String key, @NotNull TagSerializer serializer) { return fromSerializer(key, serializer); } /** * Specialized Structure tag affecting the src of the handler (i.e. overwrite all its data). *

* Must be used with care. */ public static @NotNull Tag View(@NotNull TagSerializer serializer) { return Structure("", serializer); } @ApiStatus.Experimental public static @NotNull Tag Structure(@NotNull String key, @NotNull Class type) { return Structure(key, TagRecord.serializer(type)); } @ApiStatus.Experimental public static @NotNull Tag View(@NotNull Class type) { return View(TagRecord.serializer(type)); } }