Port the model outline hook system (CM2SceneRenderDraw, ManageRenderListNode, DrawBatchProjected) from the C++ idris DLL to pure Zig. Includes D3D9 vtable hooks for stencil-based outline rendering with per-category colors and thickness. Fix GetObjectByGUID calling convention: was using fastcall (ECX/EDX) but Ghidra confirms it's __stdcall with stack params and RET 8. Fix lua_pushcclosure address from 0x6F3B80 (mid-body of another function) to 0x6F3920. Guard resolveModelOwner in DrawBatchProj with hasTargets() check and cache unit model status during ManageRenderListNode to avoid raw pointer chasing at render time. Add IsBadReadPtr validation in resolveModelOwner to match C++ IsValidReadPtr pattern for page-level memory safety.
109 lines
7.4 KiB
Markdown
109 lines
7.4 KiB
Markdown
# WoW 1.12.1 Calling Conventions — Ghidra Verified
|
|
|
|
All conventions verified against WoW.exe 1.12.1 build 5875 via Ghidra decompilation and raw byte analysis.
|
|
|
|
## Model Pipeline Hooks (outline/model_hook.zig)
|
|
|
|
| # | Address | Function | Convention | Params | Prologue | RET | Status |
|
|
|---|---------|----------|------------|--------|----------|-----|--------|
|
|
| 1 | `0x0070b360` | CM2SceneRenderDraw | `__thiscall` | ECX=this, stack: viewMatrix, batchData, batchIndices, batchCount | `55 8B EC 81 EC 80 00 00 00` (9B) | — | CORRECT |
|
|
| 2 | `0x00710b90` | CM2Model_ManageRenderListNode | `__thiscall` | ECX=model, stack: addToList | `55 8B EC 8B 45 08` (6B) | — | CORRECT |
|
|
| 3 | `0x0070cb30` | CM2Scene_DrawBatchProjected | `__fastcall` | ECX=renderContext | `55 8B EC 83 EC 10` (6B) | — | CORRECT |
|
|
|
|
## Game Function Wrappers (outline/wow.zig)
|
|
|
|
| # | Address | Function | Convention | Params | Prologue | RET | Status |
|
|
|---|---------|----------|------------|--------|----------|-----|--------|
|
|
| 4 | `0x00515970` | Script_UnitGUID | `__fastcall` | ECX=unitIdStr → EAX:EDX (64-bit) | `55 8B EC 51 56 68 90 00 00 00` | — | CORRECT |
|
|
| 5 | `0x00464870` | GetObjectByGUID | **`__stdcall`** | **stack: guidLow, guidHigh → EAX** | `55 8B EC 8B 45 08 8B 4D 0C` | **RET 8** | **FIXED** — was incorrectly using `hook.fastcall` |
|
|
| 6 | `0x006061E0` | CGUnit_C::UnitReaction | `__thiscall` | ECX=localPlayer, stack: unit → EAX (reaction int) | `53 8B DC 83 EC 08 83 E4 F8` | — | CORRECT |
|
|
|
|
### GetObjectByGUID Detail
|
|
|
|
Disassembly at `0x464870`:
|
|
```
|
|
55 PUSH EBP
|
|
8B EC MOV EBP, ESP
|
|
8B 45 08 MOV EAX, [EBP+8] ; guidLow from STACK (not ECX!)
|
|
8B 4D 0C MOV ECX, [EBP+C] ; guidHigh from STACK (not EDX!)
|
|
8B D0 MOV EDX, EAX
|
|
0B D1 OR EDX, ECX ; test if guid == 0
|
|
74 0B JZ return_zero
|
|
51 PUSH ECX ; push guidHigh for inner call
|
|
50 PUSH EAX ; push guidLow for inner call
|
|
E8 ... CALL FindObjectByGUID
|
|
5D POP EBP
|
|
C2 08 00 RET 8 ; callee cleans 8 bytes
|
|
```
|
|
|
|
The C++ reference declared this as `__fastcall(uint64_t)`. Under MSVC, `uint64_t` (8 bytes) is too large for a single 32-bit register, so `__fastcall` passes it on the stack — making it behave like `__stdcall`. The Zig code split it into two `u32` args and passed them in ECX/EDX via `hook.fastcall`, which was wrong.
|
|
|
|
The transmog addon (`transmogfix/src/main.zig:134`) and interact module (`weirdutils/src/interact.zig:50`) already had the correct push-to-stack implementation.
|
|
|
|
## Dead Overlay Functions (not yet ported — for future reference)
|
|
|
|
| # | Address | Function | Convention | Params | Status |
|
|
|---|---------|----------|------------|--------|--------|
|
|
| 7 | `0x00483EE0` | WorldProjection_WorldToScreenCoords | `__thiscall` | ECX=WorldFrame, stack: float* worldXYZ, float* screenXYZ → uint (bool) | VERIFIED |
|
|
| 8 | `0x0041ADE0` | DDCToNDC | `__fastcall` | ECX=float* outX, EDX=float* outY, stack: float inX, float inY | VERIFIED |
|
|
| 9 | `0x00609210` | CGUnit_C::GetUnitName | `__thiscall` | ECX=unit, stack: uint** param → char* | VERIFIED |
|
|
| 10 | `0x006264E0` | GetObjectName | `__fastcall` | ECX=uint64_t* guidPtr → char* | VERIFIED |
|
|
|
|
## Lua API (main.zig)
|
|
|
|
All WoW 1.12.1 Lua C API functions use `__fastcall` with L (lua_State*) in ECX.
|
|
|
|
| # | Address | Function | Convention | Params | RET | Status |
|
|
|---|---------|----------|------------|--------|-----|--------|
|
|
| 11 | `0x00704120` | FrameScript::Register | `__fastcall` | ECX=name, EDX=funcAddr | — | CORRECT |
|
|
| 12 | `0x006F3070` | lua_gettop | `__fastcall` | ECX=L → int | RET | CORRECT |
|
|
| 13 | `0x006F3080` | lua_settop | `__fastcall` | ECX=L, EDX=index | — | CORRECT |
|
|
| 14 | `0x006F3350` | lua_pushvalue | `__fastcall` | ECX=L, EDX=index | — | CORRECT |
|
|
| 15 | `0x006F3400` | lua_type | `__fastcall` | ECX=L, EDX=index → int | RET | CORRECT |
|
|
| 16 | `0x006F3510` | lua_isstring | `__fastcall` | ECX=L, EDX=index → int | RET | CORRECT |
|
|
| 17 | `0x006F3690` | lua_tostring | `__fastcall` | ECX=L, EDX=index → char* | — | CORRECT |
|
|
| 18 | `0x006F39F0` | lua_pushboolean | `__fastcall` | ECX=L, EDX=bool | — | CORRECT |
|
|
| 19 | `0x006F3890` | lua_pushstring | `__fastcall` | ECX=L, EDX=string | — | CORRECT |
|
|
| 20 | `0x006F3810` | lua_pushnumber | `__fastcall` | ECX=L, stack: f64 (8 bytes) | RET 8 | CORRECT |
|
|
| 21 | `0x006F3920` | lua_pushcclosure | `__fastcall` | ECX=L, EDX=func, stack: nupvalues | — | **FIXED** — was 0x6F3B80 (wrong addr) |
|
|
| 22 | `0x006F4940` | luaL_error | `__cdecl` | stack: L, fmt, ... | — | CORRECT |
|
|
| 23 | `0x006F4DC0` | luaL_openlib | `__fastcall` | ECX=L, EDX=libname, stack: funcs, nup | — | CORRECT |
|
|
|
|
### lua_pushcclosure Detail
|
|
|
|
Ghidra search found `lua_pushcclosure @ 006f3920`. No function exists at the old address `0x6F3B80` — it falls mid-body of another function. The wrapper was unused (never called from current code) so no crash occurred.
|
|
|
|
### lua_pushnumber Detail
|
|
|
|
Takes a `double` (8 bytes) which is too large for EDX, so it goes on the stack per `__fastcall` rules. Callee cleans with `RET 8`. The inline asm workaround in `weirdUtilsVersion` correctly does `SUB ESP,8; FSTPL (ESP); CALL` and relies on `RET 8` to rebalance.
|
|
|
|
## File/Addon Hooks (main.zig)
|
|
|
|
| # | Address | Function | Convention | Params | Prologue | RET | Status |
|
|
|---|---------|----------|------------|--------|----------|-----|--------|
|
|
| 24 | `0x0042a320` | ValidateFunctionPointer | `__fastcall` | ECX=addr | `55 8B EC 83 EC 40` (6B) | — | CORRECT (empty detour) |
|
|
| 25 | `0x00648620` | LoadFileWithTextureResourceFallback | `__stdcall` | 7 stack params | `55 8B EC 8B 4D 1C` (6B) | RET 0x1C | CORRECT |
|
|
| 26 | `0x00490250` | FrameScript_RegisterAllSystemCommands | `void(void)` | none | `56 E8 ...` (6B) | — | CORRECT (fixup at offset 1) |
|
|
| 27 | `0x0051F600` | LoadAddonsRecursively | `__fastcall` | ECX=error_handler | `53 8B 1D ...` (7B) | — | CORRECT |
|
|
| 28 | `0x006EDB90` | loadFileListWithIncludes | `__fastcall` | ECX=path, EDX=md5ctx, stack: error_handler | `55 8B EC 6A FF ...` | RET 4 | CORRECT |
|
|
| 29 | `0x004B6F70` | LoadUIBindingsFromFile | `__thiscall` | ECX=binding_mgr, stack: path, md5ctx, callback | `55 8B EC 81 EC 1C 04 00 00` | RET 0x0C | CORRECT |
|
|
| 30 | `0x0046a400` | GameEngine_MainInitialize | `void(void)` | none | `55 8B EC 83 EC 28` (6B) | — | CORRECT |
|
|
| 31 | `0x00490BD0` | World_HandlePlayerLogin | `void(void)` | none | `56 E8 ...` (6B) | — | CORRECT (fixup at offset 1) |
|
|
|
|
### Note on #31
|
|
|
|
Ghidra names this `World_HandlePlayerLogin`, not `CGGameUI_Shutdown`. The Zig hook's detour just calls the original with no extra logic, so the naming discrepancy has no functional impact.
|
|
|
|
## Utility Functions
|
|
|
|
| # | Address | Function | Convention | Params | RET | Status |
|
|
|---|---------|----------|------------|--------|-----|--------|
|
|
| 32 | `0x006462E0` | M2_AllocateModelBuffer | `__stdcall` | stack: size, source_file, line, flags | RET 0x10 | CORRECT |
|
|
| 33 | `0x007040D0` | FrameScript::GetContext | `void(void)` | none → lua_State* in EAX | RET | CORRECT (trivial: `MOV EAX,[global]; RET`) |
|
|
|
|
## Bugs Fixed (2026-02-24)
|
|
|
|
1. **GetObjectByGUID** (`outline/wow.zig`): Changed from `hook.fastcall(u32, 0x464870, lo, hi)` to inline asm `push hi; push lo; call`. The function reads params from stack `[EBP+8]`/`[EBP+C]` and does `RET 8`.
|
|
|
|
2. **lua_pushcclosure** (`main.zig`): Changed address from `0x6F3B80` to `0x6F3920`. The old address pointed into the middle of another function's body.
|