From 12db9283650784b89e6d925fdf4af15dc33e1a8e Mon Sep 17 00:00:00 2001 From: MarcelineVQ Date: Wed, 4 Mar 2026 08:44:40 -0800 Subject: [PATCH] Hook InitializeLogBuffer to redirect combat log paths from any caller SuperWoWhook.dll calls InitializeLogBuffer directly with hardcoded "Logs\WoWCombatLog.txt", bypassing the path pointer table. Hook InitializeLogBuffer itself to intercept the path argument and substitute our timestamped filename, regardless of caller. Also redirects WoWRawCombatLog.txt to a matching timestamped file. TODO: session marker (COMBATLOG_SESSION) needs to be written as the first line in each log file -- currently it appears after SuperWoW's initial writes (COMBATANT_INFO, ZONE_INFO) because the write hook fires too late. TODO: consolidate small/empty WoWCombatLog_*.txt files on startup. --- src/combatlog/RESEARCH.md | 67 ++++++++++++++++++++++-- src/combatlog/combatlog.zig | 100 +++++++++++++++++++++++++++--------- src/combatlog/offsets.zig | 6 +++ 3 files changed, 146 insertions(+), 27 deletions(-) diff --git a/src/combatlog/RESEARCH.md b/src/combatlog/RESEARCH.md index 52f35db..565e38d 100644 --- a/src/combatlog/RESEARCH.md +++ b/src/combatlog/RESEARCH.md @@ -313,11 +313,72 @@ login -- player object not in object manager yet. Use name cache instead: `RetrieveNPCDataFromCache` (0x55f080, cache at 0xc0e228) is populated before the object manager and works for the local player GUID. +## SuperWoW Combat Log Interference + +SuperWoWhook.dll has its own Lua C function `CombatLogAdd` (at 0x10001f50 in +the current build) that calls `InitializeLogBuffer` (0x0065a0c0) directly with +**hardcoded path strings**, bypassing the path pointer table at 0x0084360c entirely. + +### SuperWoW CombatLogAdd (0x10001f50) logic + +``` +CombatLogAdd(lua_State *L): + raw = LuaValueToBool(L, 2, false) // arg2: is this a raw log event? + if raw: + handle_ptr = 0x1001e4c8 // SuperWoW's own handle (in DLL .bss) + path = "Logs\WoWRawCombatLog.txt" // hardcoded at 0x1001af00 + else: + handle_ptr = 0x00b50544 // WoW's combat log handle + path = "Logs\WoWCombatLog.txt" // hardcoded at 0x1001aee8 + if *handle_ptr == 0: + CreateDirectoryRecursive("Logs") + InitializeLogBuffer(path, 4, handle_ptr) + WriteTimestampToLogBuffer(*handle_ptr, fmt, value) + return 1.0 +``` + +### Why pointer redirect sometimes fails + +When SuperWoW's `CombatLogAdd` fires before the game's `LoggingCombat(1)`: +1. SuperWoW calls `InitializeLogBuffer("Logs\WoWCombatLog.txt", 4, 0x00b50544)` + using its own hardcoded string -- our pointer at 0x00843610 is never read +2. Handle at 0x00b50544 becomes non-zero +3. When `EnableChatLogging` runs later, it sees handle != 0 and skips init +4. Result: log file created with default name despite our pointer redirect + +The "sometimes works" depends on whether the addon's `LoggingCombat(1)` or +SuperWoW's first `CombatLogAdd` call fires first. + +### Fix: Hook InitializeLogBuffer + +Instead of overwriting the path pointer, hook `InitializeLogBuffer` (0x0065a0c0) +itself. Intercept the `filePath` argument and replace: +- `"Logs\WoWCombatLog.txt"` -> our timestamped combat log path +- `"Logs\WoWRawCombatLog.txt"` -> our timestamped raw combat log path + +This works regardless of who calls InitializeLogBuffer (game or SuperWoW). + +`InitializeLogBuffer` is __stdcall with RET 0xC: (filePath, flags, *handleOut). +The path is copied into the context struct via SafeStringCopy (max 260 bytes), +so substituting a different pointer is safe. + +### SuperWoW raw log handle + +SuperWoW stores its raw combat log handle at `0x1001e4c8` (in SuperWoWhook.dll's +.bss section). This is NOT in WoW.exe's handle array -- it's SuperWoW-private. +The raw log path `"Logs\WoWRawCombatLog.txt"` is at `0x1001af00` in the DLL. + +### ShutdownMessageSystem (0x00659ec0) -- __stdcall(handle), RET 0x4 + +Proper cleanup for a log buffer handle: +1. Gets log buffer context from handle +2. If file handle != -1: flushes (WriteLogBufferToFile), closes (CloseHandle) +3. Frees context memory (FreeMemoryFromPool) + +Called from ShutdownChatSubsystem (0x00498700) which loops both handle slots. + ## Open Questions -- [ ] Where does SuperWoW write `WoWRawCombatLog.txt`? Need to check - SuperWoWhook.dll. Same redirect approach should work if it uses the - same table or a similar one. - [x] Exact prologue size of `EnableChatLogging` for hooking (need 5+ bytes). First 3 instructions = PUSH EBX; PUSH ESI; PUSH EDI = 3 bytes. Then MOV ESI,EDX = 2 bytes. Total = 5 bytes -- just enough for a jmp hook. diff --git a/src/combatlog/combatlog.zig b/src/combatlog/combatlog.zig index f0e9841..79b6e09 100644 --- a/src/combatlog/combatlog.zig +++ b/src/combatlog/combatlog.zig @@ -4,7 +4,16 @@ //! (e.g. `Logs\WoWCombatLog_20260303_193045_1234.txt`), and writes a //! `COMBATLOG_SESSION,` marker line when combat logging is enabled. //! +//! Also redirects SuperWoW's raw combat log (`WoWRawCombatLog.txt`) to a +//! matching timestamped file. SuperWoW calls InitializeLogBuffer directly with +//! hardcoded paths, bypassing the path pointer table — so we hook +//! InitializeLogBuffer itself to intercept both callers. +//! //! All DLL-side — no Lua addon needed. +//! +//! TODO: Small-file cleanup — consolidate WoWCombatLog_*.txt files < 1KB on startup +//! based on dates and session ownership +//! (empty sessions from crashes or quick relogs). const std = @import("std"); const hook = @import("zhook"); @@ -35,39 +44,47 @@ var g_mutex: ?*anyopaque = null; var g_is_hook_owner: bool = false; // ============================================================================= -// Path redirect +// Path redirect via InitializeLogBuffer hook // ============================================================================= -/// Static buffer for the redirected combat log path. Must outlive the process. -var g_path_buf: [260]u8 = undefined; +/// Static buffers for the redirected paths. Must outlive the process. +var g_combat_path: [260]u8 = undefined; +var g_raw_combat_path: [260]u8 = undefined; +var g_combat_path_len: usize = 0; +var g_raw_combat_path_len: usize = 0; -/// Saved original path pointer so we can restore on unload. +/// Also overwrite the path pointer table as a belt-and-suspenders measure +/// for the normal game code path (EnableChatLogging reads from here). var g_original_path_ptr: u32 = 0; -fn setupPathRedirect() void { - // Save the original pointer value - g_original_path_ptr = hook.readMem(u32, o.COMBAT_LOG_PATH_PTR); - +fn setupPaths() void { var st: SYSTEMTIME = undefined; GetLocalTime(&st); const pid = GetCurrentProcessId(); + const ts = .{ st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, pid }; - const path = std.fmt.bufPrint(&g_path_buf, "Logs\\WoWCombatLog_{d:0>4}{d:0>2}{d:0>2}_{d:0>2}{d:0>2}{d:0>2}_{d}.txt", .{ - st.wYear, st.wMonth, st.wDay, - st.wHour, st.wMinute, st.wSecond, - pid, - }) catch { - con.print("[combatlog] path format error\n"); - return; - }; - g_path_buf[path.len] = 0; + if (std.fmt.bufPrint(&g_combat_path, "Logs\\WoWCombatLog_{d:0>4}{d:0>2}{d:0>2}_{d:0>2}{d:0>2}{d:0>2}_{d}.txt", ts)) |p| { + g_combat_path[p.len] = 0; + g_combat_path_len = p.len; + con.fmt("[combatlog] combat path: {s}\n", .{p}); + } else |_| { + con.print("[combatlog] combat path format error\n"); + } - // Overwrite the pointer at 0x00843610 to point to our buffer. - // .data section is RW, no VirtualProtect needed. - const ptr_bytes: [4]u8 = @bitCast(@intFromPtr(&g_path_buf)); - hook.writeMem(o.COMBAT_LOG_PATH_PTR, &ptr_bytes); + if (std.fmt.bufPrint(&g_raw_combat_path, "Logs\\WoWRawCombatLog_{d:0>4}{d:0>2}{d:0>2}_{d:0>2}{d:0>2}{d:0>2}_{d}.txt", ts)) |p| { + g_raw_combat_path[p.len] = 0; + g_raw_combat_path_len = p.len; + con.fmt("[combatlog] raw combat path: {s}\n", .{p}); + } else |_| { + con.print("[combatlog] raw combat path format error\n"); + } - con.fmt("[combatlog] path: {s}\n", .{path}); + // Also overwrite the pointer table for the normal game path + g_original_path_ptr = hook.readMem(u32, o.COMBAT_LOG_PATH_PTR); + if (g_combat_path_len > 0) { + const ptr_bytes: [4]u8 = @bitCast(@intFromPtr(&g_combat_path)); + hook.writeMem(o.COMBAT_LOG_PATH_PTR, &ptr_bytes); + } } fn restorePathPointer() void { @@ -78,6 +95,32 @@ fn restorePathPointer() void { } } +// ============================================================================= +// InitializeLogBuffer hook — intercept path argument +// ============================================================================= + +var init_log_hook: hook.Detour(fn (u32, u32, u32) callconv(sc) u32) = .{}; + +fn initLogDetour(file_path: u32, flags: u32, handle_out: u32) callconv(sc) u32 { + asm volatile ("" ::: .{ .esi = true, .edi = true, .ebx = true }); + + const path_ptr: [*:0]const u8 = @ptrFromInt(file_path); + const path_span = std.mem.span(path_ptr); + + // Check if this is a combat log path we should redirect + if (g_combat_path_len > 0 and std.mem.endsWith(u8, path_span, "WoWCombatLog.txt")) { + con.fmt("[combatlog] intercepted InitializeLogBuffer: {s} -> {s}\n", .{ path_span, g_combat_path[0..g_combat_path_len] }); + return init_log_hook.callOriginal(.{ @intFromPtr(&g_combat_path), flags, handle_out }); + } + + if (g_raw_combat_path_len > 0 and std.mem.endsWith(u8, path_span, "WoWRawCombatLog.txt")) { + con.fmt("[combatlog] intercepted InitializeLogBuffer: {s} -> {s}\n", .{ path_span, g_raw_combat_path[0..g_raw_combat_path_len] }); + return init_log_hook.callOriginal(.{ @intFromPtr(&g_raw_combat_path), flags, handle_out }); + } + + return init_log_hook.callOriginal(.{ file_path, flags, handle_out }); +} + // ============================================================================= // Player identity (same inline asm patterns as markers module) // ============================================================================= @@ -123,6 +166,7 @@ fn getNameFromGUID(guid_lo: u32, guid_hi: u32) ?[*:0]const u8 { return if (result != 0) @ptrFromInt(result) else null; } + // ============================================================================= // Session marker state // ============================================================================= @@ -215,8 +259,15 @@ pub fn installHooks() void { } g_is_hook_owner = true; - // Redirect combat log path to timestamped+PID filename - setupPathRedirect(); + // Set up timestamped paths and overwrite pointer table + setupPaths(); + + // Hook InitializeLogBuffer to intercept path from any caller (game + SuperWoW) + if (init_log_hook.attach(o.FN_INIT_LOG_BUFFER, &initLogDetour) != .ok) { + con.print("[combatlog] FAILED to hook InitializeLogBuffer!\n"); + } else { + con.print("[combatlog] hooked InitializeLogBuffer OK\n"); + } // Hook WriteFormattedLogMessage to inject session marker before the first combat log write if (write_log_hook.attach(o.FN_WRITE_FMT_LOG_MSG, &writeLogDetour) != .ok) { @@ -229,6 +280,7 @@ pub fn installHooks() void { pub fn removeHooks() void { if (g_is_hook_owner) { write_log_hook.detach(); + init_log_hook.detach(); restorePathPointer(); if (g_mutex) |m| { diff --git a/src/combatlog/offsets.zig b/src/combatlog/offsets.zig index f3682b2..80d668a 100644 --- a/src/combatlog/offsets.zig +++ b/src/combatlog/offsets.zig @@ -17,6 +17,12 @@ pub const COMBAT_LOG_HANDLE: usize = 0x00b50544; // Log writing // ============================================================================= +/// InitializeLogBuffer — __stdcall(filePath: [*:0]const u8, flags: u32, handleOut: *u32). +/// Creates a log buffer context. Copies path into context struct (max 260 bytes). +/// Returns nonzero on success. Callee cleans stack (RET 0xC). +/// Called by EnableChatLogging (game) and CombatLogAdd (SuperWoW) with hardcoded paths. +pub const FN_INIT_LOG_BUFFER: usize = 0x0065a0c0; + /// WriteFormattedLogMessage — __stdcall(handle: u32, fmt: [*:0]const u8, va_list: *anyopaque). /// Three fixed params, callee cleans stack (RET 0xC). Third arg is va_list pointer. /// Writes a timestamped, formatted line to the log buffer. Auto-flushes at 48KB.