由于 Blocktopograph 停止更新,随着MC更新迭代,其编辑MC存档越来越吃力,所以为了能够继续方便编辑基岩版的存档,我为我自己开发的软件:NaroX 研究并添加了MC编辑器相关的功能,发现目前相关方面的研究的文章挺少的,现在将一些研究心得分享给大家,希望能够给研究这方面的伙伴一点小小的帮助,制作不易,如果文章对你有帮助的话就请帮我点一个赞吧!
文章主要是针对MC基岩版存档的 level.dat 与 db 目录数据做一些研究与总结,对于levelname.txt (存档名称)、world_icon.jpeg (存档封面)、behavior_packs (行为包)、resource_packs (资源包) 等等这些内容不在研究范围以内哦~ 研究结果都是我个人逆向与参考部分开源代码实践与总结得到的,如果有错误欢迎大家指正!
文章发布时MC版本是【1.21.92.1】
这两个文件是存档的世界NBT数据,它们控制着世界生成、类型、游戏规则、存档版本、作弊、能否获取成就等等,游戏主要会读取【level.dat】,而【level.dat_old】则是备份文件,如果【level.dat】损坏则会使用【level.dat_old】替补,避免因世界NBT损坏而导致无法进入游戏。
那么我们先来了解一下 level.dat 的基本数据结构吧:

level.dat 文件头
【0 .. 3 】(4字节) 红色框的4字节是当前世界的版本,目前是【0A 00 00 00】,由于基岩版数据都是小端序的,应该倒着读,所以应该为【00 00 00 0A】,16进制的 00000A 转为 10进制 就是 10,所以当前 level.dat 数据版本是 10
【4 ..7】 (4字节) 黄色框4字节是世界NBT数据的大小,按小端读取结果是【00 00 0B 22】,那么NBT数据的大小就是 2850 个字节
后面的就是【世界NBT】的数据了,NBT数据大小与黄色框得到的数字对应,也就是有2850个字节
其实NBT格式并不复杂,我们只需要知道它是一个树状的结构即可,一个名称对应一个数值,类似json,基岩版中常用的NBT有下面几种类型(前面是NBT标签类型,后面是对应的16进制码):
EndTag = 0x00 //一般NBT结束后带有,只有一个字节
ByteTag = 0x01 //字节标签,布尔值也使用这个,值占1字节
ShortTag = 0x02 //短整数标签,值占2个字节
IntTag = 0x03 //整数标签,值占4个字节
LongTag = 0x04 //长整数标签,值占8字节
FloatTag = 0x05 //单精度浮点数,值占4字节
DoubleTag = 0x06 //双精度浮点数,值占8字节
ByteArrayTag = 0x07 //字节数组(不常见, 不过在地图NBT中能够找到)
StringTag = 0x08 //字符串标签,与文本数据有关,2字节数据长度, 然后就是字符串数据
ListTag = 0x09 //列表标签,此标签内子标签类型一致,如果没有标签则会带有一个EndTag
CompoundTag = 0x0A //复合标签,约等于一个map,可以添加任意类型的标签到里面,但标签名称不能重复
//后面这些标签可能不存在于基岩版,但也放上来吧
IntArrayTag = 0x0B //整数数组标签,存在于Java版
LongArrayTag = 0x0C //长整数数组标签,存在于Java版
ShortArrayTag = 0x64 //短整型数组标签,可能不存在,发现于BTR源码内
好了,我们了解了NBT的类型,现在来看一下它的数据应该如何解析:
每个NBT标签的结构是:1字节标签类型 + 2字节标签名长度 + 标签名(由标签名长度决定) + 值数据(由标签类型决定)
我们看下实例吧:

【CompoundTag】
1字节,当前类型:0A,对应的就是上面的 CompoundTag 类型
2字节,00 00,这个是此标签名的数据长度,转换后也就是 0,说明这个标签没有名称
由于这是一个 CompoundTag 类型,它的值数据就是后面所有的子标签,我们往后读标签直到 EndTag 结束
最后得到的就是一个无名的 CompoundTag,接下来往下看第二个标签:

【StringTag】BiomeOverride
1字节,当前类型:08,对应上面的StringTag
2字节,0D 00,转为10进制后可以知道标签名长度为 13,知道了长度就能读取标签名了
13个字节,BiomeOverride,这个就是读取到的标签名了
2字节,00 00,由于当前是字符串标签,需要读取它的值,这里是值的长度,和标签名的读取方式一样,不过由于这里得到的值长度是 0,所以没有数据,所以这里是空字符串
最终我们得到的是【StringTag】BiomeOverride 标签,这个对应的就是我们的单一生物群系标签了,值就是Wiki上的【https://zh.minecraft.wiki/w/%E5%9F%BA%E5%B2%A9%E7%89%88%E6%95%B0%E6%8D%AE%E5%80%BC】中的命名空间,由于它在 CompoundTag 之后,所以是前面我们解析的无名 CompundTag 的子标签
我们来看下一个标签:

【ByteTag】CenterMapsToOrigin
同样的,我们简单过一下了,01 是 ByteTag,标签名长度为 00 12 也就是18字节,往后读取标签名就是 CenterMapsToOrigin
最后还要读取它的值,ByteTag 的值是一个字节,所以我们再读取一个字节,也就是 00,那么这个 ByteTag 也就读完了
最终得到的就是【ByteTag】CenterMapsToOrigin,它的值就是 0,这个标签对应的就是创建的地图是否以玩家为中心
由于篇幅关系,解析NBT就到这里吧,最后我们整理一下,得到的 level_dat 文件的NBT数据按树状结构应该为:
【CompoundTag】
┣【StringTag】BiomeOverride :
┣【ByteTag】CenterMapsToOrigin:0
┗...

NaroX解析level.dat部分截图
值得注意的是,CompoundTag 如果遇到了 EndTag,也就是 00,那么就代表着这个 CompoundTag 结束了
ListTag 的子标签不带有名称,其子标签数据格式的值应为:1字节标签类型,4字节数据个数,然后读取指定个数的值数据即可,比如我有一个名称为 test 的 ListTag:[1,2,3],子标签类型是IntTag,数据有3个,那么这个ListTag最终在文件中储存的值数据应为:
09 //ListTag 类型, byte数据(1字节)
04 00 //test标签名字符串长度,short数据(2字节), 也就是小端数值4
74 65 73 74 //标签名 test 的16进制码
//后面是ListTag值数据
03 //子标签类型, byte数据(1字节), 由于这是一个 IntTag 列表, 那么类型就是 03
03 00 00 00 //子标签个数, int数据(4字节), 我们有3个子标签
//后面是这个ListTag的子标签
01 00 00 00 //第0个标签
02 00 00 00 //第1个标签
03 00 00 00 //第2个标签
如果 ListTag 没有子标签,那么只需要在它的值数据设为 EndTag,也就是 00 即可
解析完NBT格式后,level.dat文件应该就正常到结尾了,我们如果需要修改level.dat文件,可以按下面步骤来做:
修改好NBT数据,整理为16进制
计算整个NBT数据的长度
在 level.dat 内写入 4字节版本 (0A 00 00 00)
写入4字节NBT数据长度
写入NBT数据即可
解析完 level.dat 后,我们可以看一下有哪些好玩和有用的NBT吧:
FlatWorldLayers 标签就是当前超平坦预设,一般为json文本,有些旧版的存档这里可能为null,但也是可以修改添加json来自定义超平坦的,biome_id就是这个超平坦预设的群系,取值【https://zh.minecraft.wiki/w/%E5%9F%BA%E5%B2%A9%E7%89%88%E6%95%B0%E6%8D%AE%E5%80%BC#%E7%94%9F%E7%89%A9%E7%BE%A4%E7%B3%BBID】,block_layers就是当前超平坦方块,block_name是方块的命名空间,count就是有几层,方块是从底向上生成的,所以最上面的block_layers生成在底部,后面的生成在上面,encoding_version是格式版本,一般不建议修改,world_version是世界版本,与新版是否从y=-64开始生成有关,一般不建议修改,structure_options是结构生成,目前始终为null,可能mojang并没有放出来,可能还没做好?
MinimumCompatibleClientVersion 标签是此存档最后支持的MC版本,如果想用低版本进入这个存档就修改这部分
对于新版本获取成就的检测,比以前要严格不少,首先游戏模式必须是生存或冒险,不能用组件包,ForceGameType(强制游戏模式)设为0(生存),GameType(游戏模式)设为0(生存),chestsEnabled(是否作弊)设为0,commandsEnabled(允许管理员权限)设为0,hasBeenLoadedInCreate(是否用创造模式打开存档)设为0,这样你的存档应该能重新拥有成就了
RandomSeed标签就是当前世界的随机种子
LevelName是存档名称,修改这里会改变游戏中的存档名,然后生成存档目录下的levelname.txt文件
isHardCore是否极限模式,设为0就关闭极限模式了
abilities是世界玩家权限,可以控制能否攻击生物、飞行、放置破坏方块等等
worldStartCount标签是关闭游戏次数,每次退出游戏会-1,新版本的实体生成与存储与他有关,以后文章会介绍这部分
还有其他规则可以参考Wiki【https://zh.minecraft.wiki/w/%E5%9F%BA%E5%B2%A9%E7%89%88%E5%AD%98%E6%A1%A3%E6%A0%BC%E5%BC%8F】
本期我们就简单了解一下level.dat的数据吧,下一篇带大家了解一下DB内的数据,如果有疑惑可以在评论区讨论或私信up哦!