diff --git a/src/main/java/org/glavo/nbt/NBTPath.java b/src/main/java/org/glavo/nbt/NBTPath.java index a6fdc2e..5abfe54 100644 --- a/src/main/java/org/glavo/nbt/NBTPath.java +++ b/src/main/java/org/glavo/nbt/NBTPath.java @@ -22,6 +22,8 @@ import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.Nullable; +import java.util.*; + /// An NBT path is a descriptive string used to specify one or more particular elements from an NBT data tree. /// /// @see NBT Path - Minecraft Wiki @@ -34,6 +36,25 @@ static NBTPath of(String path) throws IllegalArgumentException { return new SNBTParser(path, 0, path.length()).nextPath(); } + /// Get the path from the root to the given tag. + /// + /// @return the path or `null` if parent is null. + /// @throws IllegalStateException when the expected root doesn't match the actual root. + @Contract(pure = true) + static @Nullable NBTPath of(T tag) throws IllegalArgumentException, IllegalStateException { + return NBTPathImpl.of(tag, null); + } + + /// Get the path from the root to the given tag. + /// + /// @param expectedRoot the expected root instead of the top of the tree. + /// @return the path or `null` if parent is null. + /// @throws IllegalStateException when the expected root doesn't match the actual root. + @Contract(pure = true) + static @Nullable NBTPath of(T tag, Tag expectedRoot) throws IllegalArgumentException, IllegalStateException { + return NBTPathImpl.of(tag, expectedRoot); + } + /// Returns the tag type of this path. @Contract(pure = true) @Nullable TagType getTagType(); @@ -50,4 +71,16 @@ static NBTPath of(String path) throws IllegalArgumentException { @Contract(pure = true) NBTPath withTagType(TagType tagType) throws IllegalStateException; + /// Returns the path string. + /// + /// @param omitDots `true` to omit dots if possible. + @Contract(pure = true) + String toPathString(boolean omitDots); + + /// Returns the path string with dots omitted if possible. + @Contract(pure = true) + default String toPathString() { + return toPathString(true); + } + } diff --git a/src/main/java/org/glavo/nbt/internal/path/NBTPathImpl.java b/src/main/java/org/glavo/nbt/internal/path/NBTPathImpl.java index 6a9c432..332df4f 100644 --- a/src/main/java/org/glavo/nbt/internal/path/NBTPathImpl.java +++ b/src/main/java/org/glavo/nbt/internal/path/NBTPathImpl.java @@ -15,6 +15,7 @@ */ package org.glavo.nbt.internal.path; +import org.glavo.nbt.NBTElement; import org.glavo.nbt.NBTParent; import org.glavo.nbt.NBTPath; import org.glavo.nbt.chunk.Chunk; @@ -22,14 +23,14 @@ import org.glavo.nbt.internal.snbt.SNBTWriter; import org.glavo.nbt.io.SNBTCodec; import org.glavo.nbt.tag.CompoundTag; +import org.glavo.nbt.tag.ParentTag; import org.glavo.nbt.tag.Tag; import org.glavo.nbt.tag.TagType; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; import java.io.IOException; -import java.util.Arrays; -import java.util.Objects; +import java.util.*; import java.util.stream.Stream; public final class NBTPathImpl implements NBTPath { @@ -59,6 +60,41 @@ public static Stream select(NBTParent parent, NBTPath) tags; } + @SuppressWarnings("unchecked") + public static @Nullable NBTPath of(T tag, @Nullable Tag expectedRoot) throws IllegalArgumentException, IllegalStateException { + List paths = new ArrayList<>(); + + NBTPathNode indicator = NBTPathImpl.getIndicator(tag); + NBTParent next = tag.getParent(); + while (next != null && indicator != null) { + paths.add(indicator); + indicator = NBTPathImpl.getIndicator(next); + if (next == expectedRoot) break; + else next = next.getParent(); + } + + if (expectedRoot != null && expectedRoot != next) { + throw new IllegalStateException("Unexpected root tag " + expectedRoot + ", expected " + next + "."); + } else if (paths.isEmpty()) { + return null; + } + Collections.reverse(paths); + return new NBTPathImpl<>(paths.toArray(NBTPathNode[]::new), (TagType) tag.getType()); + } + + /// Get the indicator of the given tag, depends on its parent tag. + /// + /// @return the name node if parent is [CompoundTag], the index node if parent is other [ParentTag], or `null` if parent is null. + public static @Nullable NBTPathNode getIndicator(@Nullable NBTElement tag) { + if (!(tag instanceof Tag currentTag)) return null; + NBTParent parentTag = tag.getParent(); + if (parentTag instanceof ParentTag) { + return parentTag instanceof CompoundTag ? new NBTPathNode.NamedSubTag(currentTag.getName()) : new NBTPathNode.Index(currentTag.getIndex()); + } else { // Chunk, ChunkRegion or null + return null; + } + } + private final NBTPathNode @Unmodifiable [] nodes; private final @Nullable TagType tagType; @@ -107,33 +143,31 @@ public int hashCode() { } @Override - public String toString() { - if (cachedString == null) { - StringBuilder builder = new StringBuilder(); - - if (tagType != null) { - builder.append("<").append(tagType).append("> "); + public String toPathString(boolean omitDots) { + StringBuilder builder = new StringBuilder(); + + SNBTWriter writer = new SNBTWriter<>(SNBTCodec.ofCompact(), builder); + for (int i = 0; i < nodes.length; i++) { + NBTPathNode node = nodes[i]; + try { + node.appendTo(writer); + } catch (IOException e) { + throw new AssertionError(e); } - - var writer = new SNBTWriter<>(SNBTCodec.ofCompact(), builder); - - boolean first = true; - for (NBTPathNode node : nodes) { - if (first) { - first = false; - } else if (node.needDot()) { - writer.getAppendable().append('.'); - } - - try { - node.appendTo(writer); - } catch (IOException e) { - throw new AssertionError(e); - } + if (i + 1 < nodes.length && (!omitDots || nodes[i + 1].needDot())) { + writer.getAppendable().append('.'); } + } - builder.append(']'); - cachedString = builder.toString(); + return builder.toString(); + } + + @Override + public String toString() { + if (cachedString == null) { + String pathString = toPathString(); + if (tagType != null) pathString = "<" + tagType + ">" + " " + pathString; + cachedString = pathString; } return cachedString; diff --git a/src/test/java/org/glavo/nbt/NBTPathTest.java b/src/test/java/org/glavo/nbt/NBTPathTest.java index ce5d648..152321a 100644 --- a/src/test/java/org/glavo/nbt/NBTPathTest.java +++ b/src/test/java/org/glavo/nbt/NBTPathTest.java @@ -15,18 +15,15 @@ */ package org.glavo.nbt; +import org.glavo.nbt.chunk.Chunk; +import org.glavo.nbt.chunk.ChunkRegion; import org.glavo.nbt.io.NBTCodec; import org.glavo.nbt.tag.*; import org.junit.jupiter.api.Test; import java.util.List; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertIterableEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.*; final class NBTPathTest { @@ -188,4 +185,124 @@ void testInvalidPathSyntax() { assertThrows(IllegalArgumentException.class, () -> NBTPath.of("\"unterminated")); assertThrows(IllegalArgumentException.class, () -> NBTPath.of("[2147483648]")); } + + @Test + void testPathString() { + assertEquals("{}", NBTPath.of("{}").toPathString()); + assertEquals("{Invisible:1B}", NBTPath.of("{Invisible:1b}").toPathString()); + assertEquals("\"A Very Cool Name[]\"", NBTPath.of("\"A Very Cool Name[]\"").toPathString()); + assertEquals("\"A Very Cool Name[]\"{}", NBTPath.of("\"A Very Cool Name[]\"{}").toPathString()); + assertEquals("\"A Very Cool Name[]\"[]", NBTPath.of("\"A Very Cool Name[]\"[]").toPathString()); + assertEquals("\"A Very Cool Name[]\"[{}]", NBTPath.of("\"A Very Cool Name[]\"[{}]").toPathString()); + assertEquals("\"A Very Cool Name[]\"[{Count:25B}]", NBTPath.of("\"A Very Cool Name[]\"[{Count:25b}]").toPathString()); + assertEquals("\"A Very Cool Name[]\"[][][]", NBTPath.of("\"A Very Cool Name[]\"[][][]").toPathString()); + assertEquals("foo.bar", NBTPath.of("foo.bar").toPathString()); + assertEquals("foo.bar[]", NBTPath.of("foo.bar.[]").toPathString()); + assertEquals("foo.bar[{}]", NBTPath.of("foo.bar.[{}]").toPathString()); + assertEquals("foo.bar[0]", NBTPath.of("foo.bar.[0]").toPathString()); + assertEquals("foo.bar[-1]", NBTPath.of("foo.bar.[-1]").toPathString()); + assertEquals("foo.bar.\"0123\"", NBTPath.of("foo.bar.\"0123\"").toPathString()); + } + + @Test + void testPathStringKeepDots() { + assertEquals("{}", NBTPath.of("{}").toPathString(false)); + assertEquals("{Invisible:1B}", NBTPath.of("{Invisible:1b}").toPathString(false)); + assertEquals("\"A Very Cool Name[]\"", NBTPath.of("\"A Very Cool Name[]\"").toPathString(false)); + assertEquals("\"A Very Cool Name[]\"{}", NBTPath.of("\"A Very Cool Name[]\"{}").toPathString(false)); + assertEquals("\"A Very Cool Name[]\".[]", NBTPath.of("\"A Very Cool Name[]\"[]").toPathString(false)); + assertEquals("\"A Very Cool Name[]\".[{}]", NBTPath.of("\"A Very Cool Name[]\"[{}]").toPathString(false)); + assertEquals("\"A Very Cool Name[]\".[{Count:25B}]", NBTPath.of("\"A Very Cool Name[]\"[{Count:25b}]").toPathString(false)); + assertEquals("\"A Very Cool Name[]\".[].[].[]", NBTPath.of("\"A Very Cool Name[]\"[][][]").toPathString(false)); + assertEquals("foo.bar", NBTPath.of("foo.bar").toPathString(false)); + assertEquals("foo.bar.[]", NBTPath.of("foo.bar.[]").toPathString(false)); + assertEquals("foo.bar.[{}]", NBTPath.of("foo.bar.[{}]").toPathString(false)); + assertEquals("foo.bar.[0]", NBTPath.of("foo.bar.[0]").toPathString(false)); + assertEquals("foo.bar.[-1]", NBTPath.of("foo.bar.[-1]").toPathString(false)); + assertEquals("foo.bar.\"0123\"", NBTPath.of("foo.bar.\"0123\"").toPathString(false)); + } + + @Test + void testOfPath() { + CompoundTag root = new CompoundTag().setName("root"); + IntTag tag; + + root.addTag("foo", new CompoundTag() + .addTag("bar", new ListTag() + .addTag(new IntTag(0)) + .addTag(new IntTag(1)) + .addTag(tag = new IntTag(2)) + )); + + NBTPath pathTo2 = NBTPath.of(tag, root); + assertNotNull(pathTo2); + assertEquals(2, root.getFirstInt(pathTo2)); + assertEquals(NBTPath.of("foo.bar[2]").withTagType(TagType.INT), pathTo2); + } + + @Test + void testOfPath2() { + CompoundTag root = new CompoundTag().setName("root"); + CompoundTag expectedRoot; + IntTag tag; + + root.addTag("foo", expectedRoot = new CompoundTag() + .addTag("bar", new CompoundTag() + .addTag("baz", new ListTag() + .addTag(new IntTag(0)) + .addTag(new IntTag(1)) + .addTag(tag = new IntTag(2)) + ))); + + NBTPath pathTo2 = NBTPath.of(tag, expectedRoot); + assertNotNull(pathTo2); + assertEquals(2, expectedRoot.getFirstInt(pathTo2)); + assertEquals(NBTPath.of("bar.baz[2]").withTagType(TagType.INT), pathTo2); + } + + @Test + void testOfPath3() { + CompoundTag root = new CompoundTag().setName("root"); + StringTag tag; + + root.addTag("Very Cool Name", new CompoundTag() + .addTag("bar", new CompoundTag() + .addTag("baz", tag = new StringTag(":D")))); + + NBTPath pathToSmile = NBTPath.of(tag, root); + NBTPath pathToRoot = NBTPath.of(tag); + assertNotNull(pathToSmile); + assertEquals(":D", root.getFirstString(pathToSmile)); + assertEquals(pathToSmile, pathToRoot); + assertEquals(NBTPath.of("\"Very Cool Name\".bar.baz").withTagType(TagType.STRING), pathToSmile); + } + + @Test + void testOfPath4() { + ChunkRegion chunkRegion = new ChunkRegion(); + Chunk chunk = chunkRegion.getChunk(0, 0); + CompoundTag rootTag; + chunk.setRootTag(rootTag = new CompoundTag()); + StringTag testTag; + rootTag.addTag("test", testTag = new StringTag("TEST")); + + NBTPath pathToTest = NBTPath.of(testTag); + assertNotNull(pathToTest); + assertEquals("test", pathToTest.toPathString()); + assertEquals("TEST", chunkRegion.getFirstString(pathToTest)); + assertEquals("TEST", chunk.getFirstString(pathToTest)); + } + + @Test + void testOfPath5() { + CompoundTag rootTag; + Chunk chunk = new Chunk(rootTag = new CompoundTag()); + StringTag testTag; + rootTag.addTag("test", testTag = new StringTag("TEST")); + + NBTPath path = NBTPath.of(testTag); + assertNotNull(path); + assertEquals("TEST", chunk.getFirstString(path)); + assertEquals("test", path.toPathString()); + } }