Zh:NBT
二进制命名标签(Named Binary Tag,NBT)文件格式是一种非常简单、尽管有些烦人(我们真的需要另一个格式吗?)[见讨论]的结构化二进制格式,Minecraft游戏将它用于多种用途。因此,一些第三方实用工具也利用了这种格式。你可以在本文底部找到示例文件。
Mojang还发布了一个参考实现和他们的Anvil转换工具,在此处可用:https://mojang.com/2012/02/new-minecraft-map-format-anvil/
Contents
当前用途
NBT格式目前在多个地方使用,主要是:
- 在协议中作为槽数据的一部分。
- 多人游戏中保存的服务器列表(
servers.dat
)。 - 玩家数据(单人游戏和多人游戏,每个玩家一个文件),包含如物品栏和位置等内容。
- 已保存的世界(单人游戏和多人游戏)
- 包含常规信息(出生点、一天中的时间等)的世界索引文件
- 区块数据(见区域文件)
不幸的是,作为开发者遇到的NBT文件会以三种不同的方式存储,这就有些有趣了。
库
有很多很多用于操纵NBT的库,它们用各种语言写成,且每个语言都有好几种,例如:
- C
- C#
- D
- Go (New)
- Go (Old, without TagLongArray)
- Java (Old, without TagLongArray)
- Java
- Javascript
- PHP
- Python
- Ruby
- Rust
- Scala
- Kotlin(非常简易,200行)
- Kotlin Multiplatform
- 你的想法…
除非你有特定的目标或许可要求,否则强烈建议使用其中一种现有库。
实用工具
几乎每个第三方Minecraft应用程序都在某种程度上使用了NBT。还有一些专用的NBT编辑器,如果您要开发自己的NBT库,这可能会有帮助,包括:
- NBTEdit(C#,单一功能),最早的NBT编辑器之一。
- NEINedit(Obj-C),一个OS X限定的编辑器。
- nbt2yaml(Python),提供了通过YAML格式使用命令行编辑NBT,以及快速而极简的NBT解析/渲染API的功能。
- nbted(Rust;CC0),提供了通过你的$EDITOR使用命令行编辑NBT文件的功能。
- nbt2json(Golang;MIT)NBT至JSON/YAML双向转换的命令行实用工具。支持MCPE-NBT。可用以库的形式使用。
规范
NBT文件格式非常简单,编写读写它的库也很简单。此格式支持13种数据类型,其中一种用于闭合复合标签。强烈建议你阅读整个部分,否则可能会遇到问题。
类型ID | 类型名称 | 负载大小(字节) | 描述 |
---|---|---|---|
0 | TAG_End | 0 | 表明TAG_Compound的结尾。它只会在TAG_Compound内部使用,而且即使在TAG_Compound中也不会被指名。 |
1 | TAG_Byte | 1 | 单个有符号字节 |
2 | TAG_Short | 2 | 单个有符号的大字节序16位整型 |
3 | TAG_Int | 4 | 单个有符号的大字节序32位整型 |
4 | TAG_Long | 8 | 单个有符号的大字节序64位整型 |
5 | TAG_Float | 4 | 单个大字节序的IEEE-754单精度浮点数(可能为NaN) |
6 | TAG_Double | 8 | 单个大字节序的IEEE-754双精度浮点数(可能为NaN) |
7 | TAG_Byte_Array | ... | 一个包含长度前缀的有符号字节数组。前缀是一个有符号整型(即4字节) |
8 | TAG_String | ... | 一个包含长度前缀的modified UTF-8字符串。前缀是一个无符号短整型(即2字节)来表明字符串以字节为单位的长度。 |
9 | TAG_List | ... | 一个相同类型无名标签的列表。列表的前缀是它包含项目的Type ID (即1字节)列表的长度就是一个有符号整型(即4字节)。如果列表的长度为0或为负,则类型可能是0(TAG_End),但反之它必须是其他的什么类型。(Notch式的实现在这种情况下使用了TAG_End,但在另一个Mojang参考实现中使用了1,总之解析器应该在长度<=0时接受任何类型)
|
10 | TAG_Compound | ... | 有效地列出named标签。不能保证顺序。 |
11 | TAG_Int_Array | ... | 一个带长度前缀的有符号整型数组。前缀是一个有符号整型(即4字节),表示4字节整型的数量。 |
12 | TAG_Long_Array | ... | 一个带长度前缀的有符号整型数组。前缀是一个有符号整型(即4字节),表示8字节整型的数量。 |
这几件简单的事情需要记住:
- 表示数字的数据类型在Java版中为大字节序,而携带版为小字节序。除非使用Java,否则你很有可能必须将其交换为小字节序。见维基百科上有关字节序的文章。
- 每个NBT文件将总是隐式地包含在复合标签中,并且也以TAG_Compound开头。
- NBT文件的结构由TAG_List和TAG_Compound类型定义,因为这样的标签本身仅包含负载,但是这取决于标签中包含的内容,它也可能包含其他头。即,如果位于复合标签内部,则每个标签都将以TAG_id开头,然后是字符串(标签名),最后是负载。在列表中时,它仅是负载,因为没有名称,并且标签类型在列表的开头给出。
例如,这是磁盘上的TAG_Short
的示例布局:
Type ID | Length of Name | Name | Payload | |
---|---|---|---|---|
Decoded | 2 | 9 | shortTest
|
32767
|
On Disk (in hex) | 02
|
00 09
|
73 68 6F 72 74 54 65 73 74
|
7F FF
|
如果此TAG_Short
曾经在TAG_List
中,则它仅是负载,因为类型是隐式的且列表第一级中的标签是没有名称的。
示例
Markus最初提供了两个实际上的示例文件(test.nbt
& bigtest.nbt
)用于测试你的实现。下面提供的示例输出是使用PyNBT的debug-nbt工具生成的。
test.nbt
第一个示例是一个未压缩的“Hello World”NBT示例。如果你正确地解析了它,你会得到与下列类似的结构:
TAG_Compound('hello world'): 1 entry { TAG_String('name'): 'Bananrama' }
这里是示例的说明:
(整个内容隐含在一个复合中) | 类型ID(隐含复合中的第一个元素) | 根复合的名称长度 | 根复合的名称 | 根复合中第一个元素的类型ID | 根中第一个元素的名称长度 | 第一个元素的名称 | 字符串长度 | 字符串 | 标签末尾(根复合的) |
---|---|---|---|---|---|---|---|---|---|
被编码的 | Compound | 11 | hello world | String | 4 | name | 9 | Bananrama | |
磁盘中(以十六进制) | 0a
|
00 0b
|
68 65 6c 6c 6f 20 77 6f 72 6c 64
|
08
|
00 04
|
6e 61 6d 65
|
00 09
|
42 61 6e 61 6e 72 61 6d 61
|
00
|
bigtest.nbt
第二个示例是每个可用标签的gzip压缩的测试。如果你的程序可以成功解析此文件,那么你做得很好。请注意,如上所述,TAG_List下的标签没有名称。
TAG_Compound('Level'): 11 entries { TAG_Compound('nested compound test'): 2 entries { TAG_Compound('egg'): 2 entries { TAG_String('name'): 'Eggbert' TAG_Float('value'): 0.5 } TAG_Compound('ham'): 2 entries { TAG_String('name'): 'Hampus' TAG_Float('value'): 0.75 } } TAG_Int('intTest'): 2147483647 TAG_Byte('byteTest'): 127 TAG_String('stringTest'): 'HELLO WORLD THIS IS A TEST STRING \xc3\x85\xc3\x84\xc3\x96!' TAG_List('listTest (long)'): 5 entries { TAG_Long(None): 11 TAG_Long(None): 12 TAG_Long(None): 13 TAG_Long(None): 14 TAG_Long(None): 15 } TAG_Double('doubleTest'): 0.49312871321823148 TAG_Float('floatTest'): 0.49823147058486938 TAG_Long('longTest'): 9223372036854775807L TAG_List('listTest (compound)'): 2 entries { TAG_Compound(None): 2 entries { TAG_Long('created-on'): 1264099775885L TAG_String('name'): 'Compound tag #0' } TAG_Compound(None): 2 entries { TAG_Long('created-on'): 1264099775885L TAG_String('name'): 'Compound tag #1' } } TAG_Byte_Array('byteArrayTest (the first 1000 values of (n*n*255+n*7)%100, starting with n=0 (0, 62, 34, 16, 8, ...))'): [1000 bytes] TAG_Short('shortTest'): 32767 }
servers.dat
servers.dat文件包含你已添加到游戏中的多人服务器的列表。这有点混乱,此文件将总是未压缩的。以下是在servers.dat中看到的结构示例。
TAG_Compound(''): 1 entry { TAG_List('servers'): 2 entries { TAG_Compound(None): 3 entries { TAG_Byte('acceptTextures'): 1 (Automatically accept resourcepacks from this server) TAG_String('ip'): '199.167.132.229:25620' TAG_String('name'): 'Dainz1 - Creative' } TAG_Compound(None): 3 entries { TAG_String('icon'): 'iVBORw0KGgoAAAANUhEUgAAAEAAAABACA...' (The base64-encoded server icon. Trimmed here for the example's sake) TAG_String('ip'): '76.127.122.65:25565' TAG_String('name'): 'minstarmin4' } } }
level.dat
最后一个示例是单人游戏的level.dat,它使用gzip压缩。注意玩家的物品栏和常规世界细节如出生位置、世界名称和游戏种子。
TAG_Compound(''): 1 entry { TAG_Compound('Data'): 17 entries { TAG_Byte('raining'): 0 TAG_Long('RandomSeed'): 3142388825013346304L TAG_Int('SpawnX'): 0 TAG_Int('SpawnZ'): 0 TAG_Long('LastPlayed'): 1323133681772L TAG_Int('GameType'): 1 TAG_Int('SpawnY'): 63 TAG_Byte('MapFeatures'): 1 TAG_Compound('Player'): 24 entries { TAG_Int('XpTotal'): 0 TAG_Compound('abilities'): 4 entries { TAG_Byte('instabuild'): 1 TAG_Byte('flying'): 1 TAG_Byte('mayfly'): 1 TAG_Byte('invulnerable'): 1 } TAG_Int('XpLevel'): 0 TAG_Int('Score'): 0 TAG_Short('Health'): 20 TAG_List('Inventory'): 13 entries { TAG_Compound(None): 4 entries { TAG_Byte('Count'): 1 TAG_Byte('Slot'): 0 TAG_Short('id'): 24 TAG_Short('Damage'): 0 } TAG_Compound(None): 4 entries { TAG_Byte('Count'): 1 TAG_Byte('Slot'): 1 TAG_Short('id'): 25 TAG_Short('Damage'): 0 } TAG_Compound(None): 4 entries { TAG_Byte('Count'): 1 TAG_Byte('Slot'): 2 TAG_Short('id'): 326 TAG_Short('Damage'): 0 } TAG_Compound(None): 4 entries { TAG_Byte('Count'): 1 TAG_Byte('Slot'): 3 TAG_Short('id'): 29 TAG_Short('Damage'): 0 } TAG_Compound(None): 4 entries { TAG_Byte('Count'): 10 TAG_Byte('Slot'): 4 TAG_Short('id'): 69 TAG_Short('Damage'): 0 } TAG_Compound(None): 4 entries { TAG_Byte('Count'): 3 TAG_Byte('Slot'): 5 TAG_Short('id'): 33 TAG_Short('Damage'): 0 } TAG_Compound(None): 4 entries { TAG_Byte('Count'): 43 TAG_Byte('Slot'): 6 TAG_Short('id'): 356 TAG_Short('Damage'): 0 } TAG_Compound(None): 4 entries { TAG_Byte('Count'): 64 TAG_Byte('Slot'): 7 TAG_Short('id'): 331 TAG_Short('Damage'): 0 } TAG_Compound(None): 4 entries { TAG_Byte('Count'): 20 TAG_Byte('Slot'): 8 TAG_Short('id'): 76 TAG_Short('Damage'): 0 } TAG_Compound(None): 4 entries { TAG_Byte('Count'): 64 TAG_Byte('Slot'): 9 TAG_Short('id'): 331 TAG_Short('Damage'): 0 } TAG_Compound(None): 4 entries { TAG_Byte('Count'): 1 TAG_Byte('Slot'): 10 TAG_Short('id'): 323 TAG_Short('Damage'): 0 } TAG_Compound(None): 4 entries { TAG_Byte('Count'): 16 TAG_Byte('Slot'): 11 TAG_Short('id'): 331 TAG_Short('Damage'): 0 } TAG_Compound(None): 4 entries { TAG_Byte('Count'): 1 TAG_Byte('Slot'): 12 TAG_Short('id'): 110 TAG_Short('Damage'): 0 } } TAG_Short('HurtTime'): 0 TAG_Short('Fire'): -20 TAG_Float('foodExhaustionLevel'): 0.0 TAG_Float('foodSaturationLevel'): 5.0 TAG_Int('foodTickTimer'): 0 TAG_Short('SleepTimer'): 0 TAG_Short('DeathTime'): 0 TAG_List('Rotation'): 2 entries { TAG_Float(None): 1151.9342041015625 TAG_Float(None): 32.249679565429688 } TAG_Float('XpP'): 0.0 TAG_Float('FallDistance'): 0.0 TAG_Short('Air'): 300 TAG_List('Motion'): 3 entries { TAG_Double(None): -2.9778325794951344e-11 TAG_Double(None): -0.078400001525878907 TAG_Double(None): 1.1763942772801152e-11 } TAG_Int('Dimension'): 0 TAG_Byte('OnGround'): 1 TAG_List('Pos'): 3 entries { TAG_Double(None): 256.87499499518492 TAG_Double(None): 112.62000000476837 TAG_Double(None): -34.578128612797634 } TAG_Byte('Sleeping'): 0 TAG_Short('AttackTime'): 0 TAG_Int('foodLevel'): 20 } TAG_Int('thunderTime'): 2724 TAG_Int('version'): 19132 TAG_Int('rainTime'): 5476 TAG_Long('Time'): 128763 TAG_Byte('thundering'): 1 TAG_Byte('hardcore'): 0 TAG_Long('SizeOnDisk'): 0 TAG_String('LevelName'): 'Sandstone Test World' } }
下载
- test.nbt/hello_world.nbt(未压缩)
- bigtest.nbt(gzip压缩)
- NaN-value-double.dat(已压缩,原始版本未知)