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.