Publish source: Unlicense, public README, repo hygiene
The remote was previously a distribution-only point for pre-built DLLs. This opens the source. - LICENSE: Unlicense, with a GPL-3.0 carve-out for src/dpslog/WeirdDPSMate (a DPSMate fork that keeps its own license) - README.md replaces the stale internal one with the user-facing docs from DLL_README.md, swapping the 'Why No Source Code?' section for build and layout notes. DLL_README.md is dropped; one README now serves both. - RELEASING.md: drop the trim-the-README-per-release dance and the remote/WeirdUtils/ distribution clone, both obsolete now - gitignore agent/editor scratch, build caches, the vendored WSBT addon, and the WeirdThreat/uwu-logs checkouts (separate upstream repos) - Commit outstanding module work: superweirdo, clickthrough portal visuals, transform44 decompiles, worldmarkers demo presets, tools/
This commit is contained in:
+31
-11
@@ -128,14 +128,25 @@ fn mapWowSections() bool {
|
||||
// Map additional pages for runtime constants that live outside .rdata:
|
||||
// 0x80C000-0x813000 covers 0x80C5C8 (billboard epsilon) and 0x811610 (SHORT_TO_FLOAT)
|
||||
// 0xCF0000-0xCF1000 covers 0xCF04C4 (boneKeyframe init flag) and 0xCF043C (pivot constants)
|
||||
// 0x876000-0x877000 covers 0x876504 (matrix-multiply dispatch table — statically
|
||||
// holds 0x74A7AD initializer; we write 0x74A7C6 directly to skip CPUID init)
|
||||
_ = mapZeroed(0x80C000, 0x8000); // covers 0x80C000-0x814000
|
||||
_ = mapZeroed(0xCF0000, 0x1000); // covers 0xCF0000-0xCF1000
|
||||
_ = mapZeroed(0x876000, 0x1000); // covers 0x876504 dispatch table (real-game-bytes bench)
|
||||
// Write runtime constant values
|
||||
@as(*align(1) u32, @ptrFromInt(0x811610)).* = 0x38000100; // SHORT_TO_FLOAT ~1/32767
|
||||
@as(*align(1) u32, @ptrFromInt(0x8029D4)).* = 0x34800000; // billboard epsilon
|
||||
@as(*align(1) u32, @ptrFromInt(0x80C5C8)).* = 0x35800000; // billboard sq epsilon
|
||||
@as(*align(1) u32, @ptrFromInt(0x80297C)).* = 0x40400000; // 3.0
|
||||
@as(*align(1) u32, @ptrFromInt(0x802990)).* = 0x40C00000; // 6.0
|
||||
// NOTE: benching against the real game x87 function at 0x714260 requires
|
||||
// initialising several dispatch tables in .data (0x876504, 0x876594, ...)
|
||||
// that select x87 vs SSE math implementations based on CPUID. Running the
|
||||
// game's init at 0x74FB49 pulls in heap/init dependencies we don't satisfy.
|
||||
// For now, the bench uses the Zig reimpl baseline (transformImpl_BASELINE).
|
||||
// Real-game-bytes parity is better done in-game via a hook diff.
|
||||
@as(*align(1) u32, @ptrFromInt(0x876504)).* = 0x0074A7C6;
|
||||
print(" [debug] *(0x876504) = 0x{x}\n", .{@as(*align(1) u32, @ptrFromInt(0x876504)).*});
|
||||
sections_mapped = true;
|
||||
return true;
|
||||
}
|
||||
@@ -977,6 +988,8 @@ pub fn main() void {
|
||||
report("__ftol", t_best, s_best, mismatches == 0);
|
||||
}
|
||||
|
||||
} // close outer disabled block so transform44 runs standalone
|
||||
|
||||
// =========================================================================
|
||||
// transform44: SSE implementation benchmark — comprehensive fixture
|
||||
// Exercises: bone loop (rot/trans/scale/static/billboard), texAnim,
|
||||
@@ -1663,7 +1676,11 @@ pub fn main() void {
|
||||
const sb: u32 = @bitCast(@as(f32, 1.0));
|
||||
const transformImpl_SSE = @extern(*const fn (u32, u32, u32, u32, u32) callconv(.{ .x86_thiscall = .{} }) void, .{ .name = "transformImpl_SSE" });
|
||||
const transformImpl_SSE64 = @extern(*const fn (u32, u32, u32, u32, u32) callconv(.{ .x86_thiscall = .{} }) void, .{ .name = "transformImpl_SSE64" });
|
||||
const transformImpl_BASELINE = @extern(*const fn (u32, u32, u32, u32, u32) callconv(.c) void, .{ .name = "transformImpl_BASELINE" });
|
||||
// Baseline: Zig reimplementation (bit-identical to bone_sse by construction).
|
||||
// Attempting to call the real game function at 0x714260 requires running
|
||||
// game init code (CPUID-based dispatch population, possibly heap setup)
|
||||
// that isn't feasible from this isolated bench fixture. See mapWowSections.
|
||||
const transformImpl_BASELINE = @extern(*const fn (u32, u32, u32, u32, u32) callconv(.{ .x86_thiscall = .{} }) void, .{ .name = "transformImpl_BASELINE" });
|
||||
|
||||
// Pre-set boneKeyframe init flag so we skip the atexit call (Windows CRT, can't run on Linux)
|
||||
@as(*u8, @ptrFromInt(0xCF04C4)).* = 1;
|
||||
@@ -1687,7 +1704,7 @@ pub fn main() void {
|
||||
|
||||
// --- Benchmark both BASELINE and SSE ---
|
||||
const run_bench_fn = struct {
|
||||
fn run(func: *const fn (u32, u32, u32, u32, u32) callconv(.c) void, so2: u32, pm: u32, pp: u32, po: u32, sb2: u32, scene: *[0x400]u8, actx: *[0x20]u8, iters: u32) u64 {
|
||||
fn run(func: *const fn (u32, u32, u32, u32, u32) callconv(.{ .x86_thiscall = .{} }) void, so2: u32, pm: u32, pp: u32, po: u32, sb2: u32, scene: *[0x400]u8, actx: *[0x20]u8, iters: u32) u64 {
|
||||
var best_inner: u64 = std.math.maxInt(u64);
|
||||
for (0..5) |_| {
|
||||
const t = rdtsc();
|
||||
@@ -1714,6 +1731,9 @@ pub fn main() void {
|
||||
const pp = @intFromPtr(&pos);
|
||||
const po = @intFromPtr(&ofs);
|
||||
|
||||
const best_base = run_bench_fn(transformImpl_BASELINE, so, pm, pp, po, sb, &scene_obj, &anim_ctx_mem, T44_ITERS);
|
||||
const avg_base = best_base / T44_ITERS;
|
||||
|
||||
const best_sse = run_bench_fn(transformImpl_SSE, so, pm, pp, po, sb, &scene_obj, &anim_ctx_mem, T44_ITERS);
|
||||
const avg_sse = best_sse / T44_ITERS;
|
||||
|
||||
@@ -1731,7 +1751,7 @@ pub fn main() void {
|
||||
const best_sse64 = run_bench_fn(transformImpl_SSE64, so, pm, pp, po, sb, &scene_obj, &anim_ctx_mem, T44_ITERS);
|
||||
const avg_sse64 = best_sse64 / T44_ITERS;
|
||||
|
||||
print(" BASELINE: {d} cycles/call (frozen)\n", .{BASELINE_CYCLES});
|
||||
print(" BASELINE: {d} cycles/call (frozen ref {d})\n", .{ avg_base, BASELINE_CYCLES });
|
||||
print(" SSE: {d} cycles/call", .{avg_sse});
|
||||
if (avg_sse < BASELINE_CYCLES) {
|
||||
const pct = (BASELINE_CYCLES - avg_sse) * 100 / BASELINE_CYCLES;
|
||||
@@ -1782,7 +1802,7 @@ pub fn main() void {
|
||||
};
|
||||
|
||||
const reset_and_run = struct {
|
||||
fn go(func: *const fn (u32, u32, u32, u32, u32) callconv(.c) void, so3: u32, pm3: u32, pp3: u32, po3: u32, sb3: u32, scene3: *[0x400]u8, actx3: *[0x20]u8, brt3: [*]u8, bc: usize) void {
|
||||
fn go(func: *const fn (u32, u32, u32, u32, u32) callconv(.{ .x86_thiscall = .{} }) void, so3: u32, pm3: u32, pp3: u32, po3: u32, sb3: u32, scene3: *[0x400]u8, actx3: *[0x20]u8, brt3: [*]u8, bc: usize) void {
|
||||
wu(u32, actx3[0x0C..0x10], 500, .little);
|
||||
wu(u32, scene3[0x40..0x44], 0, .little);
|
||||
// Re-init bone_rt anim_slot/sec_slot fields
|
||||
@@ -1849,14 +1869,14 @@ pub fn main() void {
|
||||
// calcColorValues_SSE -- disabled: no standalone SSE export yet
|
||||
// bench_calcColorValues();
|
||||
|
||||
// si_frustumCullBBox -- fastcall(bbox_ECX, flags_EDX, radius_stack) -> u32
|
||||
bench_frustumCullBBox();
|
||||
if (false) { // disabled: pulls in silicon_sse exports not linked into bench
|
||||
// si_frustumCullBBox -- fastcall(bbox_ECX, flags_EDX, radius_stack) -> u32
|
||||
bench_frustumCullBBox();
|
||||
|
||||
// si_processLinkedListCollision -- fastcall(listHead_ECX, queryBox_EDX, resultBuf_stack, flags_stack) -> u32
|
||||
// Builds a fake linked list with 8 nodes to benchmark AABB overlap test.
|
||||
bench_processLinkedListCollision();
|
||||
|
||||
} // end disabled block
|
||||
// si_processLinkedListCollision -- fastcall(listHead_ECX, queryBox_EDX, resultBuf_stack, flags_stack) -> u32
|
||||
// Builds a fake linked list with 8 nodes to benchmark AABB overlap test.
|
||||
bench_processLinkedListCollision();
|
||||
}
|
||||
|
||||
print("\n", .{});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
# SuperWoWhook + Timber UnitBuff Crash
|
||||
|
||||
## Bug
|
||||
|
||||
SuperWoWhook.dll crashes at DLL offset 0x1688 (`MOVZX ESI, word [ESI+EAX*2]`)
|
||||
when UnitBuff is called for another player who has no buff in the queried slot.
|
||||
Not a WeirdUtils or Timber bug -- root cause is a data error in SuperWoWhook's
|
||||
inline patching template.
|
||||
|
||||
## Root Cause
|
||||
|
||||
SuperWoWhook's `SuperWoW_BuildUnitBuffHook` (0x10002460) patches UnitBuff's
|
||||
return epilogues with CALL instructions that redirect to DLL formatter blocks.
|
||||
Each CALL site has a paired return-landing patch that rewrites the bytes after
|
||||
the CALL with proper register-restore code (POP EDI; POP ESI; POP EBX; ...).
|
||||
|
||||
Template entries 88-103 contain 8 CALL+landing pairs. All 7 first pairs follow
|
||||
the pattern `landing = CALL_addr + 5`:
|
||||
|
||||
| Pair | CALL addr | Landing | OK? |
|
||||
|------|------------|------------|----------------|
|
||||
| 0 | 0x519AF1 | 0x519AF6 | +5, correct |
|
||||
| 1 | 0x519B19 | 0x519B1E | +5, correct |
|
||||
| 2 | 0x519C48 | 0x519C4D | +5, correct |
|
||||
| 3 | 0x519C6D | 0x519C72 | +5, correct |
|
||||
| 4 | 0x519708 | 0x51970D | +5, correct |
|
||||
| 5 | 0x519729 | 0x51972E | +5, correct |
|
||||
| 6 | 0x51981D | 0x519822 | +5, correct |
|
||||
| **7**| **0x51983B** | **0x519830** | **-16, WRONG** |
|
||||
|
||||
Pair 7's landing is at 0x519830 (should be 0x519840). The template dword at
|
||||
`0x1001B240 + 103*4 = 0x1001B3DC` contains `0x00519830` instead of `0x00519840`.
|
||||
|
||||
### What happens
|
||||
|
||||
The "other player no buff" path (JE at 0x519803 taken -> 0x519829):
|
||||
|
||||
1. `lua_pushnil` called at 0x51982B, returns to 0x519830
|
||||
2. Code at 0x519830 was patched with `5F 5E 5B 8B` (meant for 0x519840)
|
||||
3. This creates: `POP EDI; POP ESI; POP EBX; MOV EBP,[EDX+0]` -- the `8B` from
|
||||
the patch combines with `6A 00` from the original code to form `MOV EBP,[EDX]`
|
||||
4. EBP is overwritten with whatever EDX points to (addon string data: "AltA")
|
||||
5. `lua_pushnumber` called at 0x519836 with shifted stack
|
||||
6. CALL at 0x51983B jumps to formatter `UnitBuff_ReturnFieldU16_3vals` (0x10001680)
|
||||
7. Formatter reads `[EBP-0x10]` with EBP = 0x616C7441 -> ACCESS_VIOLATION
|
||||
|
||||
Meanwhile, 0x519840 (the actual CALL return address) is never patched and has
|
||||
`00 00 5B 8B E5 5D C3` -- `ADD [EAX],AL` would crash even if the formatter survived.
|
||||
|
||||
### Why intermittent
|
||||
|
||||
The crash only triggers when:
|
||||
- Querying buffs on another player (not self) -- self uses Timber's TW path
|
||||
- That player has no buff in the queried slot -- triggers the "no buff" JE path
|
||||
- EDX happens to point to readable memory -- if [EDX] faults, different crash site
|
||||
|
||||
### Why Timber-specific reports
|
||||
|
||||
- On vanilla, the same bug exists but is less visible: the "other player" path is
|
||||
rarely exercised by addons compared to the "self" path
|
||||
- On Timber, the "self" path is redirected through TW code (bypassing SuperWoWhook),
|
||||
so addons calling UnitBuff("player", N) never hit SuperWoWhook's patches -- only
|
||||
UnitBuff("target", N) etc. can trigger it
|
||||
|
||||
## Fix
|
||||
|
||||
Single byte fix in SuperWoWhook.dll: change the dword at file offset corresponding
|
||||
to VA `0x1001B3DC` from `0x00519830` to `0x00519840`.
|
||||
|
||||
## Crash Signature
|
||||
|
||||
- ACCESS_VIOLATION at SuperWoWhook offset 0x1688
|
||||
- `MOVZX ESI, word [ESI + EAX*2]` with invalid address
|
||||
- EBP = ASCII text (e.g. 0x616C7441 = "AltA") -- corrupted by MOV EBP,[EDX]
|
||||
- Stack contains 0x00519840 and addon strings ("DBG:AceEvent20Frame")
|
||||
|
||||
## Ghidra Labels
|
||||
|
||||
SuperWoWhook.dll:
|
||||
- `SuperWoW_BuildUnitBuffHook` (0x10002460)
|
||||
- `UnitBuff_ReturnFieldU16_3vals` (0x10001680) -- crash site
|
||||
- Template at 0x1001B240 (197 dwords), replacement data at local_328
|
||||
|
||||
WoW.exe (Timber):
|
||||
- UnitBuff other-player path: 0x519780-0x519846
|
||||
- JE at 0x519803: "no buff found" branch to 0x519829
|
||||
- Two epilogues: 0x51981D (buff found, pair 6 OK), 0x51983B (no buff, pair 7 BROKEN)
|
||||
@@ -0,0 +1,285 @@
|
||||
# SuperWoWhook vs Timber: UnitBuff/UnitDebuff Patch Conflict
|
||||
|
||||
## Overview
|
||||
|
||||
Both Timber (modified WoW.exe) and SuperWoWhook.dll extend `Lua_UnitBuff` and
|
||||
`Lua_UnitDebuff` to return extra values (spell ID, dispel type). SuperWoWhook
|
||||
patches vanilla return sites that Timber has already moved or replaced, causing
|
||||
stack corruption and crashes.
|
||||
|
||||
## What Each System Does
|
||||
|
||||
### Vanilla WoW (unmodified)
|
||||
- `UnitBuff(unit, index)` returns 2 values: texture, count
|
||||
- `UnitDebuff(unit, index)` returns 3 values: texture, count, dispelType
|
||||
|
||||
### Timber (TW_ extensions at 0xD06xxx)
|
||||
- `UnitBuff` returns 3 values: texture, count, **spellID**
|
||||
- `UnitDebuff` returns 4 values: texture, count, dispelType, **spellID**
|
||||
- Implemented via JMP patches into TW_ code at 0xD06xxx
|
||||
|
||||
### SuperWoWhook (runtime code injection)
|
||||
- Extends both functions to add similar extra return values
|
||||
- Built for vanilla byte layout -- patches specific return epilogues
|
||||
- Uses WriteProcessMemory at runtime to overwrite code
|
||||
|
||||
## UnitBuff Conflict (0x519500)
|
||||
|
||||
### Vanilla return path (found match, has icon):
|
||||
```
|
||||
519703: CALL 0x6F3810 ; lua_pushnumber(count)
|
||||
519708: POP EDI ; epilogue
|
||||
519709: POP ESI ; <-- SuperWoWhook patches 4 bytes here
|
||||
51970A: MOV EAX, 0x2 ; (replaces return count + epilogue)
|
||||
51970F: POP EBX
|
||||
519710: MOV ESP, EBP
|
||||
519712: POP EBP
|
||||
519713: RET ; returns 2 values
|
||||
```
|
||||
|
||||
### Timber replaces this with:
|
||||
```
|
||||
519703: JMP 0x00D06585 ; -> TW_UnitBuff_PushDuration
|
||||
(0x519708-0x519713 is now dead code, never reached)
|
||||
519714: MOV ESI, [EBP-0x4] ; (nil icon path continues here)
|
||||
519717: MOV ECX, ESI
|
||||
...
|
||||
```
|
||||
|
||||
### What TW_UnitBuff_PushDuration does (0xD06585):
|
||||
```
|
||||
D06585: CALL 0x6F3810 ; lua_pushnumber(count) -- was on FPU stack
|
||||
D0658A: SUB ESP, 0x8
|
||||
D0658D: MOV ECX, ESI
|
||||
D0658F: MOV EAX, [EBP-0x24] ; load saved spell ID (stashed by TW_Lua_UnitBuff_Extended)
|
||||
D06594: MOV dword [EBP-0x18], EAX
|
||||
D06597: FILD dword [EBP-0x18] ; convert to float
|
||||
D0659A: FSTP qword [ESP]
|
||||
D0659D: CALL 0x6F3810 ; lua_pushnumber(spellID)
|
||||
D065A2: JMP 0xD066AF ; -> ReturnConstant3_b (return 3)
|
||||
```
|
||||
|
||||
**Conflict**: SuperWoWhook writes 4 bytes at 0x519709 -- dead code in Timber
|
||||
(JMP at 0x519703 bypasses it). This specific patch is **harmless** since it
|
||||
never executes.
|
||||
|
||||
## UnitDebuff Conflict (0x519860) -- THE CRASH
|
||||
|
||||
### Vanilla return path 1 (has dispel type string):
|
||||
```
|
||||
519AEC: CALL 0x6F3890 ; lua_pushstring(dispelType)
|
||||
519AF1: POP EDI ; epilogue
|
||||
519AF2: POP ESI ; <-- SuperWoWhook patches 4 bytes here
|
||||
519AF3: MOV EAX, 0x3 ; (replaces return count + epilogue)
|
||||
519AF8: POP EBX
|
||||
519AF9: MOV ESP, EBP
|
||||
519AFB: POP EBP
|
||||
519AFC: RET ; returns 3 values
|
||||
```
|
||||
|
||||
### Timber replaces this with:
|
||||
```
|
||||
519AEC: JMP 0x00D065B4 ; -> TW_UnitDebuff_PushDispelType
|
||||
(0x519AF1-0x519AFC is dead code, never reached by original path)
|
||||
519AFD: MOV ESI, [EBP-0xC] ; (nil path starts here -- DIFFERENT CODE)
|
||||
519B00: MOV ECX, ESI ; <-- This is where 0x519AF2+0x0E lands!
|
||||
519B02: CALL 0x6F37F0 ; lua_pushnil
|
||||
...
|
||||
```
|
||||
|
||||
**Conflict**: SuperWoWhook writes 4 bytes at **0x519AF2**. In vanilla this was
|
||||
`POP ESI; MOV EAX, 0x3` (the return-3 epilogue). In Timber this is dead code
|
||||
between the JMP at 0x519AEC and the nil path at 0x519AFD. SuperWoWhook writes
|
||||
a relative jump here. The bytes are technically dead on the Timber happy path,
|
||||
BUT if SuperWoWhook's other patches redirect execution INTO this dead zone,
|
||||
the corrupted bytes execute and crash.
|
||||
|
||||
### Vanilla return path 2 (nil dispel, fallback):
|
||||
```
|
||||
519B14: CALL 0x6F37F0 ; lua_pushnil
|
||||
519B19: POP EDI ; epilogue
|
||||
519B1A: POP ESI ; <-- SuperWoWhook patches 4 bytes here
|
||||
519B1B: MOV EAX, 0x3
|
||||
519B20: POP EBX
|
||||
519B21: MOV ESP, EBP
|
||||
519B23: POP EBP
|
||||
519B24: RET ; returns 3 values
|
||||
```
|
||||
|
||||
### Timber replaces this with:
|
||||
```
|
||||
519B14: JMP 0x00D065D4 ; -> TW_UnitDebuff_PushNilFallback
|
||||
(0x519B19-0x519B24 is dead code)
|
||||
519B25: LEA ECX, [EBP-0x20] ; (next function or unrelated code)
|
||||
519B28: CALL 0x496400
|
||||
...
|
||||
```
|
||||
|
||||
**Conflict**: SuperWoWhook writes 4 bytes at **0x519B1A**. In Timber this is
|
||||
dead code after the JMP at 0x519B14. Timber's disassembler doesn't even show
|
||||
instructions at 0x519B1A. SuperWoWhook writes corrupted jump bytes into this
|
||||
dead zone. Same risk as above.
|
||||
|
||||
### UnitDebuff return paths that are IDENTICAL (safe):
|
||||
```
|
||||
519C49: POP ESI ; <-- SuperWoWhook patches (SAME in both)
|
||||
519C4A: MOV EAX, 0x3
|
||||
|
||||
519C6E: POP ESI ; <-- SuperWoWhook patches (SAME in both)
|
||||
519C6F: MOV EAX, 0x3
|
||||
```
|
||||
These paths were NOT modified by Timber. SuperWoWhook's patches here are safe.
|
||||
|
||||
## Crash Mechanism
|
||||
|
||||
### Execution trace
|
||||
|
||||
The crash stack has return address `0x51983B` (UnitBuff epilogue: POP EDI, POP ESI,
|
||||
MOV EAX 2, ... RET). This is a UnitBuff return path that Timber did NOT modify --
|
||||
the "no match" or error path returning 2 values. From this path, execution entered
|
||||
SuperWoWhook's return-value formatter (offset 0x1688) via one of the 27 rel32 JMP
|
||||
patches.
|
||||
|
||||
The crash block at 0x10001688 reads `[EBP-0x10]` (aura data ptr) and `[EBP-0x8]`
|
||||
(aura index) from UnitBuff's stack frame. These are locals set up by UnitBuff's
|
||||
prologue. If EBP is valid, this works.
|
||||
|
||||
### The corruption
|
||||
|
||||
EBP = `0x616C7441` = ASCII "AltA". This is string data, not a stack address. The
|
||||
saved EBP was overwritten BEFORE SuperWoWhook's code runs -- SuperWoWhook's code
|
||||
is the victim, not the cause of the corruption.
|
||||
|
||||
The string "AltA" likely comes from `ChatFrameEditBox:SetAltArrowKeyMode(false)` --
|
||||
called by pfUI or shaguTweaks during an AceEvent OnUpdate handler. The stack also
|
||||
contains "DBG:AceEvent20Frame".
|
||||
|
||||
### Possible causes
|
||||
|
||||
1. **Lua addon buffer overflow**: a Lua addon (pfUI, shaguTweaks) called from an
|
||||
AceEvent OnUpdate handler overflows a fixed-size buffer in the C call chain,
|
||||
writing "AltArrowKeyMode" string data over the saved EBP on the stack. This
|
||||
happens on a re-entrant Lua call from within UnitBuff processing.
|
||||
|
||||
2. **SuperWoWhook code cave stack collision**: SuperWoWhook's code cave may use
|
||||
stack space that overlaps with Timber's larger UnitBuff frame (Timber adds
|
||||
`[EBP-0x24]` for spell ID). If SuperWoWhook's code cave uses the same stack
|
||||
offsets for different purposes, the frames collide.
|
||||
|
||||
3. **Re-entrant UnitBuff call**: UnitBuff -> SuperWoWhook -> fires Lua event
|
||||
(UNIT_CASTEVENT) -> addon handler calls UnitBuff again -> second invocation
|
||||
corrupts the first's stack frame.
|
||||
|
||||
### What SuperWoWhook patches are harmless on Timber
|
||||
|
||||
All 5 UnitBuff/UnitDebuff return-epilogue patches (0x519709, 0x519AF2, 0x519B1A,
|
||||
0x519C49, 0x519C6E) land on dead code or unmodified code. These patches alone
|
||||
don't cause the crash -- they redirect to SuperWoWhook's formatters which work
|
||||
fine IF EBP is valid.
|
||||
|
||||
### Confirmed: Stack Frame Size Differs
|
||||
|
||||
**Vanilla UnitBuff prologue:**
|
||||
```
|
||||
519503: SUB ESP, 0x20 ; 32 bytes of locals
|
||||
```
|
||||
|
||||
**Timber UnitBuff prologue:**
|
||||
```
|
||||
519503: SUB ESP, 0x24 ; 36 bytes of locals (+4 for [EBP-0x24] spell ID)
|
||||
```
|
||||
|
||||
Timber enlarged the stack frame by 4 bytes to store the spell ID at `[EBP-0x24]`.
|
||||
However, this alone does NOT explain the crash:
|
||||
|
||||
- The formatter blocks use EBP-relative addressing (`[EBP-0x10]`, `[EBP-0x8]`)
|
||||
which is unaffected by the frame size change
|
||||
- The formatters end with `RET`, bypassing the original epilogue entirely --
|
||||
they never do `POP EBP` so the shifted saved-register positions don't matter
|
||||
- If this were the sole cause, UnitBuff would crash on EVERY call, not intermittently
|
||||
|
||||
### Root Cause: Formatter Uses Wrong Locals on Other-Player Path
|
||||
|
||||
**Confirmed via Unicorn x86 emulation.**
|
||||
|
||||
SuperWoWhook's formatter at offset 0x1680 does:
|
||||
```
|
||||
MOV ESI, [EBP-0x10] ; assumes: unit data pointer
|
||||
MOV EAX, [EBP-0x8] ; assumes: aura slot index
|
||||
MOVZX ESI, word [ESI+EAX*2] ; reads u16 from aura array
|
||||
```
|
||||
|
||||
This is correct for the **local player** aura iteration path (0x51960F+),
|
||||
where `[EBP-0x10]` = unit object and `[EBP-0x8]` = aura iteration index.
|
||||
|
||||
But SuperWoWhook patches the **other player** return sites at 0x51981D and
|
||||
0x51983B with E9 JMPs to this same formatter. On the other-player path:
|
||||
|
||||
- `[EBP-0x10]` = unit object pointer (from ClntObjMgrObjectPtr)
|
||||
- `[EBP-0x8]` = NOT an aura index -- it's the Lua buff index from lua_tonumber
|
||||
|
||||
The formatter computes `unit_obj + lua_buff_index * 2` and reads a u16.
|
||||
This is NOT a valid aura array access. Depending on the unit pointer and
|
||||
buff index values:
|
||||
|
||||
- Usually: reads from valid heap -> returns garbage data (wrong but no crash)
|
||||
- Sometimes: reads from unmapped memory or guard page -> ACCESS_VIOLATION
|
||||
|
||||
The EBP="AltA" corruption in the crash reports is a RED HERRING. The real
|
||||
crash is the formatter reading `[ESI+EAX*2]` with ESI=unit_ptr and EAX=buff_index
|
||||
on a code path where those locals hold different data than expected. When
|
||||
the computed address (unit_ptr + buff_index*2) happens to land on unmapped
|
||||
memory, it crashes. When it lands on mapped memory, it silently returns
|
||||
wrong data.
|
||||
|
||||
### Emulation Evidence
|
||||
|
||||
Unicorn x86 emulation of Timber+SuperWoWhook patched UnitBuff:
|
||||
|
||||
**Test 1** (no-icon other-player path -> 0x51983B -> formatter):
|
||||
```
|
||||
STEP 11: SW PATCH (nil-icon other) EBP=0x0010effc
|
||||
STEP 12: *** SW FORMATTER *** EBP=0x0010effc ESI=0x00000000
|
||||
MEM ERR: [0x01000000] at EIP=0x10001688 (MOVZX ESI, [ESI+EAX*2])
|
||||
```
|
||||
|
||||
**Test 2** (has-icon other-player path -> 0x51981D -> formatter):
|
||||
```
|
||||
STEP 13: *** SW FORMATTER *** EBP=0x0010effc ESI=0x02000000
|
||||
MEM ERR: [0x01000000] at EIP=0x10001688 (MOVZX ESI, [ESI+EAX*2])
|
||||
```
|
||||
|
||||
Both crash at the same instruction. The formatter works on the local-player
|
||||
path (tested separately, returns correctly), but crashes on the other-player
|
||||
path because the locals at [EBP-0x10] and [EBP-0x8] mean different things.
|
||||
|
||||
### Why It's Intermittent
|
||||
|
||||
The crash only occurs when `unit_ptr + buff_index*2` points to unmapped
|
||||
memory. Most unit object pointers are in the heap (0x1xxxxxxx-0x3xxxxxxx
|
||||
range) and buff indices are small (0-31), so `unit_ptr + 0..62` usually
|
||||
lands in mapped heap. The crash happens when:
|
||||
- The unit was recently freed (dangling pointer)
|
||||
- The unit is at a high heap address where +index*2 crosses a page boundary
|
||||
- Memory pressure causes the page to be unmapped
|
||||
|
||||
### Fix
|
||||
|
||||
SuperWoWhook should use **different formatters** for the local-player and
|
||||
other-player return paths, since the stack frame locals differ between them.
|
||||
Or it should not patch the other-player return sites (0x51981D, 0x51983B)
|
||||
at all, since Timber doesn't extend those paths with spell ID anyway.
|
||||
|
||||
## Fix Options
|
||||
|
||||
1. **SuperWoWhook detects Timber**: check if 0x519703 is a JMP (byte 0xE9)
|
||||
before patching. If so, skip UnitBuff/UnitDebuff patches since Timber
|
||||
already provides spell ID.
|
||||
|
||||
2. **Timber provides a flag**: export a marker (global variable or named
|
||||
mutex) that SuperWoWhook checks before patching.
|
||||
|
||||
3. **Users disable SuperWoWhook**: since Timber already provides the extra
|
||||
return values, SuperWoWhook's UnitBuff extension is redundant on Timber.
|
||||
|
||||
Not a WeirdUtils issue. WeirdUtils is not loaded in either crash.
|
||||
@@ -196,7 +196,7 @@ pub fn installHooks() void {
|
||||
g_is_hook_owner = result.is_owner;
|
||||
if (!g_is_hook_owner) return;
|
||||
|
||||
log = logging.Logger.open(module_name, .both);
|
||||
log = logging.Logger.open(module_name, .console);
|
||||
_ = cotp_hook.attach(ADDR_CheckObjectTypePermissions, &checkObjTypeDetour);
|
||||
_ = wit_hook.attach(ADDR_WorldIntersectionTest, &worldIntersectDetour);
|
||||
// _ = portal_visual.install();
|
||||
|
||||
@@ -0,0 +1,340 @@
|
||||
//! portal_visual -- grey out unusable player-summoned portals/rituals.
|
||||
//!
|
||||
//! Hooks the M2 render pipeline to desaturate GO types 18 (ritual) and 22
|
||||
//! (portal) whose creator is a player not in the local player's group.
|
||||
//!
|
||||
//! Three hooks:
|
||||
//! ManageRenderListNode (0x710B90) -- tags portal models when added to render list
|
||||
//! DrawBatchProjected (0x70CB30) -- sets rendering_portal flag around batch draw
|
||||
//! DrawIndexedPrimitive (D3D9 vtable[82]) -- swaps pixel shader to desaturate
|
||||
|
||||
const hook = @import("zhook");
|
||||
const wow = @import("../wow.zig");
|
||||
const offsets = @import("../offsets.zig");
|
||||
const portal_filter = @import("portal_filter.zig");
|
||||
const logging = @import("../logging.zig");
|
||||
|
||||
var log: logging.Logger = .{};
|
||||
|
||||
const WINAPI = @import("std").builtin.CallingConvention.winapi;
|
||||
extern "kernel32" fn IsBadReadPtr(lp: u32, ucb: u32) callconv(WINAPI) i32;
|
||||
extern "kernel32" fn VirtualProtect(addr: *anyopaque, size: usize, new: u32, old: *u32) callconv(WINAPI) i32;
|
||||
|
||||
const MODEL_OWNER: usize = 0x28;
|
||||
const RENDER_CTX_MODEL: usize = 0x3310;
|
||||
|
||||
// Tagged portal model set -- direct-mapped cache
|
||||
const TAG_SIZE: u32 = 256;
|
||||
const TAG_MASK: u32 = TAG_SIZE - 1;
|
||||
var tagged_models: [TAG_SIZE]u32 = .{0} ** TAG_SIZE;
|
||||
|
||||
// Stores the GO object pointer alongside the model tag so we can access entity
|
||||
const TagEntry = struct { model: u32 = 0, owner: u32 = 0 };
|
||||
var tagged_entries: [TAG_SIZE]TagEntry = .{TagEntry{}} ** TAG_SIZE;
|
||||
|
||||
// Track entities we've already triggered fade on (one-shot test)
|
||||
var fade_triggered: [TAG_SIZE]u32 = .{0} ** TAG_SIZE;
|
||||
|
||||
// CreateFadeEffect: __thiscall(entity_ECX, fadeTime_f32_stack)
|
||||
const ENTITY_OFFSET: usize = 0x88;
|
||||
const CreateFadeEffectFn = fn (u32, f32) callconv(hook.cc.thiscall) void;
|
||||
const createFadeEffect: *const CreateFadeEffectFn = @ptrFromInt(0x672DF0);
|
||||
|
||||
// Deferred fade queue -- CreateFadeEffect is NOT safe to call during render list
|
||||
// traversal (ManageRenderListNode). Queue owner ptrs and process next frame.
|
||||
const FADE_QUEUE_SIZE: u32 = 16;
|
||||
var fade_queue: [FADE_QUEUE_SIZE]u32 = .{0} ** FADE_QUEUE_SIZE;
|
||||
var fade_queue_count: u32 = 0;
|
||||
|
||||
fn queueFade(owner: u32) void {
|
||||
if (fade_queue_count < FADE_QUEUE_SIZE) {
|
||||
fade_queue[fade_queue_count] = owner;
|
||||
fade_queue_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn processFadeQueue() void {
|
||||
var i: u32 = 0;
|
||||
while (i < fade_queue_count) : (i += 1) {
|
||||
triggerFade(fade_queue[i]);
|
||||
fade_queue[i] = 0;
|
||||
}
|
||||
fade_queue_count = 0;
|
||||
}
|
||||
|
||||
fn triggerFade(owner: u32) void {
|
||||
if (IsBadReadPtr(owner, 0x90) != 0) return;
|
||||
const entity = hook.readMem(u32, owner + ENTITY_OFFSET);
|
||||
if (entity == 0) return;
|
||||
if (IsBadReadPtr(entity, 0xC0) != 0) return;
|
||||
const scene_obj = hook.readMem(u32, entity + 0x88);
|
||||
if (scene_obj == 0) return;
|
||||
log.fmt("triggerFade: owner=0x{x} entity=0x{x} scene=0x{x}\n", .{ owner, entity, scene_obj });
|
||||
createFadeEffect(entity, 1.0);
|
||||
}
|
||||
|
||||
fn classifyModel(model: u32) void {
|
||||
const owner = hook.readMem(u32, model + MODEL_OWNER);
|
||||
if (owner == 0) return;
|
||||
if (IsBadReadPtr(owner, 0x20) != 0) return;
|
||||
const obj_type = hook.readMem(u32, owner + 0x14);
|
||||
if (obj_type != 5) {
|
||||
if (tagged_models[model & TAG_MASK] == model) {
|
||||
tagged_models[model & TAG_MASK] = 0;
|
||||
tagged_entries[model & TAG_MASK] = .{};
|
||||
}
|
||||
return;
|
||||
}
|
||||
const desc = wow.getDescriptor(owner);
|
||||
if (!wow.isValidPtr(desc)) return;
|
||||
const go_type = hook.readMem(u32, desc + offsets.DESC_GO_TYPE);
|
||||
if ((go_type == 18 or go_type == 22) and portal_filter.shouldFilter(desc)) {
|
||||
tagged_models[model & TAG_MASK] = model;
|
||||
tagged_entries[model & TAG_MASK] = .{ .model = model, .owner = owner };
|
||||
} else {
|
||||
if (tagged_models[model & TAG_MASK] == model) {
|
||||
tagged_models[model & TAG_MASK] = 0;
|
||||
tagged_entries[model & TAG_MASK] = .{};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn isTagged(model: u32) bool {
|
||||
return tagged_models[model & TAG_MASK] == model;
|
||||
}
|
||||
|
||||
var rendering_portal: bool = false;
|
||||
var log_count: u32 = 0;
|
||||
|
||||
// =============================================================================
|
||||
// Hook: ManageRenderListNode (0x710B90)
|
||||
// =============================================================================
|
||||
|
||||
const ManageRenderFn = fn (u32, u32) callconv(hook.cc.thiscall) void;
|
||||
var manage_hook: hook.Detour(ManageRenderFn) = .{};
|
||||
|
||||
fn manageRenderDetour(model: u32, add_to_list: u32) callconv(hook.cc.thiscall) void {
|
||||
if (model != 0 and add_to_list == 1) {
|
||||
classifyModel(model);
|
||||
if (isTagged(model)) {
|
||||
const entry = tagged_entries[model & TAG_MASK];
|
||||
// One-shot: trigger fade test on first detection
|
||||
if (entry.owner != 0 and fade_triggered[model & TAG_MASK] != model) {
|
||||
fade_triggered[model & TAG_MASK] = model;
|
||||
triggerFadeTest(entry.owner);
|
||||
}
|
||||
}
|
||||
} else if (model != 0) {
|
||||
if (tagged_models[model & TAG_MASK] == model) {
|
||||
tagged_models[model & TAG_MASK] = 0;
|
||||
tagged_entries[model & TAG_MASK] = .{};
|
||||
}
|
||||
}
|
||||
manage_hook.callOriginal(.{ model, add_to_list });
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Hook: DrawBatchProjected (0x70CB30)
|
||||
// =============================================================================
|
||||
|
||||
const DrawBatchFn = fn (u32) callconv(hook.cc.thiscall) void;
|
||||
var draw_batch_hook: hook.Detour(DrawBatchFn) = .{};
|
||||
|
||||
const MODEL_ALPHA: usize = 0x180; // written by fade system via SetMemoryPointer
|
||||
const DIM_ALPHA: u32 = @bitCast(@as(f32, 0.35));
|
||||
const FULL_ALPHA: u32 = @bitCast(@as(f32, 1.0));
|
||||
|
||||
fn drawBatchDetour(ctx: u32) callconv(hook.cc.thiscall) void {
|
||||
const model_ptr = if (wow.isValidPtr(ctx +% @as(u32, @intCast(RENDER_CTX_MODEL))))
|
||||
hook.readMem(u32, ctx + RENDER_CTX_MODEL)
|
||||
else
|
||||
0;
|
||||
|
||||
if (model_ptr != 0 and isTagged(model_ptr)) {
|
||||
// Write dim alpha to the model's opacity field before batch draws
|
||||
const saved = hook.readMem(u32, model_ptr + MODEL_ALPHA);
|
||||
const dest: *u32 = @ptrFromInt(model_ptr + MODEL_ALPHA);
|
||||
dest.* = DIM_ALPHA;
|
||||
|
||||
rendering_portal = true;
|
||||
draw_batch_hook.callOriginal(.{ctx});
|
||||
rendering_portal = false;
|
||||
|
||||
dest.* = saved;
|
||||
} else {
|
||||
draw_batch_hook.callOriginal(.{ctx});
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// D3D9 DIP hook + desaturation pixel shader
|
||||
// =============================================================================
|
||||
|
||||
inline fn vt(obj: *anyopaque) [*]usize {
|
||||
return @ptrFromInt(hook.readMem(u32, @intFromPtr(obj)));
|
||||
}
|
||||
|
||||
const VT_DIP: usize = 82;
|
||||
const VT_CreatePixelShader: usize = 106;
|
||||
const VT_SetPixelShader: usize = 107;
|
||||
const VT_GetPixelShader: usize = 108;
|
||||
const VT_SetPSConstantF: usize = 109;
|
||||
|
||||
var orig_dip: usize = 0;
|
||||
var d3d9_vtable: ?[*]usize = null;
|
||||
var desat_shader: ?*anyopaque = null;
|
||||
|
||||
// ps_2_0 desaturation shader: samples texture, converts to greyscale via luminance.
|
||||
// c0 = luminance weights (0.299, 0.587, 0.114, 0.0)
|
||||
//
|
||||
// ps_2_0
|
||||
// dcl t0.xy
|
||||
// dcl_2d s0
|
||||
// texld r0, t0, s0 ; sample texture
|
||||
// dp3 r1.x, r0, c0 ; grey = dot(rgb, luma)
|
||||
// mov r1.y, r1.x ; replicate
|
||||
// mov r1.z, r1.x
|
||||
// mov r1.w, r0.w ; preserve alpha
|
||||
// mov oC0, r1
|
||||
//
|
||||
// Assembled from the D3D shader token spec (ps_2_0 format):
|
||||
const desat_shader_bytecode = [_]u32{
|
||||
0xFFFF0200, // ps_2_0
|
||||
// dcl t0.xy
|
||||
0x0200001F, 0x80000000, 0xB0030000,
|
||||
// dcl_2d s0
|
||||
0x0200001F, 0x90000000, 0xA00F0800,
|
||||
// texld r0, t0, s0
|
||||
0x03000042, 0x800F0000, 0xB0E40000, 0xA0E40800,
|
||||
// dp3 r1.x, r0, c0
|
||||
0x03000008, 0x80010001, 0x80E40000, 0xA0E40000,
|
||||
// mov r1.y, r1.x
|
||||
0x02000001, 0x80020001, 0x80000001,
|
||||
// mov r1.z, r1.x
|
||||
0x02000001, 0x80040001, 0x80000001,
|
||||
// mov r1.w, r0.w
|
||||
0x02000001, 0x80080001, 0x80FF0000,
|
||||
// mov oC0, r1
|
||||
0x02000001, 0x800F0800, 0x80E40001,
|
||||
// end
|
||||
0x0000FFFF,
|
||||
};
|
||||
|
||||
const luma_weights = [4]f32{ 0.299, 0.587, 0.114, 0.0 };
|
||||
|
||||
fn createShader(device: *anyopaque) bool {
|
||||
const createFn: *const fn (*anyopaque, [*]const u32, **anyopaque) callconv(hook.cc.stdcall) i32 =
|
||||
@ptrFromInt(vt(device)[VT_CreatePixelShader]);
|
||||
var shader: ?*anyopaque = null;
|
||||
const hr = createFn(device, &desat_shader_bytecode, @ptrCast(&shader));
|
||||
if (hr >= 0 and shader != null) {
|
||||
desat_shader = shader;
|
||||
return true;
|
||||
}
|
||||
log.fmt("CreatePixelShader failed: hr=0x{x}\n", .{@as(u32, @bitCast(hr))});
|
||||
return false;
|
||||
}
|
||||
|
||||
fn hkDIP(
|
||||
device: *anyopaque,
|
||||
prim_type: u32,
|
||||
base_vtx: i32,
|
||||
min_vtx: u32,
|
||||
num_verts: u32,
|
||||
start_idx: u32,
|
||||
prim_count: u32,
|
||||
) callconv(hook.cc.stdcall) i32 {
|
||||
const origFn: *const fn (*anyopaque, u32, i32, u32, u32, u32, u32) callconv(hook.cc.stdcall) i32 =
|
||||
@ptrFromInt(orig_dip);
|
||||
|
||||
if (rendering_portal and desat_shader != null) {
|
||||
// Save current pixel shader
|
||||
var saved_ps: ?*anyopaque = null;
|
||||
const getFn: *const fn (*anyopaque, *?*anyopaque) callconv(hook.cc.stdcall) i32 =
|
||||
@ptrFromInt(vt(device)[VT_GetPixelShader]);
|
||||
_ = getFn(device, &saved_ps);
|
||||
|
||||
// Set desaturation shader + luminance weights
|
||||
const setFn: *const fn (*anyopaque, ?*anyopaque) callconv(hook.cc.stdcall) i32 =
|
||||
@ptrFromInt(vt(device)[VT_SetPixelShader]);
|
||||
_ = setFn(device, desat_shader);
|
||||
|
||||
const setConstFn: *const fn (*anyopaque, u32, [*]const f32, u32) callconv(hook.cc.stdcall) i32 =
|
||||
@ptrFromInt(vt(device)[VT_SetPSConstantF]);
|
||||
_ = setConstFn(device, 0, &luma_weights, 1);
|
||||
|
||||
const result = origFn(device, prim_type, base_vtx, min_vtx, num_verts, start_idx, prim_count);
|
||||
|
||||
// Restore pixel shader
|
||||
_ = setFn(device, saved_ps);
|
||||
if (saved_ps) |ps| {
|
||||
const relFn: *const fn (*anyopaque) callconv(hook.cc.stdcall) u32 = @ptrFromInt(vt(ps)[2]);
|
||||
_ = relFn(ps);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return origFn(device, prim_type, base_vtx, min_vtx, num_verts, start_idx, prim_count);
|
||||
}
|
||||
|
||||
fn patchVtableEntry(vtable_ptr: [*]usize, idx: usize, new_fn: usize, old_fn: *usize) bool {
|
||||
old_fn.* = vtable_ptr[idx];
|
||||
var old_prot: u32 = 0;
|
||||
const addr: *anyopaque = @ptrFromInt(@intFromPtr(&vtable_ptr[idx]));
|
||||
if (VirtualProtect(addr, @sizeOf(usize), 0x40, &old_prot) == 0) return false;
|
||||
vtable_ptr[idx] = new_fn;
|
||||
_ = VirtualProtect(addr, @sizeOf(usize), old_prot, &old_prot);
|
||||
return true;
|
||||
}
|
||||
|
||||
fn restoreVtableEntry(vtable_ptr: [*]usize, idx: usize, old_fn: usize) void {
|
||||
var old_prot: u32 = 0;
|
||||
const addr: *anyopaque = @ptrFromInt(@intFromPtr(&vtable_ptr[idx]));
|
||||
if (VirtualProtect(addr, @sizeOf(usize), 0x40, &old_prot) == 0) return;
|
||||
vtable_ptr[idx] = old_fn;
|
||||
_ = VirtualProtect(addr, @sizeOf(usize), old_prot, &old_prot);
|
||||
}
|
||||
|
||||
fn getD3D9VTable() ?[*]usize {
|
||||
const gx = hook.readMem(u32, offsets.GX_DEVICE_PTR);
|
||||
if (gx == 0) return null;
|
||||
const dev = hook.readMem(u32, gx + offsets.GX_DEVICE_D3D_OFFSET);
|
||||
if (dev == 0) return null;
|
||||
const vtable_addr = hook.readMem(u32, dev);
|
||||
if (vtable_addr == 0) return null;
|
||||
return @ptrFromInt(vtable_addr);
|
||||
}
|
||||
|
||||
var d3d9_initialized: bool = false;
|
||||
|
||||
fn initD3D9() void {
|
||||
// DIP shader hook disabled -- testing model+0x180 alpha approach
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Install / Remove
|
||||
// =============================================================================
|
||||
|
||||
pub fn install() bool {
|
||||
log = logging.Logger.open("portal_visual", .both);
|
||||
if (manage_hook.attach(0x710B90, &manageRenderDetour) != .ok) return false;
|
||||
if (draw_batch_hook.attach(0x70CB30, &drawBatchDetour) != .ok) {
|
||||
manage_hook.detach();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Deferred D3D9 init -- call from lateInit when device exists.
|
||||
pub fn lateInit() void {
|
||||
initD3D9();
|
||||
}
|
||||
|
||||
pub fn remove() void {
|
||||
if (d3d9_vtable) |vtbl| {
|
||||
if (orig_dip != 0) restoreVtableEntry(vtbl, VT_DIP, orig_dip);
|
||||
}
|
||||
draw_batch_hook.detach();
|
||||
manage_hook.detach();
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
# Handoff: COMBAT_LOG_EVENT Filtering (CLEU vs CLEUF)
|
||||
|
||||
## Goal
|
||||
|
||||
Implement Blizzard's TBC/WotLK event filtering system so we provide both:
|
||||
- `COMBAT_LOG_EVENT` -- filtered version (only events relevant to the player's group)
|
||||
- `COMBAT_LOG_EVENT_UNFILTERED` -- all events in range (everything the client sees)
|
||||
|
||||
This matches how TBC and WotLK work: addons register for whichever they need. DPS meters
|
||||
use UNFILTERED for complete data, while UI elements use the filtered version to reduce noise.
|
||||
|
||||
## Background
|
||||
|
||||
### How Blizzard Did It
|
||||
|
||||
In TBC (2.4.0), Blizzard introduced `COMBAT_LOG_EVENT_UNFILTERED` which fires for ALL
|
||||
combat events the client receives. They also provided `COMBAT_LOG_EVENT` which fires only
|
||||
for events passing a set of filters.
|
||||
|
||||
The filtering is controlled by:
|
||||
- `CombatLogClearEntries()` -- clears the combat log
|
||||
- `CombatLogAddFilter(...)` -- adds a filter rule
|
||||
- `CombatLogResetFilter()` -- resets to default filters
|
||||
- `CombatLogGetCurrentEntry(...)` -- reads the current event (deprecated by WotLK)
|
||||
- `CombatLogGetNumEntries()` -- count of buffered events
|
||||
|
||||
Default filters include:
|
||||
- Source or dest is the player
|
||||
- Source or dest is in the player's party/raid
|
||||
- Source or dest is the player's pet/guardian
|
||||
- Events within a certain range
|
||||
|
||||
### What We Currently Have
|
||||
|
||||
We fire a single `COMBAT_LOG_EVENT` at slot 549 via SignalEventParam. Every hook fires
|
||||
every event it sees -- no filtering. This is effectively UNFILTERED behavior under the
|
||||
FILTERED event name.
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 1: Register Both Events
|
||||
|
||||
Add a second event slot for `COMBAT_LOG_EVENT_UNFILTERED`:
|
||||
- `COMBAT_LOG_EVENT` at slot 549 (existing) -- will become the filtered version
|
||||
- `COMBAT_LOG_EVENT_UNFILTERED` at slot 550 -- fires everything (current behavior)
|
||||
|
||||
In `createEventsDetour`, bump the event count by 2 instead of 1 and register both names.
|
||||
Update all fire functions to fire both events (or fire UNFILTERED always and FILTERED
|
||||
conditionally).
|
||||
|
||||
**IMPORTANT**: Slot 550 was previously identified as a float global (0x3E800000 = 0.25).
|
||||
Need to verify this is still the case or find a different slot. May need to search for
|
||||
two consecutive free slots.
|
||||
|
||||
### Phase 2: Implement Filtering Logic
|
||||
|
||||
The filter checks whether source or dest is "interesting" to the local player:
|
||||
|
||||
```zig
|
||||
fn shouldFilter(src_guid: u64, dst_guid: u64) bool {
|
||||
// Always pass if source or dest is the local player
|
||||
if (src_guid == getPlayerGUID() or dst_guid == getPlayerGUID()) return false;
|
||||
// Always pass if source or dest is in the player's group
|
||||
if (isGroupMember(src_guid) or isGroupMember(dst_guid)) return false;
|
||||
// Always pass if source or dest is a pet owned by a group member
|
||||
if (isPetOfGroupMember(src_guid) or isPetOfGroupMember(dst_guid)) return false;
|
||||
// Filter out (don't fire COMBAT_LOG_EVENT)
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
Requires:
|
||||
- `getPlayerGUID()` -- already have this (0x468550)
|
||||
- `isGroupMember(guid)` -- check party (0xBC6F48) and raid (0xB712A8) GUID arrays
|
||||
- `isPetOfGroupMember(guid)` -- check UNIT_FIELD_SUMMONEDBY descriptor, resolve owner
|
||||
|
||||
### Phase 3: Fire Functions
|
||||
|
||||
Two approaches:
|
||||
|
||||
**Option A: Double-fire**
|
||||
Every fire function fires the event twice -- once for UNFILTERED (always), once for
|
||||
FILTERED (if passes filter). Simple but doubles the SignalEventParam calls.
|
||||
|
||||
**Option B: Conditional fire with shared push**
|
||||
Push args to Lua stack once, then call SignalEvent for each registered event that should
|
||||
receive it. Requires understanding SignalEventParam internals more deeply.
|
||||
|
||||
Option A is simpler and the performance cost of an extra SignalEventParam call per event
|
||||
is negligible compared to the Lua handler execution.
|
||||
|
||||
### Phase 4: sourceFlags / destFlags
|
||||
|
||||
With filtering in place, we can also construct the COMBATLOG_OBJECT_* bitfield that
|
||||
WotLK addons use for their own filtering:
|
||||
|
||||
```
|
||||
COMBATLOG_OBJECT_AFFILIATION_MINE = 0x0001
|
||||
COMBATLOG_OBJECT_AFFILIATION_PARTY = 0x0002
|
||||
COMBATLOG_OBJECT_AFFILIATION_RAID = 0x0004
|
||||
COMBATLOG_OBJECT_AFFILIATION_OUTSIDER = 0x0008
|
||||
COMBATLOG_OBJECT_REACTION_FRIENDLY = 0x0010
|
||||
COMBATLOG_OBJECT_REACTION_NEUTRAL = 0x0020
|
||||
COMBATLOG_OBJECT_REACTION_HOSTILE = 0x0040
|
||||
COMBATLOG_OBJECT_CONTROL_PLAYER = 0x0100
|
||||
COMBATLOG_OBJECT_CONTROL_NPC = 0x0200
|
||||
COMBATLOG_OBJECT_TYPE_PLAYER = 0x0400
|
||||
COMBATLOG_OBJECT_TYPE_NPC = 0x0800
|
||||
COMBATLOG_OBJECT_TYPE_PET = 0x1000
|
||||
COMBATLOG_OBJECT_TYPE_GUARDIAN = 0x2000
|
||||
COMBATLOG_OBJECT_TYPE_OBJECT = 0x4000
|
||||
```
|
||||
|
||||
These can be constructed from:
|
||||
- GUID type bits (high nibble of GUID encodes player/creature/pet/gameobject)
|
||||
- Group membership (party/raid roster arrays)
|
||||
- Reaction (UnitReaction at 0x6061E0)
|
||||
- Ownership (UNIT_FIELD_SUMMONEDBY descriptor)
|
||||
|
||||
This is the same data needed for filtering, so it comes naturally after Phase 2.
|
||||
|
||||
### Phase 5: Lua API (Optional)
|
||||
|
||||
Provide the filter configuration API for full parity:
|
||||
- `CombatLogAddFilter(srcFlags, dstFlags, eventType)`
|
||||
- `CombatLogResetFilter()`
|
||||
- `CombatLogGetNumEntries()`
|
||||
|
||||
Most addons don't use these -- they just register for UNFILTERED and do their own
|
||||
filtering. Low priority.
|
||||
|
||||
## Key Addresses
|
||||
|
||||
- Event slot 549: COMBAT_LOG_EVENT (existing)
|
||||
- Event slot 550: needs verification (was float 0.25 in earlier research)
|
||||
- Player GUID: 0x468550 (ClntObjMgrGetActivePlayer)
|
||||
- Party GUIDs: 0xBC6F48 (array of 4 u64 GUIDs)
|
||||
- Raid roster: 0xB712A8 (array of ptrs), count at 0xB713E0
|
||||
- UnitReaction: 0x6061E0
|
||||
- UNIT_FIELD_SUMMONEDBY: descriptor offset 0x30 (index 0x0C)
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Phase 1 can start immediately
|
||||
- Phase 2 needs isGroupMember which uses existing known addresses
|
||||
- Phase 3 trivial once Phase 1+2 done
|
||||
- Phase 4 is independent enhancement, can parallelize with Phase 2
|
||||
|
||||
## Risk
|
||||
|
||||
- Slot 550 conflict with float global -- may need to find a safe slot
|
||||
- Double-firing events may interact poorly with addons that register for both
|
||||
(they'd see every event twice) -- need to ensure the events have distinct IDs
|
||||
- Performance of filter check per event should be negligible (few memory reads)
|
||||
@@ -599,6 +599,20 @@ All in the 0x51xxxx addon/UI system. Zeroing/overwriting these corrupts game sta
|
||||
SMSG_RESURRECT_FAILED (0x252).
|
||||
- [ ] **UNIT_POSITION**: Periodic or event-driven unit position data in combat log events.
|
||||
Useful for replay/analysis tools.
|
||||
- [ ] **SPELL_CAST dest coordinates**: SMSG_SPELL_START/GO include destination XYZ for
|
||||
area-targeted spells via TARGET_FLAG_DEST_LOCATION (0x40) in SpellCastTargets.
|
||||
- Target data format in packet: uint16 targetMask, then if 0x02: packed GUID (unit),
|
||||
if 0x20: 3x float (source XYZ), if 0x40: 3x float (dest XYZ)
|
||||
- Player ground AoE (Blizzard, Flamestrike, Rain of Fire, Volley): client sends clicked
|
||||
position, server echoes in both SPELL_START and SPELL_GO
|
||||
- Mob ground AoE (Ignite Earth, mob Flamestrike): AI script calls CastSpell(x,y,z,...),
|
||||
server sets 0x40 via setDestination(), coords appear in SPELL_GO
|
||||
- Self-centered AoE (Arcane Explosion, Hellfire): no 0x40, no dest coords
|
||||
- Triggered spells: dest overridden to caster position regardless of original flags
|
||||
- Already hooked at 0x6E7640 (spellStartDetour) -- just need to parse target mask and
|
||||
extract floats when 0x40 is set, emit as extra CLEU params on SPELL_CAST_START/SUCCESS
|
||||
- Server ref: SpellCastTargets::write() in tortoise-wow Spell.cpp lines 223-268
|
||||
- Enables boss mod positional warnings (e.g. Karazhan 40-man Ignite Earth ground zones)
|
||||
|
||||
### Completed reference
|
||||
- [x] RANGE_MISSED: Detected via spell ID check (75/5019) in ProcessSpellCombatResult hook.
|
||||
@@ -1010,3 +1024,91 @@ ProcessSpellDamageWithLocalization (0x629d30)
|
||||
| FrameScript_CreateEvents | FrameScript_CreateEvents | 0x703D90 |
|
||||
| FrameScript_RegisterFunction | FrameScript_RegisterFunction | 0x704120 |
|
||||
| GetSpellNameById | (SpellDb at 0xC0D780) | 0x6264b0 |
|
||||
|
||||
---
|
||||
|
||||
## Missing WotLK CLEU Subevents
|
||||
|
||||
Events present in WotLK 3.3.5 COMBAT_LOG_EVENT_UNFILTERED but not yet implemented.
|
||||
|
||||
### Loot Events (novel -- never existed in any WoW combat log)
|
||||
|
||||
LOOT was never a COMBAT_LOG_EVENT subevent in any WoW expansion. Loot tracking addons
|
||||
use separate Lua events (LOOT_OPENED, CHAT_MSG_LOOT, etc.), not the combat log.
|
||||
Adding loot to our combat log would be a novel extension, not WotLK parity.
|
||||
|
||||
Relevant vanilla packets if we ever want to add this:
|
||||
|
||||
| Packet | Opcode | Format |
|
||||
|--------|--------|--------|
|
||||
| SMSG_ITEM_PUSH_RESULT | 0x166 | playerGUID(8), received(4), created(4), showInChat(4), bagSlot(1), itemSlot(4), itemID(4), suffix(4), randomProp(4), count(4) |
|
||||
| SMSG_LOOT_ROLL | 0x2A2 | targetGUID(8), slot(4), rollerGUID(8), itemID(4), suffix(4), randomProp(4), rollNum(1), rollType(1) |
|
||||
| SMSG_LOOT_ROLL_WON | 0x29F | targetGUID(8), slot(4), itemID(4), suffix(4), randomProp(4), winnerGUID(8), rollNum(1), rollType(1) |
|
||||
| SMSG_LOOT_ALL_PASSED | 0x29E | targetGUID(8), slot(4), itemID(4), randomProp(4), suffix(4) |
|
||||
| SMSG_LOOT_MONEY_NOTIFY | 0x163 | amount(4) |
|
||||
|
||||
### Enchant Events (verified in WotLK/Cata logs)
|
||||
|
||||
Confirmed present in real WotLK combat logs. No vanilla packet equivalent -- would need
|
||||
to hook the client enchant application/removal functions.
|
||||
|
||||
| Subevent | Description |
|
||||
|----------|-------------|
|
||||
| ENCHANT_APPLIED | Item enchanted (temp or permanent) |
|
||||
| ENCHANT_REMOVED | Enchant removed/expired |
|
||||
|
||||
Verified WotLK format:
|
||||
```
|
||||
ENCHANT_APPLIED,srcGUID,srcName,srcFlags,srcRaidFlags,dstGUID,dstName,dstFlags,dstRaidFlags,"enchantName",itemID,"itemName"
|
||||
ENCHANT_REMOVED,0x0000000000000000,nil,0x80000000,0x80000000,dstGUID,dstName,dstFlags,dstRaidFlags,"enchantName",itemID,"itemName"
|
||||
```
|
||||
|
||||
Note: ENCHANT_REMOVED source is always null GUID (no "remover").
|
||||
|
||||
### SPELL_CREATE (verified in WotLK/Cata logs)
|
||||
|
||||
Fires when a player creates a game object (trap, totem, feast, cauldron).
|
||||
Uses standard spell prefix. Dest is the created GO's GUID.
|
||||
|
||||
```
|
||||
SPELL_CREATE,srcGUID,srcName,srcFlags,srcRaidFlags,goGUID,"goName",goFlags,goRaidFlags,spellID,"spellName",spellSchool
|
||||
```
|
||||
|
||||
### Combat Events (low priority)
|
||||
|
||||
| Subevent | Description | Notes |
|
||||
|----------|-------------|-------|
|
||||
| UNIT_DISSIPATES | Pet/totem/guardian despawn | Variant of UNIT_DIED, not in WotLK sample but in retail |
|
||||
| SPELL_ABSORBED | Damage absorbed by shield | Present in Shadowlands+, not in WotLK sample |
|
||||
| SPELL_HEAL_ABSORBED | Healing absorbed by anti-heal | Shadowlands+, no vanilla mechanic |
|
||||
| SWING_DAMAGE_LANDED | Melee hit after absorb | Shadowlands+, not in WotLK |
|
||||
| SPELL_BUILDING_* | Structure damage prefix | WotLK siege content, N/A in vanilla |
|
||||
| *_DURABILITY_DAMAGE | Equipment durability loss | Rarely useful |
|
||||
|
||||
### Metadata Events (Shadowlands+ only, NOT in WotLK)
|
||||
|
||||
These do NOT appear in WotLK/Cata combat logs. They were added in later expansions
|
||||
(combat log version 16+). Listed for reference only.
|
||||
|
||||
| Subevent | Description | First seen |
|
||||
|----------|-------------|------------|
|
||||
| ENCOUNTER_START | Boss encounter begins | Shadowlands+ |
|
||||
| ENCOUNTER_END | Boss encounter ends | Shadowlands+ |
|
||||
| ZONE_CHANGE | Player changes zone | Shadowlands+ |
|
||||
| COMBATANT_INFO | Gear/talent snapshot | Shadowlands+ |
|
||||
| CHALLENGE_MODE_START/END | M+ key start/end | Shadowlands+ |
|
||||
| EMOTE | Boss emote text | Shadowlands+ |
|
||||
| MAP_CHANGE | Map transition | Shadowlands+ |
|
||||
| ARENA_MATCH_START/END | Arena match | Shadowlands+ |
|
||||
|
||||
### Current Implementation Status
|
||||
|
||||
37 subevents implemented across 23 hooks (Phases 1-5 complete):
|
||||
- Damage: SWING/RANGE/SPELL/PERIODIC/SHIELD/SPLIT/ENVIRONMENTAL (7)
|
||||
- Missed: SWING/RANGE/SPELL/PERIODIC/SHIELD (5)
|
||||
- Heal: SPELL/PERIODIC (2)
|
||||
- Power: ENERGIZE/DRAIN/PERIODIC_ENERGIZE/PERIODIC_DRAIN/PERIODIC_LEECH (5)
|
||||
- Aura: APPLIED/REMOVED/DOSE(x2)/REFRESH/BROKEN/BROKEN_SPELL (7)
|
||||
- Cast: START/SUCCESS/FAILED (3)
|
||||
- Misc: INTERRUPT/DISPEL/DISPEL_FAILED/STOLEN/EXTRA_ATTACKS/SUMMON/RESURRECT/INSTAKILL (8)
|
||||
- Death: UNIT_DIED/UNIT_DESTROYED/PARTY_KILL (3)
|
||||
|
||||
@@ -486,7 +486,7 @@ local function profReport(label, tbl)
|
||||
end
|
||||
end
|
||||
|
||||
local benchActive = true
|
||||
local benchActive = false
|
||||
local benchFrame = CreateFrame("Frame")
|
||||
|
||||
local function benchCombatStart()
|
||||
|
||||
@@ -145,6 +145,14 @@ end
|
||||
local profiling = false
|
||||
local profCLEU = { events = 0, totalMs = 0, gcStart = 0 }
|
||||
local profOrig = { events = 0, totalMs = 0, gcStart = 0 }
|
||||
|
||||
-- Fine-grained timing buckets (microseconds, accumulated)
|
||||
local profDetail = { getInfo = 0, dbCalls = 0, dispatch = 0, total = 0, count = 0 }
|
||||
-- Per-DB-function timing (microseconds, accumulated)
|
||||
local profDB = {
|
||||
DamageDone = 0, DamageTaken = 0, EnemyDamage = 0, DeathHistory = 0,
|
||||
Healing = 0, HealingTaken = 0, BuildBuffs = 0, Other = 0, count = 0
|
||||
}
|
||||
local profCurrent = nil
|
||||
|
||||
-- Hook the parser's OnEvent to measure original mode
|
||||
@@ -204,6 +212,32 @@ local function profReport(label, tbl)
|
||||
"|cff00ff00[%s]|r %d events, %.1fms total, %.1f us/event, %+.1f KB gc",
|
||||
label, tbl.events, totalMs, avgUs, gcDelta))
|
||||
|
||||
-- Detail breakdown for CLEU
|
||||
if label == "CLEU" and profDetail.count > 0 then
|
||||
local avgGetInfo = profDetail.getInfo / profDetail.count
|
||||
local avgDB = profDetail.dbCalls / profDetail.count
|
||||
local avgTotal = profDetail.total / profDetail.count
|
||||
DEFAULT_CHAT_FRAME:AddMessage(string.format(
|
||||
"|cff00ff00[CLEU detail]|r getInfo=%.1f us, db+logic=%.1f us, total=%.1f us (%d events)",
|
||||
avgGetInfo, avgDB, avgTotal, profDetail.count))
|
||||
-- Per-DB-function breakdown
|
||||
if profDB.count > 0 then
|
||||
local n = profDB.count
|
||||
DEFAULT_CHAT_FRAME:AddMessage(string.format(
|
||||
"|cff00ff00[DB funcs]|r DD=%.0f DT=%.0f ED=%.0f DH=%.0f H=%.0f HT=%.0f Buf=%.0f Oth=%.0f us/evt",
|
||||
profDB.DamageDone/n, profDB.DamageTaken/n, profDB.EnemyDamage/n,
|
||||
profDB.DeathHistory/n, profDB.Healing/n, profDB.HealingTaken/n,
|
||||
profDB.BuildBuffs/n, profDB.Other/n))
|
||||
end
|
||||
profDetail.getInfo = 0
|
||||
profDetail.dbCalls = 0
|
||||
profDetail.total = 0
|
||||
profDetail.count = 0
|
||||
profDB.DamageDone = 0; profDB.DamageTaken = 0; profDB.EnemyDamage = 0
|
||||
profDB.DeathHistory = 0; profDB.Healing = 0; profDB.HealingTaken = 0
|
||||
profDB.BuildBuffs = 0; profDB.Other = 0; profDB.count = 0
|
||||
end
|
||||
|
||||
-- If we have both measurements, show comparison
|
||||
if lastCLEUAvg and lastOrigAvg and lastOrigAvg > 0 then
|
||||
local pct = ((lastCLEUAvg - lastOrigAvg) / lastOrigAvg) * 100
|
||||
@@ -216,38 +250,42 @@ local function profReport(label, tbl)
|
||||
end
|
||||
end
|
||||
|
||||
-- /dpsbench -- enables per-combat A/B profiling. Each combat: measure, report, flip.
|
||||
local benchActive = true
|
||||
-- Always-on CLEU profiling: report stats at every combat end.
|
||||
-- /dpsbench toggles A/B mode (alternates CLEU/ORIGINAL each combat for comparison).
|
||||
local benchActive = false
|
||||
|
||||
local benchFrame = CreateFrame("Frame")
|
||||
|
||||
local function benchCombatStart()
|
||||
if not benchActive then return end
|
||||
local function combatStart()
|
||||
profCurrent = cleuActive and profCLEU or profOrig
|
||||
profReset(profCurrent)
|
||||
debugprofilestart() -- start the monotonic clock for this combat
|
||||
debugprofilestart()
|
||||
profiling = true
|
||||
local label = cleuActive and "CLEU" or "ORIGINAL"
|
||||
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[DPS Bench]|r combat started, measuring %s", label))
|
||||
if benchActive then
|
||||
local label = cleuActive and "CLEU" or "ORIGINAL"
|
||||
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[DPS Bench]|r measuring %s", label))
|
||||
end
|
||||
end
|
||||
|
||||
local function benchCombatEnd()
|
||||
if not benchActive or not profiling then return end
|
||||
local function combatEnd()
|
||||
if not profiling then return end
|
||||
profiling = false
|
||||
local label = cleuActive and "CLEU" or "ORIGINAL"
|
||||
profReport(label, profCurrent)
|
||||
toggle()
|
||||
DEFAULT_CHAT_FRAME:AddMessage(string.format(
|
||||
"|cff00ff00[DPS Bench]|r next combat will use: %s", cleuActive and "CLEU" or "ORIGINAL"))
|
||||
if benchActive then
|
||||
toggle()
|
||||
DEFAULT_CHAT_FRAME:AddMessage(string.format(
|
||||
"|cff00ff00[DPS Bench]|r next combat: %s", cleuActive and "CLEU" or "ORIGINAL"))
|
||||
end
|
||||
end
|
||||
|
||||
benchFrame:RegisterEvent("PLAYER_REGEN_DISABLED")
|
||||
benchFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
|
||||
benchFrame:SetScript("OnEvent", function()
|
||||
if event == "PLAYER_REGEN_DISABLED" then
|
||||
benchCombatStart()
|
||||
combatStart()
|
||||
elseif event == "PLAYER_REGEN_ENABLED" then
|
||||
benchCombatEnd()
|
||||
combatEnd()
|
||||
end
|
||||
end)
|
||||
|
||||
@@ -256,11 +294,10 @@ SlashCmdList["DPSBENCH"] = function()
|
||||
benchActive = not benchActive
|
||||
if benchActive then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(string.format(
|
||||
"|cff00ff00[DPS Bench]|r enabled. Current mode: %s. Enter combat to begin.",
|
||||
"|cff00ff00[DPS Bench]|r A/B enabled. Current mode: %s. Enter combat to begin.",
|
||||
cleuActive and "CLEU" or "ORIGINAL"))
|
||||
else
|
||||
profiling = false
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[DPS Bench]|r disabled.")
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[DPS Bench]|r A/B disabled (stats still reported).")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -280,16 +317,17 @@ local FailDB = DPSMate.Parser.FailDB
|
||||
|
||||
cleuHandler = function()
|
||||
if not CombatLogGetCurrentEventInfo then return end
|
||||
-- Single call — positions differ by prefix type:
|
||||
-- Swing: p1=amount, p2=overkill, p3=school, ...
|
||||
-- Spell: p1=spellId, p2=spellName, p3=spellSchool, p4=amount, ...
|
||||
-- Env: p1=envType, p2=amount, ...
|
||||
|
||||
local t0 = profiling and debugprofilestop()
|
||||
|
||||
local sub, srcGUID, srcName, srcFlags, srcRaidFlags,
|
||||
dstGUID, dstName, dstFlags, dstRaidFlags,
|
||||
p1, p2, p3, p4, p5, p6, p7, p8, p9,
|
||||
p10, p11, p12 = CombatLogGetCurrentEventInfo()
|
||||
if not sub then return end
|
||||
|
||||
local t1 = profiling and debugprofilestop()
|
||||
|
||||
if not srcName or srcName == "" then srcName = "Unknown" end
|
||||
if not dstName or dstName == "" then dstName = "Unknown" end
|
||||
|
||||
@@ -306,11 +344,17 @@ cleuHandler = function()
|
||||
local crush = crushing and 1 or 0
|
||||
local hit = (crit == 0 and glanc == 0 and crush == 0) and 1 or 0
|
||||
|
||||
local ta, tb
|
||||
if profiling then ta = debugprofilestop() end
|
||||
DB:DamageDone(srcName, AAttack, hit, crit, 0, 0, 0, 0, amount, glanc, 0)
|
||||
if profiling then tb = debugprofilestop(); profDB.DamageDone = profDB.DamageDone + (tb - ta); ta = tb end
|
||||
DB:DamageTaken(dstName, AAttack, hit, crit, 0, 0, 0, 0, amount, srcName, crush, 0)
|
||||
if profiling then tb = debugprofilestop(); profDB.DamageTaken = profDB.DamageTaken + (tb - ta); ta = tb end
|
||||
DB:EnemyDamage(1, DPSMateEDT, dstName, AAttack, hit, crit, 0, 0, 0, 0, amount, srcName, 0, crush)
|
||||
DB:EnemyDamage(2, DPSMateEDD, srcName, AAttack, hit, crit, 0, 0, 0, 0, amount, dstName, 0, 0)
|
||||
if profiling then tb = debugprofilestop(); profDB.EnemyDamage = profDB.EnemyDamage + (tb - ta); ta = tb end
|
||||
DB:DeathHistory(dstName, srcName, AAttack, amount, hit, crit, "hit", crush)
|
||||
if profiling then tb = debugprofilestop(); profDB.DeathHistory = profDB.DeathHistory + (tb - ta); profDB.count = profDB.count + 1 end
|
||||
if absorbed > 0 then
|
||||
DB:SetUnregisterVariables(absorbed, AAttack, srcName)
|
||||
DB:Absorb(AAttack, dstName, srcName)
|
||||
@@ -346,11 +390,17 @@ cleuHandler = function()
|
||||
local hit = (crit == 0 and glanc == 0 and crush == 0) and 1 or 0
|
||||
local abilityName = (sub == "SPELL_PERIODIC_DAMAGE") and (spellName .. "(Periodic)") or spellName
|
||||
|
||||
local ta, tb
|
||||
if profiling then ta = debugprofilestop() end
|
||||
DB:DamageDone(srcName, abilityName, hit, crit, 0, 0, 0, 0, amount, glanc, 0)
|
||||
if profiling then tb = debugprofilestop(); profDB.DamageDone = profDB.DamageDone + (tb - ta); ta = tb end
|
||||
DB:DamageTaken(dstName, abilityName, hit, crit, 0, 0, 0, 0, amount, srcName, crush, 0)
|
||||
if profiling then tb = debugprofilestop(); profDB.DamageTaken = profDB.DamageTaken + (tb - ta); ta = tb end
|
||||
DB:EnemyDamage(1, DPSMateEDT, dstName, abilityName, hit, crit, 0, 0, 0, 0, amount, srcName, 0, crush)
|
||||
DB:EnemyDamage(2, DPSMateEDD, srcName, abilityName, hit, crit, 0, 0, 0, 0, amount, dstName, 0, 0)
|
||||
if profiling then tb = debugprofilestop(); profDB.EnemyDamage = profDB.EnemyDamage + (tb - ta); ta = tb end
|
||||
DB:DeathHistory(dstName, srcName, abilityName, amount, hit, crit, "hit", crush)
|
||||
if profiling then tb = debugprofilestop(); profDB.DeathHistory = profDB.DeathHistory + (tb - ta); profDB.count = profDB.count + 1 end
|
||||
if spellSchool then DB:AddSpellSchool(abilityName, spellSchool) end
|
||||
if absorbed > 0 then
|
||||
DB:SetUnregisterVariables(absorbed, abilityName, srcName)
|
||||
@@ -396,10 +446,15 @@ cleuHandler = function()
|
||||
local effective = amount - overheal
|
||||
if effective < 0 then effective = 0 end
|
||||
|
||||
local ta, tb
|
||||
if profiling then ta = debugprofilestop() end
|
||||
DB:Healing(1, DPSMateHealingTaken, srcName, spellName, hit, crit, effective)
|
||||
DB:Healing(2, DPSMateOverhealing, srcName, spellName, hit, crit, overheal)
|
||||
if profiling then tb = debugprofilestop(); profDB.Healing = profDB.Healing + (tb - ta); ta = tb end
|
||||
DB:HealingTaken(1, DPSMateHealingTaken, srcName, spellName, hit, crit, effective, dstName)
|
||||
if profiling then tb = debugprofilestop(); profDB.HealingTaken = profDB.HealingTaken + (tb - ta); ta = tb end
|
||||
DB:DeathHistory(dstName, srcName, spellName, effective, hit, crit, "heal", 0)
|
||||
if profiling then tb = debugprofilestop(); profDB.DeathHistory = profDB.DeathHistory + (tb - ta); profDB.count = profDB.count + 1 end
|
||||
|
||||
-- ========================================================================
|
||||
-- AURA events (+ absorb shield lifecycle)
|
||||
@@ -481,6 +536,14 @@ cleuHandler = function()
|
||||
if not Parser.petToOwnerMap[dstName] then Parser.petToOwnerMap[dstName] = {} end
|
||||
Parser.petToOwnerMap[dstName][srcName] = true
|
||||
end
|
||||
|
||||
if profiling and t0 then
|
||||
local t2 = debugprofilestop()
|
||||
profDetail.getInfo = profDetail.getInfo + (t1 - t0)
|
||||
profDetail.dbCalls = profDetail.dbCalls + (t2 - t1)
|
||||
profDetail.total = profDetail.total + (t2 - t0)
|
||||
profDetail.count = profDetail.count + 1
|
||||
end
|
||||
end
|
||||
|
||||
-- Set the measured wrapper as the actual handler
|
||||
|
||||
@@ -875,6 +875,7 @@ function DPSMate.DB:OnGroupUpdate()
|
||||
local num = GetNumRaidMembers()
|
||||
DPSMate.Parser.TargetParty = {}
|
||||
DPSMate.Parser.petToOwnerMap = {}
|
||||
petQualifiedCache = {}
|
||||
if num<=0 then
|
||||
type = "party"
|
||||
num = GetNumPartyMembers()
|
||||
@@ -993,18 +994,36 @@ function DPSMate.DB:OnGroupUpdate()
|
||||
DPSMate.Parser:AssociateShaman("None", "None", true)
|
||||
end
|
||||
|
||||
-- Cache: raw pet name -> qualified "PetName (OwnerName)" to avoid repeated pairs() iteration
|
||||
local petQualifiedCache = {}
|
||||
|
||||
function DPSMate.DB:BuildUser(Dname, Dclass)
|
||||
if not Dname then Dname = "?!NIL Name?!" end
|
||||
-- Auto-qualify raw pet/totem names via petToOwnerMap (single-owner only)
|
||||
local pom = DPSMate.Parser.petToOwnerMap
|
||||
if pom and pom[Dname] then
|
||||
local count, singleOwner = 0, nil
|
||||
for o, _ in pairs(pom[Dname]) do
|
||||
count = count + 1
|
||||
singleOwner = o
|
||||
end
|
||||
if count == 1 then
|
||||
Dname = Dname .. " (" .. singleOwner .. ")"
|
||||
-- Fast path: already-known non-pet name — skip regex + petToOwnerMap entirely
|
||||
local u = DPSUser[Dname]
|
||||
if u then return u[1] end
|
||||
-- Check pet name cache (avoids repeated pairs() iteration on petToOwnerMap)
|
||||
local cached = petQualifiedCache[Dname]
|
||||
if cached then
|
||||
u = DPSUser[cached]
|
||||
if u then return u[1] end
|
||||
Dname = cached
|
||||
else
|
||||
-- Auto-qualify raw pet/totem names via petToOwnerMap (single-owner only)
|
||||
local pom = DPSMate.Parser.petToOwnerMap
|
||||
if pom and pom[Dname] then
|
||||
local count, singleOwner = 0, nil
|
||||
for o, _ in pairs(pom[Dname]) do
|
||||
count = count + 1
|
||||
singleOwner = o
|
||||
end
|
||||
if count == 1 then
|
||||
local qualified = Dname .. " (" .. singleOwner .. ")"
|
||||
petQualifiedCache[Dname] = qualified
|
||||
u = DPSUser[qualified]
|
||||
if u then return u[1] end
|
||||
Dname = qualified
|
||||
end
|
||||
end
|
||||
end
|
||||
local _,_, pet,owner = strfind(Dname,"(.+)%s%((.+)%)")
|
||||
@@ -1102,7 +1121,8 @@ function DPSMate.DB:Threat(cause, spellname, target, value, amount)
|
||||
target = self:BuildUser(target)
|
||||
cause = self:BuildUser(cause)
|
||||
spellname = self:BuildAbility(spellname)
|
||||
for cat, val in pairs(tablemodes) do
|
||||
for cat = 1, 2 do
|
||||
local val = tablemodes[cat]
|
||||
if not DPSThreat[cat] then DPSThreat[cat] = {} end
|
||||
if not DPSThreat[cat][cause] then
|
||||
DPSThreat[cat][cause] = {}
|
||||
@@ -1743,7 +1763,7 @@ function DPSMate.DB:RegisterAbsorb(owner, ability, abilityTarget)
|
||||
owner = self:BuildUser(owner)
|
||||
abilityTarget = self:BuildUser(abilityTarget)
|
||||
ability = self:BuildAbility(ability)
|
||||
for cat, val in pairs(tablemodes) do
|
||||
for cat = 1, 2 do local val = tablemodes[cat]
|
||||
if not DPSAbsorb[cat] then DPSAbsorb[cat] = {} end
|
||||
if not DPSAbsorb[cat][abilityTarget] then
|
||||
DPSAbsorb[cat][abilityTarget] = {}
|
||||
@@ -1777,7 +1797,7 @@ function DPSMate.DB:UnregisterAbsorb(ability, abilityTarget)
|
||||
ability = self:BuildAbility(ability)
|
||||
abilityTarget = self:BuildUser(abilityTarget)
|
||||
local AbsorbingAbility
|
||||
for cat, val in pairs(tablemodes) do
|
||||
for cat = 1, 2 do local val = tablemodes[cat]
|
||||
AbsorbingAbility = self:GetActiveAbsorbAbilityByPlayer(ability, abilityTarget, cat)
|
||||
if AbsorbingAbility[1] then
|
||||
path = DPSAbsorb[cat][abilityTarget][AbsorbingAbility[1]][AbsorbingAbility[2]][AbsorbingAbility[3]]["i"]
|
||||
@@ -1889,7 +1909,7 @@ function DPSMate.DB:Absorb(ability, abilityTarget, incTarget)
|
||||
abilityTarget = self:BuildUser(abilityTarget)
|
||||
ability = self:BuildAbility(ability)
|
||||
local AbsorbingAbility
|
||||
for cat, val in pairs(tablemodes) do
|
||||
for cat = 1, 2 do local val = tablemodes[cat]
|
||||
local abilityName = DPSMate:GetAbilityById(ability)
|
||||
local abilityEntry = abilityName and DPSMateAbility[abilityName]
|
||||
if not abilityEntry then break end
|
||||
@@ -2094,7 +2114,7 @@ function DPSMate.DB:Dispels(cause, Dname, target, ability)
|
||||
target = self:BuildUser(target)
|
||||
Dname = self:BuildAbility(Dname)
|
||||
ability = self:BuildAbility(ability)
|
||||
for cat, val in pairs(tablemodes) do
|
||||
for cat = 1, 2 do local val = tablemodes[cat]
|
||||
if not DPSDispel[cat] then DPSDispel[cat] = {} end
|
||||
if not DPSDispel[cat][cause] then
|
||||
DPSDispel[cat][cause] = {
|
||||
@@ -2130,13 +2150,13 @@ function DPSMate.DB:UnregisterDeath(target)
|
||||
if strfind(target, "%s") then return end
|
||||
target = self:BuildUser(target)
|
||||
local p
|
||||
for cat, val in pairs(tablemodes) do
|
||||
for cat = 1, 2 do
|
||||
if DPSDeath[cat] and DPSDeath[cat][target] and DPSDeath[cat][target][1] then
|
||||
DPSDeath[cat][target][1]["i"][1]=1
|
||||
DPSDeath[cat][target][1]["i"][2]=GameTime_GT()
|
||||
if cat==1 and DPSMate.Parser.TargetParty[DPSMate:GetUserById(target)] and DPSDeath[cat][target][1][1] then
|
||||
p = DPSDeath[cat][target][1][1]
|
||||
DPSMate:Broadcast(4, DPSMate:GetUserById(target), DPSMate:GetUserById(p[1]), DPSMate:GetAbilityById(p[2]), p[3])
|
||||
DPSMate:Broadcast(4, DPSMate:GetUserById(target), DPSMate:GetUserById(p[1]), DPSMate:GetAbilityById(p[2]), p[3])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -2148,7 +2168,8 @@ function DPSMate.DB:DeathHistory(target, cause, ability, amount, hit, crit, type
|
||||
cause = self:BuildUser(cause)
|
||||
ability = self:BuildAbility(ability)
|
||||
local hitCritCrush
|
||||
for cat, val in pairs(tablemodes) do
|
||||
for cat = 1, 2 do
|
||||
local val = tablemodes[cat]
|
||||
if not DPSDeath[cat] then DPSDeath[cat] = {} end
|
||||
if not DPSDeath[cat][target] then
|
||||
DPSDeath[cat][target] = {}
|
||||
@@ -2245,7 +2266,7 @@ function DPSMate.DB:Kick(cause, target, causeAbility, targetAbility)
|
||||
cause = self:BuildUser(cause)
|
||||
causeAbility = self:BuildAbility(causeAbility)
|
||||
targetAbility = self:BuildAbility(targetAbility)
|
||||
for cat, val in pairs(tablemodes) do
|
||||
for cat = 1, 2 do local val = tablemodes[cat]
|
||||
if not DPSInterrupt[cat] then DPSInterrupt[cat] = {} end
|
||||
if not DPSInterrupt[cat][cause] then
|
||||
DPSInterrupt[cat][cause] = {
|
||||
@@ -2310,7 +2331,7 @@ function DPSMate.DB:BuildBuffs(cause, target, ability, bool)
|
||||
target = self:BuildUser(target)
|
||||
cause = self:BuildUser(cause)
|
||||
ability = self:BuildAbility(ability)
|
||||
for cat, val in pairs(tablemodes) do
|
||||
for cat = 1, 2 do local val = tablemodes[cat]
|
||||
if not DPSAurasGained[cat] then DPSAurasGained[cat] = {} end
|
||||
if not DPSAurasGained[cat][target] then
|
||||
DPSAurasGained[cat][target] = {}
|
||||
@@ -2347,7 +2368,7 @@ function DPSMate.DB:DestroyBuffs(target, ability)
|
||||
target = self:BuildUser(target)
|
||||
ability = self:BuildAbility(ability)
|
||||
local TL
|
||||
for cat, val in pairs(tablemodes) do
|
||||
for cat = 1, 2 do local val = tablemodes[cat]
|
||||
if not DPSAurasGained[cat] then DPSAurasGained[cat] = {} end
|
||||
if not DPSAurasGained[cat][target] then
|
||||
DPSAurasGained[cat][target] = {}
|
||||
@@ -2557,7 +2578,7 @@ function DPSMate.DB:BuildFail(type, user, cause, ability, amount)
|
||||
cause = self:BuildUser(cause)
|
||||
ability = self:BuildAbility(ability)
|
||||
local time = GameTime_GT()
|
||||
for cat, val in pairs(tablemodes) do
|
||||
for cat = 1, 2 do local val = tablemodes[cat]
|
||||
if not DPSFail[cat] then DPSFail[cat] = {} end
|
||||
if not DPSFail[cat][cause] then
|
||||
DPSFail[cat][cause] = {}
|
||||
|
||||
@@ -836,3 +836,43 @@ Called from:
|
||||
### Module currently DISABLED by default
|
||||
|
||||
Build flag changed to `orelse false` in build.zig. Enable with `-Dframecrash=true`.
|
||||
|
||||
---
|
||||
|
||||
## Uninvestigated: NULL this in PrepareModelForRender (0x710450)
|
||||
|
||||
Crash observed 2026-03-25:
|
||||
```
|
||||
0x0071045A: MOV EAX, [ESI + 0x10] -- ESI=ECX=0 (NULL this pointer)
|
||||
ACCESS_VIOLATION reading 0x00000010
|
||||
```
|
||||
|
||||
Call chain:
|
||||
```
|
||||
CM2Scene_ExecuteRenderPass (0x708900)
|
||||
-> RenderBatches (0x70B371)
|
||||
-> DrawCallback (0x70D960)
|
||||
-> ValidateClientObjectAndProcessWithManagerLookup (0x6C7780)
|
||||
-> PrepareModelForRender (0x710450) -- CRASH
|
||||
```
|
||||
|
||||
Root cause: at 0x6C77A8, `MOV EAX, [EAX+0xD8]` reads a field that can be NULL. The
|
||||
parent object is NULL-checked but the +0xD8 field is not. Then `MOV ECX, EAX` passes
|
||||
NULL as this to PrepareModelForRender.
|
||||
|
||||
```asm
|
||||
0x6C77A4: TEST EAX, EAX ; parent object null check
|
||||
0x6C77A6: JZ skip ; OK
|
||||
0x6C77A8: MOV EAX, [EAX+0xD8] ; read child -- CAN BE NULL
|
||||
0x6C77AE: PUSH 0
|
||||
0x6C77B0: PUSH 0
|
||||
0x6C77B2: MOV ECX, EAX ; this = possibly NULL
|
||||
0x6C77B4: CALL PrepareModelForRender ; CRASH if ECX=0
|
||||
```
|
||||
|
||||
PrepareModelForRender: thiscall(ECX=this, stack: 2 params), RET 0x8.
|
||||
|
||||
Fix options:
|
||||
1. Hook PrepareModelForRender with NULL-this guard (protects all callers)
|
||||
2. Patch call site at 0x6C77AE to add TEST EAX,EAX + JZ (needs code cave, only 11 bytes available for 15 bytes of patched code)
|
||||
3. Binary patch PrepareModelForRender prologue (needs to preserve RET 0x8 stack cleanup)
|
||||
|
||||
@@ -3,5 +3,4 @@
|
||||
## Notes: Session-based combat and chat log management
|
||||
## Version: 1.0
|
||||
|
||||
|
||||
LogSessions.lua
|
||||
|
||||
@@ -550,14 +550,14 @@ fn worldPosToMinimapCoords(
|
||||
});
|
||||
}
|
||||
|
||||
fn getFrameUnkScale(info: u32) f32 {
|
||||
fn getFrameUnkScale(info: u32) ?f32 {
|
||||
const frame = hook.readMem(u32, info + ADDR.MI_FRAME);
|
||||
if (!isValidPtr(frame)) return 1.0;
|
||||
if (!isValidPtr(frame)) return null;
|
||||
const fsp = frame + ADDR.FRAME_SCRIPT_PART;
|
||||
const vtable = hook.readMem(u32, fsp);
|
||||
if (!isValidPtr(vtable)) return 1.0;
|
||||
if (!isValidPtr(vtable)) return null;
|
||||
const fn_addr = hook.readMem(u32, vtable + 7 * 4);
|
||||
if (!isValidPtr(fn_addr)) return 1.0;
|
||||
if (!isValidPtr(fn_addr)) return null;
|
||||
// __thiscall(fsp_ECX) → f32 on FPU ST(0)
|
||||
return hook.call(fn (u32) callconv(hook.cc.thiscall) f32, fn_addr, .{fsp});
|
||||
}
|
||||
@@ -837,6 +837,10 @@ fn trackObject(info: u32, guid_lo: u32, guid_hi: u32, blip: Blip) void {
|
||||
|
||||
// Cache minimap info per frame — same for all objects in one enumeration cycle
|
||||
if (!g_minimap_info.valid) {
|
||||
// If the minimap frame scale can't be resolved (transient state during
|
||||
// zone transitions), skip all blips this cycle rather than projecting
|
||||
// with a wrong scale that pushes blips outside the circle.
|
||||
const unk_scale = getFrameUnkScale(info) orelse return;
|
||||
g_minimap_info = .{
|
||||
.cur = .{
|
||||
.x = hook.readMem(f32, info + ADDR.MI_POS),
|
||||
@@ -845,7 +849,7 @@ fn trackObject(info: u32, guid_lo: u32, guid_hi: u32, blip: Blip) void {
|
||||
},
|
||||
.radius = hook.readMem(f32, info + ADDR.MI_RADIUS),
|
||||
.layout_scale = hook.readMem(f32, info + ADDR.MI_LAYOUT_SCALE),
|
||||
.unk_scale = getFrameUnkScale(info),
|
||||
.unk_scale = unk_scale,
|
||||
.valid = true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
//! SuperWeirdo -- GO loot sparkle for interactable objects
|
||||
//!
|
||||
//! Replicates the SuperWoW loot sparkle feature via two patches and three
|
||||
//! cleanup hooks:
|
||||
//!
|
||||
//! Patches:
|
||||
//! 1. UpdateUnitStatusFlags (0x60c520) status flags initializer:
|
||||
//! 0x60c560: imm32 0x00000000 -> 0x00002000
|
||||
//! Sets bit 13 in status flags, enabling sparkle visual on qualifying objects.
|
||||
//!
|
||||
//! 2. AttachSpellVisualToUnit (0x61fc00) object type mask:
|
||||
//! 0x61fc1c: MOV ECX,0x8 -> MOV ECX,0x1
|
||||
//! Changes type filter from Unit-only (0x8) to Object base (0x1),
|
||||
//! allowing visual attachments on GOs.
|
||||
//!
|
||||
//! 3. Data constant at 0x838f7c: 0x14 -> 0x0D
|
||||
//! Adjusts sparkle-related rendering parameter.
|
||||
//!
|
||||
//! Cleanup hooks (prevent leaked render nodes on GO lifecycle changes):
|
||||
//! Vanilla GOs never had visual attachments, so their destroy/update paths
|
||||
//! don't clean them up. We hook three GO lifecycle functions and walk the
|
||||
//! render node list (obj+0xB4) to unlink+release any sparkle nodes
|
||||
//! (identified by node+0x2C & 0x8).
|
||||
|
||||
const std = @import("std");
|
||||
const hook = @import("zhook");
|
||||
const logging = @import("../logging.zig");
|
||||
const mod_mutex = @import("../mutex.zig");
|
||||
|
||||
pub const module_name: [*:0]const u8 = "superweirdo";
|
||||
|
||||
// -- Sparkle cleanup logic ----------------------------------------------------
|
||||
|
||||
/// Walk obj+0xB4 render node linked list. Find the first node with flag 0x8
|
||||
/// set at node+0x2C (visual attachment marker), unlink and release it.
|
||||
fn cleanupSparkleNodes(obj: u32) void {
|
||||
var node: u32 = hook.readMem(u32, obj + 0xB4);
|
||||
while (node != 0) {
|
||||
const flags: u8 = hook.readMem(u8, node + 0x2C);
|
||||
if (flags & 0x8 != 0) {
|
||||
// UnlinkObjectFromList: __thiscall(ECX=node)
|
||||
hook.call(fn (u32) callconv(hook.cc.thiscall) void, 0x6203a0, .{node});
|
||||
// DecrementRefCountAndCleanup: __thiscall(ECX=node)
|
||||
hook.call(fn (u32) callconv(hook.cc.thiscall) void, 0x6210e0, .{node});
|
||||
return;
|
||||
}
|
||||
node = hook.readMem(u32, node + 0x8C);
|
||||
}
|
||||
}
|
||||
|
||||
// -- Detour hooks -------------------------------------------------------------
|
||||
|
||||
// Hook 1: UpdateGameObjectAnimationAndDB (0x5f7c40)
|
||||
// __thiscall(ECX=this), 3 stack params, RET 0xC
|
||||
const GoAnimFn = fn (u32, u32, u32, u32) callconv(hook.cc.thiscall) void;
|
||||
var go_anim_hook: hook.Detour(GoAnimFn) = .{};
|
||||
|
||||
fn goAnimDetour(this: u32, p1: u32, p2: u32, p3: u32) callconv(hook.cc.thiscall) void {
|
||||
go_anim_hook.callOriginal(.{ this, p1, p2, p3 });
|
||||
cleanupSparkleNodes(this);
|
||||
}
|
||||
|
||||
// Hook 2: GO update function (0x5f7ed0)
|
||||
// __thiscall(ECX=this), no stack params, RET
|
||||
const GoThiscallFn = fn (u32) callconv(hook.cc.thiscall) void;
|
||||
var go_update_hook: hook.Detour(GoThiscallFn) = .{};
|
||||
|
||||
fn goUpdateDetour(this: u32) callconv(hook.cc.thiscall) void {
|
||||
go_update_hook.callOriginal(.{this});
|
||||
cleanupSparkleNodes(this);
|
||||
}
|
||||
|
||||
// Hook 3: GameObjectCacheCallback (0x5f7e40)
|
||||
// __thiscall(ECX=this), no stack params, RET
|
||||
var go_cache_hook: hook.Detour(GoThiscallFn) = .{};
|
||||
|
||||
fn goCacheDetour(this: u32) callconv(hook.cc.thiscall) void {
|
||||
cleanupSparkleNodes(this);
|
||||
go_cache_hook.callOriginal(.{this});
|
||||
}
|
||||
|
||||
// -- Byte patches -------------------------------------------------------------
|
||||
|
||||
const Patch = struct {
|
||||
addr: usize,
|
||||
old: []const u8,
|
||||
new: []const u8,
|
||||
desc: []const u8,
|
||||
};
|
||||
|
||||
const byte_patches = [_]Patch{
|
||||
// UpdateUnitStatusFlags: init flags to 0x2000 (bit 13) instead of 0
|
||||
.{
|
||||
.addr = 0x0060c560,
|
||||
.old = &.{ 0x00, 0x00, 0x00, 0x00 },
|
||||
.new = &.{ 0x00, 0x20, 0x00, 0x00 },
|
||||
.desc = "status flags init 0x2000",
|
||||
},
|
||||
// AttachSpellVisualToUnit: type mask 0x8 (Unit) -> 0x1 (Object base)
|
||||
.{
|
||||
.addr = 0x0061fc1c,
|
||||
.old = &.{ 0xB9, 0x08, 0x00, 0x00 },
|
||||
.new = &.{ 0xB9, 0x01, 0x00, 0x00 },
|
||||
.desc = "visual attach type mask",
|
||||
},
|
||||
// Data value at 0x838f7c: 0x14 -> 0x0D
|
||||
.{
|
||||
.addr = 0x00838f7c,
|
||||
.old = &.{ 0x14, 0x00, 0x00, 0x00 },
|
||||
.new = &.{ 0x0D, 0x00, 0x00, 0x00 },
|
||||
.desc = "sparkle data constant",
|
||||
},
|
||||
};
|
||||
|
||||
// -- Module interface ---------------------------------------------------------
|
||||
|
||||
var g_mutex: ?*anyopaque = null;
|
||||
var g_is_hook_owner: bool = false;
|
||||
var g_patches_applied: [byte_patches.len]bool = .{false} ** byte_patches.len;
|
||||
var log: logging.Logger = .{};
|
||||
|
||||
pub fn isActive() bool {
|
||||
return g_is_hook_owner;
|
||||
}
|
||||
|
||||
pub fn installHooks() void {
|
||||
const result = mod_mutex.acquire(module_name);
|
||||
g_mutex = result.handle;
|
||||
g_is_hook_owner = result.is_owner;
|
||||
if (!g_is_hook_owner) return;
|
||||
|
||||
log = logging.Logger.open(module_name, .console);
|
||||
|
||||
// Byte patches
|
||||
inline for (byte_patches, 0..) |p, i| {
|
||||
const current = hook.readMem([p.old.len]u8, p.addr);
|
||||
if (std.mem.eql(u8, ¤t, p.old)) {
|
||||
hook.writeProtected(p.addr, p.new);
|
||||
g_patches_applied[i] = true;
|
||||
log.fmt("{s}: patched\n", .{p.desc});
|
||||
} else if (std.mem.eql(u8, ¤t, p.new)) {
|
||||
g_patches_applied[i] = true;
|
||||
log.fmt("{s}: already active\n", .{p.desc});
|
||||
} else {
|
||||
log.fmt("{s}: unexpected bytes at 0x{x}, skipping\n", .{ p.desc, p.addr });
|
||||
}
|
||||
}
|
||||
|
||||
// Detour hooks for GO sparkle cleanup
|
||||
if (go_anim_hook.attach(0x5f7c40, &goAnimDetour) != .ok) {
|
||||
log.print("WARN: failed to attach goAnimDetour\n");
|
||||
}
|
||||
if (go_update_hook.attach(0x5f7ed0, &goUpdateDetour) != .ok) {
|
||||
log.print("WARN: failed to attach goUpdateDetour\n");
|
||||
}
|
||||
if (go_cache_hook.attach(0x5f7e40, &goCacheDetour) != .ok) {
|
||||
log.print("WARN: failed to attach goCacheDetour\n");
|
||||
}
|
||||
log.print("GO cleanup hooks installed\n");
|
||||
}
|
||||
|
||||
pub fn removeHooks() void {
|
||||
if (!g_is_hook_owner) return;
|
||||
|
||||
go_cache_hook.detach();
|
||||
go_update_hook.detach();
|
||||
go_anim_hook.detach();
|
||||
|
||||
inline for (byte_patches, 0..) |p, i| {
|
||||
if (g_patches_applied[i]) {
|
||||
hook.writeProtected(p.addr, p.old);
|
||||
g_patches_applied[i] = false;
|
||||
log.fmt("{s}: restored\n", .{p.desc});
|
||||
}
|
||||
}
|
||||
|
||||
log.close();
|
||||
mod_mutex.release(&g_mutex);
|
||||
g_is_hook_owner = false;
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
# WoW 1.12.1 CPU Profiling Analysis
|
||||
|
||||
Source: `perf.data.perfparser` (July 2025 recording via hotspot)
|
||||
Exported: `cycles.out` (stack-collapsed format)
|
||||
|
||||
## Full CPU Time Breakdown
|
||||
|
||||
| % | Category | Notes |
|
||||
|---|---|---|
|
||||
| 33.18% | **Hooked WoW functions** | 38 hooks in transform44 module |
|
||||
| 19.19% | GPU/Driver | d3d9.dll (DXVK) + amdvlk32.so -- untouchable |
|
||||
| 18.74% | WoW long tail | ~2000+ functions each <0.15% -- not worth hooking |
|
||||
| 13.15% | WoW mid-tier | 50 functions at 0.15-0.39% -- hookable but diminishing returns |
|
||||
| 8.43% | Lua VM | lua_vm_execute, luaS_newlstr, etc. -- interpreter overhead |
|
||||
| 5.38% | Unresolved | WoW.exe code not in Ghidra symbol map |
|
||||
| 1.93% | Wine/System | ntdll, kernel32, wine internals |
|
||||
|
||||
## Hooked Functions (38 total, by self-time %)
|
||||
|
||||
### Already existed (6 hooks)
|
||||
| % | Address | Name | Convention |
|
||||
|---|---|---|---|
|
||||
| 2.80% | 0x714260 | transformMatrix4x4 | thiscall RET 0x10 |
|
||||
| 2.06% | 0x713d50 | findInterpolationIndices | thiscall RET 0x10 |
|
||||
| 0.73% | 0x707680 | renderFrame | thiscall RET 0x4 |
|
||||
| 0.44% | 0x713ea0 | interpolateAnimationKeyframes | fastcall RET 0x8 |
|
||||
| - | 0x708900 | executeSceneRenderPass | thiscall RET 0x4 |
|
||||
| - | 0x76FB00 | RenderTextureQuads | fastcall RET |
|
||||
| - | 0x616620 | CMovement::Process | thiscall RET 0x8 |
|
||||
|
||||
### New perf-identified hotspots (28 hooks)
|
||||
| % | Address | Name | Convention |
|
||||
|---|---|---|---|
|
||||
| 3.95% | 0x6318c0 | ClipPolygonToSinglePlane | stdcall RET 0x4 |
|
||||
| 3.65% | 0x5ca2d0 | GetOrCreateCharacterGlyph | stdcall RET 0x8 |
|
||||
| 1.73% | 0x7b2a50 | RenderParticleSprites | thiscall RET 0x8 |
|
||||
| 1.57% | 0x6abc40 | processLinkedListCollision | thiscall RET 0x8 |
|
||||
| 1.18% | 0x6afad0 | UpdateEntityAndChunksPositions | thiscall RET |
|
||||
| 1.15% | 0x765650 | renderAllFrameLayers | thiscall RET 0x4 |
|
||||
| 1.07% | 0x5ccbe0 | RenderTextToVertexBuffer | thiscall RET 0x18 |
|
||||
| 1.02% | 0x58a3d0 | RenderComplexGeometry | stdcall RET 0x24 |
|
||||
| 0.93% | 0x6c1f70 | updateEntitiesInBounds | thiscall RET 0x4 |
|
||||
| 0.88% | 0x5cdf40 | updateTextFrameCounter | thiscall RET |
|
||||
| 0.66% | 0x6816f0 | AddToSpatialGrid | thiscall RET |
|
||||
| 0.65% | 0x7c29f0 | ray_tri_intersect_idx_ushort | stdcall RET 0x10 |
|
||||
| 0.64% | 0x710b90 | ManageLinkedListNode | thiscall RET 0x4 |
|
||||
| 0.63% | 0x7b9b10 | calculateColorValues | thiscall RET 0x18 |
|
||||
| 0.61% | 0x686640 | SetVector3 | thiscall RET |
|
||||
| 0.59% | 0x6b8c60 | PerformSpatialCulling | thiscall RET 0x8 |
|
||||
| 0.59% | 0x6b88e0 | performCollisionDetection | thiscall RET 0x8 |
|
||||
| 0.55% | 0x7b5a10 | ProcessActiveParticles | stdcall RET 0x8 |
|
||||
| 0.53% | 0x404130 | CallbackIterator | stdcall RET 0x10 |
|
||||
| 0.50% | 0x464890 | FindObjectByGUID | stdcall RET 0x8 |
|
||||
| 0.49% | 0x632700 | RayTriangleIntersection | thiscall RET 0x20 |
|
||||
| 0.48% | 0x70cb30 | DrawBatchProj | thiscall RET |
|
||||
| 0.47% | 0x702000 | FindLuaFunction | stdcall RET 0x4 |
|
||||
| 0.44% | 0x5a0f50 | RenderSpriteQuads | thiscall RET 0xc |
|
||||
| 0.44% | 0x718960 | renderSceneNode | thiscall RET |
|
||||
| 0.44% | 0x6cffc0 | generateTerrainChunk | thiscall RET |
|
||||
| 0.43% | 0x593840 | D3D_SetTexture | thiscall RET 0x8 |
|
||||
| 0.42% | 0x6b8b70 | checkBoundingBoxIntersection | stdcall RET 0x8 |
|
||||
|
||||
### Unresolved-callee hooks (4 hooks)
|
||||
| % | Address | Name | Convention |
|
||||
|---|---|---|---|
|
||||
| ~0.5% | 0x7bdd60 | rotateMatrixByAxisAngle | thiscall RET 0xc |
|
||||
| ~0.1% | 0x632460 | BuildTrianglePlanes | thiscall RET 0xc |
|
||||
| ~0.2% | 0x7b3d20 | SetupParticleRendering | thiscall RET 0x4 |
|
||||
| ~0.2% | 0x5ce0c0 | renderTextLine | thiscall RET 0x10 |
|
||||
|
||||
## Unhooked Mid-Tier (50 functions, 13.15% total)
|
||||
|
||||
Not worth individual hooks -- too small or too high-frequency (hook overhead would distort):
|
||||
|
||||
| % | Name | Why not hook |
|
||||
|---|---|---|
|
||||
| 0.39% | compareRenderItemsExtended | Sort comparator, millions of calls |
|
||||
| 0.39% | GetCachedData | Cache accessor, extremely hot path |
|
||||
| 0.39% | raycastPickObjects | Moderate frequency |
|
||||
| 0.35% | inflateDecodeLiteralsAndLengths | Decompression, bursty |
|
||||
| 0.34% | UpdateParticlePhysics | Per-particle, very hot |
|
||||
| 0.34% | updateAnimationSystem | Could be interesting entry point |
|
||||
| 0.32% | ClntObjMgrObjectPtr | Object lookup, called everywhere |
|
||||
| 0.29% | multiplyMatrix4x4 | Tiny function, massive call count |
|
||||
| 0.26% | quickSortArray | Sort impl, millions of comparisons |
|
||||
| ... | (40 more at 0.15-0.31%) | |
|
||||
|
||||
## Key Insights
|
||||
|
||||
1. **GPU/Driver is 19%** -- nothing we can do about d3d9.dll/amdvlk overhead
|
||||
2. **Lua VM is 8.4%** -- addon code execution, not optimizable from DLL side
|
||||
3. **Frustum clipping (ClipPolygonToSinglePlane) is the #1 WoW hotspot at 3.95%** -- pure math, SSE candidate via binary patch (no hook overhead)
|
||||
4. **Font rendering (GetOrCreateCharacterGlyph) is #2 at 3.65%** -- potential cache optimization
|
||||
5. **Bone pipeline (t44 + findInterp + interpKf) totals ~5.3%** -- SSE interp hook overhead negates savings; binary patch or full t44 rewrite needed
|
||||
6. **Hook overhead matters** -- for functions called >10k/frame (lerp, matrix multiply, sort comparators), detour trampoline cost (~30 cycles) exceeds any savings
|
||||
|
||||
## Files
|
||||
|
||||
- `cycles.out` -- raw stack-collapsed perf data
|
||||
- `perf.data.perfparser` -- hotspot binary cache (5.3GB)
|
||||
- Ghidra symbols: `/media/faststore/tmp/Dis/symbols.nm`
|
||||
@@ -1007,7 +1007,7 @@ fn calcScaledInverse(this_mat: u32, out: u32, scale: f32) void {
|
||||
// mat3(offset_vec3*), mat4(scale_float_bits)
|
||||
// =============================================================================
|
||||
|
||||
export fn transformImpl_BASELINE(this: u32, mat1: u32, mat2: u32, mat3: u32, mat4: u32) callconv(.c) void {
|
||||
export fn transformImpl_BASELINE(this: u32, mat1: u32, mat2: u32, mat3: u32, mat4: u32) callconv(.{ .x86_thiscall = .{} }) void {
|
||||
|
||||
@setEvalBranchQuota(50000);
|
||||
// =========================================================================
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
// RTQ (RenderTextureQuads) full rendering pipeline
|
||||
// All functions in the per-item draw call chain
|
||||
//
|
||||
// Call chain per item:
|
||||
// RenderTextureQuads (0x76FB00) -- the outer loop
|
||||
// -> InitializeRenderingPipeline (0x58A2A0) -- stores vertCount, calls RenderComplexGeometry
|
||||
// -> RenderComplexGeometry (0x58A3D0) -- format detect, VB create/fill, DrawPrimitive
|
||||
// -> CreateVertexBuffer (0x58A140 -> 0x594500) -- pool VB allocation
|
||||
// -> LockVertexBuffer (0x58A080) -- vtable call to CGxDevice+0xA8
|
||||
// -> [per-vertex interleave loop]
|
||||
// -> UnlockVertexBuffer (0x58A0A0) -- vtable call to CGxDevice+0xAC
|
||||
// -> DrawPrimitive (0x58A7C0) -- state update + SetRenderingCommand
|
||||
// -> RenderVertexBuffer (0x58A2E0) -- creates INDEX VB, issues DrawIndexedPrimitive
|
||||
// -> CreateAndBindVertexBuffer (0x58A750) -- index VB (bufferType=1)
|
||||
// -> CallGfxDeviceMethod_Wrapper (0x58A830) -- actual D3D9 DrawIndexedPrimitive
|
||||
// -> EmptyRenderFunction (0x58A340) -- RET (no-op)
|
||||
|
||||
// === InitializeRenderingPipeline (0x58A2A0) -- 54 bytes ===
|
||||
// __fastcall(ECX=vertCount, EDX=xyzPtr, 11 stack params)
|
||||
// Stores vertCount to global g_currentPrimitiveType and tail-calls RenderComplexGeometry.
|
||||
void __fastcall InitializeRenderingPipeline(int vertCount, void** xyzPtr, /* ...11 stack params */)
|
||||
{
|
||||
g_currentPrimitiveType = (void*)vertCount; // stored at some global
|
||||
RenderComplexGeometry(vertCount, xyzPtr, /* forward all params */);
|
||||
}
|
||||
|
||||
// === CreateVertexBuffer wrapper (0x58A140) -- 24 bytes ===
|
||||
// __fastcall(ECX=bufferType, EDX=vertexSize, stack: vertexCount)
|
||||
// Thin wrapper: loads CGxDevice from global, calls D3D_CreateVertexBuffer.
|
||||
void __fastcall CreateVertexBuffer(int bufferType, int vertexSize, int vertexCount)
|
||||
{
|
||||
D3D_CreateVertexBuffer(CGxDeviceD3d__device, bufferType, vertexSize, vertexCount);
|
||||
}
|
||||
|
||||
// === D3D_CreateVertexBuffer (0x594500) -- 75 bytes ===
|
||||
// __thiscall(ECX=CGxDevice, stack: bufferType, vertexSize, vertexCount)
|
||||
// Returns a pool handle. The pool is indexed by bufferType at CGxDevice+0x26CC.
|
||||
// Reuses existing D3D9 VB unless the requested size exceeds current allocation.
|
||||
int __thiscall D3D_CreateVertexBuffer(void* this, int bufferType, int vertexSize, int vertexCount)
|
||||
{
|
||||
int poolHandle = *(int*)((int)this + bufferType * 4 + 0x26CC);
|
||||
int d3dVB = *(int*)(poolHandle + 8);
|
||||
if (d3dVB != 0 && *(uint*)(d3dVB + 0x10) < (uint)(vertexSize * vertexCount)) {
|
||||
// Existing VB too small -- resize via vtable call
|
||||
(*(code**)(*this + 0xA0))(d3dVB, vertexSize * vertexCount);
|
||||
}
|
||||
SetDimensionsAndSize(poolHandle, vertexSize, vertexCount);
|
||||
return poolHandle;
|
||||
}
|
||||
|
||||
// === SetDimensionsAndSize (0x5946F0) -- 32 bytes ===
|
||||
void SetDimensionsAndSize(int handle, int vertexSize, int vertexCount)
|
||||
{
|
||||
*(int*)(handle + 0x0C) = vertexSize;
|
||||
*(int*)(handle + 0x10) = vertexCount;
|
||||
*(int*)(handle + 0x14) = vertexSize * vertexCount;
|
||||
*(char*)(handle + 0x1C) = 0; // clear dirty flag
|
||||
}
|
||||
|
||||
// === LockVertexBuffer (0x58A080) -- 18 bytes ===
|
||||
// __fastcall(ECX=poolHandle)
|
||||
// Calls CGxDevice vtable[0xA8/4 = 42] to lock the D3D9 VB.
|
||||
// Returns pointer to locked VB memory.
|
||||
void* __fastcall LockVertexBuffer(void* poolHandle)
|
||||
{
|
||||
return (*(code**)(*(int*)CGxDeviceD3d__device + 0xA8))(poolHandle);
|
||||
}
|
||||
|
||||
// === UnlockVertexBuffer (0x58A0A0) -- 27 bytes ===
|
||||
// __fastcall(ECX=poolHandle, EDX=byteCount)
|
||||
// Calls CGxDevice vtable[0xAC/4 = 43] to unlock, then marks pool handle active.
|
||||
void __fastcall UnlockVertexBuffer(int poolHandle, int byteCount)
|
||||
{
|
||||
(*(code**)(*(int*)CGxDeviceD3d__device + 0xAC))(poolHandle, byteCount);
|
||||
SetObjectActiveFlag(poolHandle);
|
||||
}
|
||||
|
||||
// === UpdateBufferData (0x58A0C0) -- 57 bytes ===
|
||||
// __fastcall(ECX=poolHandle, EDX=srcData, stack: byteCount, unused)
|
||||
// Used by CreateAndBindVertexBuffer for INDEX buffer filling.
|
||||
// If byteCount==0, auto-computes from handle's vertexSize*vertexCount.
|
||||
void __fastcall UpdateBufferData(int poolHandle, void* srcData, int byteCount, void* unused)
|
||||
{
|
||||
if (byteCount == 0) {
|
||||
byteCount = *(int*)(poolHandle + 0x10) * *(int*)(poolHandle + 0x0C);
|
||||
}
|
||||
(*(code**)(*(int*)CGxDeviceD3d__device + 0xB0))(poolHandle, srcData, byteCount, unused);
|
||||
SetObjectActiveFlag(poolHandle);
|
||||
}
|
||||
|
||||
// === DrawPrimitive / GxDevice dispatch (0x58A7C0) -- 54 bytes ===
|
||||
// __fastcall(ECX=poolHandle, EDX=formatCode)
|
||||
// Reads per-format state from table at 0x809C00 (16 bytes per entry):
|
||||
// +0x00: ptr to state array (GxDevice render state descriptors)
|
||||
// +0x04: state array element count
|
||||
// +0x08: primitive type mapping
|
||||
// +0x0C: dirty flags bitmask
|
||||
// Applies state, marks dirty, stores draw command in CGxDevice for D3D9 submission.
|
||||
void __fastcall DrawPrimitive(int poolHandle, int formatCode)
|
||||
{
|
||||
int* stateTable = (int*)(0x809C00 + formatCode * 0x10);
|
||||
UpdateGfxStateArray(poolHandle, stateTable[0], stateTable[1]);
|
||||
MarkStateDirty(stateTable[3]);
|
||||
SetRenderingCommand(CGxDeviceD3d__device, poolHandle, formatCode);
|
||||
}
|
||||
|
||||
// === SetRenderingCommand (0x592AA0) -- 40 bytes ===
|
||||
// __thiscall(ECX=CGxDevice, stack: poolHandle, formatCode)
|
||||
// Stores the draw command into CGxDevice for later D3D9 submission.
|
||||
void __thiscall SetRenderingCommand(void* this, void* poolHandle, int formatCode)
|
||||
{
|
||||
*(int*)((int)this + 0x27E0) = formatCode;
|
||||
*(void**)((int)this + 0x27E4) = poolHandle;
|
||||
*(int*)((int)this + 0x27E8) = *(int*)(0x809C08 + formatCode * 0x10);
|
||||
}
|
||||
|
||||
// === RenderVertexBuffer (0x58A2E0) -- 82 bytes ===
|
||||
// __fastcall(ECX=primType, EDX=vertCount, stack: indexPtr)
|
||||
// Creates index VB (bufferType=1), binds it, issues DrawIndexedPrimitive.
|
||||
void __fastcall RenderVertexBuffer(int primType, int vertCount, void* indexPtr)
|
||||
{
|
||||
if (g_currentPrimitiveType != NULL) {
|
||||
CreateAndBindVertexBuffer(vertCount, indexPtr); // index VB
|
||||
short adjustedPrimType = (short)g_currentPrimitiveType - 1;
|
||||
// Build draw call struct on stack
|
||||
struct { void* primPtr; void* unused; short vertCount; short flags; } call;
|
||||
call.primPtr = primType;
|
||||
call.unused = NULL;
|
||||
call.vertCount = vertCount;
|
||||
call.flags = 0;
|
||||
CallGfxDeviceMethod_Wrapper(&call, 1); // -> D3D9 DrawIndexedPrimitive
|
||||
}
|
||||
}
|
||||
|
||||
// === CreateAndBindVertexBuffer (0x58A750) -- 44 bytes ===
|
||||
// __fastcall(ECX=vertCount, EDX=dataPtr)
|
||||
// Used for INDEX buffer (bufferType=1, vertexSize=2 = sizeof(u16)).
|
||||
void __fastcall CreateAndBindVertexBuffer(int vertCount, void* dataPtr)
|
||||
{
|
||||
int handle = CreateVertexBuffer(1, 2, vertCount); // pool slot 1, 2 bytes/index
|
||||
UpdateBufferData(handle, dataPtr, 0, NULL);
|
||||
SetStreamSource(handle); // bind as index stream
|
||||
}
|
||||
|
||||
// === Vertex format tables (from game memory) ===
|
||||
//
|
||||
// Stride table at 0x85A7A8 (indexed by format code):
|
||||
// fmt 0: stride=12 xyz only
|
||||
// fmt 1: stride=24 xyz + defaultTC
|
||||
// fmt 2: stride=28 xyz + defaultTC + additional(4)
|
||||
// fmt 3: stride=32 xyz + defaultTC + texcoord1(8)
|
||||
// fmt 4: stride=36 xyz + defaultTC + additional(4) + texcoord1(8)
|
||||
// fmt 5: stride=40 xyz + defaultTC + texcoord1(8) + texcoord2(8)
|
||||
// fmt 6: stride=44 xyz + defaultTC + additional(4) + texcoord1(8) + texcoord2(8)
|
||||
// fmt 7: stride=16 xyz + additional(4)
|
||||
// fmt 8: stride=24 xyz + additional(4) + texcoord1(8)
|
||||
// fmt 9: stride=32 xyz + additional(4) + texcoord1(8) + texcoord2(8)
|
||||
// fmt 10: stride=20 xyz + texcoord1(8)
|
||||
// fmt 11: stride=28 xyz + texcoord1(8) + texcoord2(8)
|
||||
//
|
||||
// Element offset table at 0x8097A8 (indexed by format*13 + element):
|
||||
// Element 0 = xyz position (always at offset 0)
|
||||
// Element 3 = defaultTexCoord
|
||||
// Element 4 = additionalData (color)
|
||||
// Element 5 = texcoord1
|
||||
// Element 6 = texcoord2
|
||||
// Value -1 = element not present in this format
|
||||
@@ -0,0 +1,166 @@
|
||||
// RenderComplexGeometry (0x58A3D0) -- 886 bytes
|
||||
// __fastcall(ECX=vertCount, EDX=xyzPtr, 11 stack params)
|
||||
//
|
||||
// Called from InitializeRenderingPipeline (0x58A2A0) which just stores
|
||||
// vertCount to global [0xC0ED2C] then tail-calls this function.
|
||||
//
|
||||
// Purpose: Determine vertex format from which input pointers are non-null,
|
||||
// create/reuse a pool VB, lock it, interleave all input arrays into the VB
|
||||
// at the computed stride, unlock, then issue a DrawPrimitive.
|
||||
//
|
||||
// Parameters (after fastcall mapping):
|
||||
// param_1 = vertCount (ECX)
|
||||
// param_2 = xyzPtr (EDX) -- 3 floats per vert, stride in param_3
|
||||
// param_3 = xyzStride (stack) -- typically 0x0C (12 bytes)
|
||||
// param_4 = defaultTexCoordPtr (stack) -- game constant at 0xCF4CF4, 3 floats/vert
|
||||
// param_5 = defaultTexCoordStride (stack) -- 0 means use param_4 as single value
|
||||
// param_6 = additionalDataPtr (stack) -- per-vert color (DWORD), or NULL
|
||||
// param_7 = additionalDataStride (stack)
|
||||
// param_8 = texCoord1Ptr (stack) -- unused in RTQ path
|
||||
// param_9 = texCoord1Stride (stack)
|
||||
// param_10 = texCoord2Ptr (stack) -- UV coords, 2 floats/vert
|
||||
// param_11 = texCoord2Stride (stack) -- typically 8
|
||||
//
|
||||
// Vertex format table:
|
||||
// Format codes 0-11, determined by which of param_4/6/8/10 are non-null.
|
||||
// Stride table at 0x85A7A8: [12,24,28,32,36,40,44,16,24,32,20,28]
|
||||
// Element offset table at 0x8097A8: indexed by (format*13 + element)*4
|
||||
// Elements: 0=xyz, 3=defaultTC, 4=additional, 5=texcoord1, 6=texcoord2
|
||||
//
|
||||
// For RTQ with additionalData (format 4, stride 36):
|
||||
// [0-11] xyz (12 bytes, 3 floats)
|
||||
// [12-23] defaultTC (12 bytes, 3 floats from 0xCF4CF4)
|
||||
// [24-27] additional (4 bytes, DWORD color)
|
||||
// [28-35] texcoord2 (8 bytes, 2 floats UV)
|
||||
//
|
||||
// For RTQ without additionalData (format 1, stride 24):
|
||||
// [0-11] xyz (12 bytes)
|
||||
// [12-23] defaultTC (12 bytes)
|
||||
//
|
||||
// Per-vertex loop:
|
||||
// 1. Copy xyz (3 dwords) at element offset 0
|
||||
// 2. Copy defaultTC (3 dwords) at element offset 3 (12 bytes)
|
||||
// 3. Check lighting flag at CGxDevice+0x258:
|
||||
// - If flag == 1: byte-swap color (BGRA -> RGBA or similar)
|
||||
// - Else: copy color as-is
|
||||
// 4. Copy additional data (1 dword) at element offset 4
|
||||
// 5. Copy texcoord1 (2 dwords) at element offset 5
|
||||
// 6. Copy texcoord2 (2 dwords) at element offset 6
|
||||
// Each pointer advances by its respective stride per vertex.
|
||||
// VB write pointer advances by the interleaved stride per vertex.
|
||||
//
|
||||
// After loop: UnlockVertexBuffer, then DrawPrimitive(poolHandle, formatCode)
|
||||
//
|
||||
// DrawPrimitive (0x58A7C0):
|
||||
// Reads from a per-format-code table at 0x809C00 (stride 0x10):
|
||||
// +0x00: state array ptr
|
||||
// +0x04: state array count
|
||||
// +0x08: D3D primitive type table index
|
||||
// +0x0C: dirty flags mask
|
||||
// Calls UpdateGfxStateArray to apply GxDevice state changes,
|
||||
// then MarkStateDirty, then SetRenderingCommand which stores
|
||||
// the draw command into CGxDevice+0x27E0..0x27E8 for later
|
||||
// submission to D3D9.
|
||||
//
|
||||
// Pool VB system (D3D_CreateVertexBuffer at 0x594500):
|
||||
// CGxDevice has pool slots at +0x26CC indexed by bufferType.
|
||||
// RenderComplexGeometry uses bufferType=0.
|
||||
// Pool handle struct:
|
||||
// +0x08: D3D9 IDirect3DVertexBuffer9*
|
||||
// +0x0C: vertexSize (bytes per vertex for this format)
|
||||
// +0x10: vertexCount
|
||||
// +0x14: total bytes (vertexSize * vertexCount)
|
||||
// +0x1C: dirty flag (cleared by SetDimensionsAndSize)
|
||||
// The pool VB is reused across calls. Only recreated if the
|
||||
// requested size (vertexSize * vertexCount) exceeds the current
|
||||
// allocation at +0x14. This means no D3D9 CreateVertexBuffer
|
||||
// overhead on normal frames.
|
||||
//
|
||||
// RenderVertexBuffer (0x58A2E0):
|
||||
// Called AFTER RenderComplexGeometry returns. Uses a DIFFERENT pool
|
||||
// (bufferType=1) for index data. Creates/binds an index VB with
|
||||
// the quad indices {0,1,2,0,2,3}, then calls DrawIndexedPrimitive
|
||||
// via CallGfxDeviceMethod_Wrapper.
|
||||
|
||||
void __fastcall
|
||||
RenderComplexGeometry(
|
||||
int vertCount, // ECX
|
||||
void** xyzPtr, // EDX
|
||||
int xyzStride, // [ebp+0x08]
|
||||
void** defaultTCPtr, // [ebp+0x0C]
|
||||
int defaultTCStride, // [ebp+0x10]
|
||||
void** additionalPtr, // [ebp+0x14]
|
||||
int additionalStride, // [ebp+0x18]
|
||||
void** texCoord1Ptr, // [ebp+0x1C]
|
||||
int texCoord1Stride, // [ebp+0x20]
|
||||
void** texCoord2Ptr, // [ebp+0x24]
|
||||
int texCoord2Stride) // [ebp+0x28]
|
||||
{
|
||||
// Step 1: Format detection -- determine which of the 12 interleaved
|
||||
// vertex formats to use based on which input pointers are non-null.
|
||||
int formatCode = 1; // default: xyz + defaultTC
|
||||
// Complex nested-if tree mapping (defaultTC, additional, tc1, tc2)
|
||||
// presence to format codes 0-11. See format table above.
|
||||
|
||||
// Step 2: Get stride and allocate pool VB
|
||||
int stride = GetDataPointerByIndex(formatCode); // stride table lookup
|
||||
void* poolHandle = CreateVertexBuffer(0, stride, vertCount);
|
||||
char* vbData = LockVertexBuffer(poolHandle);
|
||||
|
||||
// Step 3: Compute write pointers for each element within the VB
|
||||
char* xyzDst = vbData + GetMatrixElementPointer(formatCode, 0);
|
||||
char* tcDst = (defaultTCPtr) ? vbData + GetMatrixElementPointer(formatCode, 3) : &dummy;
|
||||
char* addDst = (additionalPtr) ? vbData + GetMatrixElementPointer(formatCode, 4) : &dummy;
|
||||
char* tc1Dst = (texCoord1Ptr) ? vbData + GetMatrixElementPointer(formatCode, 5) : &dummy;
|
||||
char* tc2Dst = (texCoord2Ptr) ? vbData + GetMatrixElementPointer(formatCode, 6) : &dummy;
|
||||
|
||||
// Per-element advance: stride if present, 0 if writing to dummy
|
||||
int tcAdv = defaultTCPtr ? stride : 0;
|
||||
int addAdv = additionalPtr ? stride : 0;
|
||||
int tc1Adv = texCoord1Ptr ? stride : 0;
|
||||
int tc2Adv = texCoord2Ptr ? stride : 0;
|
||||
|
||||
// Step 4: Per-vertex interleave loop
|
||||
for (int v = 0; v < vertCount; v++) {
|
||||
// XYZ: always 12 bytes (3 floats)
|
||||
*(int*)(xyzDst + 0) = *(int*)(xyzPtr + 0);
|
||||
*(int*)(xyzDst + 4) = *(int*)(xyzPtr + 4);
|
||||
*(int*)(xyzDst + 8) = *(int*)(xyzPtr + 8);
|
||||
xyzDst += stride;
|
||||
xyzPtr += xyzStride;
|
||||
|
||||
// DefaultTC: 12 bytes (3 floats)
|
||||
*(int*)(tcDst + 0) = *(int*)(defaultTCPtr + 0);
|
||||
*(int*)(tcDst + 4) = *(int*)(defaultTCPtr + 4);
|
||||
*(int*)(tcDst + 8) = *(int*)(defaultTCPtr + 8);
|
||||
tcDst += tcAdv;
|
||||
defaultTCPtr += defaultTCStride;
|
||||
|
||||
// Additional (color): conditional byte-swap based on lighting flag
|
||||
int lightingInfo = UpdateLightingOffset(); // returns CGxDevice + 0x23C
|
||||
if (*(int*)(lightingInfo + 0x1C) == 1) {
|
||||
// Byte swap: BGRA -> RGBA (swap bytes 0 and 2)
|
||||
*(int*)(addDst) = CONCAT(byte3, byte0, byte1, byte2);
|
||||
} else {
|
||||
*(int*)(addDst) = *(int*)(additionalPtr);
|
||||
}
|
||||
addDst += addAdv;
|
||||
additionalPtr += additionalStride;
|
||||
|
||||
// TexCoord1: 8 bytes (2 floats)
|
||||
*(int*)(tc1Dst + 0) = *(int*)(texCoord1Ptr + 0);
|
||||
*(int*)(tc1Dst + 4) = *(int*)(texCoord1Ptr + 4);
|
||||
tc1Dst += tc1Adv;
|
||||
texCoord1Ptr += texCoord1Stride;
|
||||
|
||||
// TexCoord2: 8 bytes (2 floats)
|
||||
*(int*)(tc2Dst + 0) = *(int*)(texCoord2Ptr + 0);
|
||||
*(int*)(tc2Dst + 4) = *(int*)(texCoord2Ptr + 4);
|
||||
tc2Dst += tc2Adv;
|
||||
texCoord2Ptr += texCoord2Stride;
|
||||
}
|
||||
|
||||
// Step 5: Finalize
|
||||
UnlockVertexBuffer(poolHandle, stride * vertCount);
|
||||
DrawPrimitive(poolHandle, formatCode);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,255 @@
|
||||
// =============================================================================
|
||||
// processLinkedListCollision @ 0x006abc40
|
||||
// Function range: 0x006abc40 -- 0x006abd88 (329 bytes)
|
||||
// Generated by Ghidra headless analysis
|
||||
// =============================================================================
|
||||
|
||||
// =============================================================================
|
||||
// SECTION 1: DECOMPILATION
|
||||
// =============================================================================
|
||||
|
||||
|
||||
undefined * __fastcall
|
||||
processLinkedListCollision(int *param_1,float *param_2,uint *param_3,uint param_4)
|
||||
|
||||
{
|
||||
int iVar1;
|
||||
uint uVar2;
|
||||
int iVar3;
|
||||
undefined *puVar4;
|
||||
undefined4 *puVar5;
|
||||
undefined **ppuVar6;
|
||||
undefined *local_28;
|
||||
undefined *local_24;
|
||||
undefined *local_20;
|
||||
undefined *local_1c;
|
||||
undefined *local_18;
|
||||
undefined *local_14;
|
||||
undefined *local_10;
|
||||
undefined *local_c;
|
||||
undefined *local_8;
|
||||
|
||||
if ((param_4 & 0xf0000f) != 0) {
|
||||
local_8 = (undefined *)param_1[2];
|
||||
local_10 = (undefined *)param_1;
|
||||
local_c = (undefined *)param_2;
|
||||
if ((((uint)local_8 & 1) != 0) || (local_8 == (undefined *)0x0)) {
|
||||
local_8 = (undefined *)0x0;
|
||||
}
|
||||
while ((puVar4 = local_8, ((uint)local_8 & 1) == 0 && (local_8 != (undefined *)0x0))) {
|
||||
iVar1 = *(int *)(local_8 + 4);
|
||||
if (((*(ushort *)(iVar1 + 0xc) & 0x100) == 0) &&
|
||||
((*(undefined **)(iVar1 + 0x8c) != PTR_00c89f20 && (*(int *)(iVar1 + 0x88) != 0)))) {
|
||||
if (*(int *)(iVar1 + 0x180) == 0 && *(int *)(iVar1 + 0x184) == 0) {
|
||||
uVar2 = param_4 & 0xf;
|
||||
}
|
||||
else {
|
||||
uVar2 = param_4 & 0xf00000;
|
||||
}
|
||||
if (uVar2 != 0) {
|
||||
if (-1 < (char)*(ushort *)(iVar1 + 0xc)) {
|
||||
return (undefined *)0x0;
|
||||
}
|
||||
puVar5 = (undefined4 *)(iVar1 + 0x14c);
|
||||
ppuVar6 = &local_28;
|
||||
for (iVar3 = 6; iVar3 != 0; iVar3 = iVar3 + -1) {
|
||||
*ppuVar6 = (undefined *)*puVar5;
|
||||
puVar5 = puVar5 + 1;
|
||||
ppuVar6 = ppuVar6 + 1;
|
||||
}
|
||||
if (((((float)local_28 < *(float *)(local_c + 0xc) !=
|
||||
((float)local_28 == *(float *)(local_c + 0xc))) &&
|
||||
((float)local_24 < *(float *)(local_c + 0x10) !=
|
||||
((float)local_24 == *(float *)(local_c + 0x10)))) &&
|
||||
((float)local_20 < *(float *)(local_c + 0x14) !=
|
||||
((float)local_20 == *(float *)(local_c + 0x14)))) &&
|
||||
(((*(float *)local_c <= (float)local_1c && (*(float *)(local_c + 4) <= (float)local_18)
|
||||
) && (*(float *)(local_c + 8) <= (float)local_14)))) {
|
||||
addGeometryToBuffer((float *)local_c,iVar1,param_3);
|
||||
puVar4 = local_8;
|
||||
}
|
||||
*(undefined **)(iVar1 + 0x8c) = PTR_00c89f20;
|
||||
}
|
||||
}
|
||||
local_8 = *(undefined **)(puVar4 + *(int *)local_10 + 4);
|
||||
}
|
||||
}
|
||||
return (undefined *)0x1;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// SECTION 2: FULL DISASSEMBLY
|
||||
// =============================================================================
|
||||
//
|
||||
// 0x006abc40 55 PUSH EBP
|
||||
// 0x006abc41 8b ec MOV EBP,ESP
|
||||
// 0x006abc43 83 ec 24 SUB ESP,0x24
|
||||
// 0x006abc46 53 PUSH EBX
|
||||
// 0x006abc47 8b c1 MOV EAX,ECX
|
||||
// 0x006abc49 8b 4d 0c MOV ECX,dword ptr [EBP + 0xc]
|
||||
// 0x006abc4c f7 c1 0f 00 f0 00 TEST ECX,0xf0000f
|
||||
// 0x006abc52 56 PUSH ESI
|
||||
// 0x006abc53 57 PUSH EDI
|
||||
// 0x006abc54 89 55 f8 MOV dword ptr [EBP + -0x8],EDX
|
||||
// 0x006abc57 89 45 f4 MOV dword ptr [EBP + -0xc],EAX
|
||||
// 0x006abc5a 0f 84 1b 01 00 00 JZ 0x006abd7b
|
||||
// 0x006abc60 8b 40 08 MOV EAX,dword ptr [EAX + 0x8]
|
||||
// 0x006abc63 a8 01 TEST AL,0x1
|
||||
// 0x006abc65 75 04 JNZ 0x006abc6b
|
||||
// 0x006abc67 85 c0 TEST EAX,EAX
|
||||
// 0x006abc69 75 07 JNZ 0x006abc72
|
||||
// 0x006abc6b 33 d2 XOR EDX,EDX
|
||||
// 0x006abc6d 89 55 fc MOV dword ptr [EBP + -0x4],EDX
|
||||
// 0x006abc70 eb 06 JMP 0x006abc78
|
||||
// 0x006abc72 89 45 fc MOV dword ptr [EBP + -0x4],EAX
|
||||
// 0x006abc75 8b 55 fc MOV EDX,dword ptr [EBP + -0x4]
|
||||
// 0x006abc78 f6 c2 01 TEST DL,0x1
|
||||
// 0x006abc7b 0f 85 fa 00 00 00 JNZ 0x006abd7b
|
||||
// 0x006abc81 85 d2 TEST EDX,EDX
|
||||
// 0x006abc83 0f 84 f2 00 00 00 JZ 0x006abd7b
|
||||
// 0x006abc89 8b 5a 04 MOV EBX,dword ptr [EDX + 0x4]
|
||||
// 0x006abc8c 66 8b 43 0c MOV AX,word ptr [EBX + 0xc]
|
||||
// 0x006abc90 f6 c4 01 TEST AH,0x1
|
||||
// 0x006abc93 0f 85 c1 00 00 00 JNZ 0x006abd5a
|
||||
// 0x006abc99 8b 35 20 9f c8 00 MOV ESI,dword ptr [0x00c89f20]
|
||||
// 0x006abc9f 39 b3 8c 00 00 00 CMP dword ptr [EBX + 0x8c],ESI
|
||||
// 0x006abca5 0f 84 af 00 00 00 JZ 0x006abd5a
|
||||
// 0x006abcab 8b b3 88 00 00 00 MOV ESI,dword ptr [EBX + 0x88]
|
||||
// 0x006abcb1 85 f6 TEST ESI,ESI
|
||||
// 0x006abcb3 0f 84 a1 00 00 00 JZ 0x006abd5a
|
||||
// 0x006abcb9 8b b3 80 01 00 00 MOV ESI,dword ptr [EBX + 0x180]
|
||||
// 0x006abcbf 0b b3 84 01 00 00 OR ESI,dword ptr [EBX + 0x184]
|
||||
// 0x006abcc5 74 08 JZ 0x006abccf
|
||||
// 0x006abcc7 f7 c1 00 00 f0 00 TEST ECX,0xf00000
|
||||
// 0x006abccd eb 03 JMP 0x006abcd2
|
||||
// 0x006abccf f6 c1 0f TEST CL,0xf
|
||||
// 0x006abcd2 0f 84 82 00 00 00 JZ 0x006abd5a
|
||||
// 0x006abcd8 84 c0 TEST AL,AL
|
||||
// 0x006abcda 0f 89 90 00 00 00 JNS 0x006abd70
|
||||
// 0x006abce0 8d b3 4c 01 00 00 LEA ESI,[EBX + 0x14c]
|
||||
// 0x006abce6 b9 06 00 00 00 MOV ECX,0x6
|
||||
// 0x006abceb 8d 7d dc LEA EDI,[EBP + -0x24]
|
||||
// 0x006abcee f3 a5 MOVSD.REP ES:EDI,ESI
|
||||
// 0x006abcf0 d9 45 dc FLD float ptr [EBP + -0x24]
|
||||
// 0x006abcf3 8b 4d f8 MOV ECX,dword ptr [EBP + -0x8]
|
||||
// 0x006abcf6 d8 59 0c FCOMP float ptr [ECX + 0xc]
|
||||
// 0x006abcf9 df e0 FNSTSW AX
|
||||
// 0x006abcfb f6 c4 41 TEST AH,0x41
|
||||
// 0x006abcfe 7a 4e JP 0x006abd4e
|
||||
// 0x006abd00 d9 45 e0 FLD float ptr [EBP + -0x20]
|
||||
// 0x006abd03 d8 59 10 FCOMP float ptr [ECX + 0x10]
|
||||
// 0x006abd06 df e0 FNSTSW AX
|
||||
// 0x006abd08 f6 c4 41 TEST AH,0x41
|
||||
// 0x006abd0b 7a 41 JP 0x006abd4e
|
||||
// 0x006abd0d d9 45 e4 FLD float ptr [EBP + -0x1c]
|
||||
// 0x006abd10 d8 59 14 FCOMP float ptr [ECX + 0x14]
|
||||
// 0x006abd13 df e0 FNSTSW AX
|
||||
// 0x006abd15 f6 c4 41 TEST AH,0x41
|
||||
// 0x006abd18 7a 34 JP 0x006abd4e
|
||||
// 0x006abd1a d9 45 e8 FLD float ptr [EBP + -0x18]
|
||||
// 0x006abd1d d8 19 FCOMP float ptr [ECX]
|
||||
// 0x006abd1f df e0 FNSTSW AX
|
||||
// 0x006abd21 f6 c4 01 TEST AH,0x1
|
||||
// 0x006abd24 75 28 JNZ 0x006abd4e
|
||||
// 0x006abd26 d9 45 ec FLD float ptr [EBP + -0x14]
|
||||
// 0x006abd29 d8 59 04 FCOMP float ptr [ECX + 0x4]
|
||||
// 0x006abd2c df e0 FNSTSW AX
|
||||
// 0x006abd2e f6 c4 01 TEST AH,0x1
|
||||
// 0x006abd31 75 1b JNZ 0x006abd4e
|
||||
// 0x006abd33 d9 45 f0 FLD float ptr [EBP + -0x10]
|
||||
// 0x006abd36 d8 59 08 FCOMP float ptr [ECX + 0x8]
|
||||
// 0x006abd39 df e0 FNSTSW AX
|
||||
// 0x006abd3b f6 c4 01 TEST AH,0x1
|
||||
// 0x006abd3e 75 0e JNZ 0x006abd4e
|
||||
// 0x006abd40 8b 45 08 MOV EAX,dword ptr [EBP + 0x8]
|
||||
// 0x006abd43 50 PUSH EAX
|
||||
// 0x006abd44 8b d3 MOV EDX,EBX
|
||||
// 0x006abd46 e8 45 00 00 00 CALL 0x006abd90
|
||||
// 0x006abd4b 8b 55 fc MOV EDX,dword ptr [EBP + -0x4]
|
||||
// 0x006abd4e 8b 0d 20 9f c8 00 MOV ECX,dword ptr [0x00c89f20]
|
||||
// 0x006abd54 89 8b 8c 00 00 00 MOV dword ptr [EBX + 0x8c],ECX
|
||||
// 0x006abd5a 8b 45 f4 MOV EAX,dword ptr [EBP + -0xc]
|
||||
// 0x006abd5d 8b 00 MOV EAX,dword ptr [EAX]
|
||||
// 0x006abd5f 8b 4c 10 04 MOV ECX,dword ptr [EAX + EDX*0x1 + 0x4]
|
||||
// 0x006abd63 03 c2 ADD EAX,EDX
|
||||
// 0x006abd65 89 4d fc MOV dword ptr [EBP + -0x4],ECX
|
||||
// 0x006abd68 8b 4d 0c MOV ECX,dword ptr [EBP + 0xc]
|
||||
// 0x006abd6b e9 05 ff ff ff JMP 0x006abc75
|
||||
// 0x006abd70 5f POP EDI
|
||||
// 0x006abd71 5e POP ESI
|
||||
// 0x006abd72 33 c0 XOR EAX,EAX
|
||||
// 0x006abd74 5b POP EBX
|
||||
// 0x006abd75 8b e5 MOV ESP,EBP
|
||||
// 0x006abd77 5d POP EBP
|
||||
// 0x006abd78 c2 08 00 RET 0x8
|
||||
// 0x006abd7b 5f POP EDI
|
||||
// 0x006abd7c 5e POP ESI
|
||||
// 0x006abd7d b8 01 00 00 00 MOV EAX,0x1
|
||||
// 0x006abd82 5b POP EBX
|
||||
// 0x006abd83 8b e5 MOV ESP,EBP
|
||||
// 0x006abd85 5d POP EBP
|
||||
// 0x006abd86 c2 08 00 RET 0x8
|
||||
//
|
||||
// Total instructions: 108
|
||||
|
||||
// =============================================================================
|
||||
// SECTION 3: PROLOGUE (first 30 instructions)
|
||||
// =============================================================================
|
||||
//
|
||||
// 0x006abc40 55 PUSH EBP
|
||||
// 0x006abc41 8b ec MOV EBP,ESP
|
||||
// 0x006abc43 83 ec 24 SUB ESP,0x24
|
||||
// 0x006abc46 53 PUSH EBX
|
||||
// 0x006abc47 8b c1 MOV EAX,ECX
|
||||
// 0x006abc49 8b 4d 0c MOV ECX,dword ptr [EBP + 0xc]
|
||||
// 0x006abc4c f7 c1 0f 00 f0 00 TEST ECX,0xf0000f
|
||||
// 0x006abc52 56 PUSH ESI
|
||||
// 0x006abc53 57 PUSH EDI
|
||||
// 0x006abc54 89 55 f8 MOV dword ptr [EBP + -0x8],EDX
|
||||
// 0x006abc57 89 45 f4 MOV dword ptr [EBP + -0xc],EAX
|
||||
// 0x006abc5a 0f 84 1b 01 00 00 JZ 0x006abd7b
|
||||
// 0x006abc60 8b 40 08 MOV EAX,dword ptr [EAX + 0x8]
|
||||
// 0x006abc63 a8 01 TEST AL,0x1
|
||||
// 0x006abc65 75 04 JNZ 0x006abc6b
|
||||
// 0x006abc67 85 c0 TEST EAX,EAX
|
||||
// 0x006abc69 75 07 JNZ 0x006abc72
|
||||
// 0x006abc6b 33 d2 XOR EDX,EDX
|
||||
// 0x006abc6d 89 55 fc MOV dword ptr [EBP + -0x4],EDX
|
||||
// 0x006abc70 eb 06 JMP 0x006abc78
|
||||
// 0x006abc72 89 45 fc MOV dword ptr [EBP + -0x4],EAX
|
||||
// 0x006abc75 8b 55 fc MOV EDX,dword ptr [EBP + -0x4]
|
||||
// 0x006abc78 f6 c2 01 TEST DL,0x1
|
||||
// 0x006abc7b 0f 85 fa 00 00 00 JNZ 0x006abd7b
|
||||
// 0x006abc81 85 d2 TEST EDX,EDX
|
||||
// 0x006abc83 0f 84 f2 00 00 00 JZ 0x006abd7b
|
||||
// 0x006abc89 8b 5a 04 MOV EBX,dword ptr [EDX + 0x4]
|
||||
// 0x006abc8c 66 8b 43 0c MOV AX,word ptr [EBX + 0xc]
|
||||
// 0x006abc90 f6 c4 01 TEST AH,0x1
|
||||
// 0x006abc93 0f 85 c1 00 00 00 JNZ 0x006abd5a
|
||||
|
||||
// =============================================================================
|
||||
// SECTION 4: ALL RET INSTRUCTIONS (stack cleanup indicator)
|
||||
// =============================================================================
|
||||
//
|
||||
// 0x006abd78 c2 08 00 RET 0x8
|
||||
// 0x006abd86 c2 08 00 RET 0x8
|
||||
|
||||
// =============================================================================
|
||||
// SECTION 5: XREFS TO 0x006abc40 (callers)
|
||||
// =============================================================================
|
||||
//
|
||||
// 0x006aad4c [UNCONDITIONAL_CALL] in processGeometryBoundsCheck (0x006aaab0)
|
||||
// 0x006ab4a1 [UNCONDITIONAL_CALL] in processTerrainChunkMeshGeneration (0x006aadc0)
|
||||
//
|
||||
// Total callers: 2
|
||||
|
||||
// =============================================================================
|
||||
// SECTION 6: XREFS FROM 0x006abc40 (callees)
|
||||
// =============================================================================
|
||||
//
|
||||
// 0x006abd90 addGeometryToBuffer
|
||||
//
|
||||
// Total callees: 1
|
||||
|
||||
@@ -0,0 +1,659 @@
|
||||
// =============================================================================
|
||||
// renderTextToBuffer @ 0x005CDC20
|
||||
// Decompiled from WoW.exe 1.12.1 (build 5875) via Ghidra 11.4.2
|
||||
// Single caller of RenderTextToVertexBuffer (0x5CCBE0)
|
||||
// =============================================================================
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// CALLING CONVENTION (verified from assembly)
|
||||
// -----------------------------------------------------------------------------
|
||||
// __thiscall: ECX = this (text object ptr), saved to EBX immediately
|
||||
// 0x005cdc20 PUSH EBP
|
||||
// 0x005cdc21 MOV EBP,ESP
|
||||
// 0x005cdc23 SUB ESP,0x74
|
||||
// 0x005cdc26 PUSH EBX
|
||||
// 0x005cdc27 PUSH ESI
|
||||
// 0x005cdc28 MOV EBX,ECX <-- this ptr saved to EBX
|
||||
// ...
|
||||
// 0x005cdee8 RET <-- single RET, no stack cleanup = __thiscall (0 stack params)
|
||||
//
|
||||
// Signature: void __thiscall renderTextToBuffer(TextObject* this)
|
||||
// No stack parameters. Single RET (no RET N).
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// XREFS TO (callers)
|
||||
// -----------------------------------------------------------------------------
|
||||
// 0x005cd6aa from validateAndPrepareText (UNCONDITIONAL_CALL)
|
||||
// 0x005cd426 from GetVertexBufferData (UNCONDITIONAL_CALL)
|
||||
//
|
||||
// Two callers:
|
||||
// 1. validateAndPrepareText (0x5cd6aa) -- validation/preparation path
|
||||
// 2. GetVertexBufferData (0x5cd426) -- vertex buffer retrieval path
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// XREFS FROM (callees)
|
||||
// -----------------------------------------------------------------------------
|
||||
// 0x005c6fa0 ConvertPixelsToScreen
|
||||
// 0x0040a2b0 __ftol (float-to-long)
|
||||
// 0x005c2810 ParseTextFormatCodes
|
||||
// 0x005c7260 WrapTextToWidth
|
||||
// 0x005c7010 ConvertPixelsToScreenAlt
|
||||
// 0x005ccbe0 RenderTextToVertexBuffer <<<< the target
|
||||
// 0x005cd310 AddRectangleToBuffer
|
||||
// 0x005cdf70 finalizeTextLayout
|
||||
// 0x005cd4d0 renderFadeEffect
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// TEXT OBJECT FIELD MAP (offsets from this/EBX)
|
||||
// -----------------------------------------------------------------------------
|
||||
// +0x1c float lineHeight (or spacing-related metric)
|
||||
// +0x28 float xOffset (used when bit7 of flags is clear)
|
||||
// +0x2c float color/style data (passed to RenderTextToVertexBuffer)
|
||||
// +0x34 float indentOrShadow (used when flags bit0 is set)
|
||||
// +0x3c float maxWidth (passed to WrapTextToWidth)
|
||||
// +0x40 float maxHeight (vertical overflow check)
|
||||
// +0x44 void* fontObject (passed to WrapTextToWidth as ECX)
|
||||
// +0x48 char* textString (the actual text to render)
|
||||
// +0x54 int alignment (0=left, 1=center, 2=right)
|
||||
// +0x58 float some metric (initial value for local_c / line step)
|
||||
// +0x5c uint flags bitfield:
|
||||
// bit0: has indent/shadow
|
||||
// bit1 (0x02): single-line mode (breaks loop after first line)
|
||||
// bit5 (0x20): has fade effect
|
||||
// bit7 (0x80): pixel mode vs screen mode
|
||||
// +0x60 uint resultFlags (OR'd with per-line flags from RenderTextToVertexBuffer)
|
||||
// +0x68 int fadeStart (passed to renderFadeEffect)
|
||||
// +0x6c int fadeEnd (passed to renderFadeEffect)
|
||||
// +0x90 uint lineCountOutput (zeroed at start, not the dirty flag)
|
||||
// +0x9c int LINE COUNTER / DIRTY FLAG -- THE KEY FIELD
|
||||
// - Checked FIRST: if (this+0x9c != 0) return immediately
|
||||
// - Incremented after each line rendered
|
||||
// - Also incremented for format-code-only lines (ParseTextFormatCodes returns 2)
|
||||
// - Acts as both "already rendered" guard AND line counter
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// DIRTY/VALID FLAG ANALYSIS
|
||||
// -----------------------------------------------------------------------------
|
||||
//
|
||||
// The field at +0x9c is the critical gate. The function's VERY FIRST check is:
|
||||
//
|
||||
// if (this->field_0x9c != 0) return; // already rendered, skip
|
||||
// if (this->textString == NULL) return; // no text
|
||||
// if (*this->textString == '\0') return; // empty text
|
||||
//
|
||||
// Assembly proof:
|
||||
// 0x005cdc2a MOV EAX,dword ptr [EBX + 0x9c]
|
||||
// 0x005cdc33 CMP EAX,EDI ; EDI = 0
|
||||
// 0x005cdc35 JNZ 0x005cdee2 ; bail if non-zero
|
||||
// 0x005cdc3b MOV EAX,dword ptr [EBX + 0x48]
|
||||
// 0x005cdc3e CMP EAX,EDI
|
||||
// 0x005cdc40 JZ 0x005cdee2 ; bail if text ptr is NULL
|
||||
// 0x005cdc46 CMP byte ptr [EAX],0x0
|
||||
// 0x005cdc49 JZ 0x005cdee2 ; bail if text is empty
|
||||
//
|
||||
// Then +0x9c is INCREMENTED after each rendered line:
|
||||
// 0x005cde55 MOV ECX,dword ptr [EBX + 0x9c]
|
||||
// 0x005cde5e INC ECX
|
||||
// 0x005cde62 MOV dword ptr [EBX + 0x9c],ECX
|
||||
//
|
||||
// And also incremented for format-code lines (ParseTextFormatCodes == 2):
|
||||
// 0x005cdd81 MOV EAX,dword ptr [EBX + 0x9c]
|
||||
// 0x005cdd8c INC EAX
|
||||
// 0x005cdd90 MOV dword ptr [EBX + 0x9c],EAX
|
||||
//
|
||||
// CONCLUSION: +0x9c serves as BOTH:
|
||||
// 1. A "dirty/needs-render" flag (0 = needs render, non-zero = already done)
|
||||
// 2. A line counter (counts lines processed during rendering)
|
||||
//
|
||||
// To force re-render: set this->field_0x9c = 0
|
||||
// To prevent render: set this->field_0x9c = non-zero
|
||||
//
|
||||
// The callers (validateAndPrepareText, GetVertexBufferData) presumably
|
||||
// reset +0x9c to 0 when text changes, triggering re-render on next call.
|
||||
|
||||
// +0x90 is zeroed at entry:
|
||||
// 0x005cdc4f MOV dword ptr [EBX + 0x90],EDI ; = 0
|
||||
// This appears to be a separate output counter, not the dirty flag.
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// FLOW SUMMARY
|
||||
// -----------------------------------------------------------------------------
|
||||
//
|
||||
// 1. Guard: if +0x9c != 0 || textString is null/empty -> return
|
||||
// 2. Zero +0x90 (line output counter?)
|
||||
// 3. Compute vertical layout params from +0x1c, +0x58, flags
|
||||
// 4. Loop over text lines:
|
||||
// a. Check vertical overflow (accumulated height vs +0x40 maxHeight)
|
||||
// b. ParseTextFormatCodes -- handle color/format escapes (returns 2 = consumed)
|
||||
// c. WrapTextToWidth -- break text at word boundaries for +0x3c width
|
||||
// d. Handle alignment (+0x54): left(0), center(1), right(2) via ConvertPixelsToScreenAlt
|
||||
// e. CALL RenderTextToVertexBuffer(this, textPtr, charCount, &color, &offset, &flags, &state)
|
||||
// f. Increment +0x9c (line counter)
|
||||
// g. If state==2, call AddRectangleToBuffer (highlight/selection rect)
|
||||
// h. OR per-line flags into +0x60
|
||||
// i. If flags bit1 (single-line mode), break
|
||||
// j. Advance to next line
|
||||
// 5. Call finalizeTextLayout(this)
|
||||
// 6. If flags bit5 (fade), call renderFadeEffect(this, fadeStart, fadeEnd)
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// RenderTextToVertexBuffer CALL SITE DETAIL (at 0x5CDE50)
|
||||
// -----------------------------------------------------------------------------
|
||||
// __thiscall: ECX = this (text object)
|
||||
// Stack args (6, pushed right-to-left):
|
||||
// push &state (EBP-0x74, local_74 area -- tracks render state, value 2 = highlight)
|
||||
// push &flags (EBP-0x24, output flags OR'd into +0x60)
|
||||
// push charCount (EBP-0x18, from WrapTextToWidth output)
|
||||
// push &offset (EBP-0x48, vertical/horizontal position floats)
|
||||
// push &color (EBP-0x30, color/style data)
|
||||
// push textPtr (ESI, current position in text string)
|
||||
//
|
||||
// Assembly at call site:
|
||||
// 0x005cde39 LEA EAX,[EBP + -0x74]
|
||||
// 0x005cde3c PUSH EAX ; &state
|
||||
// 0x005cde3d LEA ECX,[EBP + -0x24]
|
||||
// 0x005cde40 PUSH ECX ; &flags
|
||||
// 0x005cde41 MOV ECX,dword ptr [EBP + -0x18]
|
||||
// 0x005cde44 LEA EDX,[EBP + -0x48]
|
||||
// 0x005cde47 PUSH EDX ; &offset
|
||||
// 0x005cde48 LEA EAX,[EBP + -0x30]
|
||||
// 0x005cde4b PUSH EAX ; &color
|
||||
// 0x005cde4c PUSH ECX ; charCount
|
||||
// 0x005cde4d PUSH ESI ; textPtr
|
||||
// 0x005cde4e MOV ECX,EBX ; this
|
||||
// 0x005cde50 CALL 0x005ccbe0 ; RenderTextToVertexBuffer
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// DECOMPILED C (Ghidra raw output, lightly annotated)
|
||||
// =============================================================================
|
||||
|
||||
/* WARNING: Variable defined which should be unmapped: local_88 */
|
||||
void __fastcall renderTextToBuffer(void *param_1) // ECX = this
|
||||
{
|
||||
byte bVar1;
|
||||
uint uVar2;
|
||||
undefined *puVar3;
|
||||
int iVar4;
|
||||
undefined **ppuVar5;
|
||||
byte *pbVar6;
|
||||
float unaff_EDI;
|
||||
undefined4 *puVar7;
|
||||
float10 extraout_ST0;
|
||||
float10 fVar8;
|
||||
float10 extraout_ST0_00;
|
||||
float10 extraout_ST0_01;
|
||||
float10 extraout_ST0_02;
|
||||
ulonglong uVar9;
|
||||
float10 *pfVar10;
|
||||
float fVar11;
|
||||
undefined *local_88;
|
||||
undefined *local_78;
|
||||
undefined *local_74;
|
||||
undefined *local_70;
|
||||
undefined *local_6c;
|
||||
undefined *local_68;
|
||||
undefined *local_4c;
|
||||
undefined *local_48;
|
||||
undefined *local_44;
|
||||
undefined *local_40;
|
||||
uint uStack_3c;
|
||||
undefined *local_38;
|
||||
undefined *local_34;
|
||||
undefined *local_30;
|
||||
undefined *local_2c;
|
||||
undefined *local_28;
|
||||
undefined *local_24;
|
||||
undefined *local_20;
|
||||
undefined *local_1c;
|
||||
undefined *local_18;
|
||||
undefined *local_14;
|
||||
undefined *local_10;
|
||||
undefined *local_c;
|
||||
byte local_5;
|
||||
|
||||
// --- GUARD: dirty/valid check ---
|
||||
if (((*(int *)((int)param_1 + 0x9c) != 0) || // already rendered?
|
||||
(*(char **)((int)param_1 + 0x48) == (char *)0x0)) || // no text ptr?
|
||||
(**(char **)((int)param_1 + 0x48) == '\0')) { // empty text?
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Reset line output counter ---
|
||||
*(undefined4 *)((int)param_1 + 0x90) = 0;
|
||||
|
||||
// --- Read fade params ---
|
||||
local_2c = *(undefined **)((int)param_1 + 0x6c); // fadeEnd
|
||||
local_38 = *(undefined **)((int)param_1 + 0x68); // fadeStart
|
||||
|
||||
// --- Convert line height to screen coords ---
|
||||
ConvertPixelsToScreen(
|
||||
(void *)(*(uint *)((int)param_1 + 0x5c) >> 7 & 1),
|
||||
(float10 *)-*(float *)((int)param_1 + 0x1c),
|
||||
unaff_EDI);
|
||||
|
||||
uVar2 = *(uint *)((int)param_1 + 0x5c) & 0x80; // pixel mode flag
|
||||
local_4c = (undefined *)0x0;
|
||||
|
||||
if (uVar2 == 0) {
|
||||
local_44 = *(undefined **)((int)param_1 + 0x28); // xOffset
|
||||
local_48 = (undefined *)(float)extraout_ST0;
|
||||
} else {
|
||||
local_48 = (undefined *)0x0;
|
||||
local_44 = (undefined *)0x0;
|
||||
}
|
||||
|
||||
local_34 = *(undefined **)((int)param_1 + 0x2c); // color/style
|
||||
local_10 = *(undefined **)((int)param_1 + 0x58); // line step metric
|
||||
pbVar6 = *(byte **)((int)param_1 + 0x48); // text string ptr
|
||||
|
||||
// Init state vars
|
||||
local_78 = (undefined *)0x0;
|
||||
local_70 = (undefined *)0x0;
|
||||
local_68 = (undefined *)0x0;
|
||||
local_20 = (undefined *)0x0;
|
||||
local_1c = (undefined *)0x0;
|
||||
local_14 = (undefined *)0x0;
|
||||
|
||||
// Compute line height (pixel mode vs screen mode)
|
||||
if (uVar2 == 0) {
|
||||
// Screen mode: round to integer pixels, then back to screen coords
|
||||
local_40 = PTR_00c2b9a0;
|
||||
uStack_3c = 0;
|
||||
uVar9 = __ftol();
|
||||
local_40 = (undefined *)uVar9;
|
||||
uStack_3c = 0;
|
||||
local_18 = (undefined *)(float)(uVar9 & 0xffffffff);
|
||||
local_10 = (undefined *)(float)((float10)(uVar9 & 0xffffffff) / extraout_ST0_00);
|
||||
ConvertPixelsToScreen((void *)0x0, *(float10 **)((int)param_1 + 0x1c), unaff_EDI);
|
||||
fVar8 = extraout_ST0_01 + (float10)(float)local_18;
|
||||
} else {
|
||||
// Pixel mode: just add
|
||||
fVar8 = (float10)(float)local_10 + (float10)*(float *)((int)param_1 + 0x1c);
|
||||
}
|
||||
|
||||
local_c = (undefined *)(float)fVar8; // total line step (height + spacing)
|
||||
local_5 = 1; // first line flag
|
||||
local_74 = (undefined *)((float)local_c + (float)local_48); // current Y position
|
||||
|
||||
bVar1 = *pbVar6;
|
||||
local_18 = (undefined *)0x0; // accumulated vertical height
|
||||
local_6c = local_48;
|
||||
|
||||
// --- MAIN RENDERING LOOP: iterate over lines ---
|
||||
do {
|
||||
// Check: end of text OR vertical overflow
|
||||
if (((bVar1 == 0) ||
|
||||
(*(float *)((int)param_1 + 0x40) <= (float)local_18)) ||
|
||||
(local_18 = (undefined *)((float)local_18 + *(float *)((int)param_1 + 0x1c) + (float)local_10),
|
||||
bVar1 == 0))
|
||||
goto LAB_005cdebc; // done
|
||||
|
||||
// Parse format/color codes
|
||||
puVar3 = ParseTextFormatCodes(
|
||||
pbVar6, &local_30, (uint *)0x0,
|
||||
*(uint *)((int)param_1 + 0x5c), &uStack_3c);
|
||||
|
||||
local_14 = (undefined *)0x0;
|
||||
|
||||
if (puVar3 == (undefined *)0x2) {
|
||||
// Format code consumed entire segment -- adjust position, skip render
|
||||
local_48 = (undefined *)((float)local_48 - (float)local_c);
|
||||
*(int *)((int)param_1 + 0x9c) = *(int *)((int)param_1 + 0x9c) + 1; // increment line counter
|
||||
pbVar6 = pbVar6 + (int)local_30;
|
||||
} else {
|
||||
// Check word wrap flag
|
||||
if (((uint)*(float10 **)((int)param_1 + 0x5c) & 1) == 0) {
|
||||
local_24 = (undefined *)0x0;
|
||||
} else {
|
||||
local_24 = *(undefined **)((int)param_1 + 0x34); // indent
|
||||
}
|
||||
|
||||
pfVar10 = *(float10 **)((int)param_1 + 0x1c);
|
||||
|
||||
// Word-wrap the text to fit maxWidth
|
||||
WrapTextToWidth(
|
||||
*(float10 **)((int)param_1 + 0x44), // font
|
||||
pbVar6, // text
|
||||
pfVar10, // line height
|
||||
*(float *)((int)param_1 + 0x3c), // maxWidth
|
||||
(int *)&local_1c, // out: charCount
|
||||
(float *)&local_14, // out: line width
|
||||
&local_20, // out: next line ptr
|
||||
(float10 *)local_24, // indent
|
||||
*(float10 **)((int)param_1 + 0x5c), // flags
|
||||
&local_5); // first line flag
|
||||
|
||||
// Check if wrapping produced valid output
|
||||
if (((local_20 == pbVar6) || (local_20 == (undefined *)0x0)) ||
|
||||
((local_1c == (undefined *)0x0 && (*local_20 == '\0')))) {
|
||||
LAB_005cdebc:
|
||||
// --- FINALIZE ---
|
||||
finalizeTextLayout((int)param_1);
|
||||
if ((*(byte *)((int)param_1 + 0x5c) & 0x20) == 0) {
|
||||
return;
|
||||
}
|
||||
// Fade effect
|
||||
if ((local_38 == (undefined *)0xffffffff) &&
|
||||
(local_2c == (undefined *)0xffffffff)) {
|
||||
return;
|
||||
}
|
||||
renderFadeEffect(param_1, (int)local_38, (int)local_2c);
|
||||
return;
|
||||
}
|
||||
|
||||
// --- ALIGNMENT ---
|
||||
puVar3 = local_14;
|
||||
if (*(int *)((int)param_1 + 0x54) == 2) {
|
||||
// Right-aligned
|
||||
LAB_005cde14:
|
||||
ConvertPixelsToScreenAlt(
|
||||
(void *)(*(uint *)((int)param_1 + 0x5c) >> 7 & 1),
|
||||
(float10 *)-(float)puVar3, unaff_EDI);
|
||||
local_4c = (undefined *)(float)extraout_ST0_02;
|
||||
} else if (*(int *)((int)param_1 + 0x54) == 1) {
|
||||
// Center-aligned
|
||||
puVar3 = (undefined *)((float)local_14 * StaticFloat0_5);
|
||||
goto LAB_005cde14;
|
||||
}
|
||||
// else: left-aligned, local_4c stays 0
|
||||
|
||||
local_28 = (undefined *)0x0;
|
||||
if (local_78 != (undefined *)0x0) {
|
||||
local_70 = local_4c;
|
||||
}
|
||||
|
||||
fVar11 = 8.528623e-39; // junk / uninitialized
|
||||
puVar3 = local_1c;
|
||||
|
||||
// --- THE CALL: render this line's glyphs to vertex buffer ---
|
||||
RenderTextToVertexBuffer(
|
||||
param_1, // this (ECX)
|
||||
pbVar6, // textPtr (current line start)
|
||||
(int)local_1c, // charCount
|
||||
(uint *)&local_34, // &color/style
|
||||
(float *)&local_4c, // &position offset
|
||||
(uint *)&local_28, // &output flags
|
||||
(int *)&local_78); // &render state
|
||||
|
||||
// Increment line counter (+0x9c)
|
||||
*(int *)((int)param_1 + 0x9c) = *(int *)((int)param_1 + 0x9c) + 1;
|
||||
|
||||
// If state == 2, add highlight/selection rectangle
|
||||
if (local_78 == (undefined *)0x2) {
|
||||
ppuVar5 = &local_74;
|
||||
puVar7 = (undefined4 *)&stack0xffffff5c;
|
||||
for (iVar4 = 8; iVar4 != 0; iVar4 = iVar4 + -1) {
|
||||
*puVar7 = *ppuVar5;
|
||||
ppuVar5 = ppuVar5 + 1;
|
||||
puVar7 = puVar7 + 1;
|
||||
}
|
||||
AddRectangleToBuffer(param_1, (float)pfVar10, fVar11, (float)pbVar6, (float)puVar3);
|
||||
}
|
||||
|
||||
// Advance vertical position
|
||||
local_48 = (undefined *)((float)local_48 - (float)local_c);
|
||||
|
||||
// Accumulate result flags
|
||||
*(uint *)((int)param_1 + 0x60) = *(uint *)((int)param_1 + 0x60) | (uint)local_28;
|
||||
|
||||
// Move to next line
|
||||
pbVar6 = local_20;
|
||||
|
||||
// Single-line mode check: if bit1 set, done after first line
|
||||
if ((*(byte *)((int)param_1 + 0x5c) & 2) != 0)
|
||||
goto LAB_005cdebc;
|
||||
}
|
||||
|
||||
bVar1 = *pbVar6;
|
||||
local_6c = (undefined *)((float)local_6c - (float)local_c);
|
||||
local_74 = (undefined *)((float)local_74 - (float)local_c);
|
||||
} while (true);
|
||||
}
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// FULL DISASSEMBLY LISTING
|
||||
// =============================================================================
|
||||
//
|
||||
// 0x005cdc20 PUSH EBP
|
||||
// 0x005cdc21 MOV EBP,ESP
|
||||
// 0x005cdc23 SUB ESP,0x74
|
||||
// 0x005cdc26 PUSH EBX
|
||||
// 0x005cdc27 PUSH ESI
|
||||
// 0x005cdc28 MOV EBX,ECX
|
||||
// 0x005cdc2a MOV EAX,dword ptr [EBX + 0x9c]
|
||||
// 0x005cdc30 PUSH EDI
|
||||
// 0x005cdc31 XOR EDI,EDI
|
||||
// 0x005cdc33 CMP EAX,EDI
|
||||
// 0x005cdc35 JNZ 0x005cdee2
|
||||
// 0x005cdc3b MOV EAX,dword ptr [EBX + 0x48]
|
||||
// 0x005cdc3e CMP EAX,EDI
|
||||
// 0x005cdc40 JZ 0x005cdee2
|
||||
// 0x005cdc46 CMP byte ptr [EAX],0x0
|
||||
// 0x005cdc49 JZ 0x005cdee2
|
||||
// 0x005cdc4f MOV dword ptr [EBX + 0x90],EDI
|
||||
// 0x005cdc55 FLD float ptr [EBX + 0x1c]
|
||||
// 0x005cdc58 MOV ECX,dword ptr [EBX + 0x6c]
|
||||
// 0x005cdc5b FCHS
|
||||
// 0x005cdc5d MOV EAX,dword ptr [EBX + 0x68]
|
||||
// 0x005cdc60 PUSH ECX
|
||||
// 0x005cdc61 MOV dword ptr [EBP + -0x28],ECX
|
||||
// 0x005cdc64 FSTP float ptr [ESP]
|
||||
// 0x005cdc67 MOV ECX,dword ptr [EBX + 0x5c]
|
||||
// 0x005cdc6a SHR ECX,0x7
|
||||
// 0x005cdc6d AND ECX,0x1
|
||||
// 0x005cdc70 MOV dword ptr [EBP + -0x34],EAX
|
||||
// 0x005cdc73 CALL 0x005c6fa0
|
||||
// 0x005cdc78 MOV EAX,dword ptr [EBX + 0x5c]
|
||||
// 0x005cdc7b AND EAX,0x80
|
||||
// 0x005cdc80 MOV dword ptr [EBP + -0x48],EDI
|
||||
// 0x005cdc83 JZ 0x005cdc8f
|
||||
// 0x005cdc85 FSTP ST0
|
||||
// 0x005cdc87 MOV dword ptr [EBP + -0x44],EDI
|
||||
// 0x005cdc8a MOV dword ptr [EBP + -0x40],EDI
|
||||
// 0x005cdc8d JMP 0x005cdc98
|
||||
// 0x005cdc8f MOV EDX,dword ptr [EBX + 0x28]
|
||||
// 0x005cdc92 FSTP float ptr [EBP + -0x44]
|
||||
// 0x005cdc95 MOV dword ptr [EBP + -0x40],EDX
|
||||
// 0x005cdc98 CMP EAX,EDI
|
||||
// 0x005cdc9a MOV ECX,dword ptr [EBX + 0x2c]
|
||||
// 0x005cdc9d MOV EDX,dword ptr [EBX + 0x58]
|
||||
// 0x005cdca0 MOV ESI,dword ptr [EBX + 0x48]
|
||||
// 0x005cdca3 MOV dword ptr [EBP + -0x30],ECX
|
||||
// 0x005cdca6 MOV dword ptr [EBP + -0x74],EDI
|
||||
// 0x005cdca9 MOV dword ptr [EBP + -0x6c],0x0
|
||||
// 0x005cdcb0 MOV dword ptr [EBP + -0x64],0x0
|
||||
// 0x005cdcb7 MOV dword ptr [EBP + -0x1c],EDI
|
||||
// 0x005cdcba MOV dword ptr [EBP + -0x18],EDI
|
||||
// 0x005cdcbd MOV dword ptr [EBP + -0x10],0x0
|
||||
// 0x005cdcc4 MOV dword ptr [EBP + -0xc],EDX
|
||||
// 0x005cdcc7 JZ 0x005cdcd1
|
||||
// 0x005cdcc9 FLD float ptr [EBP + -0xc]
|
||||
// 0x005cdccc FADD float ptr [EBX + 0x1c]
|
||||
// 0x005cdccf JMP 0x005cdd10
|
||||
// 0x005cdcd1 MOV EAX,[0x00c2b9a0]
|
||||
// 0x005cdcd6 MOV dword ptr [EBP + -0x3c],EAX
|
||||
// 0x005cdcd9 MOV dword ptr [EBP + -0x38],EDI
|
||||
// 0x005cdcdc FILD qword ptr [EBP + -0x3c]
|
||||
// 0x005cdcdf FLD float ptr [EBP + -0xc]
|
||||
// 0x005cdce2 FMUL ST1
|
||||
// 0x005cdce4 FADD float ptr [0x00808120]
|
||||
// 0x005cdcea CALL 0x0040a2b0
|
||||
// 0x005cdcef MOV dword ptr [EBP + -0x3c],EAX
|
||||
// 0x005cdcf2 MOV dword ptr [EBP + -0x38],EDI
|
||||
// 0x005cdcf5 FILD qword ptr [EBP + -0x3c]
|
||||
// 0x005cdcf8 MOV ECX,dword ptr [EBX + 0x1c]
|
||||
// 0x005cdcfb PUSH ECX
|
||||
// 0x005cdcfc FST float ptr [EBP + -0x14]
|
||||
// 0x005cdcff XOR ECX,ECX
|
||||
// 0x005cdd01 FDIV ST0,ST1
|
||||
// 0x005cdd03 FSTP float ptr [EBP + -0xc]
|
||||
// 0x005cdd06 FSTP ST0
|
||||
// 0x005cdd08 CALL 0x005c6fa0
|
||||
// 0x005cdd0d FADD float ptr [EBP + -0x14]
|
||||
// 0x005cdd10 MOV EDX,dword ptr [EBP + -0x44]
|
||||
// 0x005cdd13 FSTP float ptr [EBP + -0x8]
|
||||
// 0x005cdd16 FLD float ptr [EBP + -0x8]
|
||||
// 0x005cdd19 MOV byte ptr [EBP + -0x1],0x1
|
||||
// 0x005cdd1d FADD float ptr [EBP + -0x44]
|
||||
// 0x005cdd20 MOV CL,byte ptr [ESI]
|
||||
// 0x005cdd22 TEST CL,CL
|
||||
// 0x005cdd24 MOV dword ptr [EBP + -0x68],EDX
|
||||
// 0x005cdd27 FSTP float ptr [EBP + -0x70]
|
||||
// 0x005cdd2a MOV dword ptr [EBP + -0x14],0x0
|
||||
// 0x005cdd31 JZ 0x005cdebc
|
||||
// 0x005cdd37 FLD float ptr [EBP + -0x14]
|
||||
// 0x005cdd3a FCOMP float ptr [EBX + 0x40]
|
||||
// 0x005cdd3d FNSTSW AX
|
||||
// 0x005cdd3f TEST AH,0x5
|
||||
// 0x005cdd42 JP 0x005cdebc
|
||||
// 0x005cdd48 TEST CL,CL
|
||||
// 0x005cdd4a FLD float ptr [EBP + -0x14]
|
||||
// 0x005cdd4d FADD float ptr [EBX + 0x1c]
|
||||
// 0x005cdd50 FADD float ptr [EBP + -0xc]
|
||||
// 0x005cdd53 FSTP float ptr [EBP + -0x14]
|
||||
// 0x005cdd56 JZ 0x005cdebc
|
||||
// 0x005cdd5c MOV ECX,dword ptr [EBX + 0x5c]
|
||||
// 0x005cdd5f LEA EAX,[EBP + -0x38]
|
||||
// 0x005cdd62 PUSH EAX
|
||||
// 0x005cdd63 PUSH ECX
|
||||
// 0x005cdd64 PUSH EDI
|
||||
// 0x005cdd65 LEA EDX,[EBP + -0x2c]
|
||||
// 0x005cdd68 MOV ECX,ESI
|
||||
// 0x005cdd6a CALL 0x005c2810
|
||||
// 0x005cdd6f CMP EAX,0x2
|
||||
// 0x005cdd72 MOV dword ptr [EBP + -0x10],0x0
|
||||
// 0x005cdd79 JNZ 0x005cdd9b
|
||||
// 0x005cdd7b FLD float ptr [EBP + -0x44]
|
||||
// 0x005cdd7e MOV ECX,dword ptr [EBP + -0x2c]
|
||||
// 0x005cdd81 MOV EAX,dword ptr [EBX + 0x9c]
|
||||
// 0x005cdd87 FSUB float ptr [EBP + -0x8]
|
||||
// 0x005cdd8a ADD ESI,ECX
|
||||
// 0x005cdd8c INC EAX
|
||||
// 0x005cdd8d FSTP float ptr [EBP + -0x44]
|
||||
// 0x005cdd90 MOV dword ptr [EBX + 0x9c],EAX
|
||||
// 0x005cdd96 JMP 0x005cdea0
|
||||
// 0x005cdd9b MOV EAX,dword ptr [EBX + 0x5c]
|
||||
// 0x005cdd9e TEST AL,0x1
|
||||
// 0x005cdda0 JZ 0x005cddaa
|
||||
// 0x005cdda2 MOV EDX,dword ptr [EBX + 0x34]
|
||||
// 0x005cdda5 MOV dword ptr [EBP + -0x20],EDX
|
||||
// 0x005cdda8 JMP 0x005cddb1
|
||||
// 0x005cddaa MOV dword ptr [EBP + -0x20],0x0
|
||||
// 0x005cddb1 MOV EDX,dword ptr [EBP + -0x20]
|
||||
// 0x005cddb4 LEA ECX,[EBP + -0x1]
|
||||
// 0x005cddb7 PUSH ECX
|
||||
// 0x005cddb8 PUSH EAX
|
||||
// 0x005cddb9 PUSH EDX
|
||||
// 0x005cddba LEA EAX,[EBP + -0x1c]
|
||||
// 0x005cddbd PUSH EAX
|
||||
// 0x005cddbe MOV EAX,dword ptr [EBX + 0x3c]
|
||||
// 0x005cddc1 LEA ECX,[EBP + -0x10]
|
||||
// 0x005cddc4 PUSH ECX
|
||||
// 0x005cddc5 MOV ECX,dword ptr [EBX + 0x1c]
|
||||
// 0x005cddc8 LEA EDX,[EBP + -0x18]
|
||||
// 0x005cddcb PUSH EDX
|
||||
// 0x005cddcc PUSH EAX
|
||||
// 0x005cddcd PUSH ECX
|
||||
// 0x005cddce MOV ECX,dword ptr [EBX + 0x44]
|
||||
// 0x005cddd1 MOV EDX,ESI
|
||||
// 0x005cddd3 CALL 0x005c7260
|
||||
// 0x005cddd8 MOV EAX,dword ptr [EBP + -0x1c]
|
||||
// 0x005cdddb CMP EAX,ESI
|
||||
// 0x005cdddd JZ 0x005cdebc
|
||||
// 0x005cdde3 CMP EAX,EDI
|
||||
// 0x005cdde5 JZ 0x005cdebc
|
||||
// 0x005cddeb CMP dword ptr [EBP + -0x18],EDI
|
||||
// 0x005cddee JNZ 0x005cddf9
|
||||
// 0x005cddf0 CMP byte ptr [EAX],0x0
|
||||
// 0x005cddf3 JZ 0x005cdebc
|
||||
// 0x005cddf9 MOV EAX,dword ptr [EBX + 0x54]
|
||||
// 0x005cddfc CMP EAX,0x2
|
||||
// 0x005cddff JNZ 0x005cde06
|
||||
// 0x005cde01 FLD float ptr [EBP + -0x10]
|
||||
// 0x005cde04 JMP 0x005cde14
|
||||
// 0x005cde06 CMP EAX,0x1
|
||||
// 0x005cde09 JNZ 0x005cde2b
|
||||
// 0x005cde0b FLD float ptr [EBP + -0x10]
|
||||
// 0x005cde0e FMUL float ptr [0x007ffa24]
|
||||
// 0x005cde14 PUSH ECX
|
||||
// 0x005cde15 FCHS
|
||||
// 0x005cde17 MOV ECX,dword ptr [EBX + 0x5c]
|
||||
// 0x005cde1a FSTP float ptr [ESP]
|
||||
// 0x005cde1d SHR ECX,0x7
|
||||
// 0x005cde20 AND ECX,0x1
|
||||
// 0x005cde23 CALL 0x005c7010
|
||||
// 0x005cde28 FSTP float ptr [EBP + -0x48]
|
||||
// 0x005cde2b CMP dword ptr [EBP + -0x74],EDI
|
||||
// 0x005cde2e MOV dword ptr [EBP + -0x24],EDI
|
||||
// 0x005cde31 JZ 0x005cde39
|
||||
// 0x005cde33 MOV EDX,dword ptr [EBP + -0x48]
|
||||
// 0x005cde36 MOV dword ptr [EBP + -0x6c],EDX
|
||||
// 0x005cde39 LEA EAX,[EBP + -0x74]
|
||||
// 0x005cde3c PUSH EAX
|
||||
// 0x005cde3d LEA ECX,[EBP + -0x24]
|
||||
// 0x005cde40 PUSH ECX
|
||||
// 0x005cde41 MOV ECX,dword ptr [EBP + -0x18]
|
||||
// 0x005cde44 LEA EDX,[EBP + -0x48]
|
||||
// 0x005cde47 PUSH EDX
|
||||
// 0x005cde48 LEA EAX,[EBP + -0x30]
|
||||
// 0x005cde4b PUSH EAX
|
||||
// 0x005cde4c PUSH ECX
|
||||
// 0x005cde4d PUSH ESI
|
||||
// 0x005cde4e MOV ECX,EBX
|
||||
// 0x005cde50 CALL 0x005ccbe0
|
||||
// 0x005cde55 MOV ECX,dword ptr [EBX + 0x9c]
|
||||
// 0x005cde5b MOV EAX,dword ptr [EBP + -0x74]
|
||||
// 0x005cde5e INC ECX
|
||||
// 0x005cde5f CMP EAX,0x2
|
||||
// 0x005cde62 MOV dword ptr [EBX + 0x9c],ECX
|
||||
// 0x005cde68 JNZ 0x005cde82
|
||||
// 0x005cde6a SUB ESP,0x20
|
||||
// 0x005cde6d MOV EDI,ESP
|
||||
// 0x005cde6f MOV ECX,0x8
|
||||
// 0x005cde74 LEA ESI,[EBP + -0x70]
|
||||
// 0x005cde77 MOVSD.REP ES:EDI,ESI
|
||||
// 0x005cde79 MOV ECX,EBX
|
||||
// 0x005cde7b CALL 0x005cd310
|
||||
// 0x005cde80 XOR EDI,EDI
|
||||
// 0x005cde82 FLD float ptr [EBP + -0x44]
|
||||
// 0x005cde85 MOV ECX,dword ptr [EBX + 0x60]
|
||||
// 0x005cde88 MOV EDX,dword ptr [EBP + -0x24]
|
||||
// 0x005cde8b FSUB float ptr [EBP + -0x8]
|
||||
// 0x005cde8e MOV AL,byte ptr [EBX + 0x5c]
|
||||
// 0x005cde91 MOV ESI,dword ptr [EBP + -0x1c]
|
||||
// 0x005cde94 OR ECX,EDX
|
||||
// 0x005cde96 FSTP float ptr [EBP + -0x44]
|
||||
// 0x005cde99 TEST AL,0x2
|
||||
// 0x005cde9b MOV dword ptr [EBX + 0x60],ECX
|
||||
// 0x005cde9e JNZ 0x005cdebc
|
||||
// 0x005cdea0 FLD float ptr [EBP + -0x68]
|
||||
// 0x005cdea3 MOV CL,byte ptr [ESI]
|
||||
// 0x005cdea5 TEST CL,CL
|
||||
// 0x005cdea7 FSUB float ptr [EBP + -0x8]
|
||||
// 0x005cdeaa FSTP float ptr [EBP + -0x68]
|
||||
// 0x005cdead FLD float ptr [EBP + -0x70]
|
||||
// 0x005cdeb0 FSUB float ptr [EBP + -0x8]
|
||||
// 0x005cdeb3 FSTP float ptr [EBP + -0x70]
|
||||
// 0x005cdeb6 JNZ 0x005cdd37
|
||||
// 0x005cdebc MOV ECX,EBX
|
||||
// 0x005cdebe CALL 0x005cdf70
|
||||
// 0x005cdec3 TEST byte ptr [EBX + 0x5c],0x20
|
||||
// 0x005cdec7 JZ 0x005cdee2
|
||||
// 0x005cdec9 MOV EAX,dword ptr [EBP + -0x34]
|
||||
// 0x005cdecc CMP EAX,-0x1
|
||||
// 0x005cdecf JNZ 0x005cded6
|
||||
// 0x005cded1 CMP dword ptr [EBP + -0x28],EAX
|
||||
// 0x005cded4 JZ 0x005cdee2
|
||||
// 0x005cded6 MOV ECX,dword ptr [EBP + -0x28]
|
||||
// 0x005cded9 PUSH ECX
|
||||
// 0x005cdeda PUSH EAX
|
||||
// 0x005cdedb MOV ECX,EBX
|
||||
// 0x005cdedd CALL 0x005cd4d0
|
||||
// 0x005cdee2 POP EDI
|
||||
// 0x005cdee3 POP ESI
|
||||
// 0x005cdee4 POP EBX
|
||||
// 0x005cdee5 MOV ESP,EBP
|
||||
// 0x005cdee7 POP EBP
|
||||
// 0x005cdee8 RET
|
||||
@@ -108,7 +108,7 @@ const OtherPending = struct {
|
||||
var g_other_pending: [OTHER_PENDING_SIZE]OtherPending = [1]OtherPending{.{}} ** OTHER_PENDING_SIZE;
|
||||
var g_other_pending_count: i32 = 0;
|
||||
|
||||
var g_cached_visible_item: [19]u32 = .{0} ** 19;
|
||||
|
||||
|
||||
const UNIT_CACHE_SIZE: u32 = 64;
|
||||
|
||||
@@ -193,8 +193,6 @@ fn cachePlayerState() bool {
|
||||
|
||||
const field_index = PLAYER_VISIBLE_ITEM_1_0 + (slot * VISIBLE_ITEM_STRIDE);
|
||||
g_cache.visible_items[i] = hook.readMem(u32, g_cache.player_desc + field_index * 4);
|
||||
|
||||
g_cached_visible_item[i] = g_cache.visible_items[i];
|
||||
}
|
||||
|
||||
g_cache.valid = true;
|
||||
@@ -488,20 +486,23 @@ fn hookSetBlock(obj: u32, index: u32, value: u32) callconv(hook.cc.thiscall) u32
|
||||
if (!g_cache.valid or g_cache.player_obj != obj) {
|
||||
_ = cachePlayerState();
|
||||
}
|
||||
if (val == 0 and g_cached_visible_item[slot] != 0) {
|
||||
// Read live descriptor — g_cached_visible_item can be stale if
|
||||
// another DLL (e.g. VanillaHelpers) writes directly to the
|
||||
// descriptor, bypassing SetBlock and our hook.
|
||||
const current_visible = readUnitVisibleItem(obj, slot);
|
||||
if (val == 0 and current_visible != 0) {
|
||||
// CLEAR detected - check if INV_SLOT is already empty (real unequip)
|
||||
if (g_cache.valid and g_cache.equipped_guids[slot] == 0) {
|
||||
g_cached_visible_item[slot] = 0;
|
||||
return callOriginalSetBlock(obj, index, value);
|
||||
}
|
||||
|
||||
// Start transmog pattern tracking
|
||||
if (!g_local_pending[slot].active) g_local_pending_count += 1;
|
||||
g_local_pending[slot].original_visible_item = g_cached_visible_item[slot];
|
||||
g_local_pending[slot].original_visible_item = current_visible;
|
||||
g_local_pending[slot].timestamp = now;
|
||||
g_local_pending[slot].active = true;
|
||||
g_local_pending[slot].has_durability = false;
|
||||
log.fmt("BLOCK clear slot={d:2} item=0x{X:0>8}\n", .{ slot, g_cached_visible_item[slot] });
|
||||
log.fmt("BLOCK clear slot={d:2} item=0x{X:0>8}\n", .{ slot, current_visible });
|
||||
return 1; // Block the clear
|
||||
} else if (val != 0 and g_local_pending[slot].active) {
|
||||
if (val == g_local_pending[slot].original_visible_item) {
|
||||
@@ -537,10 +538,6 @@ fn hookSetBlock(obj: u32, index: u32, value: u32) callconv(hook.cc.thiscall) u32
|
||||
}
|
||||
}
|
||||
|
||||
// Update cache for non-blocked writes
|
||||
if (val != 0) {
|
||||
g_cached_visible_item[slot] = val;
|
||||
}
|
||||
} else if (g_enabled) {
|
||||
// =========== OTHER PLAYERS ===========
|
||||
const guid = getUnitGuid(obj);
|
||||
@@ -628,7 +625,6 @@ fn hookSetBlock(obj: u32, index: u32, value: u32) callconv(hook.cc.thiscall) u32
|
||||
const field_index = PLAYER_VISIBLE_ITEM_1_0 + (@as(u32, @intCast(es)) * VISIBLE_ITEM_STRIDE);
|
||||
_ = callOriginalSetBlock(obj, field_index, 0);
|
||||
|
||||
g_cached_visible_item[es] = 0;
|
||||
g_local_pending[es].active = false;
|
||||
g_local_pending[es].has_durability = false;
|
||||
g_local_pending_count -= 1;
|
||||
@@ -780,7 +776,6 @@ pub fn installHooks() void {
|
||||
g_other_pending_count = 0;
|
||||
g_cache = .{};
|
||||
g_unit_cache = [1]UnitVisualState{.{}} ** UNIT_CACHE_SIZE;
|
||||
g_cached_visible_item = .{0} ** 19;
|
||||
|
||||
// Hook 1: SetBlock
|
||||
if (set_block_hook.attach(ADDR_SetBlock, &hookSetBlock) != .ok) return;
|
||||
|
||||
@@ -1098,7 +1098,7 @@ pub fn calcScaledInverse(this_mat: u32, out: u32, scale: f32) void {
|
||||
// mat3(offset_vec3*), mat4(scale_float_bits)
|
||||
// =============================================================================
|
||||
|
||||
pub fn transformImpl_SSE(this: u32, mat1: u32, mat2: u32, mat3: u32, mat4: u32) callconv(.{ .x86_thiscall = .{} }) void {
|
||||
pub export fn transformImpl_SSE(this: u32, mat1: u32, mat2: u32, mat3: u32, mat4: u32) callconv(.{ .x86_thiscall = .{} }) void {
|
||||
|
||||
@setEvalBranchQuota(50000);
|
||||
// =========================================================================
|
||||
|
||||
@@ -284,7 +284,7 @@ fn attachmentRecursion64(this: u32, model_hdr: u32, bone_out_base: u32, frame_ct
|
||||
// scene objects stay on the f64 pipeline.
|
||||
// =============================================================================
|
||||
|
||||
pub fn transformImpl_SSE64(this: u32, mat1: u32, mat2: u32, mat3: u32, mat4: u32) callconv(.{ .x86_thiscall = .{} }) void {
|
||||
pub export fn transformImpl_SSE64(this: u32, mat1: u32, mat2: u32, mat3: u32, mat4: u32) callconv(.{ .x86_thiscall = .{} }) void {
|
||||
@setEvalBranchQuota(50000);
|
||||
|
||||
// Section 1: Entry checks
|
||||
|
||||
@@ -0,0 +1,564 @@
|
||||
//! transform_capture — snapshot transformMatrix4x4 inputs + outputs for offline
|
||||
//! bench/parity replay. Enabled via a feature flag in weirdperformance.zig.
|
||||
//!
|
||||
//! Design:
|
||||
//! - Hook transformMatrix4x4 (0x714260).
|
||||
//! - On entry: compute a path hash from SceneObject flags + model_hdr counts.
|
||||
//! Skip if already seen.
|
||||
//! - Walk reachable pointer graph; record each referenced memory page
|
||||
//! (4KB granularity, keyed by page address). Save INPUT snapshot.
|
||||
//! - Call the game original via callOriginal.
|
||||
//! - Save OUTPUT snapshot of all pages we know get written.
|
||||
//! - Write to disk: one .trxcap file per unique path hash.
|
||||
//!
|
||||
//! Replay (bench):
|
||||
//! - Read .trxcap file.
|
||||
//! - For each page in the input snapshot, MAP_FIXED at its VA, copy data.
|
||||
//! - Call transformImpl_SSE64 (or SSE or game's x87).
|
||||
//! - Diff memory against the saved output snapshot.
|
||||
//!
|
||||
//! This captures real game state and gives us true parity coverage, but has a
|
||||
//! state-pollution avoidance problem in-game: we can't run *both* game x87 and
|
||||
//! our SSE impls on the same live SceneObject without state bleeding between
|
||||
//! calls. The solution is to replay offline with each impl starting from a
|
||||
//! fresh copy of the captured input snapshot.
|
||||
|
||||
const std = @import("std");
|
||||
const hook = @import("zhook");
|
||||
const logging = @import("../logging.zig");
|
||||
|
||||
var log: logging.Logger = .{};
|
||||
|
||||
// Win32 file/dir APIs — we run inside a Windows DLL, no libc filesystem.
|
||||
const HANDLE = *anyopaque;
|
||||
const INVALID_HANDLE: HANDLE = @ptrFromInt(std.math.maxInt(usize));
|
||||
extern "kernel32" fn CreateFileA(
|
||||
name: [*:0]const u8,
|
||||
desiredAccess: u32,
|
||||
shareMode: u32,
|
||||
security: ?*anyopaque,
|
||||
creationDisposition: u32,
|
||||
flags: u32,
|
||||
template: ?HANDLE,
|
||||
) callconv(.{ .x86_stdcall = .{} }) HANDLE;
|
||||
extern "kernel32" fn WriteFile(
|
||||
h: HANDLE,
|
||||
buf: [*]const u8,
|
||||
bytes: u32,
|
||||
written: *u32,
|
||||
ov: ?*anyopaque,
|
||||
) callconv(.{ .x86_stdcall = .{} }) i32;
|
||||
extern "kernel32" fn CloseHandle(h: HANDLE) callconv(.{ .x86_stdcall = .{} }) i32;
|
||||
extern "kernel32" fn CreateDirectoryA(name: [*:0]const u8, security: ?*anyopaque) callconv(.{ .x86_stdcall = .{} }) i32;
|
||||
extern "kernel32" fn SetFilePointer(h: HANDLE, lo: i32, hi: ?*i32, method: u32) callconv(.{ .x86_stdcall = .{} }) u32;
|
||||
|
||||
const GENERIC_WRITE: u32 = 0x40000000;
|
||||
const FILE_SHARE_READ: u32 = 0x1;
|
||||
const CREATE_ALWAYS: u32 = 2;
|
||||
const OPEN_ALWAYS: u32 = 4;
|
||||
const FILE_ATTRIBUTE_NORMAL: u32 = 0x80;
|
||||
const FILE_END: u32 = 2;
|
||||
|
||||
fn writeAll(h: HANDLE, data: []const u8) bool {
|
||||
var written: u32 = 0;
|
||||
const ok = WriteFile(h, data.ptr, @intCast(data.len), &written, null);
|
||||
return ok != 0 and written == data.len;
|
||||
}
|
||||
|
||||
pub const module_name: [*:0]const u8 = "transform_capture";
|
||||
|
||||
// Hook into game transformMatrix4x4
|
||||
const TransformFn = fn (u32, u32, u32, u32, u32) callconv(.{ .x86_thiscall = .{} }) void;
|
||||
var transform_hook: hook.Detour(TransformFn) = .{};
|
||||
|
||||
// =============================================================================
|
||||
// SceneObject offsets (same as bone_sse, duplicated here so we don't couple)
|
||||
// =============================================================================
|
||||
|
||||
const SO_MODEL_DATA_PTR: u32 = 0x010;
|
||||
const SO_ANIM_CTX_PTR: u32 = 0x02C;
|
||||
const SO_MODEL_CTR_PTR: u32 = 0x030;
|
||||
const SO_SYNC_VALUE: u32 = 0x040;
|
||||
const SO_BONE_RT_BASE: u32 = 0x090;
|
||||
const SO_BONE_OUT_PTR: u32 = 0x094;
|
||||
const SO_TEX_ANIM_OUT: u32 = 0x0A0;
|
||||
const SO_COLOR_ANIM_OUT: u32 = 0x0A8;
|
||||
const SO_SCALE1: u32 = 0x0AC;
|
||||
const SO_SCALE2: u32 = 0x0B0;
|
||||
const SO_SCALE3: u32 = 0x0B4;
|
||||
const SO_HIERARCHY_PTR: u32 = 0x1C8;
|
||||
const SO_HIERARCHY_IDX: u32 = 0x1DC;
|
||||
const SO_FIELD_200: u32 = 0x200;
|
||||
const SO_PARTICLE1: u32 = 0x3C4;
|
||||
const SO_PARTICLE2: u32 = 0x3C8;
|
||||
const SO_PARTICLE3: u32 = 0x3D0;
|
||||
const SO_PARTICLE4: u32 = 0x3D4;
|
||||
|
||||
// =============================================================================
|
||||
// Memory helpers
|
||||
// =============================================================================
|
||||
|
||||
inline fn ru32(addr: u32) u32 {
|
||||
return @as(*const u32, @ptrFromInt(addr)).*;
|
||||
}
|
||||
inline fn ru16(addr: u32) u16 {
|
||||
return @as(*align(1) const u16, @ptrFromInt(addr)).*;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Page capture infrastructure
|
||||
// =============================================================================
|
||||
|
||||
const PAGE_SIZE: u32 = 0x1000;
|
||||
const PAGE_MASK: u32 = 0xFFFFF000;
|
||||
const MAX_CAPTURES: usize = 64; // cap on unique-hash scenarios we record
|
||||
const MAX_PAGES_PER_CAPTURE: usize = 512; // each capture can touch up to 2MB
|
||||
|
||||
/// One captured page's raw bytes plus its VA.
|
||||
const PageBlock = extern struct {
|
||||
va: u32,
|
||||
data: [PAGE_SIZE]u8,
|
||||
};
|
||||
|
||||
/// One captured scenario: input pages + output pages + entry args.
|
||||
const Capture = struct {
|
||||
path_hash: u32 = 0,
|
||||
this: u32 = 0,
|
||||
mat1: u32 = 0,
|
||||
mat2: u32 = 0,
|
||||
mat3: u32 = 0,
|
||||
mat4: u32 = 0,
|
||||
|
||||
input_page_count: u32 = 0,
|
||||
output_page_count: u32 = 0,
|
||||
input_pages: [*]PageBlock = undefined,
|
||||
output_pages: [*]PageBlock = undefined,
|
||||
};
|
||||
|
||||
// One scratch slot — we write captures to disk immediately, so only one in-
|
||||
// flight capture needs memory at a time.
|
||||
var scratch: Capture = .{};
|
||||
var scratch_input_pages: [MAX_PAGES_PER_CAPTURE]PageBlock align(16) = undefined;
|
||||
var scratch_output_pages: [MAX_PAGES_PER_CAPTURE]PageBlock align(16) = undefined;
|
||||
|
||||
// Deduplication: array of hashes we've already dumped.
|
||||
var seen_hashes: [MAX_CAPTURES]u32 = [_]u32{0} ** MAX_CAPTURES;
|
||||
var seen_hash_count: usize = 0;
|
||||
|
||||
// Re-entry guard (transformMatrix4x4 recurses into itself for attachments).
|
||||
var in_capture: bool = false;
|
||||
|
||||
// Total captures written (== seen_hash_count, kept separate for clarity)
|
||||
var capture_count: usize = 0;
|
||||
|
||||
// Total hook calls seen (for coverage statistics)
|
||||
var total_calls: u64 = 0;
|
||||
|
||||
// Captures-written-per-log-slot so we can track distribution of hash values
|
||||
var call_counts_per_hash: [MAX_CAPTURES]u32 = [_]u32{0} ** MAX_CAPTURES;
|
||||
|
||||
// How many consecutive calls have yielded no new hash (for "coverage saturated" heuristic).
|
||||
var calls_since_new_hash: u64 = 0;
|
||||
|
||||
// Output directory — set at install time. Default = current working dir.
|
||||
var capture_dir: []const u8 = ".";
|
||||
|
||||
// Per-capture scratch: pages we've already saved this iteration.
|
||||
var seen_pages: [MAX_PAGES_PER_CAPTURE]u32 = [_]u32{0} ** MAX_PAGES_PER_CAPTURE;
|
||||
var seen_count: usize = 0;
|
||||
|
||||
fn seenAdd(page_va: u32) bool {
|
||||
// Linear scan; MAX_PAGES_PER_CAPTURE is small enough that this is cheap.
|
||||
for (seen_pages[0..seen_count]) |p| if (p == page_va) return false;
|
||||
if (seen_count >= MAX_PAGES_PER_CAPTURE) return false;
|
||||
seen_pages[seen_count] = page_va;
|
||||
seen_count += 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
fn seenReset() void {
|
||||
seen_count = 0;
|
||||
}
|
||||
|
||||
/// Copy one 4KB page from game memory into the destination array.
|
||||
/// Returns true if the page was new (added to seen list).
|
||||
fn captureOnePage(addr: u32, dst: [*]PageBlock, idx: *u32) bool {
|
||||
const page_va = addr & PAGE_MASK;
|
||||
if (!seenAdd(page_va)) return false;
|
||||
if (idx.* >= MAX_PAGES_PER_CAPTURE) return false;
|
||||
dst[idx.*].va = page_va;
|
||||
@memcpy(&dst[idx.*].data, @as([*]const u8, @ptrFromInt(page_va))[0..PAGE_SIZE]);
|
||||
idx.* += 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Capture a memory range (rounded to pages).
|
||||
fn capturePagesCovering(addr: u32, size: u32, dst: [*]PageBlock, idx: *u32) void {
|
||||
if (addr == 0 or size == 0) return;
|
||||
const start_page = addr & PAGE_MASK;
|
||||
const end_addr = addr +% size;
|
||||
var cur = start_page;
|
||||
while (cur < end_addr) : (cur +%= PAGE_SIZE) {
|
||||
_ = captureOnePage(cur, dst, idx);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Graph walk — identify all pages reachable from the SceneObject that
|
||||
// transformMatrix4x4 might read.
|
||||
// =============================================================================
|
||||
|
||||
fn walkAndCapture(this: u32, mat1: u32, mat2: u32, mat3: u32, mat4: u32, dst: [*]PageBlock, idx: *u32) void {
|
||||
_ = mat4;
|
||||
|
||||
// Direct pages
|
||||
capturePagesCovering(this, 0x400, dst, idx);
|
||||
capturePagesCovering(mat1, 0x40, dst, idx);
|
||||
capturePagesCovering(mat2, 0x0C, dst, idx);
|
||||
capturePagesCovering(mat3, 0x0C, dst, idx);
|
||||
|
||||
// Following pointers
|
||||
const anim_ctx = ru32(this + SO_ANIM_CTX_PTR);
|
||||
capturePagesCovering(anim_ctx, 0x20, dst, idx);
|
||||
|
||||
const model_ctr = ru32(this + SO_MODEL_CTR_PTR);
|
||||
capturePagesCovering(model_ctr, 0x140, dst, idx);
|
||||
if (model_ctr == 0) return;
|
||||
|
||||
const model_hdr = ru32(model_ctr + 0x130);
|
||||
capturePagesCovering(model_hdr, 0x200, dst, idx);
|
||||
if (model_hdr == 0) return;
|
||||
|
||||
// model_hdr reference arrays
|
||||
const gs_durations = ru32(model_hdr + 0x18);
|
||||
const gs_count = ru32(model_hdr + 0x14);
|
||||
capturePagesCovering(gs_durations, gs_count * 4, dst, idx);
|
||||
|
||||
const anim_lookup = ru32(model_hdr + 0x20);
|
||||
capturePagesCovering(anim_lookup, 0x1000, dst, idx); // conservative; actual size unknown
|
||||
|
||||
const bone_count = ru32(model_hdr + 0x34);
|
||||
const bone_defs = ru32(model_hdr + 0x38);
|
||||
capturePagesCovering(bone_defs, bone_count * 0x6C, dst, idx);
|
||||
|
||||
// bone_rt / bone_out
|
||||
const bone_rt = ru32(this + SO_BONE_RT_BASE);
|
||||
capturePagesCovering(bone_rt, bone_count * 0x118, dst, idx);
|
||||
const bone_out = ru32(this + SO_BONE_OUT_PTR);
|
||||
capturePagesCovering(bone_out, bone_count * 0x40, dst, idx);
|
||||
|
||||
// AnimData inside bone_defs references keyframe arrays
|
||||
var bi: u32 = 0;
|
||||
var bd = bone_defs;
|
||||
while (bi < bone_count) : ({
|
||||
bi += 1;
|
||||
bd += 0x6C;
|
||||
}) {
|
||||
// Three animation tracks per bone (trans/rot/scale)
|
||||
inline for ([_]u32{ 0x0C, 0x28, 0x44 }) |track_off| {
|
||||
const ad = bd + track_off;
|
||||
const ranges = ru32(ad + 0x08);
|
||||
const ranges_count = ru32(ad + 0x04); // nRanges
|
||||
capturePagesCovering(ranges, ranges_count * 8, dst, idx);
|
||||
const ts = ru32(ad + 0x10);
|
||||
const kf_count = ru32(ad + 0x0C);
|
||||
capturePagesCovering(ts, kf_count * 4, dst, idx);
|
||||
const kf_base = ru32(ad + 0x18);
|
||||
// Stride varies by track — trans/scale=12, rot=16, but spline modes
|
||||
// use 36. Conservative: use 36 × kf_count per track.
|
||||
capturePagesCovering(kf_base, kf_count * 36, dst, idx);
|
||||
}
|
||||
}
|
||||
|
||||
// Output-buffer pages (will be WRITTEN but we need to capture input state
|
||||
// too in case the game reads before writing)
|
||||
capturePagesCovering(ru32(this + SO_TEX_ANIM_OUT), 0x200, dst, idx);
|
||||
capturePagesCovering(ru32(this + SO_COLOR_ANIM_OUT), 0x200, dst, idx);
|
||||
capturePagesCovering(ru32(this + SO_SCALE1), 0x200, dst, idx);
|
||||
capturePagesCovering(ru32(this + SO_SCALE2), 0x800, dst, idx);
|
||||
capturePagesCovering(ru32(this + SO_SCALE3), 0x800, dst, idx);
|
||||
capturePagesCovering(ru32(this + SO_HIERARCHY_PTR), 0x800, dst, idx);
|
||||
capturePagesCovering(ru32(this + SO_FIELD_200), 0x800, dst, idx);
|
||||
capturePagesCovering(ru32(this + SO_PARTICLE1), 0x800, dst, idx);
|
||||
capturePagesCovering(ru32(this + SO_PARTICLE2), 0x800, dst, idx);
|
||||
capturePagesCovering(ru32(this + SO_PARTICLE3), 0x800, dst, idx);
|
||||
capturePagesCovering(ru32(this + SO_PARTICLE4), 0x400, dst, idx);
|
||||
|
||||
// Attachment data
|
||||
const attach_count = ru32(model_hdr + 0x104);
|
||||
const attach_data = ru32(model_hdr + 0x108);
|
||||
capturePagesCovering(attach_data, attach_count * 0x30, dst, idx);
|
||||
|
||||
// Texture/color/word anim data
|
||||
inline for ([_]struct { cnt: u32, dat: u32, stride: u32 }{
|
||||
.{ .cnt = 0x54, .dat = 0x58, .stride = 0x34 }, // tex anim
|
||||
.{ .cnt = 0x64, .dat = 0x68, .stride = 0x28 }, // color anim
|
||||
.{ .cnt = 0x74, .dat = 0x78, .stride = 0x1C }, // word anim
|
||||
.{ .cnt = 0xAC, .dat = 0xB0, .stride = 0x54 }, // bone keyframe
|
||||
.{ .cnt = 0x11C, .dat = 0x120, .stride = 0xD4 }, // ribbon emitter
|
||||
.{ .cnt = 0x124, .dat = 0x128, .stride = 0x7C }, // particle emitter
|
||||
.{ .cnt = 0x134, .dat = 0x138, .stride = 0xDC }, // partsec
|
||||
.{ .cnt = 0x13C, .dat = 0x140, .stride = 0x1F8 }, // partlarge
|
||||
}) |sec| {
|
||||
const c = ru32(model_hdr + sec.cnt);
|
||||
const d = ru32(model_hdr + sec.dat);
|
||||
capturePagesCovering(d, c * sec.stride, dst, idx);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Path hash — distinguishes different code paths through transformMatrix4x4.
|
||||
// Cheap to compute; aim to hit every major branch.
|
||||
// =============================================================================
|
||||
|
||||
fn pathHash(this: u32) u32 {
|
||||
var h: u32 = 0x811C9DC5;
|
||||
|
||||
const model_ctr = ru32(this + SO_MODEL_CTR_PTR);
|
||||
if (model_ctr == 0) return h;
|
||||
const model_hdr = ru32(model_ctr + 0x130);
|
||||
if (model_hdr == 0) return h;
|
||||
|
||||
const bone_count = ru32(model_hdr + 0x34);
|
||||
h = (h ^ bone_count) *% 0x01000193;
|
||||
|
||||
const bone_defs = ru32(model_hdr + 0x38);
|
||||
if (bone_count > 0 and bone_defs != 0) {
|
||||
// Flags of first few bones (encodes billboard types, etc.)
|
||||
const max_bones = @min(bone_count, 6);
|
||||
var bi: u32 = 0;
|
||||
while (bi < max_bones) : (bi += 1) {
|
||||
const flags = ru32(bone_defs + bi * 0x6C + 0x04);
|
||||
h = (h ^ flags) *% 0x01000193;
|
||||
}
|
||||
}
|
||||
|
||||
// Which post-bone-loop sections are populated?
|
||||
inline for ([_]u32{ 0x14, 0x54, 0x64, 0x74, 0xAC, 0x104, 0x11C, 0x124, 0x134, 0x13C }) |off| {
|
||||
const v = ru32(model_hdr + off);
|
||||
h = (h ^ (if (v == 0) @as(u32, 0) else 1)) *% 0x01000193;
|
||||
}
|
||||
|
||||
// emitter_ctx presence
|
||||
const emitter_ctx = ru32(this + 0x1CC);
|
||||
h = (h ^ (if (emitter_ctx == 0) @as(u32, 0) else 1)) *% 0x01000193;
|
||||
|
||||
return h;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Detour
|
||||
// =============================================================================
|
||||
|
||||
fn transformDetour(this: u32, mat1: u32, mat2: u32, mat3: u32, mat4: u32) callconv(.{ .x86_thiscall = .{} }) void {
|
||||
// Re-entry guard: transformMatrix4x4 recurses for attachment children.
|
||||
if (in_capture) {
|
||||
transform_hook.callOriginal(.{ this, mat1, mat2, mat3, mat4 });
|
||||
return;
|
||||
}
|
||||
|
||||
total_calls +%= 1;
|
||||
|
||||
if (capture_count < MAX_CAPTURES) {
|
||||
const h = pathHash(this);
|
||||
var known_idx: ?usize = null;
|
||||
for (seen_hashes[0..seen_hash_count], 0..) |sh, i| if (sh == h) {
|
||||
known_idx = i;
|
||||
break;
|
||||
};
|
||||
if (known_idx) |idx| {
|
||||
call_counts_per_hash[idx] +%= 1;
|
||||
calls_since_new_hash +%= 1;
|
||||
} else {
|
||||
in_capture = true;
|
||||
defer in_capture = false;
|
||||
|
||||
scratch.path_hash = h;
|
||||
scratch.this = this;
|
||||
scratch.mat1 = mat1;
|
||||
scratch.mat2 = mat2;
|
||||
scratch.mat3 = mat3;
|
||||
scratch.mat4 = mat4;
|
||||
scratch.input_page_count = 0;
|
||||
scratch.output_page_count = 0;
|
||||
|
||||
seenReset();
|
||||
walkAndCapture(this, mat1, mat2, mat3, mat4, &scratch_input_pages, &scratch.input_page_count);
|
||||
|
||||
transform_hook.callOriginal(.{ this, mat1, mat2, mat3, mat4 });
|
||||
|
||||
var oi: u32 = 0;
|
||||
var pi: usize = 0;
|
||||
while (pi < scratch.input_page_count) : (pi += 1) {
|
||||
if (oi >= MAX_PAGES_PER_CAPTURE) break;
|
||||
scratch_output_pages[oi].va = scratch_input_pages[pi].va;
|
||||
@memcpy(&scratch_output_pages[oi].data, @as([*]const u8, @ptrFromInt(scratch_input_pages[pi].va))[0..PAGE_SIZE]);
|
||||
oi += 1;
|
||||
}
|
||||
scratch.output_page_count = oi;
|
||||
scratch.input_pages = &scratch_input_pages;
|
||||
scratch.output_pages = &scratch_output_pages;
|
||||
|
||||
const dump_err = dumpOneToDir(capture_dir, scratch);
|
||||
const cov_err = appendCoverageLog(h, scratch.input_page_count);
|
||||
log.fmt("capture: hash=0x{x:08} pages={d} dump={any} cov={any}\n", .{
|
||||
h, scratch.input_page_count, dump_err, cov_err,
|
||||
});
|
||||
|
||||
if (seen_hash_count < MAX_CAPTURES) {
|
||||
seen_hashes[seen_hash_count] = h;
|
||||
call_counts_per_hash[seen_hash_count] = 1;
|
||||
seen_hash_count += 1;
|
||||
}
|
||||
capture_count += 1;
|
||||
calls_since_new_hash = 0;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
calls_since_new_hash +%= 1;
|
||||
}
|
||||
|
||||
transform_hook.callOriginal(.{ this, mat1, mat2, mat3, mat4 });
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// File format
|
||||
// =============================================================================
|
||||
|
||||
const MAGIC: [4]u8 = .{ 'T', 'R', 'X', 'C' };
|
||||
const VERSION: u32 = 1;
|
||||
|
||||
const FileHeader = extern struct {
|
||||
magic: [4]u8 = MAGIC,
|
||||
version: u32 = VERSION,
|
||||
path_hash: u32,
|
||||
this: u32,
|
||||
mat1: u32,
|
||||
mat2: u32,
|
||||
mat3: u32,
|
||||
mat4: u32,
|
||||
input_page_count: u32,
|
||||
output_page_count: u32,
|
||||
};
|
||||
|
||||
/// Build a null-terminated path: "<dir>/<filename>".
|
||||
fn buildPath(buf: []u8, dir: []const u8, filename: []const u8) ![:0]const u8 {
|
||||
const total = dir.len + 1 + filename.len + 1;
|
||||
if (total > buf.len) return error.NoSpaceLeft;
|
||||
@memcpy(buf[0..dir.len], dir);
|
||||
buf[dir.len] = '/';
|
||||
@memcpy(buf[dir.len + 1 ..][0..filename.len], filename);
|
||||
buf[dir.len + 1 + filename.len] = 0;
|
||||
return buf[0 .. dir.len + 1 + filename.len :0];
|
||||
}
|
||||
|
||||
fn buildDirZ(buf: []u8, dir: []const u8) ![:0]const u8 {
|
||||
if (dir.len + 1 > buf.len) return error.NoSpaceLeft;
|
||||
@memcpy(buf[0..dir.len], dir);
|
||||
buf[dir.len] = 0;
|
||||
return buf[0..dir.len :0];
|
||||
}
|
||||
|
||||
fn dumpOneToDir(dir_path: []const u8, c: Capture) !void {
|
||||
var dir_buf: [256]u8 = undefined;
|
||||
const dir_z = try buildDirZ(&dir_buf, dir_path);
|
||||
_ = CreateDirectoryA(dir_z.ptr, null); // ignore result: already-exists is fine
|
||||
|
||||
var name_buf: [64]u8 = undefined;
|
||||
const fname = try std.fmt.bufPrint(&name_buf, "{x:08}.trxcap", .{c.path_hash});
|
||||
|
||||
var path_buf: [512]u8 = undefined;
|
||||
const path = try buildPath(&path_buf, dir_path, fname);
|
||||
|
||||
const h = CreateFileA(path.ptr, GENERIC_WRITE, FILE_SHARE_READ, null, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, null);
|
||||
if (h == INVALID_HANDLE) return error.CreateFailed;
|
||||
defer _ = CloseHandle(h);
|
||||
|
||||
const hdr = FileHeader{
|
||||
.path_hash = c.path_hash,
|
||||
.this = c.this,
|
||||
.mat1 = c.mat1,
|
||||
.mat2 = c.mat2,
|
||||
.mat3 = c.mat3,
|
||||
.mat4 = c.mat4,
|
||||
.input_page_count = c.input_page_count,
|
||||
.output_page_count = c.output_page_count,
|
||||
};
|
||||
if (!writeAll(h, std.mem.asBytes(&hdr))) return error.WriteFailed;
|
||||
if (!writeAll(h, @as([*]const u8, @ptrCast(c.input_pages))[0 .. c.input_page_count * @sizeOf(PageBlock)])) return error.WriteFailed;
|
||||
if (!writeAll(h, @as([*]const u8, @ptrCast(c.output_pages))[0 .. c.output_page_count * @sizeOf(PageBlock)])) return error.WriteFailed;
|
||||
}
|
||||
|
||||
/// Append a line to coverage.log whenever we see a new path hash.
|
||||
fn appendCoverageLog(new_hash: u32, page_count: u32) !void {
|
||||
var dir_buf: [256]u8 = undefined;
|
||||
const dir_z = try buildDirZ(&dir_buf, capture_dir);
|
||||
_ = CreateDirectoryA(dir_z.ptr, null);
|
||||
|
||||
var path_buf: [512]u8 = undefined;
|
||||
const path = try buildPath(&path_buf, capture_dir, "coverage.log");
|
||||
|
||||
const h = CreateFileA(path.ptr, GENERIC_WRITE, FILE_SHARE_READ, null, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, null);
|
||||
if (h == INVALID_HANDLE) return error.CreateFailed;
|
||||
defer _ = CloseHandle(h);
|
||||
_ = SetFilePointer(h, 0, null, FILE_END);
|
||||
|
||||
var line_buf: [256]u8 = undefined;
|
||||
const line = try std.fmt.bufPrint(&line_buf, "[#{d}] new hash 0x{x:08} (total_calls={d}, pages={d})\n", .{
|
||||
seen_hash_count + 1, new_hash, total_calls, page_count,
|
||||
});
|
||||
if (!writeAll(h, line)) return error.WriteFailed;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Install / Remove
|
||||
// =============================================================================
|
||||
|
||||
/// Install with the given output directory (relative to WoW's current working
|
||||
/// directory, usually the game install folder). Captures + coverage.log will
|
||||
/// land there. Pass "." to write them next to WoW.exe.
|
||||
pub fn install() bool {
|
||||
return installTo("transform_captures");
|
||||
}
|
||||
|
||||
pub fn installTo(dir: []const u8) bool {
|
||||
log = logging.Logger.open("transform_capture", .both);
|
||||
capture_dir = dir;
|
||||
const ok = transform_hook.attach(0x714260, &transformDetour) == .ok;
|
||||
log.fmt("install: hook_attach={any} dir={s}\n", .{ ok, dir });
|
||||
|
||||
// Smoke-test the write path so we know if file I/O is broken before any
|
||||
// transform calls arrive.
|
||||
var dir_buf: [256]u8 = undefined;
|
||||
const dir_z = buildDirZ(&dir_buf, capture_dir) catch return ok;
|
||||
const mkdir_ok = CreateDirectoryA(dir_z.ptr, null);
|
||||
log.fmt("install: mkdir returned {d}\n", .{mkdir_ok});
|
||||
|
||||
var path_buf: [512]u8 = undefined;
|
||||
const path = buildPath(&path_buf, capture_dir, "install_test.txt") catch return ok;
|
||||
const h = CreateFileA(path.ptr, GENERIC_WRITE, FILE_SHARE_READ, null, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, null);
|
||||
if (h == INVALID_HANDLE) {
|
||||
log.fmt("install: CreateFileA FAILED for {s}\n", .{path});
|
||||
} else {
|
||||
_ = writeAll(h, "transform_capture install test\n");
|
||||
_ = CloseHandle(h);
|
||||
log.fmt("install: wrote install_test.txt ok\n", .{});
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
pub fn remove() void {
|
||||
transform_hook.detach();
|
||||
}
|
||||
|
||||
pub fn getCaptureCount() usize {
|
||||
return capture_count;
|
||||
}
|
||||
|
||||
pub fn getTotalCalls() u64 {
|
||||
return total_calls;
|
||||
}
|
||||
|
||||
pub fn getCallsSinceNewHash() u64 {
|
||||
return calls_since_new_hash;
|
||||
}
|
||||
@@ -19,6 +19,7 @@ const mod_mutex = @import("../mutex.zig");
|
||||
const inflate_hook = @import("inflate_hook.zig");
|
||||
const timer_fix = @import("timer_fix.zig");
|
||||
const filecache = @import("filecache.zig");
|
||||
const transform_capture = @import("transform_capture.zig");
|
||||
|
||||
pub const module_name: [*:0]const u8 = "weirdperformance";
|
||||
|
||||
@@ -364,8 +365,17 @@ pub fn installHooks() void {
|
||||
log = logging.Logger.open(module_name, .both);
|
||||
var installed: u32 = 0;
|
||||
|
||||
// Bone transform SSE
|
||||
if (transform_hook.attach(0x714260, &bone_sse64.transformImpl_SSE64) == .ok) installed += 1;
|
||||
// Bone transform: either the f64 SSE impl for normal operation, OR the
|
||||
// transform_capture hook for recording game-x87 state to disk.
|
||||
// Compile-time flag picks one: the two are mutually exclusive because
|
||||
// capture requires the game's x87 output (bone_sse64 would replace it).
|
||||
const build_options = @import("build_options");
|
||||
const capture_mode = @hasDecl(build_options, "transform_capture") and build_options.transform_capture;
|
||||
if (capture_mode) {
|
||||
if (transform_capture.install()) installed += 1;
|
||||
} else {
|
||||
if (transform_hook.attach(0x714260, &bone_sse64.transformImpl_SSE64) == .ok) installed += 1;
|
||||
}
|
||||
|
||||
// Frustum clip SSE (1.9x speedup)
|
||||
if (clip_hook.attach(0x6318C0, &clip_sse.clipPolygonToSinglePlane) == .ok) installed += 1;
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# MPQ Load Order — WoW 1.12.1
|
||||
|
||||
`MPQ_InitializeArchives` (0x403740) loads in two phases.
|
||||
|
||||
## Phase 1 — Base archives (loaded by index)
|
||||
|
||||
Filename table at `0x82e12c`:
|
||||
|
||||
| Index | Archive |
|
||||
|-------|---------|
|
||||
| 0 | model.MPQ |
|
||||
| 1 | texture.MPQ |
|
||||
| 2 | terrain.MPQ |
|
||||
| 3 | wmo.MPQ |
|
||||
| 4 | sound.MPQ |
|
||||
| 5 | misc.MPQ |
|
||||
| 6 | interface.MPQ |
|
||||
| 7 | fonts.MPQ |
|
||||
| 8 | speech.MPQ |
|
||||
| 9 | dbc.MPQ |
|
||||
| 10 | speech2.MPQ |
|
||||
|
||||
Each opened via `OpenMPQArchiveWithPaths` (0x403b00) which tries `Data\name` then `..\Data\name`.
|
||||
|
||||
## Phase 2 — Patch archives
|
||||
|
||||
- `MPQArchiveEnumerator` (0x4039b0) discovers `patch-?.MPQ` files via glob
|
||||
- Then opens `patch.MPQ` and any discovered `patch-X.MPQ` archives
|
||||
|
||||
## Search order at file lookup time
|
||||
|
||||
`File_FindInArchive` (0x6549a0) searches the global archive array at `0x8826b4` recursively — later-registered archives (patches) are checked first. The array is searched from `count-1` down to `0`, so patches override base content.
|
||||
|
||||
## Global archive array
|
||||
|
||||
```
|
||||
Struct at 0x8826b4:
|
||||
+0x00 [0x8826b4]: capacity
|
||||
+0x04 [0x8826b8]: count
|
||||
+0x08 [0x8826bc]: array_ptr (SArchive**)
|
||||
+0x0C [0x8826c0]: growth_incr
|
||||
```
|
||||
@@ -0,0 +1,143 @@
|
||||
-- MarkerPresets: save and restore world marker layouts.
|
||||
-- Demonstrates the WorldMarker / GetWorldMarker / ClearWorldMarker API.
|
||||
--
|
||||
-- /mp save <name> — snapshot all current marker positions
|
||||
-- /mp place <name> — restore a saved layout (places all markers at once)
|
||||
-- /mp clear — clear all markers
|
||||
-- /mp list — list saved presets
|
||||
-- /mp delete <name> — delete a preset
|
||||
|
||||
local NUM_MARKERS = 5
|
||||
local PREFIX = "|cff00ccff[Markers]|r "
|
||||
|
||||
local function msg(text)
|
||||
DEFAULT_CHAT_FRAME:AddMessage(PREFIX .. text)
|
||||
end
|
||||
|
||||
local function savePreset(name)
|
||||
if not name or name == "" then
|
||||
msg("Usage: /mp save <name>")
|
||||
return
|
||||
end
|
||||
|
||||
local preset = {}
|
||||
local count = 0
|
||||
for i = 1, NUM_MARKERS do
|
||||
local x, y, z, area = GetWorldMarker(i)
|
||||
if x then
|
||||
preset[i] = { x = x, y = y, z = z, area = area }
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
|
||||
if count == 0 then
|
||||
msg("No markers to save.")
|
||||
return
|
||||
end
|
||||
|
||||
MarkerPresets_Saved[name] = preset
|
||||
msg("Saved |cffffffff" .. name .. "|r (" .. count .. " marker" .. (count > 1 and "s" or "") .. ")")
|
||||
end
|
||||
|
||||
local function placePreset(name)
|
||||
if not name or name == "" then
|
||||
msg("Usage: /mp place <name>")
|
||||
return
|
||||
end
|
||||
|
||||
local preset = MarkerPresets_Saved[name]
|
||||
if not preset then
|
||||
msg("No preset named |cffffffff" .. name .. "|r")
|
||||
return
|
||||
end
|
||||
|
||||
if not CanSetWorldMarkers() then
|
||||
msg("|cffff4444No permission|r (need leader or assist)")
|
||||
return
|
||||
end
|
||||
|
||||
-- Clear existing markers, then place the saved ones
|
||||
ClearWorldMarker()
|
||||
|
||||
local count = 0
|
||||
for i = 1, NUM_MARKERS do
|
||||
local m = preset[i]
|
||||
if m then
|
||||
WorldMarker(i, m.x, m.y, m.z)
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
|
||||
msg("Placed |cffffffff" .. name .. "|r (" .. count .. " marker" .. (count > 1 and "s" or "") .. ")")
|
||||
end
|
||||
|
||||
local function clearMarkers()
|
||||
if not CanSetWorldMarkers() then
|
||||
msg("|cffff4444No permission|r (need leader or assist)")
|
||||
return
|
||||
end
|
||||
ClearWorldMarker()
|
||||
msg("Cleared all markers.")
|
||||
end
|
||||
|
||||
local function listPresets()
|
||||
local any = false
|
||||
for name, preset in pairs(MarkerPresets_Saved) do
|
||||
local count = 0
|
||||
for i = 1, NUM_MARKERS do
|
||||
if preset[i] then count = count + 1 end
|
||||
end
|
||||
msg(" |cffffffff" .. name .. "|r — " .. count .. " marker" .. (count > 1 and "s" or ""))
|
||||
any = true
|
||||
end
|
||||
if not any then
|
||||
msg("No saved presets.")
|
||||
end
|
||||
end
|
||||
|
||||
local function deletePreset(name)
|
||||
if not name or name == "" then
|
||||
msg("Usage: /mp delete <name>")
|
||||
return
|
||||
end
|
||||
if not MarkerPresets_Saved[name] then
|
||||
msg("No preset named |cffffffff" .. name .. "|r")
|
||||
return
|
||||
end
|
||||
MarkerPresets_Saved[name] = nil
|
||||
msg("Deleted |cffffffff" .. name .. "|r")
|
||||
end
|
||||
|
||||
local f = CreateFrame("Frame")
|
||||
f:RegisterEvent("VARIABLES_LOADED")
|
||||
f:SetScript("OnEvent", function()
|
||||
MarkerPresets_Saved = MarkerPresets_Saved or {}
|
||||
end)
|
||||
|
||||
SLASH_MARKERPRESETS1 = "/mp"
|
||||
SLASH_MARKERPRESETS2 = "/markerpreset"
|
||||
SlashCmdList["MARKERPRESETS"] = function(input)
|
||||
local cmd, rest = string.match(input, "^(%S+)%s*(.*)")
|
||||
if not cmd then
|
||||
msg("Commands: save | place | clear | list | delete")
|
||||
return
|
||||
end
|
||||
|
||||
cmd = string.lower(cmd)
|
||||
local name = rest ~= "" and rest or nil
|
||||
|
||||
if cmd == "save" then
|
||||
savePreset(name)
|
||||
elseif cmd == "place" or cmd == "load" then
|
||||
placePreset(name)
|
||||
elseif cmd == "clear" then
|
||||
clearMarkers()
|
||||
elseif cmd == "list" or cmd == "ls" then
|
||||
listPresets()
|
||||
elseif cmd == "delete" or cmd == "del" or cmd == "rm" then
|
||||
deletePreset(name)
|
||||
else
|
||||
msg("Unknown command: " .. cmd)
|
||||
msg("Commands: save | place | clear | list | delete")
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,7 @@
|
||||
## Interface: 11200
|
||||
## Title: [WeirdUtils] Marker Presets
|
||||
## Notes: Save and restore world marker positions. Requires WeirdUtils World Markers.
|
||||
## Version: 1.0
|
||||
## SavedVariablesPerCharacter: MarkerPresetsDB
|
||||
|
||||
MarkerPresets.lua
|
||||
@@ -22,7 +22,6 @@ const std = @import("std");
|
||||
const hook = @import("zhook");
|
||||
const lua = @import("../lua.zig");
|
||||
const o = @import("offsets.zig");
|
||||
const offsets = @import("../offsets.zig");
|
||||
const wow = @import("../wow.zig");
|
||||
const logging = @import("../logging.zig");
|
||||
|
||||
@@ -356,7 +355,7 @@ fn placeMarker(index: usize, pos: Vec3) bool {
|
||||
// Store persistent definition
|
||||
marker_defs[index] = .{
|
||||
.pos = pos,
|
||||
.area_id = hook.readMem(u32, offsets.ZONE_AREA_ID),
|
||||
.area_id = wow.getMapId(),
|
||||
.active = true,
|
||||
};
|
||||
|
||||
@@ -590,7 +589,7 @@ fn tickAnimations() void {
|
||||
if (player != 0) {
|
||||
const player_pos = getUnitPosition(player);
|
||||
if (player_pos.x != 0 or player_pos.y != 0 or player_pos.z != 0) {
|
||||
const current_area = hook.readMem(u32, offsets.ZONE_AREA_ID);
|
||||
const current_area = wow.getMapId();
|
||||
for (0..NUM_MARKERS) |i| {
|
||||
if (!marker_defs[i].active) continue;
|
||||
if (marker_entities[i] != null) continue; // entity alive, skip
|
||||
|
||||
@@ -189,6 +189,14 @@ pub fn isInBattleground() bool {
|
||||
return map_type == o.MAP_TYPE_BATTLEGROUND;
|
||||
}
|
||||
|
||||
/// Current map ID from the object manager (ObjMgr+0xCC).
|
||||
/// Returns 0xFFFFFFFF if the object manager is not available.
|
||||
pub fn getMapId() u32 {
|
||||
const obj_mgr = hook.readMem(u32, o.OBJECT_MANAGER_PTR);
|
||||
if (obj_mgr == 0) return 0xFFFFFFFF;
|
||||
return hook.readMem(u32, obj_mgr + o.OBJMGR_MAP_ID_OFFSET);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Game function wrappers
|
||||
// =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user