Skip to content

Commit 859f32b

Browse files
committed
Add NBT read/write documentation to Chinese tutorial
1 parent 71d249a commit 859f32b

2 files changed

Lines changed: 182 additions & 17 deletions

File tree

docs/QuickStart_zh.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ try (var outputStream = new GZIPOutputStream(Files.newOutputStream(Path.of("leve
5656
}
5757
```
5858

59-
### 读取 Anvil 文件和 Region 文件
59+
## 读取 Anvil 文件和 Region 文件
6060

6161
Minecraft Java 版的世界区块信息通常存储在后缀为 `.mca` 的 Anvil 文件中。
6262
旧版本 Minecraft 的区块信息则存储在后缀为 `.mcr` 的 Region 文件中。
@@ -114,3 +114,7 @@ HelloNBT 提供了 `ExternalChunkAccessor` 接口来读取和写入外部区块
114114

115115
`readRegion``writeRegion` 方法也提供了接受 `ExternalChunkAccessor` 参数的重载,
116116
你可以使用这些重载来手动指定外部区块文件的访问方式。
117+
118+
## 完整教程
119+
120+
如果需要更深入地了解 HelloNBT 的用法,可以参阅 [HelloNBT 教程](Tutorial_zh.md)

docs/Tutorial_zh.md

Lines changed: 177 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ HelloNBT 提供了对 NBT 树的基本抽象。
3030
- `DoubleTag`: 包含一个双精度浮点数的 NBT Tag。
3131
- `StringTag`: 包含一个字符串的 NBT Tag。
3232

33-
其中 `ChunkRegion``Chunk``CompoundTag``ListTag``ByteArrayTag``IntArrayTag``LongArrayTag` 可以包含其他 `NBTElement` 作为子元素,
33+
其中 `ChunkRegion``Chunk``CompoundTag``ListTag``ByteArrayTag``IntArrayTag``LongArrayTag` 可以包含其他
34+
`NBTElement` 作为子元素,
3435
这些可以作为父元素的 `NBTElement` 都实现了 `NBTParent` 接口。
3536

3637
一个没有父元素的 `NBTElement` 称为根元素(Root Element)。根元素及其所有子元素共同构成了一个 NBT 树。
@@ -52,10 +53,14 @@ HelloNBT 提供了对 NBT 树的基本抽象。
5253
new IntTag();
5354

5455
// 创建一个值为 123 的 IntTag
55-
new IntTag(123);
56+
new
57+
58+
IntTag(123);
5659

5760
// 创建一个空的 CompoundTag
58-
new CompoundTag();
61+
new
62+
63+
CompoundTag();
5964
```
6065

6166
构造 `ListTag` 时需要传入其元素类型:
@@ -68,7 +73,9 @@ ListTag<IntTag> listTag = new ListTag<>(TagType.INT);
6873

6974
```java
7075
var intTag = new IntTag();
71-
intTag.set(123);
76+
intTag.
77+
78+
set(123);
7279
```
7380

7481
对于 `CompoundTag`,可以通过 `add` 系列方法添加子元素:
@@ -92,35 +99,49 @@ var compoundTag = new CompoundTag()
9299
// 先获取 IntTag 子元素
93100
var intSubTag = (IntTag) compoundTag.get("int");
94101

95-
compoundTag.setInt("int", 233);
102+
compoundTag.
103+
104+
setInt("int",233);
105+
106+
assert intSubTag.
96107

97-
assert intSubTag.get() == 233;
108+
get() ==233;
98109
```
99110

100111
在不存在该子元素时,`set` 系列的方法会创建一个新的子元素并添加到 `CompoundTag` 中:
101112

102113
```java
103-
assert compoundTag.getTag("anotherInt") == null;
104-
compoundTag.setInt("anotherInt", 456);
105-
assert compoundTag.getTag("anotherInt") instanceof IntTag;
106-
assert ((IntTag) compoundTag.getTag("anotherInt")).get() == 456;
114+
assert compoundTag.getTag("anotherInt") ==null;
115+
compoundTag.
116+
117+
setInt("anotherInt",456);
118+
assert compoundTag.
119+
120+
getTag("anotherInt") instanceof IntTag;
121+
assert((IntTag)compoundTag.
122+
123+
getTag("anotherInt")).
124+
125+
get() ==456;
107126
```
108127

109128
如果已存在的子标签类型与要设置的值类型不匹配,`set` 系列的方法会抛出 `IllegalStateException`
110129

111130
```java
112-
try {
113-
compoundTag.setInt("string", 233);
114-
} catch (IllegalStateException e) {
115-
// Expected IllegalStateException
116-
}
131+
try{
132+
compoundTag.setInt("string",233);
133+
}catch(
134+
IllegalStateException e){
135+
// Expected IllegalStateException
136+
}
117137
```
118138

119139
## NBTPath
120140

121141
HelloNBT 提供了对 NBTPath 的支持。
122142

123-
NBTPath 是一种查询 NBT 数据的语言,相关文档可以在 Minecraft Wiki 中查看:[NBTPath](https://zh.minecraft.wiki/w/NBT%E8%B7%AF%E5%BE%84)
143+
NBTPath 是一种查询 NBT 数据的语言,相关文档可以在 Minecraft Wiki
144+
中查看:[NBTPath](https://zh.minecraft.wiki/w/NBT%E8%B7%AF%E5%BE%84)
124145

125146
HelloNBT 中可以使用 `NBTPath.of(String)` 来解析 NBTPath 字符串:
126147

@@ -149,3 +170,143 @@ int _ = compoundTag.getFirstInt(path); // 如果不存在则抛出 NoSuchElement
149170
Integer _ = compoundTag.getFirstIntOrNull(path); // 如果不存在则返回 null
150171
int _ = compoundTag.getFirstIntOrDefault(path, 0); // 如果不存在则返回默认值
151172
```
173+
174+
## 读写 NBT 数据
175+
176+
HelloNBT 支持读取和写入 NBT 文件、Anvil 文件(`.mca`)和区域文件(`.mcr`)。
177+
178+
### `NBTCodec`
179+
180+
`NBTCodec` 类提供了读取和写入 NBT 文件、Anvil 文件和区域文件的方法。
181+
182+
可以通过 `NBTCodec.of()` 工厂方法来获取一个 `NBTCodec` 实例:
183+
184+
```java
185+
NBTCodec _ = NBTCodec.of();
186+
187+
// 如果需要读写基岩版的 NBT 数据,可以传入 MinecraftEdition.BEDROCK 参数
188+
NBTCodec _ = NBTCodec.of(MinecraftEdition.BEDROCK);
189+
```
190+
191+
`NBTCodec` 是不可变的,可以在多个线程中安全地共享。
192+
193+
在获取到 `NBTCodec` 实例后,也可以通过 `with` 系列方法来产生新的 `NBTCodec` 实例:
194+
195+
```java
196+
NBTCodec codec = NBTCodec.of()
197+
// 设置 Minecraft Edition
198+
.withEdition(MinecraftEdition.BEDROCK)
199+
// 设置外部区块文件访问器工厂
200+
.withExternalChunkAccessorFactory(ExternalChunkAccessor.emptyFactory())
201+
;
202+
```
203+
204+
`with` 系列方法不会修改原 `NBTCodec` 实例,每次调用都会返回一个新的 `NBTCodec` 实例。
205+
206+
### 读取 NBT 文件
207+
208+
`NBTCodec` 提供了 `readTag` 方法来读取一个 NBT Tag:
209+
210+
```java
211+
Tag tag = NBTCodec.of().readTag(Path.of("level.dat"));
212+
```
213+
214+
`readTag` 方法支持自动检测 NBT 数据是否被压缩,并会自动解压使用 `GZip``LZ4` 算法压缩的数据。
215+
216+
除了从文件中读取,`readTag` 方法还支持从其他来源读取数据,
217+
包括 `InputStream``ReadableByteChannel``byte[]`、和 `ByteBuffer` 等等:
218+
219+
```java
220+
byte[] bytes = Files.readAllBytes(Path.of("level.dat"));
221+
222+
// 从 byte[] 中读取 NBT 数据
223+
tag = NBTCodec.of().readTag(bytes);
224+
225+
// 从 ByteBuffer 中读取 NBT 数据
226+
tag = NBTCodec.of().readTag(ByteBuffer.wrap(bytes));
227+
228+
// 从 InputStream 中读取 NBT 数据
229+
tag = NBTCodec.of().readTag(new ByteArrayInputStream(bytes));
230+
231+
// 从 ReadableByteChannel 中读取 NBT 数据
232+
tag = NBTCodec.of().readTag(Channels.newChannel(new ByteArrayInputStream(bytes)));
233+
```
234+
235+
在读取 NBT 文件时,可以指定一个 `TagType` 来限制读取的 NBT Tag 类型。
236+
237+
例如 `level.dat` 等文件通常只包含一个 `CompoundTag`,所以可以通过给 `readTag` 方法传入 `TagType.COMPOUND` 来限制读取的 NBT Tag 类型:
238+
239+
```java
240+
CompoundTag tag = NBTCodec.of().readTag(Path.of("level.dat"), TagType.COMPOUND);
241+
```
242+
243+
### 写入 NBT 文件
244+
245+
`NBTCodec` 提供了 `writeTag` 方法来写入 NBT 文件。
246+
247+
目前 `writeTag` 方法只支持写入未经压缩的原始 NBT 数据,用户可以使用 `GZIPOutputStream` 包装输出流来压缩数据:
248+
249+
```java
250+
try (var outputStream = new GZIPOutputStream(Files.newOutputStream(Path.of("level.dat")))) {
251+
NBTCodec.of().writeTag(outputStream, tag);
252+
}
253+
```
254+
255+
### 读取 Anvil 文件和 Region 文件
256+
257+
Minecraft Java 版的世界区块信息通常存储在后缀为 `.mca` 的 Anvil 文件中。
258+
旧版本 Minecraft 的区块信息则存储在后缀为 `.mcr` 的 Region 文件中。
259+
260+
每个 Anvil 文件和 Region 文件中都存储了一个**区域(Region)**,在 HelloNBT 中使用 `ChunkRegion` 类来表示。
261+
262+
每个区域都包含了 32 x 32 个区块,HelloNBT 使用 `Chunk` 类来表示一个区块。
263+
264+
关于区域和更多信息可以在 Minecraft Wiki 中查看:[区域文件格式](https://zh.minecraft.wiki/w/%E5%8C%BA%E5%9F%9F%E6%96%87%E4%BB%B6%E6%A0%BC%E5%BC%8F)
265+
266+
在 HelloNBT 中,可以使用 `NBTCodec``readRegion` 方法来读取 Anvil 文件和 Region 文件:
267+
268+
```java
269+
ChunkRegion region = NBTCodec.of().readRegion(Path.of("region/r.0.0.mca"));
270+
```
271+
272+
### 写入 Anvil 文件和 Region 文件
273+
274+
`NBTCodec` 提供了 `writeRegion` 方法来写入 Anvil 文件和 Region 文件:
275+
276+
```java
277+
NBTCodec.of().writeRegion(Path.of("region/r.0.0.mca"), region);
278+
```
279+
280+
该方法也支持写入到 `OutputStream``SeekableByteChannel`
281+
282+
```java
283+
try (var outputStream = Files.newOutputStream(Path.of("region/r.0.0.mca"))) {
284+
NBTCodec.of().writeRegion(outputStream, region);
285+
}
286+
287+
try (var channel = Files.newByteChannel(Path.of("region/r.0.0.mca"),
288+
StandardOpenOption.WRITE,
289+
StandardOpenOption.TRUNCATE_EXISTING)) {
290+
NBTCodec.of().writeRegion(channel, region);
291+
}
292+
```
293+
294+
由于区域文件格式的限制,无法实现流式写入,所以以 `OutputStream` 为目标时 HelloNBT 会先在内存中构建完整的区域文件,
295+
再写入到输出流中,这可能会导致内存占用较大。
296+
我们更建议使用 `SeekableByteChannel` 作为目标,这样 HelloNBT 可以逐个区块写入通道,避免额外的内存占用。
297+
298+
## 外部区块文件
299+
300+
Anvil 文件中每个区块的数据最大不能超过 1020KiB。
301+
302+
有时一个区块中的数据会超过这个限制,而自 Minecraft 1.15 (19w34a) 开始,
303+
这些过大的区块会被拆分至外部区块文件(`c.<chunkX>.<chunkZ>.mcc`)中存储。
304+
305+
HelloNBT 提供了 `ExternalChunkAccessor` 接口来读取和写入外部区块文件。
306+
307+
默认情况下,`NBTCodec` 中的 `readRegion``writeRegion` 方法接受 `Path` 参数的重载会检测文件名是否匹配 `r.<regionX>.<regionZ>.mca` 的模式,
308+
如果匹配的话,则会自动使用同目录下的 `c.<chunkX>.<chunkZ>.mcc` 文件作为外部区块文件。
309+
而其他重载则不会自动使用外部区块文件,需要读取/写入外部区块文件时会直接抛出异常。
310+
311+
`readRegion``writeRegion` 方法也提供了接受 `ExternalChunkAccessor` 参数的重载,
312+
你可以使用这些重载来手动指定外部区块文件的访问方式。

0 commit comments

Comments
 (0)