跳至主要内容

魔力寶貝檔案結構分析

· 閱讀時間約 11 分鐘
Vincent Chi
Software Enineer, Backend

對我個人而言,魔力寶貝是一款意義深遠的 MMORPG。在楓之谷還未進入台灣之前,我有幸在當時國小同班同學的推薦下接觸到了這款遊戲。對於一名小學生而言,那是個相當令人著迷的中世紀奇幻冒險世界,在廣闊的地圖上冒險,甚至在初次進遊戲時就在靈堂迷路了數小時。

然而月費制的遊戲模式也注定了與小學生沒什麼關係,因此從家裡發現我拿著壓歲錢買了樂園之卵的典藏限定包(外加一頓竹筍炒肉絲的粗飽)之後,我就帶著遺憾地離開了那個世界。轉而投向楓之谷的懷抱

直到 2020 年初,當時我正在學習 Rust,而我一直堅定地相信唯有自己做出點東西才叫做學會某項技能;當時的我不自量力地根據散落在各個角落的資料,漸漸拼湊出魔力寶貝檔案結構的具體實作。即便當時以失敗告終,這篇文就再也沒能推出續集,但是這個想法一直在我心中縈繞不止。

今天,我認為是那個好日子。

檔案總覽

魔力寶貝中,Binary File 大致上可以分為圖像檔(Graphic)、動畫檔(Anime)、地圖檔(Map),以及一些是尚未被解析的檔案(尤其是 3.0 樂園之卵之後更換開發團隊,加入了許多新東西且現成逆向成果較少)。

根據不同的 PUK(其實可以理解成現代遊戲中的 DLC),會使用不同的檔案名稱後綴來辨別(以下都使用大宇資訊最原始的台服檔名,永恆初心或水藍魔力會有所差異):

  1. 神獸傳奇 + 魔弓傳奇
    • 圖像索引:GraphicInfo_66.bin
    • 圖像:Graphic_66.bin
    • 動畫索引:AnimeInfo_4.bin
    • 動畫:Anime_4.bin
  2. 龍之沙漏
    • 圖像索引:GraphicInfoEx_5.bin
    • 圖像:GraphicEx_5.bin
    • 動畫索引:AnimeInfoEx_1.Bin
    • 動畫:AnimeEx_1.Bin
  3. 樂園之卵
    • 圖像索引:GraphicInfoV3_19.bin, Puk2/GraphicInfo_PUK2_2.bin
    • 圖像:GraphicV3_19.bin, Puk2/Graphic_PUK2_2.bin
    • 動畫索引:AnimeInfoV3_8.bin, Puk2/AnimeInfo_PUK2_4.bin
    • 動畫:AnimeV3_8.bin, Puk2/Anime_PUK2_4.bin
  4. 天界騎士與星詠歌姬
    • 圖像索引:Puk3/GraphicInfo_PUK3_1.bin
    • 圖像:Puk3/Graphic_PUK3_1.bin
    • 動畫索引:Puk3/AnimeInfo_PUK3_2.bin
    • 動畫:Puk3/Anime_PUK3_2.bin
  5. 砂之記憶與覺醒之光
    • 圖像索引:GraphicInfo_Joy_125.bin
    • 圖像:Graphic_Joy_125.bin
    • 動畫索引:AnimeInfo_Joy_91.bin
    • 動畫:Anime_Joy_91.bin
  6. 輪迴之守
    • 圖像索引:GraphicInfo_Joy_CH1.bin
    • 圖像:Graphic_Joy_CH1.bin
    • 動畫索引:AnimeInfo_Joy_CH1.bin
    • 動畫:Anime_Joy_CH1.bin
  7. 天使降臨
    • 圖像索引:GraphicInfo_Joy_EX_9.bin
    • 圖像:Graphic_Joy_EX_9.bin
    • 動畫索引:AnimeInfo_Joy_EX_9.bin
    • 動畫:Anime_Joy_EX_9.bin

註:對於「PUK5 輪迴之守」及「PUK6 天使降臨」的檔案我不確定是否正確,因為我並沒有續費也沒有更新,所以檔名可能會與現在可執行版本有所落差。

圖片索引

GraphicInfo*.bin 是一個索引檔,用於查找具體圖片在 Graphic*.bin 的位址。

這個檔案由每塊 40 bytes 組合而成,為 little-edian,具體的資料定義如下:

偏移大小型別欄位說明
04int32id圖像唯一編號
44uint32addrGraphic*.bin 中的位元組偏移
84int32len資料長度(bytes)
124int32offX繪製 X 偏移(相對於錨點)
164int32offY繪製 Y 偏移(相對於錨點)
204int32width像素寬度
244int32height像素高度
281uint8gridW格子寬度(地圖格子單位)
291uint8gridH格子高度(地圖格子單位)
301uint8access通行屬性
315padding未使用
364int32mapId地圖磚瓦 ID(非零表示此圖是地圖磚瓦或隱藏調色盤)

註1:其實 id 雖然叫做「唯一編號」,但在實際檔案中是有重複的可能的… 註2:對於 mapId != 0 時,在 2.0 之前代表的意義一直都是 map tile,但是到了 3.0 之後開始出現隱藏調色盤的用途,在調色盤的區域我們會再詳細介紹。

圖片檔案

Graphic*.bin 是具體儲存圖片資料的檔案。

這個檔案會有一個 16 bytes 的檔頭,一樣是 little-edian,具體資料定義如下:

偏移大小說明
0–12Magic bytes:0x52 0x44(ASCII "RD")
21version:0 或 1 使用外部調色盤;≥ 2 內嵌調色盤
31圖像類別
4-74圖像寬度
8-114圖像高度
12-164資料長度

註1:在 Graphic*.bin 中的「圖像寬度」、「圖像高度」與「資料長度」資料可能並不準確,在構建時應以 GraphicInfo*.bin 中的資料為準。 註2:在第 3 個 byte 猜測是「圖像類別」,例如 0xbf 的話表示該圖像是用於地圖標記(例如「城下町」「海/川」這樣的地名標記)

version >= 2(僅出現在「PUK2 樂園之卵」之後)的時候,需要再向後讀 4 bytes 作為內嵌調色盤的大小:

偏移大小說明
164內嵌調色盤的大小(bytes)

注意:調色盤的大小是以「解碼後」的資料來看,所以必須先將整個資料塊解碼之後,再根據這裡的大小抓出指定的資料塊才是調色盤。

|--------------|
| Encoded Data |
|--------------|
|
| Decode
|
|----------------------------------------| |-----------------------------|
| Image Data (total_size - palette_size) | + | Palette Data (palette_size) |
|----------------------------------------| |-----------------------------|

Run Length 編解碼算法

如果每張圖都儲存原始圖片,會讓檔案大小膨脹得非常離譜,所以在 Graphic 檔案中引入了一個無損的編碼算法:自定義的 Run Length 編碼。

version 為奇數時,表示該圖像經過編碼,需要解碼後才能正常呈現;反之則為直接存入的。

每個指令會以一個 flag byte 決定指令的類型,格式如下:

高 nibble指令類型長度欄位 (Length)額外參數
0x0_Raw: 直接複製 bytes低 nibble後接 len 個原始 bytes
0x1_Raw: 直接複製 bytes(低 << 8) | 下 1 byte後接 len 個原始 bytes
0x2_Raw: 直接複製 bytes(低 << 16) | 下 2 bytes後接 len 個原始 bytes
0x8_Repeat: 重複特定值低 nibble下 1 byte 為重複值
0x9_Repeat: 重複特定值(低 << 8) | 下 1 byte先讀 1 byte 重複值,再讀 1 byte 長度低位
0xa_Repeat: 重複特定值(低 << 16) | 下 2 bytes先讀 1 byte 重複值,再讀 2 bytes 長度低位
0xc_Zero: 重複 0x00低 nibble
0xd_Zero: 重複 0x00(低 << 8) | 下 1 byte
0xe_Zero: 重複 0x00(低 << 16) | 下 2 bytes

動畫索引

對於 AnimeInfo*.bin 而言,每筆記錄固定 12 bytes,little-edian。

偏移大小型別欄位說明
04int32id動畫唯一編號
44int32addrAnime*.bin 中的位元組偏移
82int16actCnt此動畫包含的 Action 數量
102padding未使用

動畫檔案

對於 Anime*.bin 而言,由 AnimeInfo*.bin 查找的位址會指向一段連續的 Action 資料,由一個檔頭(Header)與複數個幀(Frame)組成。

動畫檔頭

每個 Action 的 header 最短 12 bytes,如為 v3 格式則為 20 bytes:

偏移大小型別欄位說明
02int16direct方向(0–7 對應八方向)
22int16action動作類型代碼
44int32duration整個動作的總時長(毫秒)
84int32frameCnt此 Action 的幀數
124(v3 預留)
164int32sentinel若值為 -1 (0xFFFFFFFF) 則為 v3 格式

v3 格式(sentinel == -1)額外讀取:

偏移大小說明
142reversed:非 0 表示動作序列需反向播放

v3 header 總大小 20 bytes;非 v3 為 12 bytes。

動畫幀

每幀固定 10 bytes,接在動畫檔頭之後連續排列:

偏移大小型別欄位說明
04int32graphicId此幀使用的 Graphic ID
42int16offX繪製 X 偏移
62int16offY繪製 Y 偏移
82int16flag旗標(目前保留)

動畫播放

對於動畫的每幀所佔用時長為 action.header.duration / frameCnt,單位是毫秒

地圖檔

地圖檔位於 map/*.dat 中,如果是剛安裝完並未啟動遊戲的話,可能會看到一片空白,這是因為地圖是在進入時才會從伺服器上被下載下來(為了實現動態迷宮功能)。

地圖檔固定由檔頭、地面層、物件層、資料層組成。

地圖檔頭

地圖檔頭固定為 20 bytes

偏移大小說明
0–23Magic bytes:MAP(0x4D, 0x41, 0x50)
3–119保留
124width:地圖格子寬度
164height:地圖格子高度

地面層、物件層與資料層

這三個層是相等大小的,其大小為 width × height × 2,事實上可以理解為三個圖層。

圖層偏移說明
地面20地面磚瓦;每個值為 MapID
物件20 + layerSize×2物件層磚瓦;每個值為 MapID
資料20 + layerSize×4通行屬性層

MapID 如果為 0 代表空格;非 0 值則需要從 GraphicInfo.mapID 反查其具體圖像。

等距座標轉換

地圖使用菱形等距投影(isometric)。格子 (col, row) 轉換為螢幕座標:

screenX = (col - row) × 32
screenY = (col + row) × 23

磚瓦基礎尺寸:寬 64 px、高 47 px(實際繪製時以 23 px 為半高)。

調色盤

X-Gate 圖像採用 256 色調色盤索引格式。每個像素儲存的是調色盤中的索引值(0–255),渲染時再查表轉為 RGBA。

外部調色盤

用於 version < 2 的圖像(Graphic header 中 version 欄位為 0 或 1)。

外部調色盤位於 bin/pal/ 目錄下,副檔名為 .cgp,大小固定 672 bytes:

(256 - 32) × 3 bytes = 224 自訂顏色 × 3 bytes BGR

注意:顏色的排列是 BGR,而非 RGB

調色盤組成(共 256 項)
索引範圍來源說明
0–15固定前綴(PREFIX_BGR16 個固定顏色,不隨 CGP 檔案改變
16–239CGP 檔案內容224 個自訂顏色(BGR 三元組)
240–255固定後綴(SUFFIX_BGR16 個固定顏色
透明色規則

調色盤索引 0(即第一個項目)固定為完全透明(Alpha = 0),不論其 RGB 值為何。其餘索引 1–255 的 Alpha 均為 255。

嵌入式調色盤

version ≥ 2 的圖像不使用外部 CGP,而是在 Graphic 資料末端自帶調色盤。

解碼後的 payload 結構:

[ 像素索引資料 ] [ 調色盤 BGR 三元組 × psz/3 項 ]
↑ 長度 = payload.length - psz ↑ 長度 = psz

其中 psz 為 header 中第 17–20 bytes 讀出的 int32。

內嵌調色盤不含前綴/後綴,索引 0 固定為透明色(Alpha = 0)。

隱藏式調色盤

從 v2 開始,部分動畫會使用一種特殊機制覆蓋預設調色盤,稱為隱藏調色盤。根據動畫版本的不同,解析方式分為兩類。

3.1 v2-palette 模式(v2、v3、v4 動畫)

GraphicInfoV3*.binGraphicV3*.bin 中,存在一批尺寸為 4×1 像素、mapId > 0 的圖像。這些圖像的 mapId 欄位被重新解釋,意義不再是地圖磚瓦,而是動畫 ID

範例:

GraphicInfo (setKey="v3_19", id=3890, width=4, height=1, mapId=110350)
→ 代表 AnimeID 110350 應使用 Graphic #3890 的內嵌調色盤

重要:即使動畫本身屬於 v3 或 v4,其隱藏調色盤仍固定查找 v2 的 "v3_19" 集合

3.2 mapid 模式(v5+ 動畫)

從 v5 起,隱藏調色盤的來源改為動畫自己所屬版本的 Graphic 集,例如在 AnimeInfo_Joy_91.bin 中的動畫,會需要找 GraphicInfo_Joy_125.binmapId === animeId 圖片的內嵌調色盤,這麼做的優勢在於不需要為每一幀都加入調色盤,能大大降低空間佔用。

結語

因為現有網路資源限制,我們這次僅說明了圖像、動畫、地圖與調色盤相關的邏輯,而這個邏輯也僅針對官方伺服器為主(對於私服,因為現有開服工具/服務都會對圖檔做加密,就超出本文的討論範圍)

給個提示:被加密後的圖檔的 Graphic*.bin 不會是 RD 開頭,如果遇到這種檔案跳過就好(其實它們的加密算法也不難,可以自己找看看 :3)

最後,基於本文的研究我建立了一個 Rust Library,並且可以編譯為 WASM 使其與 Javascript 生態系對接(我據此開發了一個資源編輯器,但因為做得滿簡陋的就沒開源出來),這個在編解碼的演算法實作上還用上了 SIMD,算是一點小小的成就(雖然單純是自己想炫技)