魔力寶貝檔案結構分析
對我個人而言,魔力寶貝是一款意義深遠的 MMORPG。在楓之谷還未進入台灣之前,我有幸在當時國小同班同學的推薦下接觸到了這款遊戲。對於一名小學生而言,那是個相當令人著迷的中世紀奇幻冒險世界,在廣闊的地圖上冒險,甚至在初次進遊戲時就在靈堂迷路了數小時。
然而月費制的遊戲模式也注定了與小學生沒什麼關係,因此從家裡發現我拿著壓歲錢買了樂園之卵的典藏限定包(外加一頓竹筍炒肉絲的粗飽)之後,我就帶著遺憾地離開了那個世界。轉而投向楓之谷的懷抱
直到 2020 年初,當時我正在學習 Rust,而我一直堅定地相信唯有自己做出點東西才叫做學會某項技能;當時的我不自量力地根據散落在各個角落的資料,漸漸拼湊出魔力寶貝檔案結構的具體實作。即便當時以失敗告終,這篇文就再也沒能推出續集,但是這個想法一直在我心中縈繞不止。
今天,我認為是那個好日子。
檔案總覽
魔力寶貝中,Binary File 大致上可以分為圖像檔(Graphic)、動畫檔(Anime)、地圖檔(Map),以及一些是尚未被解析的檔案(尤其是 3.0 樂園之卵之後更換開發團隊,加入了許多新東西且現成逆向成果較少)。
根據不同的 PUK(其實可以理解成現代遊戲中的 DLC),會使用不同的檔案名稱後綴來辨別(以下都使用大宇資訊最原始的台服檔名,永恆初心或水藍魔力會有所差異):
- 神獸傳奇 + 魔弓傳奇
- 圖像索引:
GraphicInfo_66.bin - 圖像:
Graphic_66.bin - 動畫索引:
AnimeInfo_4.bin - 動畫:
Anime_4.bin
- 圖像索引:
- 龍之沙漏
- 圖像索引:
GraphicInfoEx_5.bin - 圖像:
GraphicEx_5.bin - 動畫索引:
AnimeInfoEx_1.Bin - 動畫:
AnimeEx_1.Bin
- 圖像索引:
- 樂園之卵
- 圖像索引:
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
- 圖像索引:
- 天界騎士與星詠歌姬
- 圖像索引:
Puk3/GraphicInfo_PUK3_1.bin - 圖像:
Puk3/Graphic_PUK3_1.bin - 動畫索引:
Puk3/AnimeInfo_PUK3_2.bin - 動畫:
Puk3/Anime_PUK3_2.bin
- 圖像索引:
- 砂之記憶與覺醒之光
- 圖像索引:
GraphicInfo_Joy_125.bin - 圖像:
Graphic_Joy_125.bin - 動畫索引:
AnimeInfo_Joy_91.bin - 動畫:
Anime_Joy_91.bin
- 圖像索引:
- 輪迴之守
- 圖像索引:
GraphicInfo_Joy_CH1.bin - 圖像:
Graphic_Joy_CH1.bin - 動畫索引:
AnimeInfo_Joy_CH1.bin - 動畫:
Anime_Joy_CH1.bin
- 圖像索引:
- 天使降臨
- 圖像索引:
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,具體的資料定義如下:
| 偏移 | 大小 | 型別 | 欄位 | 說明 |
|---|---|---|---|---|
| 0 | 4 | int32 | id | 圖像唯一編號 |
| 4 | 4 | uint32 | addr | 在 Graphic*.bin 中的位元組偏移 |
| 8 | 4 | int32 | len | 資料長度(bytes) |
| 12 | 4 | int32 | offX | 繪製 X 偏移(相對於錨點) |
| 16 | 4 | int32 | offY | 繪製 Y 偏移(相對於錨點) |
| 20 | 4 | int32 | width | 像素寬度 |
| 24 | 4 | int32 | height | 像素高度 |
| 28 | 1 | uint8 | gridW | 格子寬度(地圖格子單位) |
| 29 | 1 | uint8 | gridH | 格子高度(地圖格子單位) |
| 30 | 1 | uint8 | access | 通行屬性 |
| 31 | 5 | — | padding | 未使用 |
| 36 | 4 | int32 | mapId | 地圖磚瓦 ID(非零表示此圖是地圖磚瓦或隱藏調色盤) |
註1:其實
id雖然叫做「唯一編號」,但在實際檔案中是有重複的可能的… 註2:對於mapId != 0時,在 2.0 之前代表的意義一直都是 map tile,但是到了 3.0 之後開始出現隱藏調色盤的用途,在調色盤的區域我們會再詳細介紹。
圖片檔案
Graphic*.bin 是具體儲存圖片資料的檔案。
這個檔案會有一個 16 bytes 的檔頭,一樣是 little-edian,具體資料定義如下:
| 偏移 | 大小 | 說明 |
|---|---|---|
| 0–1 | 2 | Magic bytes:0x52 0x44(ASCII "RD") |
| 2 | 1 | version:0 或 1 使用外部調色盤;≥ 2 內嵌調色盤 |
| 3 | 1 | 圖像類別 |
| 4-7 | 4 | 圖像寬度 |
| 8-11 | 4 | 圖像高度 |
| 12-16 | 4 | 資料長度 |
註1:在
Graphic*.bin中的「圖像寬度」、「圖像高度」與「資料長度」資料可能並不準確,在構建時應以GraphicInfo*.bin中的資料為準。 註2:在第 3 個 byte 猜測是「圖像類別」,例如0xbf的話表示該圖像是用於地圖標記(例如「城下町」「海/川」這樣的地名標記)
當 version >= 2(僅出現在「PUK2 樂園之卵」之後)的時候,需要再向後讀 4 bytes 作為內嵌調色盤的大小:
| 偏移 | 大小 | 說明 |
|---|---|---|
| 16 | 4 | 內嵌調色盤的大小(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。
| 偏移 | 大小 | 型別 | 欄位 | 說明 |
|---|---|---|---|---|
| 0 | 4 | int32 | id | 動畫唯一編號 |
| 4 | 4 | int32 | addr | 在 Anime*.bin 中的位元組偏移 |
| 8 | 2 | int16 | actCnt | 此動畫包含的 Action 數量 |
| 10 | 2 | — | padding | 未使用 |
動畫檔案
對於 Anime*.bin 而言,由 AnimeInfo*.bin 查找的位址會指向一段連續的 Action 資料,由一個檔頭(Header)與複數個幀(Frame)組成。
動畫檔頭
每個 Action 的 header 最短 12 bytes,如為 v3 格式則為 20 bytes:
| 偏移 | 大小 | 型別 | 欄位 | 說明 |
|---|---|---|---|---|
| 0 | 2 | int16 | direct | 方向(0–7 對應八方向) |
| 2 | 2 | int16 | action | 動作類型代碼 |
| 4 | 4 | int32 | duration | 整個動作的總時長(毫秒) |
| 8 | 4 | int32 | frameCnt | 此 Action 的幀數 |
| 12 | 4 | — | (v3 預留) | |
| 16 | 4 | int32 | sentinel | 若值為 -1 (0xFFFFFFFF) 則為 v3 格式 |
v3 格式(sentinel == -1)額外讀取:
| 偏移 | 大小 | 說明 |
|---|---|---|
| 14 | 2 | reversed:非 0 表示動作序列需反向播放 |
v3 header 總大小 20 bytes;非 v3 為 12 bytes。
動畫幀
每幀固定 10 bytes,接在動畫檔頭之後連續排列:
| 偏移 | 大小 | 型別 | 欄位 | 說明 |
|---|---|---|---|---|
| 0 | 4 | int32 | graphicId | 此幀使用的 Graphic ID |
| 4 | 2 | int16 | offX | 繪製 X 偏移 |
| 6 | 2 | int16 | offY | 繪製 Y 偏移 |
| 8 | 2 | int16 | flag | 旗標(目前保留) |
動畫播放
對於動畫的每幀所佔用時長為 action.header.duration / frameCnt,單位是毫秒
地圖檔
地圖檔位於 map/*.dat 中,如果是剛安裝完並未啟動遊戲的話,可能會看到一片空白,這是因為地圖是在進入時才會從伺服器上被下載下來(為了實現動態迷宮功能)。
地圖檔固定由檔頭、地面層、物件層、資料層組成。
地圖檔頭
地圖檔頭固定為 20 bytes
| 偏移 | 大小 | 說明 |
|---|---|---|
| 0–2 | 3 | Magic bytes:MAP(0x4D, 0x41, 0x50) |
| 3–11 | 9 | 保留 |
| 12 | 4 | width:地圖格子寬度 |
| 16 | 4 | height:地圖格子高度 |
地面層、物件層與資料層
這三個層是相等大小的,其大小為 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_BGR) | 16 個固定顏色,不隨 CGP 檔案改變 |
| 16–239 | CGP 檔案內容 | 224 個自訂顏色(BGR 三元組) |
| 240–255 | 固定後綴(SUFFIX_BGR) | 16 個固定顏色 |
透明色規則
調色盤索引 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*.bin 與 GraphicV3*.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.bin 中 mapId === animeId 圖片的內嵌調色盤,這麼做的優勢在於不需要為每一幀都加入調色盤,能大大降低空間佔用。
結語
因為現有網路資源限制,我們這次僅說明了圖像、動畫、地圖與調色盤相關的邏輯,而這個邏輯也僅針對官方伺服器為主(對於私服,因為現有開服工具/服務都會對圖檔做加密,就超出本文的討論範圍)
給個提示:被加密後的圖檔的 Graphic*.bin 不會是 RD 開頭,如果遇到這種檔案跳過就好(其實它們的加密算法也不難,可以自己找看看 :3)
最後,基於本文的研究我建立了一個 Rust Library,並且可以編譯為 WASM 使其與 Javascript 生態系對接(我據此開發了一個資源編輯器,但因為做得滿簡陋的就沒開源出來),這個在編解碼的演算法實作上還用上了 SIMD,算是一點小小的成就(雖然單純是自己想炫技)
