(我的MC是java版1.17.1,可能某些地方会和基岩版/其他java版有些许出入;有的问题,比如“主世界是什么”、“mod是什么”这种,太基础的或者和本文主题不太相关的,就不展开写了,可以自行去其他地方查一查)
MC的地图数据(就是“什么地方是什么方块”之类的)的坐标有三个轴:水平x,z轴和垂直y轴。x轴是往东/地图右越来越大,z轴是往南/地图下越来越大,y轴是垂直往上越来越大。反方向就是越来越小,而且可以有负数。后面你如果遇到三个数字,一般都是“x,y,z”的顺序;而往什么方向是增,往什么方向是减,也一般是依照这里所说的规则。
有三种“块”大小是不一样的:最基础的,就是MC里最直接的一个个/一格格方块,是单位块(block);然后是区块(chunk),1个区块包括16*n*16个单位块(水平长宽都是16,高度很高,整体是竖直的很长一条);然后是地图块(region),1个地图块包括32*32个区块(水平上俯瞰下去32行*32列,区块像薯条一样挤在一起)。最后,比如说主世界/上界的整个地图数据,就是切分成一个个地图块,然后以形如“r.?.?.mca”的一个个文件保存在.minecraft/saves/世界名/region文件夹里的。一个“r.?.?.mca”这种文件对应一个地图块。
(如果你有用voxelmap这个mod,那么显示地图的横竖grid线后,你看到的黑线就是切分区块的,红线就是切分地图块的)
三种块/坐标很容易搞混,这里我固定一种标记方法:单位块的标记用圆括号“(x,y,z)”或者“(x,z)”,区块用方括号“[x,z]”,地图块用花括号“{x,z}”。这些不同类型的坐标背后是一个尺度的问题,好比一个东西距离原点是24厘米,那么分米的话它距离原点2~3分米,米的话它距离原点0~1米。游戏里按F3会有显示一堆数据,看左边就会找到一些当前的坐标。
关于0/小数的问题:坐标是存在0的,-1和1的中间还有0,比如说存在(0,0,0)这种坐标;MC里每个方块的坐标一般是整数,假设你踩在一个方块上,按F3后,会看到“XYZ:”是玩家当前所在位置坐标,一般是小数,而它下面“Block:”就是对应的整数坐标,也就是玩家的脚所处的方块空间(而不是脚下踩的那个方块)对应的单位块坐标,这个坐标的x,z直接就等于脚下方块的x,z坐标,而y坐标减1就是脚下方块的了。
关于“包括范围”的问题:从x,z这两个水平维度来说,区块[0,0]包括单位块(0,0)~(15,15),以此类推,[1,0]包括(16,0)~(31,15)、[-1,0]包括(-16,0)~(-1,15)、[0,1]包括(0,16)~(15,31)、[0,-1]包括(0,-16)~(15,-1)等。地图块{0,0}包括区块[0,0]~[31,31],进而相当于包括单位块(0,0)~(511,511)。
(你应该明白这些大致要怎么推导计算了,我就不写公式了;我写的目的就是让你尽快清楚背后的原理,而不是我想半天弄个公式出来,又让你盯着公式头疼半天)
地图块的坐标,对应“r.?.?.mca”文件名上的坐标。比如区块{-1,1},那么文件名就是“r.-1.1.mca”。这种后缀名的文件就是MCA(Minecraft Anvil)格式,是一种二进制文件*。
(*二进制文件里面的基础元素是字节(byte/B),1字节是8比特(bit/b),1比特表示0或1两个数,进而1字节可以表示2⁸=256个数,也就是0~255;一个字节可以用两个16进制数表示,比如0x00是0这个数,0x0A是10这个数,0x10是16这个数;对于字节而言,有的数可以通过ASCII表转化为/相当于/表示某个字符,比如65对应'A',66对应'B',95对应'_'。二进制文件就是一种装着许许多多个这种叫做”字节“的东西的数据文件,以十六进制的表示法来看这种文件,里面就是一堆类似于“00 00 74 65 73 74 00 00 ...”的东西;1 KB = 1024 B,1 MB = 1024 KB)
一个.mca文件是一个地图块,相当于涉及到32*32=1024个区块,而每个区块的具体数据是各自独立的一整块一整块的。.mca文件开头的1024*8=8192个字节(= 8 KB,这个部分叫做“8K区”吧;一个区块对应8个字节),包括了每个区块的一些元数据(比如,某个区块的数据在本文件的什么位置“offset”)。那么,假设想获取某个区块的具体数据,就先从这个“8K区”里找到这个区块它的“offset”,从而跳转到后面的对应位置,就能找到这个区块相应的具体数据了。
“8K区”中,又分为“前4K区”和“后4K区”,各有1024*4=4096字节。
“前4K区”又按4个字节、4个字节地划分,每4个字节对应一个区块。最开始的4个字节对应区块[0,0],接着的4个字节对应[1,0],然后[2,0]、[3,0]...[31,0]、[0,1]、[1,1]...[30,31]、[31,31](先轮x,再轮z)。每4个字节中,前3个字节是一整个数据,是大端*(big-endian)的,代表着该区块的“offset”;后1个字节代表“区块长度(length of the chunk)”。
(*大端,big-endian,就是比如在文件里从头依次往后读取,读到“2A C8 73”这样连续三个字符,它要整体代表一个数,那么作为开头的“2A”就是这个数的高位,“73”是低位,连起来这个数是0x2AC873;小端就是反过来,开头的“2A”是这个数的低位,“73”是高位,连起来这个数是0x73C82A)
如果对应的区块没有数据,那么这4个字节都是“00”;如果“offset”是0x000002,那么对应的数据位置就是紧挨在“8K区”之后。
“后4K区”也这样每4个字节地划分并对应区块。每4个字节都是一整个数据,是大端的,代表着该区块的最后更新/保存时间“timestamp”,以纪元秒(epoch seconds)记录。
(我还没有实际去看“8K区”/.mca的二进制文件,基本直接参考MC的wiki内容,仅仅是换成容易理解的话写了出来)
“8K区”之后,就是一大块一大块的每个区块的数据。每个区块的区块数据叫做“区块数据块”吧。
“区块数据块”里,前4个字节是“后面还跟着多少/n个字节的本区块的数据”,第5个字节是“压缩类型(compression type)”,第6~(4+n)个字节是按照这种压缩类型处理过的NBT格式的数据(就是把原本NBT格式的数据压缩了再保存在里面)。
压缩类型有三种:(第5个字节如果是)“01”,就是GZip (RFC1952) (on-disk content of an Alpha chunk file);“02”,Zlib (RFC1950)(基本上实际使用的只有这个);“03”,不压缩,直接就是NBT格式。
NBT(Named Binary Tag)格式,可以单独保存为xxx.nbt(也是一种二进制文件)。(后面这句不太懂就跳过:)NBT格式是一种树形数据结构,类似文件夹和文件,有点像XML或者JSON那种层层嵌套地存放着各种数据。
NBT格式里要区分数据类型,其中包括:一些基础的类型,比如int、string等;list类型,就是里面可以装好几个某种类型的数据;compound类型,就是里面可以装各种类型的数据,有点像文件夹,进而里面可以层层嵌套着装各种各样的数据;end类型,用来放在compound的最后表示整个compound的数据完了到尾了,也大致表示“空”的意思。
(关于“数据类型是什么”、“int和string是什么”这种编程常识就不赘述了,不知道的话务必先自行了解)
写代码时,比如“int xxx = 2”就表示,这个数据的类型是int,变量名叫“xxx”,具体数据是2这个量;而在NBT格式里,这样一条数据(tag)会记录成“03 00 03 78 78 78 00 00 00 02”,其中开始的“03”(tag type / ID)表示这条数据是int类型,接着“00 03”表示变量名的长度为3,接着3个字符“78 78 78”也就是“xxx”这个变量名,接着4个字符(1个int要占4个字符)”00 00 00 02“(payload)表示2这个量。这整个一条就是一个完整的int数据。
“08”则是string,比如“08 00 01 73 00 04 74 65 73 74”中,“00 01”和“73”表示长度为1的变量名“s”,“00 04”和“74 65 73 74”表示长度为4的字符串“test”,整个相当于“string s = 'test'”
“09”则是list,比如“09 00 03 70 6f 73 03 00 00 00 03 00 00 00 01 00 00 00 02 00 00 00 03”中,“09 00 03 70 6f 73”表示这个list名叫长度为3的“pos”,后面跟着“03”和“00 00 00 03”表示要存放int数据类型,一个会有3个,再后面“00 00 00 01 00 00 00 02 00 00 00 03”就是挨在一起的1,2,3这三个量。总的来说就是“pos”这个list里装了1,2,3这三个int。
“0a”(十进制等于10)则是compound。对于前面整整3条数据(3个tag)的具体字节内容,假设分别以[int],[string],[list]来代替,那么比如“0a 00 00 [int] [string] [list] 00”就表示名叫“”(空的,没有名字)的compound数据,里面装了[int],[string],[list],而最后的“00”标志着这个compound的结尾(否则你不知道要到哪里才结束)。
更多详细的关于NBT格式的规则,可见https://minecraft.fandom.com/wiki/NBT_format中的“Binary format”以及其中的“TAG definition”。
游戏里可以用结构方块(structure block)来选定某一片立体区域/空间(这样一片立体区域/空间叫做“结构”),把里面的方块/建筑保存成这种.nbt文件(保存在.minecraft/saves/世界名/generated/minecraft/structures文件夹里),也可以从已有的.nbt文件中把方块/建筑搬进游戏里。
游戏里,放一个结构方块在地上,然后右击打开它的面板,点左下角“LOAD”按键就可以切换它的模式,按键上的字变成“SAVE”后,就变成了可以保存东西的模式。然后,可以看到面板上有两行,每一行都是三个空,第一行的三个空从左到右依次对应x,y,z,第二行也是。这些坐标都是单位块类型的坐标。第一行“Relative Position”是:如果设置为(0,0,0),就是以结构方块自己为“起点”;如果设置为(1,2,-1),就是从结构方块自己所在的位置开始,往东1格,再往上2格,再往北1格,最终到的那个方块,以这个方块为“起点”;这个“起点”就是到时候你要选一片立体区域/空间,这个立体区域/空间的“起点”是哪里,是在相对于这个结构方块本身的什么位置。第二行“Structure Size”就是你到时候想选的那片立体区域/空间的长宽高,比如设置成(1,2,3),就是x轴上1格长,y轴上2格高,z轴上3格长,而这个立体区域/空间是包括着那个“起点”方块的。最上面的Structure Name假设填入“xxx”,再按右下角的“SAVE”,就会成功保存一个“xxx.nbt”文件了。
你可以试试先用结构方块弄出一个.nbt文件,再用https://irath96.github.io/webNBT/这个在线工具来打开.nbt文件,成功打开后就能看到十六进制的字节内容。(这个工具有时候有点bug,等它页面加载完毕再上传文件/文件打开不成功再试一下)
