WZ 與 IMG 資源整合原理
本篇定位
這是 Phase 7 的第 3 篇。 MapleStory 的所有美術資源(圖像、音效、地圖、怪物資料)都封裝在
.wz檔案中。這篇解析:WZ 格式是什麼、登入器如何 Hook 資源系統、以及私服如何替換或新增遊戲資源。
一、WZ 檔案是什麼?
WZ(Wizard/Way/WZ,確切意思不明,是 Nexon 自訂格式)是 MapleStory 的資源封裝格式,類似 ZIP 但有加密層。
MapleStory 的遊戲目錄下有多個 WZ 檔案,每個負責不同類型的資源:
| WZ 檔案 | 內容 |
|---|---|
Map.wz | 地圖背景圖、地形、傳送點資料 |
Mob.wz | 怪物的圖像、動畫、攻擊動作 |
Character.wz | 角色外觀、裝備貼圖 |
Sound.wz | 音效、背景音樂(BGM) |
UI.wz | 所有 UI 元素的圖像 |
Skill.wz | 技能圖示、技能效果動畫 |
String.wz | 所有遊戲文字(物品名稱、對話等) |
Item.wz | 物品圖像、物品資料(價格、效果) |
二、WZ 檔案的內部結構
Map.wz
├── Back/ ← 背景圖像資料夾
│ ├── grassySoil.img ← IMG 節點(包含多個子屬性)
│ └── ...
├── Map/ ← 地圖資料
│ ├── Map0/
│ │ ├── 000010000.img ← 特定地圖(以地圖ID命名)
│ │ └── ...
│ └── ...
└── ...
每個 .img 是 WZ 樹狀結構中的一個節點(Node),可以包含:
- 子節點(資料夾)
- 圖像資料(Bitmap / Canvas)
- 整數、字串、向量、音頻等原始資料
三、遊戲如何讀取 WZ 資源
MapleStory 使用 IWzFileSystem 介面(類似虛擬檔案系統)來讀取 WZ:
遊戲程式碼 IWzFileSystem
│ │
├─ GetNode("Mob/100100.img") ──►│──► 解密 Mob.wz → 找到 100100.img → 回傳節點
│ │
├─ GetBitmap("UI/Basic.img/Button/OkBtn0") → 回傳 Bitmap 物件
│ │
└─ ... │
IWzFileSystem::Init(地址 0x009F7964)是初始化入口,它:
- 掃描遊戲目錄下的所有
.wz檔案 - 建立 WZ 樹狀索引(不立刻解密全部,只建立目錄結構)
- 後續按需解密(Lazy Loading)
四、登入器 Hook:Hook_sub_9F7964
MapleEzorsia-v2 Hook 了 IWzFileSystem::Init,目的是在 WZ 系統初始化之後注入自訂資源路徑:
using tIWzFileSystem_Init = void(__thiscall*)(void* thisPtr, const wchar_t* basePath);
tIWzFileSystem_Init original_IWzFileSystem_Init =
(tIWzFileSystem_Init)addys::IWzFileSystem_Init;
void __fastcall Hook_IWzFileSystem_Init(void* thisPtr, void* edx, const wchar_t* basePath) {
// 先執行原始的 Init(建立標準 WZ 索引)
original_IWzFileSystem_Init(thisPtr, basePath);
// 之後注入額外的資源路徑
// 例如:讓遊戲也能從 "CustomWZ/" 資料夾讀取資源
InjectCustomWzDirectory(thisPtr, L"CustomWZ");
}五、自訂 WZ:替換或新增資源的兩種方法
方法一:直接替換 WZ 檔案
最簡單的方法,直接把修改過的 .wz 檔案放在遊戲目錄,覆蓋原始檔案:
遊戲目錄/
├── UI.wz ← 替換成自訂版本(含私服 Logo、自訂按鈕)
├── String.wz ← 替換成含私服伺服器名稱的版本
└── MapleStory.exe
優點:簡單直接。 缺點:如果私服更新了遊戲版本,自訂 WZ 可能需要重新合併。
方法二:Patch WZ(局部替換)
不替換整個 WZ 檔案,而是在記憶體中攔截特定資源的讀取,回傳我們的版本:
// Hook StringPool::GetString 來替換特定字串
// (之前在 ReplacementFuncs.h 已介紹)
// Hook WZ 的 Bitmap 讀取,替換特定圖像
void* Hooked_GetBitmap(void* thisPtr, const wchar_t* path) {
// 替換登入畫面的 Logo 圖像
if (wcscmp(path, L"UI/Login.img/Title/maple") == 0) {
return LoadCustomBitmap("CustomWZ/my_logo.png");
}
return original_GetBitmap(thisPtr, path);
}六、IWzFileSystem::InitializeResMan 的角色
除了 Init,登入器也 Hook 了 CWvsApp::InitializeResMan(地址 0x009F7159):
// InitializeResMan 決定遊戲使用哪個資源管理器(單機 vs 線上)
// v83 的「線上模式」會從伺服器下載部分資源
// Hook 它,強制使用「本地 WZ 模式」,不嘗試從官方伺服器下載
void __fastcall Hook_InitializeResMan(void* thisPtr, void* edx) {
// 不呼叫原始函式(完全替換)
// 直接初始化為本地 WZ 模式
InitLocalResMan(thisPtr);
}為什麼要 Hook InitializeResMan?
官方 MapleStory v83 的資源管理器在連線時會嘗試從 Nexon 的 CDN 驗證資源版本。私服沒有 Nexon 的 CDN,這個請求永遠失敗,造成遊戲卡在載入畫面。強制使用本地模式跳過這個驗證步驟。
七、WZ 工具:如何製作自訂 WZ
開發者工作流程:
flowchart LR A["原始 .wz 檔案"] --> B["WzLib / MapleLib 解包"] B --> C["PNG / OGG / XML 等原始資源"] C --> D["修改(換 Logo、改文字)"] D --> E["重新打包成 .wz"] E --> F["放入遊戲目錄"]
常用的 WZ 工具:
| 工具 | 功能 |
|---|---|
| WzLib / MapleLib | C# 函式庫,可讀寫所有版本的 WZ 格式 |
| HaRepacker | 圖形介面 WZ 瀏覽器,支援匯入/匯出圖像 |
| PKG WZ Editor | 輕量 WZ 編輯器,專注於 v83 格式 |
| WzComparator | 比較兩個 WZ 檔案的差異,用於版本更新分析 |
八、常見問題
Q:WZ 檔案有加密嗎?
是的。v83 的 WZ 使用一種簡單的 XOR 加密,密鑰是已知的(逆向工程社群早已公開)。WzLib 等工具在讀取時自動解密,開發者通常不需要手動處理加密層。
Q:替換 WZ 後遊戲的 CRC 檢查會失敗嗎?
如果 CRC Hook 已正確安裝(
Hook_sub_9F4E54),CRC 檢查會被繞過,替換 WZ 不會有問題。如果忘記安裝 CRC Hook,遊戲在啟動時會偵測到 WZ 被修改並強制關閉。
延伸閱讀
- 01_config.ini設定系統 — 自訂 WZ 路徑可加入設定檔管理
- 01_ReplacementFuncs.h_函式替換技術 — Hook StringPool 替換字串的實作
- 01_如何測試與除錯DLL — Phase 8:測試自訂 WZ 是否正確載入