ZXString — MapleStory 字串型別
本篇定位
這是 Phase 9 的第 3 篇。讀完前兩篇(目錄概覽 + ZAllocEx)後,本篇說明 MapleStory 使用的自訂字串型別。這個型別幾乎在每一個「傳入角色名稱、顯示文字、查詢字串」的進階 Hook 場景都會用到。
為什麼不能直接用 std::string?一個比喻
想像你去日本旅行,想送給當地朋友一份禮物。你把禮物包好,用英式包裝紙包了起來。問題是對方的「拆禮物機器」只認識日式包裝格式,看到英式包裝就直接報錯當機。
std::string 就是英式包裝;ZXString<T> 就是日式包裝。
MapleStory 的函式內部使用自訂的 ZXString<T> 而非 std::string。兩者的記憶體版面完全不同:
std::string | ZXString<char> | |
|---|---|---|
| 記憶體版面 | [size][capacity][data ptr] 或 SSO | [T* m_pStr] → 指向 [_ZXStringData][string data] |
| 分配器 | std::allocator | ZAllocEx<ZAllocStrSelector<T>> |
| 傳給 MS 函式 | 崩潰或亂碼 | 相容 |
ZXString 的記憶體版面
ZXString<char> 物件本身(4 bytes on x86):
┌─────────────┐
│ m_pStr * │──→ 指向字串資料的起點
└─────────────┘
m_pStr 指向的位置(在 _ZXStringData 之後):
┌──────────────────────────────────────────────┐
│ _ZXStringData │
│ ┌─────────┬──────┬───────────┐ │
│ │ nRef(4) │nCap(4)│nByteLen(4)│ │
│ └─────────┴──────┴───────────┘ │
│ [H][e][l][l][o][\0] ← m_pStr 指向這裡 │
└──────────────────────────────────────────────┘
_ZXStringData 欄位說明:
| 欄位 | 型別 | 說明 |
|---|---|---|
nRef | volatile long | 引用計數。-1 = 正在被修改(GetBuffer 中) |
nCap | size_t | 已分配的字串緩衝區容量(不含 header) |
nByteLen | size_t | 字串位元組長度(>> (sizeof(T)-1) 得字元數) |
兩種 Template 特化
ZXString<char> // 1 byte per char(一般字串)
ZXString<wchar_t> // 2 bytes per char(Unicode 字串)MapleStory 角色名稱、頻道名稱等大多使用 ZXString<wchar_t>。
核心 API
建構
ZXString<char> s1; // 空字串
ZXString<char> s2("Hello", -1); // 從 C 字串建構,-1 = 自動計算長度
ZXString<wchar_t> ws(L"MapleStory"); // Unicode 字串
// 指定長度(只取前 5 個字元)
ZXString<char> s3("Hello World", 5); // → "Hello"賦值與連接
ZXString<char> s;
s = "hello"; // operator=
s += " world"; // operator+= 連接 C 字串
s += &otherZXString; // operator+= 連接另一個 ZXString
s.Assign("newval"); // 明確賦值
s.Assign(&otherZXString); // 複製(共享引用計數)比較
if (s == "hello") { } // 與 C 字串比較
if (s == &otherZXString) { } // 與 ZXString 比較
s.Compare("hello") // 回傳 BOOL
s.CompareNoCase("HELLO") // 不分大小寫比較資訊查詢
s.Length() // 字元數(非位元組數)
s.IsEmpty() // 是否為空格式化
ZXString<char> s;
s.Format("HP: %d / %d", curHP, maxHP); // 類似 sprintf清空與釋放
s.Empty(); // 引用計數 -1,若到 0 則釋放記憶體,m_pStr 設為 nullptr引用計數機制
ZXString 實作了寫時複製(Copy-on-Write):
ZXString<char> a("hello"); // a.nRef = 1
ZXString<char> b;
b.Assign(&a); // b 共享 a 的記憶體,a.nRef = 2
// 修改 b 時,ZXString 會先複製一份新記憶體(GetBuffer → ReleaseBuffer)
b = "world"; // a.nRef 回到 1,b 有自己的記憶體Hook 時初始化 m_pStr
在 Hook 函式中宣告
ZXString時,若是在 stack frame 上建立,記得確保m_pStr初始化為nullptr。 原始碼注解說明:T* m_pStr; // needs to be initialized to zero when hooking sometimes
與 MapleStory 互動的正確方式
// 錯誤做法(傳給 MS 函式會崩潰)
std::string name = "Nick";
SomeMapleFunc(name.c_str()); // ❌
// 正確做法
ZXString<wchar_t> name(L"Nick");
SomeMapleFunc(&name); // ✅ MapleStory 能正確處理Alloc / Release 內部流程
建構 / Assign:
GetBuffer(n, retain=FALSE)
→ ZAllocEx<ZAllocStrSelector<T>>::GetInstance()->Alloc(total_size)
→ 回傳指向 _ZXStringData 之後的指標
→ m_pStr = ptr
釋放:
Release()
→ InterlockedDecrement(&nRef)
→ 若 nRef <= 0:
ZAllocEx<ZAllocStrSelector<T>>::GetInstance()->Free((void**)_ZXStringData_ptr)
相關筆記
- ZAllocEx 標頭 — ZXString 的記憶體由它管理
- ZRef — 同樣有引用計數機制
- 01 — 目錄概覽