Client.cpp 整體架構導覽

本篇定位

這是 Phase 5「Client.cpp 深度解析」的第一篇,也是整個 Client 模組的地圖。 Client.cpp 是 MapleEzorsia-v2 裡最核心的業務邏輯檔案,負責所有「改變遊戲視覺呈現」的操作:解析度、UI 位置、登入畫面、選角畫面。 讀完這篇,你會看到整個模組的輪廓,後三篇再逐一深入。


一、Client 模組在專案中的位置

MapleEzorsia-v2/
├── dllmain.cpp      ← DLL 入口,呼叫 Client 的函式
├── dinput8.cpp      ← Proxy 載入器
├── MainMain.cpp     ← 初始化流程、讀設定檔
├── Client.cpp       ← 【本模組】遊戲畫面修改邏輯
├── Client.h         ← Client 的宣告
├── Memory.cpp       ← 記憶體工具(Client 大量使用)
├── AddyLocations.h  ← 地址常數(Client 的修改目標)
├── AutoTypes.h      ← 型別定義(Client 使用)
├── codecaves.h      ← Code Cave 工具(Client 用於複雜修改)
└── Detours/         ← Hook 函式庫

dllmain.cppMainFunc() 在安裝完所有 Hook 後,最後呼叫兩個 Client 函式:

// dllmain.cpp — MainFunc() 末尾
Client::UpdateGameStartup();   // 修改遊戲啟動流程
Client::UpdateResolution();    // 套用自訂解析度

二、Client.h:模組的公開介面

#pragma once
#include <Windows.h>
#include <string>
 
class Client {
public:
    // ── 靜態配置資料(從 config.ini 讀入)────────────────────────────────
    static int    m_nGameWidth;     // 目標視窗寬度
    static int    m_nGameHeight;    // 目標視窗高度
    static bool   m_bWindowed;      // 視窗模式 vs 全螢幕
 
    // ── 主要功能函式 ──────────────────────────────────────────────────────
    static void UpdateGameStartup();   // 修改遊戲初始化啟動參數
    static void UpdateResolution();    // 套用解析度設定到遊戲記憶體
 
    // ── UI 位置修正 ────────────────────────────────────────────────────────
    static void FixLoginScreen();      // 修正登入畫面元素位置
    static void FixSelectCharScreen(); // 修正選角畫面元素位置
    static void FixUIElements();       // 修正遊戲內 HUD 元素位置
 
private:
    // ── 內部輔助函式 ──────────────────────────────────────────────────────
    static void PatchResolutionInCode();  // 在機器碼中直接 patch 解析度常數
    static void PatchViewport();          // 修正 Direct3D Viewport 設定
};

所有成員都是 static:Client 是一個「工具類別(Utility Class)」,不需要建立物件,直接用類別名稱呼叫(Client::UpdateResolution())。


三、Client.cpp 的整體結構

// Client.cpp
 
#include "stdafx.h"       // 預編譯標頭(Windows.h、常用標準庫)
#include "Client.h"
#include "Memory.h"
#include "AddyLocations.h"
#include "AutoTypes.h"
#include "codecaves.h"
#include "MainMain.h"     // 讀取設定(解析度、IP 等)
 
// ── 靜態成員初始化 ──────────────────────────────────────────────────────────
int  Client::m_nGameWidth  = 1280;  // 預設值,後由 MainMain 覆寫
int  Client::m_nGameHeight = 720;
bool Client::m_bWindowed   = true;
 
// ── UpdateGameStartup ──────────────────────────────────────────────────────
void Client::UpdateGameStartup() {
    // 修改遊戲的啟動參數:
    // 1. 強制視窗模式(關閉全螢幕)
    // 2. 停用多螢幕偵測
    // 3. 修正字型渲染設定
    // (詳見 Phase5_02_解析度修改原理與實作)
}
 
// ── UpdateResolution ────────────────────────────────────────────────────────
void Client::UpdateResolution() {
    // 讀取 MainMain 儲存的解析度設定
    // 寫入遊戲記憶體中的解析度相關地址
    // Patch 遊戲程式碼中的硬式編碼解析度值
    // (詳見 Phase5_02_解析度修改原理與實作)
}
 
// ── FixLoginScreen ──────────────────────────────────────────────────────────
void Client::FixLoginScreen() {
    // 把登入畫面的 UI 元素從 800x600 座標系換算到新解析度的座標
    // (詳見 Phase5_04_登入畫面與選角畫面修正)
}
 
// ...(其餘函式)

四、Client 修改的三個層次

graph TD
    A[Client 模組的修改] --> B[層次一:記憶體資料]
    A --> C[層次二:機器碼 Patch]
    A --> D[層次三:UI 元素位置]

    B --> B1["寫入解析度到遊戲的全域變數<br/>Memory::Write&lt;int&gt;(GameWidth_Addr, 1920)"]
    C --> C1["把遊戲程式碼裡硬式編碼的<br/>800/600 數值替換成新解析度<br/>Memory::WriteProtected&lt;DWORD&gt;(addr, 1920)"]
    D --> D1["把登入按鈕、角色選擇框<br/>從 800x600 座標換算到新座標<br/>Memory::Write&lt;int&gt;(btnAddr, newX)"]

五、為什麼需要三個層次的修改?

MapleStory v83 的原始解析度是 800×600。這個數字在遊戲裡出現的地方遠比你想像的多:

層次一:全域變數

遊戲有幾個存放「目前解析度」的全域變數,Direct3D 用它們來設定 Back Buffer 大小:

0x00C4FCA4: int gameWidth  = 800   ← 寫新值最直接
0x00C4FCA8: int gameHeight = 600

層次二:硬式編碼在機器碼裡

遊戲有些地方直接把 800、600 這些數字寫死在指令的立即值(Immediate)裡:

0x009F52A0: 68 20 03 00 00   PUSH 0x320   ; 0x320 = 800,硬式編碼!
0x009F52A5: 68 58 02 00 00   PUSH 0x258   ; 0x258 = 600

如果不 patch 這些地方,即使全域變數改了,遊戲在某些函式裡還是會用 800×600。

層次三:UI 座標

遊戲的 UI 元素位置也是按照 800×600 計算的硬式座標(例如「登入按鈕在 (400, 450)」)。解析度放大後,這些元素如果不重新定位,就會擠在螢幕左上角。


六、執行時序:Client 函式什麼時候被呼叫?

sequenceDiagram
    participant DLL as dllmain.cpp
    participant Main as MainMain
    participant Client as Client
    participant Game as MapleStory.exe

    DLL->>Main: CreateInstance(MainFunc)
    Main->>Main: 讀取 config.ini(解析度 / IP)
    Main->>Main: 等待 Themida 解壓縮完成

    Main->>DLL: 呼叫 MainFunc()
    DLL->>DLL: 安裝所有 Hook(Detours)

    DLL->>Client: Client::UpdateGameStartup()
    Client->>Game: 修改啟動參數(記憶體 patch)

    DLL->>Client: Client::UpdateResolution()
    Client->>Game: 寫入新解析度到全域變數
    Client->>Game: Patch 硬式編碼的 800/600

    Note over Game: 遊戲繼續初始化...

    Game->>Client: 觸發 Hook_CWvsApp_SetUp
    Client->>Client: FixLoginScreen()
    Client->>Game: 修正登入畫面 UI 座標

七、常見問題

Q:Client.cpp 和 dllmain.cpp 的分工是什麼?

dllmain.cpp 負責「控制流程」:決定 Hook 的安裝順序、等待時機、呼叫誰。 Client.cpp 負責「業務邏輯」:知道要去記憶體哪些地方、改什麼值、如何計算新座標。 這是很標準的關注點分離(Separation of Concerns)。

Q:解析度修改要在 Hook 安裝前還是後?

必須在 Hook 安裝之後。因為解析度修改需要用到 Code Cave Hook(攔截遊戲的 Direct3D 初始化函式),如果先改記憶體但 Hook 還沒裝,遊戲之後的初始化函式會把值覆蓋回去。


延伸閱讀