前三篇建立了記憶體操作和Hook的基礎工具。這篇進入 Client.cpp——整個登入器裡最核心的業務邏輯檔案,負責所有「改變遊戲視覺呈現」的操作:解析度、UI位置、登入畫面、選角畫面。
Client 模組:三個修改層次
Client 是一個工具類別(所有成員都是static,不需要建立物件),公開介面:
class Client {
public:
static int m_nGameWidth, m_nGameHeight; // 從config.ini讀入的目標解析度
static void UpdateGameStartup(); // 修改遊戲初始化啟動參數
static void UpdateResolution(); // 套用解析度設定
static void FixLoginScreen(); // 修正登入畫面
static void FixSelectCharScreen(); // 修正選角畫面
static void FixUIElements(); // 修正遊戲內HUD
};
dllmain.cpp 的 MainFunc() 在所有Hook裝完後,最後才呼叫 Client::UpdateGameStartup() 和 Client::UpdateResolution()。
v83原生解析度800×600這個數字,在遊戲裡藏在三個不同層次,缺一不可:
| 層次 | 說明 |
|---|---|
| 全域變數 | Direct3D讀取這些變數決定Back Buffer大小 |
| 機器碼硬式編碼 | 某些函式把800/600直接寫死在指令的立即值裡 |
| UI座標 | 按鈕、血條等元素的位置也是按800×600設計的硬式座標 |
只改全域變數,遊戲會用新解析度建視窗,但某些函式內部還是硬用800×600,導致畫面被拉伸;只改機器碼不改UI座標,視窗大小對了但東西全部擠在左上角。
解析度修改:UpdateResolution()
void Client::UpdateResolution() {
int newW = Client::m_nGameWidth, newH = Client::m_nGameHeight;
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);
PatchResolutionInCode(); // 步驟二:機器碼硬式值
PatchViewport(); // 步驟三:Direct3D Viewport
}
這個寫入必須在遊戲的Direct3D初始化(
CWvsApp::SetUp)之前完成,晚了的話畫面已經照舊解析度建好,要等下次Reset才生效。
步驟二:Patch 機器碼裡的硬式值
用IDA Pro搜尋立即值 0x320(800)和 0x258(600),可以在超過30個地方找到硬式編碼,例如建立視窗的呼叫:
0x009F52A0: 68 20 03 00 00 PUSH 800 ; CreateWindowExA的nWidth參數
0x009F52A5: 68 58 02 00 00 PUSH 600
PUSH imm32 指令結構是 68 XX XX XX XX,只要覆寫立即值那4個byte(地址+1的位置):
void Client::PatchResolutionInCode() {
Memory::WriteProtected<int>(addys::Patch_CreateWindow_Width, m_nGameWidth);
Memory::WriteProtected<int>(addys::Patch_CreateWindow_Height, m_nGameHeight);
Memory::WriteProtected<int>(addys::Patch_D3DPresent_Width, m_nGameWidth);
Memory::WriteProtected<int>(addys::Patch_D3DPresent_Height, m_nGameHeight);
// ...其餘散布的patch點
}
Memory::WriteProtected<int> 自動處理小端序,不需要手動翻轉位元組順序。
步驟三:修正 Viewport
D3DVIEWPORT9 結構定義「渲染目標的哪個區域要被渲染」,遊戲初始化時設定為 {0, 0, 800, 600, ...}。不修正的話,3D渲染只會佔據畫面左上角800×600的區域,其餘是黑色:
void Client::PatchViewport() {
Memory::Write<DWORD>(addys::Viewport_Struct + 0x08, (DWORD)m_nGameWidth); // Width欄位偏移
Memory::Write<DWORD>(addys::Viewport_Struct + 0x0C, (DWORD)m_nGameHeight); // Height欄位偏移
}
UpdateGameStartup() 的補充修改
除了解析度,還處理幾個啟動行為:強制視窗模式(改寫 D3DPRESENT_PARAMETERS.Windowed 欄位為true)、NOP掉「解析度不符時彈警告」的判斷跳轉、修正非800×600解析度下的字型渲染偏移。
UI 元素座標換算
視窗大小和3D渲染範圍對了之後,UI元素(血條、快捷欄、迷你地圖…)仍然是按800×600座標系定位的硬式數值,需要換算到新解析度。
等比例換算
int Client::ScaleX(int originalX) { return (int)(originalX * ((float)m_nGameWidth / 800.0f)); }
int Client::ScaleY(int originalY) { return (int)(originalY * ((float)m_nGameHeight / 600.0f)); }
但並非所有元素都適合等比例換算,依設計意圖分成幾種錨點類型:
| 錨點類型 | 換算方式 | 範例 |
|---|---|---|
| 右上/右下角錨 | 新座標 = 新尺寸 - (800或600 - 原座標),間距固定 | 迷你地圖、HP/MP條 |
| 正下方中央錨 | 新X = 新寬度/2 + 偏移 | 快捷欄、登入按鈕 |
| 全寬延伸 | 新Width = 新寬度 | EXP條 |
| 等比例縮放 | ScaleX / ScaleY | 大部分其他元素 |
右邊緣錨點更精確:原始X=785(距右邊緣15px)換算成右邊緣錨點是
1280-15=1265,而等比例換算是785×1.6=1256——差異雖小,但「貼近邊緣」的設計意圖是「間距固定」,不是「等比例縮放」。
FixUIElements() 逐一設定各元素座標,混用上述幾種錨點:
void Client::FixUIElements() {
Memory::Write<int>(addys::MiniMap_X, ScaleX(785)); // 等比例(實務上簡化處理)
Memory::Write<int>(addys::HPBar_X, ScaleX(630));
Memory::Write<int>(addys::EXPBar_Width, m_nGameWidth); // 全寬延伸
Memory::Write<int>(addys::QuickSlot_X, ScaleX(400) - 160); // 置中
}
呼叫時機很關鍵:HUD的記憶體結構要等玩家實際進入地圖(Field)後才會建立,
FixUIElements()不能在DLL剛載入時呼叫,MapleEzorsia-v2是在CWvsApp::Run的Hook裡偵測「是否剛進入地圖」才觸發。
登入畫面與選角畫面:特殊處理
登入、選角畫面比一般HUD複雜,因為有動畫背景、輸入框等元素,且更適合用「置中」而非「等比例」處理。
登入畫面:Logo和帳密輸入框整體水平置中,輸入框寬度固定不縮放(240px已足夠輸入帳號,放大反而不美觀):
void Client::FixLoginScreen() {
int inputBoxLeft = (m_nGameWidth / 2) - 120; // 240px寬輸入框,置中
Memory::Write<int>(addys::IDInput_X, inputBoxLeft);
Memory::Write<int>(addys::IDInput_W, 240); // 寬度固定
}
登入畫面還有一個已知限制:背景的雲朵捲動動畫原本以800px寬度循環,解析度放大後捲動邊界也要跟著改,否則背景只覆蓋左邊800px、右側是黑色——但背景圖本身只有800px寬,強制拉伸到新寬度會讓紋理模糊,這是無法在不修改美術資源下完全解決的視覺瑕疵。
選角畫面同時要處理最多3個角色的站立位置、名稱標籤、翻頁箭頭、功能按鈕,多數用「平均分布」而非等比例:
void Client::FixSelectCharScreen() {
int charSpacing = m_nGameWidth / 4; // 三個角色站在1/4, 2/4, 3/4的位置
Memory::Write<int>(addys::CharSlot1_X, charSpacing * 1);
Memory::Write<int>(addys::CharSlot2_X, charSpacing * 2);
Memory::Write<int>(addys::CharSlot3_X, charSpacing * 3);
int btnCenterX = m_nGameWidth / 2; // 底部按鈕列水平置中
Memory::Write<int>(addys::StartGameBtn_X, btnCenterX - 120);
}
呼叫時機總覽
DllMain載入 → 安裝Hook → UpdateGameStartup → UpdateResolution → 遊戲啟動
↓
觸發SetUp Hook → FixLoginScreen
↓
玩家登入成功 → 觸發選角Hook → FixSelectCharScreen
↓
玩家進入地圖 → 觸發EnterField Hook → FixUIElements
角色動畫本身(行走、揮劍)不需要額外修正——那是以地圖座標系而非螢幕座標系渲染的,解析度只影響攝影機把地圖的哪個範圍投影到螢幕上。
下一篇進入進階功能與設定系統:函式替換技術、自訂記憶體分配、config.ini如何被讀取,以及WZ資源整合的原理。