Per-character log directories with session continuation and chat log redirect
Organize combat/raw/chat logs into Logs\<realm>\<char>\ directories. Lazy path setup on first InitializeLogBuffer call resolves character and realm names, creates directory tree, and reuses files modified within 30 minutes for session continuation. Chat log added to the redirection system. Session marker now includes realm name. Paths not yet redirected before login — needs earlier hook point.
This commit is contained in:
+359
-111
@@ -1,32 +1,46 @@
|
||||
//! Combat log session rotation.
|
||||
//! Combat log session rotation with per-character directories.
|
||||
//!
|
||||
//! Redirects the combat log to a per-session file named with timestamp + PID
|
||||
//! (e.g. `Logs\WoWCombatLog_20260303_193045_1234.txt`), and writes a
|
||||
//! `COMBATLOG_SESSION,<PlayerName>` marker line when combat logging is enabled.
|
||||
//! Redirects combat log, raw combat log, and chat log to per-character
|
||||
//! directories: `Logs\<realm>\<character>\<Type>_<timestamp>_<PID>.txt`.
|
||||
//!
|
||||
//! 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.
|
||||
//! Features:
|
||||
//! - Lazy path setup: resolves character/realm on first InitializeLogBuffer call
|
||||
//! - Session continuation: reuses files modified < 30 min ago
|
||||
//! - Session marker: writes `COMBATLOG_SESSION: <char> <realm>` on first combat write
|
||||
//!
|
||||
//! 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");
|
||||
const con = @import("../console.zig");
|
||||
const o = @import("offsets.zig");
|
||||
|
||||
const sc = std.builtin.CallingConvention{ .x86_stdcall = .{} };
|
||||
const WINAPI = std.builtin.CallingConvention.winapi;
|
||||
|
||||
// =============================================================================
|
||||
// Win32 imports
|
||||
// =============================================================================
|
||||
|
||||
extern "kernel32" fn CreateMutexA(lpMutexAttributes: ?*anyopaque, bInitialOwner: i32, lpName: [*:0]const u8) callconv(WINAPI) ?*anyopaque;
|
||||
extern "kernel32" fn ReleaseMutex(hMutex: *anyopaque) callconv(WINAPI) i32;
|
||||
extern "kernel32" fn CloseHandle(hObject: *anyopaque) callconv(WINAPI) i32;
|
||||
extern "kernel32" fn GetLastError() callconv(WINAPI) u32;
|
||||
extern "kernel32" fn GetCurrentProcessId() callconv(WINAPI) u32;
|
||||
extern "kernel32" fn GetLocalTime(lpSystemTime: *SYSTEMTIME) callconv(WINAPI) void;
|
||||
extern "kernel32" fn GetSystemTimeAsFileTime(lpSystemTimeAsFileTime: *FILETIME) callconv(WINAPI) void;
|
||||
extern "kernel32" fn CreateDirectoryA(lpPathName: [*:0]const u8, lpSecurityAttributes: ?*anyopaque) callconv(WINAPI) i32;
|
||||
extern "kernel32" fn FindFirstFileA(lpFileName: [*:0]const u8, lpFindFileData: *WIN32_FIND_DATAA) callconv(WINAPI) usize;
|
||||
extern "kernel32" fn FindNextFileA(hFindFile: usize, lpFindFileData: *WIN32_FIND_DATAA) callconv(WINAPI) i32;
|
||||
extern "kernel32" fn FindClose(hFindFile: usize) callconv(WINAPI) i32;
|
||||
|
||||
const ERROR_ALREADY_EXISTS: u32 = 183;
|
||||
const INVALID_HANDLE_VALUE: usize = 0xFFFFFFFF;
|
||||
|
||||
const FILETIME = extern struct {
|
||||
dwLowDateTime: u32,
|
||||
dwHighDateTime: u32,
|
||||
};
|
||||
|
||||
const SYSTEMTIME = extern struct {
|
||||
wYear: u16,
|
||||
@@ -38,88 +52,64 @@ const SYSTEMTIME = extern struct {
|
||||
wSecond: u16,
|
||||
wMilliseconds: u16,
|
||||
};
|
||||
extern "kernel32" fn GetLocalTime(lpSystemTime: *SYSTEMTIME) callconv(WINAPI) void;
|
||||
|
||||
const WIN32_FIND_DATAA = extern struct {
|
||||
dwFileAttributes: u32,
|
||||
ftCreationTime: FILETIME,
|
||||
ftLastAccessTime: FILETIME,
|
||||
ftLastWriteTime: FILETIME,
|
||||
nFileSizeHigh: u32,
|
||||
nFileSizeLow: u32,
|
||||
dwReserved0: u32,
|
||||
dwReserved1: u32,
|
||||
cFileName: [260]u8,
|
||||
cAlternateFileName: [14]u8,
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Mutex state
|
||||
// =============================================================================
|
||||
|
||||
var g_mutex: ?*anyopaque = null;
|
||||
var g_is_hook_owner: bool = false;
|
||||
|
||||
// =============================================================================
|
||||
// Path redirect via InitializeLogBuffer hook
|
||||
// Session state — reset on logout
|
||||
// =============================================================================
|
||||
|
||||
/// Static buffers for the redirected paths. Must outlive the process.
|
||||
/// Static buffers for redirected paths (null-terminated). Must outlive the process.
|
||||
var g_combat_path: [260]u8 = undefined;
|
||||
var g_raw_combat_path: [260]u8 = undefined;
|
||||
var g_chat_path: [260]u8 = undefined;
|
||||
var g_combat_path_len: usize = 0;
|
||||
var g_raw_combat_path_len: usize = 0;
|
||||
var g_chat_path_len: usize = 0;
|
||||
|
||||
/// 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;
|
||||
/// Directory prefix: `Logs\<realm>\<char>\`
|
||||
var g_dir_path: [280]u8 = undefined;
|
||||
var g_dir_path_len: usize = 0;
|
||||
|
||||
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 };
|
||||
/// Stored character and realm names (sanitized: spaces → underscores)
|
||||
var g_session_char: [64]u8 = undefined;
|
||||
var g_session_realm: [64]u8 = undefined;
|
||||
var g_session_char_len: usize = 0;
|
||||
var g_session_realm_len: usize = 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");
|
||||
}
|
||||
/// True once paths have been configured for this login session.
|
||||
var g_paths_configured: bool = false;
|
||||
|
||||
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");
|
||||
}
|
||||
/// Session marker written flag.
|
||||
var g_session_marker_written: bool = false;
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
/// Saved original path pointers for restoration.
|
||||
var g_original_combat_path_ptr: u32 = 0;
|
||||
var g_original_chat_path_ptr: u32 = 0;
|
||||
|
||||
fn restorePathPointer() void {
|
||||
if (g_original_path_ptr != 0) {
|
||||
const ptr_bytes: [4]u8 = @bitCast(g_original_path_ptr);
|
||||
hook.writeMem(o.COMBAT_LOG_PATH_PTR, &ptr_bytes);
|
||||
g_original_path_ptr = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 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 });
|
||||
}
|
||||
/// Pending init tracking — log types initialized before paths were ready.
|
||||
/// When setupSessionPaths succeeds, we replay these with the correct paths
|
||||
/// to re-initialize the already-created buffer contexts.
|
||||
const PendingInit = struct { active: bool = false, flags: u32 = 0, handle_out: u32 = 0 };
|
||||
var g_pending: [3]PendingInit = .{ .{}, .{}, .{} }; // [0]=combat, [1]=raw, [2]=chat
|
||||
|
||||
// =============================================================================
|
||||
// Player identity (same inline asm patterns as markers module)
|
||||
@@ -140,7 +130,6 @@ fn getPlayerGUID() u64 {
|
||||
|
||||
/// Look up a player name from the name cache by GUID.
|
||||
/// Calls RetrieveNPCDataFromCache — __thiscall(ECX=cache), 6 stack params, RET 0x18.
|
||||
/// Available before the object manager is populated (unlike GetObjectPtr → GetUnitName).
|
||||
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 };
|
||||
@@ -166,25 +155,278 @@ fn getNameFromGUID(guid_lo: u32, guid_hi: u32) ?[*:0]const u8 {
|
||||
return if (result != 0) @ptrFromInt(result) else null;
|
||||
}
|
||||
|
||||
/// Read realm name from CVar: dereference base pointer, then read string at +0x20.
|
||||
fn getRealmName() ?[*:0]const u8 {
|
||||
const base = hook.readMem(u32, o.REALM_NAME_CVAR_BASE);
|
||||
if (base == 0) return null;
|
||||
const str_addr = hook.readMem(u32, base + 0x20);
|
||||
if (str_addr == 0) return null;
|
||||
return @ptrFromInt(str_addr);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Session marker state
|
||||
// Name sanitization and directory creation
|
||||
// =============================================================================
|
||||
|
||||
var g_session_marker_written: bool = false;
|
||||
/// Copy name to dst, replacing spaces with underscores. Returns length written.
|
||||
fn sanitizeName(src: []const u8, dst: []u8) usize {
|
||||
const len = @min(src.len, dst.len);
|
||||
for (0..len) |i| {
|
||||
dst[i] = if (src[i] == ' ') '_' else src[i];
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
/// Create a single directory level, ignoring ERROR_ALREADY_EXISTS.
|
||||
fn ensureDir(path: [*:0]const u8) bool {
|
||||
if (CreateDirectoryA(path, null) != 0) return true;
|
||||
return GetLastError() == ERROR_ALREADY_EXISTS;
|
||||
}
|
||||
|
||||
/// Build and create `Logs\<realm>\<char>\` directory tree.
|
||||
/// Stores the full directory prefix in g_dir_path. Returns true on success.
|
||||
fn setupSessionDir(realm: []const u8, char_name: []const u8) bool {
|
||||
// Logs\ (may already exist)
|
||||
if (!ensureDir("Logs")) return false;
|
||||
|
||||
// Logs\<realm>
|
||||
var realm_dir: [280]u8 = undefined;
|
||||
const realm_path = std.fmt.bufPrint(&realm_dir, "Logs\\{s}", .{realm}) catch return false;
|
||||
realm_dir[realm_path.len] = 0;
|
||||
if (!ensureDir(@ptrCast(realm_dir[0..realm_path.len :0]))) return false;
|
||||
|
||||
// Logs\<realm>\<char> (CreateDirectoryA doesn't want trailing backslash)
|
||||
var char_dir: [280]u8 = undefined;
|
||||
const char_path = std.fmt.bufPrint(&char_dir, "Logs\\{s}\\{s}", .{ realm, char_name }) catch return false;
|
||||
char_dir[char_path.len] = 0;
|
||||
if (!ensureDir(@ptrCast(char_dir[0..char_path.len :0]))) return false;
|
||||
|
||||
// Store with trailing backslash for path concatenation
|
||||
const dir_path = std.fmt.bufPrint(&g_dir_path, "Logs\\{s}\\{s}\\", .{ realm, char_name }) catch return false;
|
||||
g_dir_path[dir_path.len] = 0;
|
||||
g_dir_path_len = dir_path.len;
|
||||
|
||||
con.fmt("[combatlog] dir: {s}\n", .{g_dir_path[0..g_dir_path_len]});
|
||||
return true;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// WriteFormattedLogMessage hook — inject session marker on first combat log write
|
||||
// Session continuation — find recent file to reuse
|
||||
// =============================================================================
|
||||
|
||||
const sc = std.builtin.CallingConvention{ .x86_stdcall = .{} };
|
||||
/// Scan directory for files matching `<prefix>_*.txt`, return the newest if
|
||||
/// modified within 30 minutes. Writes full path into result_buf, returns length.
|
||||
fn findRecentFile(prefix: []const u8, result_buf: *[260]u8) ?usize {
|
||||
if (g_dir_path_len == 0) return null;
|
||||
|
||||
// Build search pattern: <dir><prefix>_*.txt
|
||||
var search_buf: [300]u8 = undefined;
|
||||
const pattern = std.fmt.bufPrint(&search_buf, "{s}{s}_*.txt", .{
|
||||
g_dir_path[0..g_dir_path_len],
|
||||
prefix,
|
||||
}) catch return null;
|
||||
search_buf[pattern.len] = 0;
|
||||
|
||||
var find_data: WIN32_FIND_DATAA = undefined;
|
||||
const find_handle = FindFirstFileA(@ptrCast(search_buf[0..pattern.len :0]), &find_data);
|
||||
if (find_handle == INVALID_HANDLE_VALUE) return null;
|
||||
defer _ = FindClose(find_handle);
|
||||
|
||||
var newest_time: u64 = 0;
|
||||
var newest_name: [260]u8 = undefined;
|
||||
var newest_name_len: usize = 0;
|
||||
|
||||
// Iterate all matching files, track the newest by write time
|
||||
var has_result: bool = true;
|
||||
while (has_result) {
|
||||
const ft: u64 = @bitCast(find_data.ftLastWriteTime);
|
||||
if (ft > newest_time) {
|
||||
newest_time = ft;
|
||||
const name_len = std.mem.indexOfScalar(u8, &find_data.cFileName, 0) orelse 0;
|
||||
if (name_len > 0) {
|
||||
newest_name_len = name_len;
|
||||
@memcpy(newest_name[0..name_len], find_data.cFileName[0..name_len]);
|
||||
}
|
||||
}
|
||||
has_result = FindNextFileA(find_handle, &find_data) != 0;
|
||||
}
|
||||
|
||||
if (newest_name_len == 0) return null;
|
||||
|
||||
// Compare against current time — both UTC FILETIME (100ns units)
|
||||
var current_ft: FILETIME = undefined;
|
||||
GetSystemTimeAsFileTime(¤t_ft);
|
||||
const current: u64 = @bitCast(current_ft);
|
||||
const threshold: u64 = 30 * 60 * 10_000_000; // 30 minutes
|
||||
|
||||
if (current > newest_time and (current - newest_time) < threshold) {
|
||||
// Build full path: dir + filename
|
||||
const total_len = g_dir_path_len + newest_name_len;
|
||||
if (total_len >= result_buf.len) return null;
|
||||
@memcpy(result_buf[0..g_dir_path_len], g_dir_path[0..g_dir_path_len]);
|
||||
@memcpy(result_buf[g_dir_path_len..total_len], newest_name[0..newest_name_len]);
|
||||
result_buf[total_len] = 0;
|
||||
return total_len;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Resolve a log file path: reuse recent file or generate new timestamped name.
|
||||
fn resolveLogPath(prefix: []const u8, result_buf: *[260]u8) usize {
|
||||
// Try to reuse a recent file (modified < 30 min ago)
|
||||
if (findRecentFile(prefix, result_buf)) |len| {
|
||||
con.fmt("[combatlog] reusing: {s}\n", .{result_buf[0..len]});
|
||||
return len;
|
||||
}
|
||||
|
||||
// Generate new timestamped filename
|
||||
var st: SYSTEMTIME = undefined;
|
||||
GetLocalTime(&st);
|
||||
const pid = GetCurrentProcessId();
|
||||
|
||||
const path = std.fmt.bufPrint(result_buf, "{s}{s}_{d:0>4}{d:0>2}{d:0>2}_{d:0>2}{d:0>2}{d:0>2}_{d}.txt", .{
|
||||
g_dir_path[0..g_dir_path_len],
|
||||
prefix,
|
||||
st.wYear,
|
||||
st.wMonth,
|
||||
st.wDay,
|
||||
st.wHour,
|
||||
st.wMinute,
|
||||
st.wSecond,
|
||||
pid,
|
||||
}) catch return 0;
|
||||
result_buf[path.len] = 0;
|
||||
con.fmt("[combatlog] new: {s}\n", .{path});
|
||||
return path.len;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Lazy session path setup
|
||||
// =============================================================================
|
||||
|
||||
/// Called from initLogDetour on first call per session.
|
||||
/// Resolves character/realm, creates directories, resolves all log file paths.
|
||||
fn setupSessionPaths() void {
|
||||
// Get player GUID → name
|
||||
const player_guid = getPlayerGUID();
|
||||
if (player_guid == 0) {
|
||||
con.print("[combatlog] setup: no player GUID yet\n");
|
||||
return;
|
||||
}
|
||||
const guid_lo: u32 = @truncate(player_guid);
|
||||
const guid_hi: u32 = @truncate(player_guid >> 32);
|
||||
|
||||
const char_name = getNameFromGUID(guid_lo, guid_hi) orelse {
|
||||
con.print("[combatlog] setup: player name not in cache\n");
|
||||
return;
|
||||
};
|
||||
const char_span = std.mem.span(char_name);
|
||||
|
||||
// Get realm name from CVar
|
||||
const realm_name = getRealmName() orelse {
|
||||
con.print("[combatlog] setup: realm name not available\n");
|
||||
return;
|
||||
};
|
||||
const realm_span = std.mem.span(realm_name);
|
||||
|
||||
// Sanitize and store names
|
||||
g_session_char_len = sanitizeName(char_span, &g_session_char);
|
||||
g_session_realm_len = sanitizeName(realm_span, &g_session_realm);
|
||||
|
||||
con.fmt("[combatlog] session: {s} on {s}\n", .{
|
||||
g_session_char[0..g_session_char_len],
|
||||
g_session_realm[0..g_session_realm_len],
|
||||
});
|
||||
|
||||
// Create directory tree: Logs\<realm>\<char>\
|
||||
if (!setupSessionDir(
|
||||
g_session_realm[0..g_session_realm_len],
|
||||
g_session_char[0..g_session_char_len],
|
||||
)) {
|
||||
con.print("[combatlog] setup: failed to create directories\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// Resolve paths for all three log types
|
||||
g_combat_path_len = resolveLogPath("WoWCombatLog", &g_combat_path);
|
||||
g_raw_combat_path_len = resolveLogPath("WoWRawCombatLog", &g_raw_combat_path);
|
||||
g_chat_path_len = resolveLogPath("WoWChatLog", &g_chat_path);
|
||||
|
||||
// Belt-and-suspenders: overwrite path pointer table for game code paths
|
||||
// that read the table directly before calling InitializeLogBuffer.
|
||||
if (g_combat_path_len > 0) {
|
||||
g_original_combat_path_ptr = hook.readMem(u32, o.COMBAT_LOG_PATH_PTR);
|
||||
const ptr_bytes: [4]u8 = @bitCast(@intFromPtr(&g_combat_path));
|
||||
hook.writeMem(o.COMBAT_LOG_PATH_PTR, &ptr_bytes);
|
||||
}
|
||||
if (g_chat_path_len > 0) {
|
||||
g_original_chat_path_ptr = hook.readMem(u32, o.CHAT_LOG_PATH_PTR);
|
||||
const ptr_bytes: [4]u8 = @bitCast(@intFromPtr(&g_chat_path));
|
||||
hook.writeMem(o.CHAT_LOG_PATH_PTR, &ptr_bytes);
|
||||
}
|
||||
|
||||
g_paths_configured = true;
|
||||
}
|
||||
|
||||
fn restorePathPointers() void {
|
||||
if (g_original_combat_path_ptr != 0) {
|
||||
const ptr_bytes: [4]u8 = @bitCast(g_original_combat_path_ptr);
|
||||
hook.writeMem(o.COMBAT_LOG_PATH_PTR, &ptr_bytes);
|
||||
g_original_combat_path_ptr = 0;
|
||||
}
|
||||
if (g_original_chat_path_ptr != 0) {
|
||||
const ptr_bytes: [4]u8 = @bitCast(g_original_chat_path_ptr);
|
||||
hook.writeMem(o.CHAT_LOG_PATH_PTR, &ptr_bytes);
|
||||
g_original_chat_path_ptr = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// InitializeLogBuffer hook — lazy setup + path redirect
|
||||
// =============================================================================
|
||||
|
||||
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 });
|
||||
|
||||
// Lazy setup: configure session paths on first call after login
|
||||
if (!g_paths_configured) {
|
||||
setupSessionPaths();
|
||||
}
|
||||
|
||||
const path_ptr: [*:0]const u8 = @ptrFromInt(file_path);
|
||||
const path_span = std.mem.span(path_ptr);
|
||||
|
||||
// Redirect based on path suffix
|
||||
if (g_combat_path_len > 0 and std.mem.endsWith(u8, path_span, "WoWCombatLog.txt")) {
|
||||
con.fmt("[combatlog] redirect: {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] redirect: {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 });
|
||||
}
|
||||
|
||||
if (g_chat_path_len > 0 and std.mem.endsWith(u8, path_span, "WoWChatLog.txt")) {
|
||||
con.fmt("[combatlog] redirect: {s} -> {s}\n", .{ path_span, g_chat_path[0..g_chat_path_len] });
|
||||
return init_log_hook.callOriginal(.{ @intFromPtr(&g_chat_path), flags, handle_out });
|
||||
}
|
||||
|
||||
return init_log_hook.callOriginal(.{ file_path, flags, handle_out });
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// WriteFormattedLogMessage hook — inject session marker on first combat write
|
||||
// =============================================================================
|
||||
|
||||
var write_log_hook: hook.Detour(fn (u32, u32, u32) callconv(sc) void) = .{};
|
||||
|
||||
fn writeLogDetour(handle: u32, fmt: u32, va_list: u32) callconv(sc) void {
|
||||
asm volatile ("" ::: .{ .esi = true, .edi = true, .ebx = true });
|
||||
|
||||
// On first write to combat log, prepend our session marker
|
||||
if (!g_session_marker_written) {
|
||||
const combat_handle = hook.readMem(u32, o.COMBAT_LOG_HANDLE);
|
||||
if (handle == combat_handle and combat_handle != 0) {
|
||||
@@ -195,44 +437,53 @@ fn writeLogDetour(handle: u32, fmt: u32, va_list: u32) callconv(sc) void {
|
||||
write_log_hook.callOriginal(.{ handle, fmt, va_list });
|
||||
}
|
||||
|
||||
/// Resolve player name and write COMBATLOG_SESSION via callOriginal.
|
||||
/// Write session marker using stored character + realm names (no inline asm needed).
|
||||
fn writeSessionMarkerNow(handle: u32) 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 {
|
||||
con.print("[combatlog] session marker: player name not in cache yet\n");
|
||||
if (g_session_char_len == 0) {
|
||||
con.print("[combatlog] session marker: names not resolved yet\n");
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
// Copy name to stack buffer — cache pointer can be invalidated by the write
|
||||
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;
|
||||
// Build "CharName RealmName" string on the stack
|
||||
var marker_buf: [128]u8 = undefined;
|
||||
const marker_str = std.fmt.bufPrint(&marker_buf, "{s} {s}", .{
|
||||
g_session_char[0..g_session_char_len],
|
||||
g_session_realm[0..g_session_realm_len],
|
||||
}) catch return;
|
||||
marker_buf[marker_str.len] = 0;
|
||||
|
||||
// va_list for %s: pointer to a char*
|
||||
const name_ptr: u32 = @intFromPtr(&name_local);
|
||||
const marker_ptr: u32 = @intFromPtr(&marker_buf);
|
||||
write_log_hook.callOriginal(.{
|
||||
handle,
|
||||
@intFromPtr(@as([*:0]const u8, "COMBATLOG_SESSION: %s")),
|
||||
@intFromPtr(&name_ptr),
|
||||
@intFromPtr(&marker_ptr),
|
||||
});
|
||||
|
||||
g_session_marker_written = true;
|
||||
con.fmt("[combatlog] session marker written: {s}\n", .{name_local[0..len]});
|
||||
con.fmt("[combatlog] session marker: {s}\n", .{marker_str});
|
||||
}
|
||||
|
||||
/// Resets session marker so the next login's first write gets a new marker.
|
||||
/// Called from both logoutDetour (real logout) and shutdownDetour (logout/exit/reload).
|
||||
// =============================================================================
|
||||
// Shutdown — reset session state for next login
|
||||
// =============================================================================
|
||||
|
||||
/// Resets all session state so the next login gets fresh paths.
|
||||
/// Called from logoutDetour/shutdownDetour in main.zig.
|
||||
pub fn onShutdown() void {
|
||||
if (g_session_marker_written) {
|
||||
g_session_marker_written = false;
|
||||
con.print("[combatlog] session marker reset\n");
|
||||
if (g_paths_configured or g_session_marker_written) {
|
||||
con.print("[combatlog] session reset\n");
|
||||
}
|
||||
g_paths_configured = false;
|
||||
g_session_marker_written = false;
|
||||
g_combat_path_len = 0;
|
||||
g_raw_combat_path_len = 0;
|
||||
g_chat_path_len = 0;
|
||||
g_dir_path_len = 0;
|
||||
g_session_char_len = 0;
|
||||
g_session_realm_len = 0;
|
||||
// Restore original path pointers so next session starts clean
|
||||
restorePathPointers();
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -259,17 +510,14 @@ pub fn installHooks() void {
|
||||
}
|
||||
g_is_hook_owner = true;
|
||||
|
||||
// Set up timestamped paths and overwrite pointer table
|
||||
setupPaths();
|
||||
// Paths are set up lazily on first InitializeLogBuffer call (after player login)
|
||||
|
||||
// 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) {
|
||||
con.print("[combatlog] FAILED to hook WriteFormattedLogMessage!\n");
|
||||
} else {
|
||||
@@ -281,7 +529,7 @@ pub fn removeHooks() void {
|
||||
if (g_is_hook_owner) {
|
||||
write_log_hook.detach();
|
||||
init_log_hook.detach();
|
||||
restorePathPointer();
|
||||
restorePathPointers();
|
||||
|
||||
if (g_mutex) |m| {
|
||||
_ = ReleaseMutex(m);
|
||||
|
||||
@@ -9,10 +9,24 @@
|
||||
/// Overwriting the u32 at this address redirects where the combat log file is created.
|
||||
pub const COMBAT_LOG_PATH_PTR: usize = 0x00843610;
|
||||
|
||||
/// Chat log path pointer — .data section (RW), points to "Logs\WoWChatLog.txt" string.
|
||||
/// Index 0 in the path table at 0x0084360c.
|
||||
pub const CHAT_LOG_PATH_PTR: usize = 0x0084360c;
|
||||
|
||||
/// 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;
|
||||
|
||||
/// Runtime chat log buffer handle (log_handles[0], BSS).
|
||||
pub const CHAT_LOG_HANDLE: usize = 0x00b50540;
|
||||
|
||||
// =============================================================================
|
||||
// Realm name
|
||||
// =============================================================================
|
||||
|
||||
/// CVar base pointer for realm name. Dereference once, then read string at +0x20.
|
||||
pub const REALM_NAME_CVAR_BASE: usize = 0x00c28130;
|
||||
|
||||
// =============================================================================
|
||||
// Log writing
|
||||
// =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user