Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/main/java/org/glavo/nbt/NBTPath.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 <a href="https://minecraft.wiki/w/NBT_path">NBT Path - Minecraft Wiki</a>
Expand All @@ -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 <T extends Tag> NBTPath<T> 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 <T extends Tag> NBTPath<T> 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<T> getTagType();
Expand All @@ -50,4 +71,16 @@ static NBTPath<?> of(String path) throws IllegalArgumentException {
@Contract(pure = true)
<T2 extends Tag> NBTPath<T2> withTagType(TagType<T2> 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);
}

}
86 changes: 60 additions & 26 deletions src/main/java/org/glavo/nbt/internal/path/NBTPathImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,22 @@
*/
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;
import org.glavo.nbt.chunk.ChunkRegion;
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<T extends Tag> implements NBTPath<T> {
Expand Down Expand Up @@ -59,6 +60,41 @@ public static <T extends Tag> Stream<T> select(NBTParent<?> parent, NBTPath<? ex
return (Stream<T>) tags;
}

@SuppressWarnings("unchecked")
public static @Nullable <T extends Tag> NBTPath<T> of(T tag, @Nullable Tag expectedRoot) throws IllegalArgumentException, IllegalStateException {
List<NBTPathNode> 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<T>) 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<?>) {
Comment thread
Taskeren marked this conversation as resolved.
return parentTag instanceof CompoundTag ? new NBTPathNode.NamedSubTag(currentTag.getName()) : new NBTPathNode.Index(currentTag.getIndex());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

本审查建议由 GPT-5 生成


[P2] 先支持 ListTag 根再返回索引路径

tag 的 parent 是 ListTag/ArrayTag(或 expectedRoot 本身是这类 ParentTag)时,这里会生成 [index] 作为相对路径;但 NBTPathImpl.select(...) 对初始 parent 只处理 CompoundTagChunkRegionChunk,传入 ListTag/ArrayTag 会直接从 Stream.empty() 开始。因此 NBTPath.of(child, list) 返回的路径无法通过 list.getFirstTag(path) 找回 child,新的 of(Tag, Tag) API 在非 Compound 根上会失效;要么限制/拒绝这类根,要么让选择器从所有 ParentTag 根开始。

} else { // Chunk, ChunkRegion or null
return null;
}
}

private final NBTPathNode @Unmodifiable [] nodes;
private final @Nullable TagType<T> tagType;

Expand Down Expand Up @@ -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<StringBuilder> 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;
Expand Down
129 changes: 123 additions & 6 deletions src/test/java/org/glavo/nbt/NBTPathTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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<IntTag>()
.addTag(new IntTag(0))
.addTag(new IntTag(1))
.addTag(tag = new IntTag(2))
));

NBTPath<IntTag> 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<IntTag>()
.addTag(new IntTag(0))
.addTag(new IntTag(1))
.addTag(tag = new IntTag(2))
)));

NBTPath<IntTag> 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<StringTag> pathToSmile = NBTPath.of(tag, root);
NBTPath<StringTag> 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<StringTag> 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<StringTag> path = NBTPath.of(testTag);
assertNotNull(path);
assertEquals("TEST", chunk.getFirstString(path));
assertEquals("test", path.toPathString());
}
}
Loading