@@ -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 树的基本抽象。
5253new 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
7075var 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 子元素
93100var 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
121141HelloNBT 提供了对 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
125146HelloNBT 中可以使用 ` NBTPath.of(String) ` 来解析 NBTPath 字符串:
126147
@@ -149,3 +170,143 @@ int _ = compoundTag.getFirstInt(path); // 如果不存在则抛出 NoSuchElement
149170Integer _ = compoundTag. getFirstIntOrNull(path); // 如果不存在则返回 null
150171int _ = 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