1. MCA格式、NBT格式入門


(我的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,等它頁面加載完畢再上傳文件/文件打開不成功再試一下)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM