解析度修改原理與實作
本篇定位
這是 Phase 5 的第 2 篇,深入解析
Client::UpdateResolution()的完整實作。 MapleStory v83 預設只支援 800×600,要讓它跑在現代螢幕上(如 1280×720、1920×1080),需要在三個層次同時修改記憶體。
一、問題全景:800×600 藏在哪些地方?
用 IDA Pro 搜尋立即值 0x320(800)和 0x258(600),可以找到超過 30 個出現位置。根據功能分類:
| 類型 | 出現位置 | 修改方式 |
|---|---|---|
| Direct3D 全域變數 | GameWidth_Addr / GameHeight_Addr | Memory::Write |
| Back Buffer 設定 | BackBuffer_Width / BackBuffer_Height | Memory::Write |
| 機器碼立即值 | 散布於 CWvsApp::SetUp、CWvsApp::Run | Memory::WriteProtected |
| Viewport 設定 | D3DVIEWPORT9 結構體 | Code Cave Hook |
| UI 硬式座標 | 登入 / 選角 / HUD 各處 | Memory::Write(見 03 / 04) |
二、UpdateResolution() 完整流程
void Client::UpdateResolution() {
// 取得從 config.ini 讀入的目標解析度
int newW = Client::m_nGameWidth; // 例如 1280
int newH = Client::m_nGameHeight; // 例如 720
// ── 步驟一:寫入全域變數 ──────────────────────────────────────────────
Memory::Write<int>(addys::GameWidth_Addr, newW);
Memory::Write<int>(addys::GameHeight_Addr, newH);
Memory::Write<int>(addys::BackBuffer_Width, newW);
Memory::Write<int>(addys::BackBuffer_Height, newH);
// ── 步驟二:Patch 機器碼中的硬式解析度值 ────────────────────────────
PatchResolutionInCode();
// ── 步驟三:修正 Viewport(視口)設定 ────────────────────────────────
PatchViewport();
std::cout << "[Client] Resolution set to " << newW << "x" << newH << std::endl;
}三、步驟一:寫入全域變數
最直接的修改,遊戲的 Direct3D 初始化函式會讀取這些變數來建立 Back Buffer:
// addys::GameWidth_Addr = 0x00C4FCA4
// addys::GameHeight_Addr = 0x00C4FCA8
Memory::Write<int>(addys::GameWidth_Addr, 1280);
Memory::Write<int>(addys::GameHeight_Addr, 720);記憶體變化:
修改前:
0x00C4FCA4: 20 03 00 00 → 0x0320 = 800
0x00C4FCA8: 58 02 00 00 → 0x0258 = 600
修改後:
0x00C4FCA4: 00 05 00 00 → 0x0500 = 1280
0x00C4FCA8: D0 02 00 00 → 0x02D0 = 720
時機很重要
這個寫入必須在遊戲的 Direct3D 初始化(
CWvsApp::SetUp)之前完成。如果 SetUp 已經讀過這兩個變數並建立 Back Buffer,修改就晚了,需要等下一次 Reset(如切換全螢幕時)才會生效。
四、步驟二:Patch 機器碼中的硬式值
為什麼需要這一步?
遊戲的某些函式不從全域變數讀解析度,而是把數字直接硬式編碼在組合語言裡:
IDA Pro 反組譯結果(CWvsApp::SetUp 的片段):
0x009F52A0: 68 20 03 00 00 PUSH 800 ; 直接 push 數字 800!
0x009F52A5: 68 58 02 00 00 PUSH 600
0x009F52AA: E8 XX XX XX XX CALL CreateWindowExA
這裡的 0x320(800)和 0x258(600)是 CreateWindowExA 的 nWidth 和 nHeight 參數。就算全域變數改了,這個 PUSH 800 還是會建出一個 800px 寬的視窗。
如何 Patch
PUSH imm32 指令的機器碼結構:
68 XX XX XX XX
│ └──────────── 立即值(4 byte,小端序)
└─────────────── PUSH imm32 的 opcode
我們只要覆寫那 4 個立即值的 byte(位於指令開頭 +1 的位置):
void Client::PatchResolutionInCode() {
int newW = Client::m_nGameWidth;
int newH = Client::m_nGameHeight;
// 每個 patch 點:地址 + 1(跳過 opcode byte,直接寫立即值)
// CWvsApp::SetUp 裡建立主視窗的呼叫
Memory::WriteProtected<int>(addys::Patch_CreateWindow_Width, newW);
Memory::WriteProtected<int>(addys::Patch_CreateWindow_Height, newH);
// CWvsApp::SetUp 裡設定 D3DPRESENT_PARAMETERS 的呼叫
Memory::WriteProtected<int>(addys::Patch_D3DPresent_Width, newW);
Memory::WriteProtected<int>(addys::Patch_D3DPresent_Height, newH);
// CWvsApp::Run 裡的視窗大小重置
Memory::WriteProtected<int>(addys::Patch_Reset_Width, newW);
Memory::WriteProtected<int>(addys::Patch_Reset_Height, newH);
// 其他散布的 patch 點...
}Patch 前後的記憶體狀態:
【Patch 前】
0x009F52A0: 68 20 03 00 00 PUSH 800
0x009F52A5: 68 58 02 00 00 PUSH 600
【Patch 後:目標解析度 1280x720】
0x009F52A0: 68 00 05 00 00 PUSH 1280 (0x500 = 1280,小端序 00 05 00 00)
0x009F52A5: 68 D0 02 00 00 PUSH 720 (0x2D0 = 720,小端序 D0 02 00 00)
小端序(Little Endian)提醒
x86 CPU 用小端序存放多 byte 整數:最低位 byte 放在最低地址。 1280 =
0x00000500,小端序寫法:00 05 00 00(由低到高 byte)。Memory::WriteProtected<int>自動處理端序,不需要手動翻轉。
五、步驟三:修正 Viewport(視口)
D3DVIEWPORT9 是 Direct3D 9 定義「渲染目標的哪個區域要被渲染」的結構:
// DirectX SDK 定義
typedef struct D3DVIEWPORT9 {
DWORD X; // 左上角 X
DWORD Y; // 左上角 Y
DWORD Width; // 寬度
DWORD Height; // 高度
float MinZ; // 深度值下限(通常 0.0f)
float MaxZ; // 深度值上限(通常 1.0f)
} D3DVIEWPORT9;遊戲在初始化 D3D 時設定 Viewport 為 {0, 0, 800, 600, 0.0f, 1.0f},如果不修正,3D 渲染會只佔據畫面左上角 800×600 的區域,其餘部分是黑色。
void Client::PatchViewport() {
// Viewport 結構體放在遊戲記憶體的固定地址
// 直接寫入 Width 和 Height 欄位
DWORD vpWidth = (DWORD)Client::m_nGameWidth;
DWORD vpHeight = (DWORD)Client::m_nGameHeight;
// +0x08 = Width 欄位的偏移(X=4byte, Y=4byte, then Width)
Memory::Write<DWORD>(addys::Viewport_Struct + 0x08, vpWidth);
// +0x0C = Height 欄位的偏移
Memory::Write<DWORD>(addys::Viewport_Struct + 0x0C, vpHeight);
}六、UpdateGameStartup() 的補充
除了解析度,UpdateGameStartup() 還修改幾個遊戲啟動行為:
void Client::UpdateGameStartup() {
// 1. 強制視窗模式(覆蓋 D3DPRESENT_PARAMETERS.Windowed 欄位)
// 遊戲預設全螢幕,改成 TRUE 讓它以視窗跑
Memory::WriteProtected<BYTE>(addys::Patch_Windowed_Flag, 0x01);
// 2. 停用「解析度不符時彈警告」的檢查
// 原本:if (width != 800 || height != 600) ShowWarning();
// NOP 掉這個條件跳轉
Memory::Nop(addys::Patch_ResCheck_Jump, 6);
// 3. 修正字型渲染
// v83 的字型在非 800x600 解析度下位置偏移,
// patch 字型渲染函式的偏移計算
Memory::WriteProtected<int>(addys::Patch_FontOffset_X, 0);
Memory::WriteProtected<int>(addys::Patch_FontOffset_Y, 0);
}七、解析度修改全流程圖
sequenceDiagram participant MM as MainMain participant C as Client participant Mem as Memory participant Game as MapleStory MM->>C: UpdateGameStartup() C->>Mem: WriteProtected(Windowed_Flag, TRUE) C->>Mem: Nop(ResCheck_Jump, 6) MM->>C: UpdateResolution() C->>Mem: Write(GameWidth_Addr, 1280) C->>Mem: Write(GameHeight_Addr, 720) C->>Mem: Write(BackBuffer_Width, 1280) C->>Mem: Write(BackBuffer_Height, 720) C->>Mem: WriteProtected(Patch_CreateWindow_Width, 1280) C->>Mem: WriteProtected(Patch_CreateWindow_Height, 720) C->>Mem: WriteProtected(Patch_D3DPresent_Width, 1280) C->>Mem: WriteProtected(Patch_D3DPresent_Height, 720) C->>Mem: Write(Viewport_Width, 1280) C->>Mem: Write(Viewport_Height, 720) Note over Game: 遊戲以 1280x720 視窗啟動 Game->>C: 觸發 CWvsApp::SetUp Hook C->>C: FixLoginScreen()
八、常見問題
Q:修改解析度後遊戲畫面被拉伸了怎麼辦?
拉伸代表只修改了全域變數,但沒有 Patch 所有硬式編碼的值。遊戲用修改後的解析度建立視窗,但內部的 2D 精靈(Sprite)渲染仍以 800×600 為坐標系繪製,導致拉伸感。需要確認所有的 patch 點都被覆蓋。
Q:如何找到所有需要 Patch 的地址?
在 IDA Pro 中搜尋立即值(Search → Immediate value)輸入
0x320(800)和0x258(600),逐一確認每個出現位置的語意,判斷是否需要修改。配合動態偵錯(OllyDbg / x64dbg)可以驗證哪些路徑實際會被執行到。
Q:為什麼不直接 Hook CWvsApp::SetUp,在 Hook 函式裡改參數?
理論上可以,但 SetUp 的函式非常複雜(有數百條指令),攔截後要完整還原所有參數非常繁瑣且容易出錯。直接 Patch 記憶體中的立即值更精確、影響範圍更小、更容易驗證。
延伸閱讀
- 01_Client.cpp整體架構導覽 — Client 模組的全貌
- 03_UI元素位置修正 — 解析度改了之後,UI 座標如何換算
- 02_Memory.cpp_讀寫記憶體工具 — WriteProtected 的實作原理
- 03_AddyLocations.h_地址表解析 — 所有 patch 點的地址