前三篇建立了記憶體操作和Hook的基礎工具。這篇進入 Client.cpp——整個登入器裡最核心的業務邏輯檔案,負責所有「改變遊戲視覺呈現」的操作:解析度、UI位置、登入畫面、選角畫面。

Client 模組:三個修改層次

Client 是一個工具類別(所有成員都是static,不需要建立物件),公開介面:

cpp
📋 複製
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.cppMainFunc() 在所有Hook裝完後,最後才呼叫 Client::UpdateGameStartup()Client::UpdateResolution()

v83原生解析度800×600這個數字,在遊戲裡藏在三個不同層次,缺一不可:

層次說明
全域變數Direct3D讀取這些變數決定Back Buffer大小
機器碼硬式編碼某些函式把800/600直接寫死在指令的立即值裡
UI座標按鈕、血條等元素的位置也是按800×600設計的硬式座標

只改全域變數,遊戲會用新解析度建視窗,但某些函式內部還是硬用800×600,導致畫面被拉伸;只改機器碼不改UI座標,視窗大小對了但東西全部擠在左上角。

解析度修改:UpdateResolution()

cpp
📋 複製
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個地方找到硬式編碼,例如建立視窗的呼叫:

text
📋 複製
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的位置):

cpp
📋 複製
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的區域,其餘是黑色:

cpp
📋 複製
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座標系定位的硬式數值,需要換算到新解析度。

等比例換算

cpp
📋 複製
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() 逐一設定各元素座標,混用上述幾種錨點:

cpp
📋 複製
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已足夠輸入帳號,放大反而不美觀):

cpp
📋 複製
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個角色的站立位置、名稱標籤、翻頁箭頭、功能按鈕,多數用「平均分布」而非等比例:

cpp
📋 複製
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);
}

呼叫時機總覽

text
📋 複製
DllMain載入 → 安裝Hook → UpdateGameStartup → UpdateResolution → 遊戲啟動
                                                                    ↓
                                          觸發SetUp Hook → FixLoginScreen
                                                                    ↓
                                        玩家登入成功 → 觸發選角Hook → FixSelectCharScreen
                                                                    ↓
                                    玩家進入地圖 → 觸發EnterField Hook → FixUIElements

角色動畫本身(行走、揮劍)不需要額外修正——那是以地圖座標系而非螢幕座標系渲染的,解析度只影響攝影機把地圖的哪個範圍投影到螢幕上。


下一篇進入進階功能與設定系統:函式替換技術、自訂記憶體分配、config.ini如何被讀取,以及WZ資源整合的原理。