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 ex
return (Stream) 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());
+ }
}