From 0ce1c5792c1b2059edd735eb4c604e1a1c451a6b Mon Sep 17 00:00:00 2001 From: Taskeren Date: Sun, 7 Jun 2026 16:38:18 +0800 Subject: [PATCH 1/9] Add NBTPath#of(Tag, Tag) --- src/main/java/org/glavo/nbt/NBTPath.java | 40 +++++++++++++ src/test/java/org/glavo/nbt/TestNBTPath.java | 62 ++++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 src/test/java/org/glavo/nbt/TestNBTPath.java diff --git a/src/main/java/org/glavo/nbt/NBTPath.java b/src/main/java/org/glavo/nbt/NBTPath.java index a6fdc2e..de17b54 100644 --- a/src/main/java/org/glavo/nbt/NBTPath.java +++ b/src/main/java/org/glavo/nbt/NBTPath.java @@ -17,11 +17,15 @@ import org.glavo.nbt.internal.path.NBTPathImpl; import org.glavo.nbt.internal.snbt.SNBTParser; +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.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 +38,42 @@ 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. + /// + /// @param expectedRoot the expected root instead of the top of the tree. + /// @return the path or `null` if parent is null. + @SuppressWarnings({"DataFlowIssue", "unchecked"}) + @Contract(pure = true) + static @Nullable NBTPath of(T tag, @Nullable Tag expectedRoot) { + ParentTag parentTag = tag.getParentTag(); + if (parentTag == null) return null; + + List paths = new ArrayList<>(); + paths.add(getIndicator(tag)); + while (true) { + if (parentTag == expectedRoot) break; + paths.add(getIndicator(parentTag)); + ParentTag parent = parentTag.getParentTag(); + if (parent == null) break; + parentTag = parent; + } + + if (parentTag != expectedRoot) + throw new IllegalStateException("Unexpected root tag " + parentTag + ", expected " + expectedRoot + "."); + Collections.reverse(paths); + String pathString = String.join(".", paths); + return (NBTPath) of(pathString).withTagType(tag.getType()); + } + + /// Get the indicator of the given tag, depends on its parent tag. + /// + /// @return the name if parent is [CompoundTag], the index if parent is other [ParentTag], or `null` if parent is null. + @Contract(pure = true) + static @Nullable String getIndicator(Tag tag) { + ParentTag parentTag = tag.getParentTag(); + return parentTag == null ? null : parentTag instanceof CompoundTag ? tag.getName() : ('[' + String.valueOf(tag.getIndex()) + ']'); + } + /// Returns the tag type of this path. @Contract(pure = true) @Nullable TagType getTagType(); diff --git a/src/test/java/org/glavo/nbt/TestNBTPath.java b/src/test/java/org/glavo/nbt/TestNBTPath.java new file mode 100644 index 0000000..1d0b841 --- /dev/null +++ b/src/test/java/org/glavo/nbt/TestNBTPath.java @@ -0,0 +1,62 @@ +/* + * Copyright 2026 Taskeren + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.glavo.nbt; + +import org.glavo.nbt.tag.*; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +final class TestNBTPath { + + @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); + } +} From 6feb8db7f7d6cd9a6c8042afbffa3e0859edbe76 Mon Sep 17 00:00:00 2001 From: Taskeren Date: Sun, 7 Jun 2026 16:43:11 +0800 Subject: [PATCH 2/9] Add throwing information --- src/main/java/org/glavo/nbt/NBTPath.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/glavo/nbt/NBTPath.java b/src/main/java/org/glavo/nbt/NBTPath.java index de17b54..d718aee 100644 --- a/src/main/java/org/glavo/nbt/NBTPath.java +++ b/src/main/java/org/glavo/nbt/NBTPath.java @@ -42,9 +42,10 @@ static NBTPath of(String path) throws IllegalArgumentException { /// /// @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. @SuppressWarnings({"DataFlowIssue", "unchecked"}) @Contract(pure = true) - static @Nullable NBTPath of(T tag, @Nullable Tag expectedRoot) { + static @Nullable NBTPath of(T tag, @Nullable Tag expectedRoot) throws IllegalArgumentException, IllegalStateException { ParentTag parentTag = tag.getParentTag(); if (parentTag == null) return null; From 2436731ed1558a30f2fe2de8ee8fa8d8eba6b72e Mon Sep 17 00:00:00 2001 From: Taskeren Date: Mon, 8 Jun 2026 22:51:54 +0800 Subject: [PATCH 3/9] Add NBTPath#toPathString --- src/main/java/org/glavo/nbt/NBTPath.java | 12 +++++ .../glavo/nbt/internal/path/NBTPathImpl.java | 46 +++++++++---------- src/test/java/org/glavo/nbt/NBTPathTest.java | 36 +++++++++++++++ 3 files changed, 70 insertions(+), 24 deletions(-) diff --git a/src/main/java/org/glavo/nbt/NBTPath.java b/src/main/java/org/glavo/nbt/NBTPath.java index d718aee..5331679 100644 --- a/src/main/java/org/glavo/nbt/NBTPath.java +++ b/src/main/java/org/glavo/nbt/NBTPath.java @@ -91,4 +91,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..d063e42 100644 --- a/src/main/java/org/glavo/nbt/internal/path/NBTPathImpl.java +++ b/src/main/java/org/glavo/nbt/internal/path/NBTPathImpl.java @@ -107,33 +107,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..84721e8 100644 --- a/src/test/java/org/glavo/nbt/NBTPathTest.java +++ b/src/test/java/org/glavo/nbt/NBTPathTest.java @@ -188,4 +188,40 @@ 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)); + } } From 7617c8c5c7923356e279c6804e103b85186539d9 Mon Sep 17 00:00:00 2001 From: Taskeren Date: Mon, 8 Jun 2026 23:09:36 +0800 Subject: [PATCH 4/9] Fix --- src/main/java/org/glavo/nbt/NBTPath.java | 21 ++++++------------- .../glavo/nbt/internal/path/NBTPathImpl.java | 9 ++++++++ src/test/java/org/glavo/nbt/TestNBTPath.java | 15 +++++++++++++ 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/glavo/nbt/NBTPath.java b/src/main/java/org/glavo/nbt/NBTPath.java index 5331679..93ba15f 100644 --- a/src/main/java/org/glavo/nbt/NBTPath.java +++ b/src/main/java/org/glavo/nbt/NBTPath.java @@ -16,8 +16,8 @@ package org.glavo.nbt; import org.glavo.nbt.internal.path.NBTPathImpl; +import org.glavo.nbt.internal.path.NBTPathNode; import org.glavo.nbt.internal.snbt.SNBTParser; -import org.glavo.nbt.tag.CompoundTag; import org.glavo.nbt.tag.ParentTag; import org.glavo.nbt.tag.Tag; import org.glavo.nbt.tag.TagType; @@ -49,11 +49,11 @@ static NBTPath of(String path) throws IllegalArgumentException { ParentTag parentTag = tag.getParentTag(); if (parentTag == null) return null; - List paths = new ArrayList<>(); - paths.add(getIndicator(tag)); + List paths = new ArrayList<>(); + paths.add(NBTPathImpl.getIndicator(tag)); while (true) { if (parentTag == expectedRoot) break; - paths.add(getIndicator(parentTag)); + paths.add(NBTPathImpl.getIndicator(parentTag)); ParentTag parent = parentTag.getParentTag(); if (parent == null) break; parentTag = parent; @@ -62,17 +62,8 @@ static NBTPath of(String path) throws IllegalArgumentException { if (parentTag != expectedRoot) throw new IllegalStateException("Unexpected root tag " + parentTag + ", expected " + expectedRoot + "."); Collections.reverse(paths); - String pathString = String.join(".", paths); - return (NBTPath) of(pathString).withTagType(tag.getType()); - } - - /// Get the indicator of the given tag, depends on its parent tag. - /// - /// @return the name if parent is [CompoundTag], the index if parent is other [ParentTag], or `null` if parent is null. - @Contract(pure = true) - static @Nullable String getIndicator(Tag tag) { - ParentTag parentTag = tag.getParentTag(); - return parentTag == null ? null : parentTag instanceof CompoundTag ? tag.getName() : ('[' + String.valueOf(tag.getIndex()) + ']'); + NBTPathNode[] nodes = paths.toArray(NBTPathNode[]::new); + return (NBTPath) new NBTPathImpl<>(nodes, tag.getType()); } /// Returns the tag type of this path. 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 d063e42..3ded6e9 100644 --- a/src/main/java/org/glavo/nbt/internal/path/NBTPathImpl.java +++ b/src/main/java/org/glavo/nbt/internal/path/NBTPathImpl.java @@ -22,6 +22,7 @@ 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; @@ -59,6 +60,14 @@ public static Stream select(NBTParent parent, NBTPath) tags; } + /// 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(Tag tag) { + ParentTag parentTag = tag.getParentTag(); + return parentTag == null ? null : parentTag instanceof CompoundTag ? new NBTPathNode.NamedSubTag(tag.getName()) : new NBTPathNode.Index(tag.getIndex()); + } + private final NBTPathNode @Unmodifiable [] nodes; private final @Nullable TagType tagType; diff --git a/src/test/java/org/glavo/nbt/TestNBTPath.java b/src/test/java/org/glavo/nbt/TestNBTPath.java index 1d0b841..4a94ab7 100644 --- a/src/test/java/org/glavo/nbt/TestNBTPath.java +++ b/src/test/java/org/glavo/nbt/TestNBTPath.java @@ -59,4 +59,19 @@ void testOfPath2() { 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); + assertNotNull(pathToSmile); + assertEquals(":D", root.getFirstString(pathToSmile)); + assertEquals(NBTPath.of("\"Very Cool Name\".bar.baz").withTagType(TagType.STRING), pathToSmile); + } } From 656c87b8e8cb6aa9b507553e43b689886d312e21 Mon Sep 17 00:00:00 2001 From: Taskeren Date: Mon, 8 Jun 2026 23:09:57 +0800 Subject: [PATCH 5/9] Merge tests --- src/test/java/org/glavo/nbt/NBTPathTest.java | 61 ++++++++++++++-- src/test/java/org/glavo/nbt/TestNBTPath.java | 77 -------------------- 2 files changed, 55 insertions(+), 83 deletions(-) delete mode 100644 src/test/java/org/glavo/nbt/TestNBTPath.java diff --git a/src/test/java/org/glavo/nbt/NBTPathTest.java b/src/test/java/org/glavo/nbt/NBTPathTest.java index 84721e8..ea2816e 100644 --- a/src/test/java/org/glavo/nbt/NBTPathTest.java +++ b/src/test/java/org/glavo/nbt/NBTPathTest.java @@ -21,12 +21,8 @@ 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.*; +import static org.junit.jupiter.api.Assertions.assertNotNull; final class NBTPathTest { @@ -224,4 +220,57 @@ void testPathStringKeepDots() { 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); + assertNotNull(pathToSmile); + assertEquals(":D", root.getFirstString(pathToSmile)); + assertEquals(NBTPath.of("\"Very Cool Name\".bar.baz").withTagType(TagType.STRING), pathToSmile); + } } diff --git a/src/test/java/org/glavo/nbt/TestNBTPath.java b/src/test/java/org/glavo/nbt/TestNBTPath.java deleted file mode 100644 index 4a94ab7..0000000 --- a/src/test/java/org/glavo/nbt/TestNBTPath.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2026 Taskeren - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.glavo.nbt; - -import org.glavo.nbt.tag.*; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; - -final class TestNBTPath { - - @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); - assertNotNull(pathToSmile); - assertEquals(":D", root.getFirstString(pathToSmile)); - assertEquals(NBTPath.of("\"Very Cool Name\".bar.baz").withTagType(TagType.STRING), pathToSmile); - } -} From 3e247e8bd48ec263fde5034aedeee851c62cdf9e Mon Sep 17 00:00:00 2001 From: Taskeren Date: Tue, 9 Jun 2026 14:05:38 +0800 Subject: [PATCH 6/9] Fix doc --- src/main/java/org/glavo/nbt/NBTPath.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/glavo/nbt/NBTPath.java b/src/main/java/org/glavo/nbt/NBTPath.java index 93ba15f..a2647db 100644 --- a/src/main/java/org/glavo/nbt/NBTPath.java +++ b/src/main/java/org/glavo/nbt/NBTPath.java @@ -38,7 +38,7 @@ 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. + /// Get the path from the root tag 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. From 85da61fba0c8843edb914f1abc620c606df36a38 Mon Sep 17 00:00:00 2001 From: Taskeren Date: Wed, 1 Jul 2026 23:58:13 +0800 Subject: [PATCH 7/9] Fix expected null --- src/main/java/org/glavo/nbt/NBTPath.java | 7 ++++--- src/test/java/org/glavo/nbt/NBTPathTest.java | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/glavo/nbt/NBTPath.java b/src/main/java/org/glavo/nbt/NBTPath.java index a2647db..603e49f 100644 --- a/src/main/java/org/glavo/nbt/NBTPath.java +++ b/src/main/java/org/glavo/nbt/NBTPath.java @@ -52,14 +52,15 @@ static NBTPath of(String path) throws IllegalArgumentException { List paths = new ArrayList<>(); paths.add(NBTPathImpl.getIndicator(tag)); while (true) { - if (parentTag == expectedRoot) break; - paths.add(NBTPathImpl.getIndicator(parentTag)); + NBTPathNode parentIndicator = NBTPathImpl.getIndicator(parentTag); + if (parentTag == expectedRoot || parentIndicator == null) break; + paths.add(parentIndicator); ParentTag parent = parentTag.getParentTag(); if (parent == null) break; parentTag = parent; } - if (parentTag != expectedRoot) + if (expectedRoot != null && parentTag != expectedRoot) throw new IllegalStateException("Unexpected root tag " + parentTag + ", expected " + expectedRoot + "."); Collections.reverse(paths); NBTPathNode[] nodes = paths.toArray(NBTPathNode[]::new); diff --git a/src/test/java/org/glavo/nbt/NBTPathTest.java b/src/test/java/org/glavo/nbt/NBTPathTest.java index ea2816e..0ea8bc2 100644 --- a/src/test/java/org/glavo/nbt/NBTPathTest.java +++ b/src/test/java/org/glavo/nbt/NBTPathTest.java @@ -22,7 +22,6 @@ import java.util.List; import static org.junit.jupiter.api.Assertions.*; -import static org.junit.jupiter.api.Assertions.assertNotNull; final class NBTPathTest { @@ -269,8 +268,10 @@ void testOfPath3() { .addTag("baz", tag = new StringTag(":D")))); NBTPath pathToSmile = NBTPath.of(tag, root); + NBTPath pathToRoot = NBTPath.of(tag, null); assertNotNull(pathToSmile); assertEquals(":D", root.getFirstString(pathToSmile)); + assertEquals(pathToSmile, pathToRoot); assertEquals(NBTPath.of("\"Very Cool Name\".bar.baz").withTagType(TagType.STRING), pathToSmile); } } From dce1b9c56fa371330bd81bab5e4b1005b343749a Mon Sep 17 00:00:00 2001 From: Taskeren Date: Thu, 2 Jul 2026 00:28:27 +0800 Subject: [PATCH 8/9] Add chunk and chunkregion support --- src/main/java/org/glavo/nbt/NBTPath.java | 5 ++--- .../glavo/nbt/internal/path/NBTPathImpl.java | 12 +++++++++--- src/test/java/org/glavo/nbt/NBTPathTest.java | 18 ++++++++++++++++++ 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/glavo/nbt/NBTPath.java b/src/main/java/org/glavo/nbt/NBTPath.java index 603e49f..8649673 100644 --- a/src/main/java/org/glavo/nbt/NBTPath.java +++ b/src/main/java/org/glavo/nbt/NBTPath.java @@ -18,7 +18,6 @@ import org.glavo.nbt.internal.path.NBTPathImpl; import org.glavo.nbt.internal.path.NBTPathNode; import org.glavo.nbt.internal.snbt.SNBTParser; -import org.glavo.nbt.tag.ParentTag; import org.glavo.nbt.tag.Tag; import org.glavo.nbt.tag.TagType; import org.jetbrains.annotations.Contract; @@ -46,7 +45,7 @@ static NBTPath of(String path) throws IllegalArgumentException { @SuppressWarnings({"DataFlowIssue", "unchecked"}) @Contract(pure = true) static @Nullable NBTPath of(T tag, @Nullable Tag expectedRoot) throws IllegalArgumentException, IllegalStateException { - ParentTag parentTag = tag.getParentTag(); + NBTParent parentTag = tag.getParent(); if (parentTag == null) return null; List paths = new ArrayList<>(); @@ -55,7 +54,7 @@ static NBTPath of(String path) throws IllegalArgumentException { NBTPathNode parentIndicator = NBTPathImpl.getIndicator(parentTag); if (parentTag == expectedRoot || parentIndicator == null) break; paths.add(parentIndicator); - ParentTag parent = parentTag.getParentTag(); + NBTParent parent = parentTag.getParent(); if (parent == null) break; parentTag = parent; } 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 3ded6e9..16b5560 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; @@ -63,9 +64,14 @@ public static Stream select(NBTParent parent, NBTPath parentTag = tag.getParentTag(); - return parentTag == null ? null : parentTag instanceof CompoundTag ? new NBTPathNode.NamedSubTag(tag.getName()) : new NBTPathNode.Index(tag.getIndex()); + public static @Nullable NBTPathNode getIndicator(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; diff --git a/src/test/java/org/glavo/nbt/NBTPathTest.java b/src/test/java/org/glavo/nbt/NBTPathTest.java index 0ea8bc2..f3b2494 100644 --- a/src/test/java/org/glavo/nbt/NBTPathTest.java +++ b/src/test/java/org/glavo/nbt/NBTPathTest.java @@ -15,6 +15,8 @@ */ 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; @@ -274,4 +276,20 @@ void testOfPath3() { 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, null); + assertNotNull(pathToTest); + assertEquals("test", pathToTest.toPathString()); + assertEquals("TEST", chunkRegion.getFirstString(pathToTest)); + assertEquals("TEST", chunk.getFirstString(pathToTest)); + } } From 58da77a25f53b4901b2cc4a5a5ddf2d02fe5ac6f Mon Sep 17 00:00:00 2001 From: Taskeren Date: Fri, 3 Jul 2026 04:20:51 +0800 Subject: [PATCH 9/9] Resolve --- src/main/java/org/glavo/nbt/NBTPath.java | 35 +++++++------------ .../glavo/nbt/internal/path/NBTPathImpl.java | 27 ++++++++++++-- src/test/java/org/glavo/nbt/NBTPathTest.java | 17 +++++++-- 3 files changed, 51 insertions(+), 28 deletions(-) diff --git a/src/main/java/org/glavo/nbt/NBTPath.java b/src/main/java/org/glavo/nbt/NBTPath.java index 8649673..5abfe54 100644 --- a/src/main/java/org/glavo/nbt/NBTPath.java +++ b/src/main/java/org/glavo/nbt/NBTPath.java @@ -16,7 +16,6 @@ package org.glavo.nbt; import org.glavo.nbt.internal.path.NBTPathImpl; -import org.glavo.nbt.internal.path.NBTPathNode; import org.glavo.nbt.internal.snbt.SNBTParser; import org.glavo.nbt.tag.Tag; import org.glavo.nbt.tag.TagType; @@ -37,33 +36,23 @@ static NBTPath of(String path) throws IllegalArgumentException { return new SNBTParser(path, 0, path.length()).nextPath(); } - /// Get the path from the root tag to the given tag. + /// 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. - @SuppressWarnings({"DataFlowIssue", "unchecked"}) @Contract(pure = true) - static @Nullable NBTPath of(T tag, @Nullable Tag expectedRoot) throws IllegalArgumentException, IllegalStateException { - NBTParent parentTag = tag.getParent(); - if (parentTag == null) return null; - - List paths = new ArrayList<>(); - paths.add(NBTPathImpl.getIndicator(tag)); - while (true) { - NBTPathNode parentIndicator = NBTPathImpl.getIndicator(parentTag); - if (parentTag == expectedRoot || parentIndicator == null) break; - paths.add(parentIndicator); - NBTParent parent = parentTag.getParent(); - if (parent == null) break; - parentTag = parent; - } + static @Nullable NBTPath of(T tag) throws IllegalArgumentException, IllegalStateException { + return NBTPathImpl.of(tag, null); + } - if (expectedRoot != null && parentTag != expectedRoot) - throw new IllegalStateException("Unexpected root tag " + parentTag + ", expected " + expectedRoot + "."); - Collections.reverse(paths); - NBTPathNode[] nodes = paths.toArray(NBTPathNode[]::new); - return (NBTPath) new NBTPathImpl<>(nodes, tag.getType()); + /// 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. 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 16b5560..332df4f 100644 --- a/src/main/java/org/glavo/nbt/internal/path/NBTPathImpl.java +++ b/src/main/java/org/glavo/nbt/internal/path/NBTPathImpl.java @@ -30,8 +30,7 @@ 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 { @@ -61,10 +60,32 @@ 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(NBTElement tag) { + public static @Nullable NBTPathNode getIndicator(@Nullable NBTElement tag) { if (!(tag instanceof Tag currentTag)) return null; NBTParent parentTag = tag.getParent(); if (parentTag instanceof ParentTag) { diff --git a/src/test/java/org/glavo/nbt/NBTPathTest.java b/src/test/java/org/glavo/nbt/NBTPathTest.java index f3b2494..152321a 100644 --- a/src/test/java/org/glavo/nbt/NBTPathTest.java +++ b/src/test/java/org/glavo/nbt/NBTPathTest.java @@ -270,7 +270,7 @@ void testOfPath3() { .addTag("baz", tag = new StringTag(":D")))); NBTPath pathToSmile = NBTPath.of(tag, root); - NBTPath pathToRoot = NBTPath.of(tag, null); + NBTPath pathToRoot = NBTPath.of(tag); assertNotNull(pathToSmile); assertEquals(":D", root.getFirstString(pathToSmile)); assertEquals(pathToSmile, pathToRoot); @@ -286,10 +286,23 @@ void testOfPath4() { StringTag testTag; rootTag.addTag("test", testTag = new StringTag("TEST")); - NBTPath pathToTest = NBTPath.of(testTag, null); + 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()); + } }