ZXString — MapleStory 字串型別

本篇定位

這是 Phase 9 的第 3 篇。讀完前兩篇(目錄概覽 + ZAllocEx)後,本篇說明 MapleStory 使用的自訂字串型別。這個型別幾乎在每一個「傳入角色名稱、顯示文字、查詢字串」的進階 Hook 場景都會用到。


為什麼不能直接用 std::string?一個比喻

想像你去日本旅行,想送給當地朋友一份禮物。你把禮物包好,用英式包裝紙包了起來。問題是對方的「拆禮物機器」只認識日式包裝格式,看到英式包裝就直接報錯當機。

std::string 就是英式包裝;ZXString<T> 就是日式包裝。

MapleStory 的函式內部使用自訂的 ZXString<T> 而非 std::string。兩者的記憶體版面完全不同

std::stringZXString<char>
記憶體版面[size][capacity][data ptr] 或 SSO[T* m_pStr] → 指向 [_ZXStringData][string data]
分配器std::allocatorZAllocEx<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 欄位說明:

欄位型別說明
nRefvolatile long引用計數。-1 = 正在被修改(GetBuffer 中)
nCapsize_t已分配的字串緩衝區容量(不含 header)
nByteLensize_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)

相關筆記