Fix combatlog player name resolution: stdcall ABI for GetObjectPtr

The old code called GetObjectByGUID (0x464870) as __fastcall with the
GUID in ECX/EDX, but the function is actually __stdcall with the u64
GUID on the stack. This ABI mismatch caused a crash.

Switched to perfboost's two-step approach with correct conventions:
  1. GetObjectPtr (0x464870) — __stdcall(u64 guid) → object ptr
  2. CGUnit_C::GetUnitName (0x609210) — __thiscall(ECX=unit, 0) → char*

Also added ESI/EDI/EBX clobber barrier in the EnableChatLogging detour
and copy name to stack buffer before log write.
This commit is contained in:
MarcelineVQ
2026-03-03 14:22:56 -08:00
parent 4af033d01d
commit aa6ea3c558
2 changed files with 100 additions and 37 deletions
+54 -37
View File
@@ -95,31 +95,35 @@ fn getPlayerGUID() u64 {
return (@as(u64, hi) << 32) | lo;
}
/// Look up a player name from the name cache by GUID.
/// Calls RetrieveNPCDataFromCache — __thiscall(ECX=cache), 6 stack params, RET 0x18.
fn getNameFromGUID(guid_lo: u32, guid_hi: u32) ?[*:0]const u8 {
if (guid_lo == 0 and guid_hi == 0) return null;
var name_buf: [2]u32 = .{ 0, 0 };
const stack_args = [6]u32{
guid_lo,
guid_hi,
@intFromPtr(&name_buf),
0, 0, 0,
};
const result: u32 = asm volatile (
\\ push 20(%[a])
\\ push 16(%[a])
\\ push 12(%[a])
\\ push 8(%[a])
\\ push 4(%[a])
\\ push (%[a])
/// Look up a unit name by GUID — two-step approach matching perfboost:
/// 1. GetObjectPtr (0x464870) — __stdcall(u64 guid) → object ptr in EAX
/// 2. CGUnit_C::GetUnitName (0x609210) — __thiscall(ECX=unit, stack: 0) → char*
fn getNameFromGUID(guid: u64) ?[*:0]const u8 {
if (guid == 0) return null;
// Step 1: GUID → object pointer via __stdcall (GUID pushed on stack as 8 bytes)
const guid_lo: u32 = @truncate(guid);
const guid_hi: u32 = @truncate(guid >> 32);
const obj: u32 = asm volatile (
\\ push %[hi]
\\ push %[lo]
\\ call *%[func]
: [ret] "={eax}" (-> u32),
: [_] "{ecx}" (@as(u32, o.NAME_CACHE_OBJ)),
[a] "r" (&stack_args),
[func] "r" (@as(u32, o.FN_NAME_CACHE_LOOKUP)),
: [lo] "r" (guid_lo),
[hi] "r" (guid_hi),
[func] "r" (@as(u32, o.FN_GET_OBJECT_PTR)),
: .{ .ecx = true, .edx = true, .memory = true, .cc = true });
return if (result != 0) @ptrFromInt(result) else null;
if (obj == 0) return null;
// Step 2: object pointer → name (__thiscall: ECX=this, push flag=0)
const result: u32 = asm volatile (
\\ push $0
\\ call *%[func]
: [ret] "={eax}" (-> u32),
: [_] "{ecx}" (obj),
[func] "r" (@as(u32, o.FN_GET_UNIT_NAME)),
: .{ .ecx = true, .edx = true, .memory = true, .cc = true });
return if (result != 0 and result >= 0x10000) @ptrFromInt(result) else null;
}
// =============================================================================
@@ -138,9 +142,7 @@ fn maybeWriteSessionMarker() void {
const player_guid = getPlayerGUID();
if (player_guid == 0) return;
const guid_lo: u32 = @truncate(player_guid);
const guid_hi: u32 = @truncate(player_guid >> 32);
const name = getNameFromGUID(guid_lo, guid_hi) orelse {
const name = getNameFromGUID(player_guid) orelse {
// TODO: If player name is unavailable at this point (e.g.
// LoggingCombat enabled before login), could hook OnWorldUpdate for a
// per-frame retry until name is available.
@@ -148,15 +150,19 @@ fn maybeWriteSessionMarker() void {
return;
};
// WriteFormattedLogMessage — __cdecl(handle, fmt, va_list)
// Third arg is a va_list (pointer to the arg list), NOT the arg itself.
const fmt_str: [*:0]const u8 = "COMBATLOG_SESSION,%s";
var va_args = [1]u32{@intFromPtr(name)};
const cdecl_args = [3]u32{
combat_handle,
@intFromPtr(fmt_str),
@intFromPtr(&va_args),
};
// Copy name to a stack buffer — the name cache pointer can be
// invalidated by game-side log writes.
var name_local: [49]u8 = undefined;
const name_span = std.mem.span(name);
const len = @min(name_span.len, name_local.len - 1);
@memcpy(name_local[0..len], name_span[0..len]);
name_local[len] = 0;
con.fmt("[combatlog] player name: '{s}' (ptr=0x{x:0>8}, len={d})\n", .{
name_local[0..len], @intFromPtr(name), len,
});
const args = [3]u32{ combat_handle, @intFromPtr(@as([*:0]const u8, "COMBATLOG_SESSION,%s")), @intFromPtr(&name_local) };
asm volatile (
\\ push 8(%[a])
\\ push 4(%[a])
@@ -164,12 +170,13 @@ fn maybeWriteSessionMarker() void {
\\ call *%[func]
\\ add $12, %%esp
:
: [a] "r" (&cdecl_args),
: [a] "r" (&args),
[func] "r" (@as(u32, o.FN_WRITE_FMT_LOG_MSG)),
: .{ .eax = true, .ecx = true, .edx = true, .memory = true, .cc = true });
: .{ .eax = true, .ecx = true, .edx = true, .memory = true, .cc = true }
);
g_session_marker_written = true;
con.fmt("[combatlog] session: {s}\n", .{std.mem.span(name)});
con.fmt("[combatlog] session: {s}\n", .{name_local[0..len]});
}
// =============================================================================
@@ -181,6 +188,16 @@ const fc = std.builtin.CallingConvention{ .x86_fastcall = .{} };
var enable_logging_hook: hook.Detour(fn (u32, u32) callconv(fc) u32) = .{};
fn enableChatLoggingDetour(lua_state: u32, index: u32) callconv(fc) u32 {
// The Lua VM's luaCallFunction (0x6F6050) stores luaState in ESI and the
// C function pointer in EDI, dispatches via CALL EDI, then reads [ESI+0x8]
// expecting callee-saved registers preserved. Zig may not push ESI/EDI/EBX
// in this function's prologue if it doesn't allocate them itself, but subcalls
// (callOriginal wrapper, inline asm game calls) can clobber them without the
// compiler knowing. This barrier forces Zig to push/pop ESI/EDI/EBX in the
// prologue/epilogue and avoid using them for intermediates — guaranteeing
// they're correctly restored on return to the Lua VM.
asm volatile ("" : : : .{ .esi = true, .edi = true, .ebx = true });
const result = enable_logging_hook.callOriginal(.{ lua_state, index });
// index 1 = combat log
if (index == 1 and !g_session_marker_written) {
+46
View File
@@ -0,0 +1,46 @@
//! Address constants for combat log session rotation.
// =============================================================================
// Combat log path and state
// =============================================================================
/// Path table pointer — .data section (RW), points to "Logs\WoWCombatLog.txt" string.
/// Index 0 (0x0084360c) = chat log, index 1 (0x00843610) = combat log.
/// Overwriting the u32 at this address redirects where the combat log file is created.
pub const COMBAT_LOG_PATH_PTR: usize = 0x00843610;
/// Runtime combat log buffer handle (log_handles[1], BSS).
/// Non-zero when combat logging is active. Read to check if we can write log lines.
pub const COMBAT_LOG_HANDLE: usize = 0x00b50544;
// =============================================================================
// Log writing
// =============================================================================
/// WriteFormattedLogMessage — __cdecl(handle: u32, fmt: [*:0]const u8, ...).
/// Writes a timestamped, formatted line to the log buffer. Auto-flushes at 48KB.
pub const FN_WRITE_FMT_LOG_MSG: usize = 0x0065ac20;
// =============================================================================
// Combat log enable
// =============================================================================
/// EnableChatLogging — __fastcall(ECX=lua_State, EDX=index).
/// index 0=chat, 1=combat. Called by LoggingCombat()/LoggingChat() Lua functions.
/// Reads path from path_table[index] and opens log file on first enable.
pub const FN_ENABLE_CHAT_LOGGING: usize = 0x0049fe50;
// =============================================================================
// Player identity (shared with markers module)
// =============================================================================
/// GetPlayerGUID — __fastcall(), no params, returns EAX(low):EDX(high).
pub const FN_GET_PLAYER_GUID: usize = 0x00468550;
/// GetObjectPtr — __stdcall(u64 guid) → object ptr in EAX.
/// GUID is pushed on the stack as 8 bytes. (perfboost: 0x464870)
pub const FN_GET_OBJECT_PTR: usize = 0x00464870;
/// CGUnit_C::GetUnitName — __thiscall(ECX=unit_ptr, stack: flag=0) → char* in EAX.
/// (perfboost: 0x609210)
pub const FN_GET_UNIT_NAME: usize = 0x00609210;