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
44 changes: 44 additions & 0 deletions src/main/java/org/glavo/nbt/NBTPath.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,16 @@
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.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 <a href="https://minecraft.wiki/w/NBT_path">NBT Path - Minecraft Wiki</a>
Expand All @@ -34,6 +38,34 @@ 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.
///
/// @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 <T extends Tag> NBTPath<T> of(T tag, @Nullable Tag expectedRoot) throws IllegalArgumentException, IllegalStateException {
ParentTag<?> parentTag = tag.getParentTag();

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] 这里改用 tag.getParentTag() 会把 parent 是 ChunkrootTag 当成“没有 parent”。但 Tag#getParent() 允许 parent 是 ChunkNBTPathImpl.select 也支持从 Chunk 查询。结果是 NBTPath.of(chunk.getRootTag(), null) 返回 null,而 chunk 内部子 tag 也无法自然生成相对 Chunk 可用的 path,调用方必须额外知道并传入 chunk.getRootTag(),和 NBTParent 支持的根类型不一致。

if (parentTag == null) return null;

List<NBTPathNode> paths = new ArrayList<>();
paths.add(NBTPathImpl.getIndicator(tag));
while (true) {
if (parentTag == expectedRoot) break;
paths.add(NBTPathImpl.getIndicator(parentTag));
ParentTag<?> parent = parentTag.getParentTag();
if (parent == null) break;
parentTag = parent;
}

if (parentTag != expectedRoot)

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] expectedRoot == null 会让非根 tag 固定抛 IllegalStateException。例如 root.addTag("x", tag) 后调用 NBTPath.of(tag, null),循环会停在实际 top root,随后这里的 parentTag != expectedRoot 恒为 true。这个 nullable 参数看起来应支持“使用实际 top of tree”,但当前实现只在显式传入 root tag 时可用,测试也没有覆盖 null root。

throw new IllegalStateException("Unexpected root tag " + parentTag + ", expected " + expectedRoot + ".");
Collections.reverse(paths);
NBTPathNode[] nodes = paths.toArray(NBTPathNode[]::new);
return (NBTPath<T>) new NBTPathImpl<>(nodes, tag.getType());
}

/// Returns the tag type of this path.
@Contract(pure = true)
@Nullable TagType<T> getTagType();
Expand All @@ -50,4 +82,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);
}

}
55 changes: 31 additions & 24 deletions src/main/java/org/glavo/nbt/internal/path/NBTPathImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -59,6 +60,14 @@ public static <T extends Tag> Stream<T> select(NBTParent<?> parent, NBTPath<? ex
return (Stream<T>) 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<T> tagType;

Expand Down Expand Up @@ -107,33 +116,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('.');
}
}

return builder.toString();
}

builder.append(']');
cachedString = builder.toString();
@Override
public String toString() {
if (cachedString == null) {
String pathString = toPathString();
if (tagType != null) pathString = "<" + tagType + ">" + " " + pathString;
cachedString = pathString;
}

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

Expand Down Expand Up @@ -188,4 +184,93 @@ 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);
assertNotNull(pathToSmile);
assertEquals(":D", root.getFirstString(pathToSmile));
assertEquals(NBTPath.of("\"Very Cool Name\".bar.baz").withTagType(TagType.STRING), pathToSmile);
}
}
Loading