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)是初始化入口,它:

  1. 掃描遊戲目錄下的所有 .wz 檔案
  2. 建立 WZ 樹狀索引(不立刻解密全部,只建立目錄結構)
  3. 後續按需解密(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 / MapleLibC# 函式庫,可讀寫所有版本的 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 被修改並強制關閉。


延伸閱讀