- New bigcursor module skeleton with build integration and mutex - Rename combatlog module to logsessions (directory, files, build options, imports, console prefixes, mutex name, DLL variant, docs) - Increase log session continuation threshold from 30 to 60 minutes - Update DLL_README log sessions section
563 lines
22 KiB
Zig
563 lines
22 KiB
Zig
//! Log session rotation with per-character directories.
|
|
//!
|
|
//! Redirects combat log, raw combat log, and chat log to per-character
|
|
//! directories: `Logs\<realm>\<character>\<Type>_<timestamp>_<PID>.txt`.
|
|
//!
|
|
//! Features:
|
|
//! - Early path setup: hooks HandleCharacterSelection to resolve character/realm
|
|
//! from the select screen data before world loading begins
|
|
//! - Session continuation: reuses files modified < 60 min ago
|
|
//! - Session marker: writes `COMBATLOG_SESSION: <char> <realm>` on first combat write
|
|
//!
|
|
//! All DLL-side — no Lua addon needed.
|
|
|
|
const std = @import("std");
|
|
const hook = @import("zhook");
|
|
const con = @import("../console.zig");
|
|
const lua = @import("../lua.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,
|
|
wMonth: u16,
|
|
wDayOfWeek: u16,
|
|
wDay: u16,
|
|
wHour: u16,
|
|
wMinute: u16,
|
|
wSecond: u16,
|
|
wMilliseconds: u16,
|
|
};
|
|
|
|
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;
|
|
|
|
// =============================================================================
|
|
// Session state — reset on logout
|
|
// =============================================================================
|
|
|
|
/// 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;
|
|
|
|
/// Directory prefix: `Logs\<realm>\<char>\`
|
|
var g_dir_path: [280]u8 = undefined;
|
|
var g_dir_path_len: usize = 0;
|
|
|
|
/// 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;
|
|
|
|
/// True once paths have been configured for this login session.
|
|
var g_paths_configured: bool = false;
|
|
|
|
/// Per-log-type session marker flags.
|
|
var g_combat_marker_written: bool = false;
|
|
var g_chat_marker_written: bool = false;
|
|
var g_raw_marker_written: bool = false;
|
|
|
|
/// Raw combat log handle address — captured from initLogDetour when SuperWoW
|
|
/// calls InitializeLogBuffer for WoWRawCombatLog. We don't know this address
|
|
/// statically; SuperWoW passes it as handle_out.
|
|
var g_raw_combat_handle_addr: u32 = 0;
|
|
|
|
/// Saved original path pointers for restoration.
|
|
var g_original_combat_path_ptr: u32 = 0;
|
|
var g_original_chat_path_ptr: u32 = 0;
|
|
|
|
|
|
// =============================================================================
|
|
// Character / realm identity
|
|
// =============================================================================
|
|
|
|
/// 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);
|
|
}
|
|
|
|
/// Read the selected character's name from the character select screen data.
|
|
/// Available when the player clicks Enter World (before world loading begins).
|
|
fn getCharSelectName() ?[*:0]const u8 {
|
|
const index = hook.readMem(i32, o.CHAR_SELECT_INDEX);
|
|
if (index < 0) return null;
|
|
const count = hook.readMem(i32, o.CHAR_LIST_COUNT);
|
|
if (index >= count) return null;
|
|
const list_base = hook.readMem(u32, o.CHAR_LIST_BASE);
|
|
if (list_base == 0) return null;
|
|
const entry_addr = list_base + @as(u32, @intCast(index)) * o.CHAR_ENTRY_SIZE + o.CHAR_NAME_OFFSET;
|
|
// Name is a C string embedded in the entry struct (not a pointer)
|
|
return @ptrFromInt(entry_addr);
|
|
}
|
|
|
|
// =============================================================================
|
|
// Name sanitization and directory creation
|
|
// =============================================================================
|
|
|
|
/// 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("[logsessions] dir: {s}\n", .{g_dir_path[0..g_dir_path_len]});
|
|
return true;
|
|
}
|
|
|
|
// =============================================================================
|
|
// Session continuation — find recent file to reuse
|
|
// =============================================================================
|
|
|
|
/// Scan directory for files matching `<prefix>_*.txt`, return the newest if
|
|
/// modified within 60 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 = 60 * 60 * 10_000_000; // 60 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 < 60 min ago)
|
|
if (findRecentFile(prefix, result_buf)) |len| {
|
|
con.fmt("[logsessions] reusing: {s}\n", .{result_buf[0..len]});
|
|
return len;
|
|
}
|
|
|
|
// Generate new timestamped filename
|
|
var st: SYSTEMTIME = undefined;
|
|
GetLocalTime(&st);
|
|
|
|
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}.txt", .{
|
|
g_dir_path[0..g_dir_path_len],
|
|
prefix,
|
|
st.wYear,
|
|
st.wMonth,
|
|
st.wDay,
|
|
st.wHour,
|
|
st.wMinute,
|
|
st.wSecond,
|
|
}) catch return 0;
|
|
result_buf[path.len] = 0;
|
|
con.fmt("[logsessions] new: {s}\n", .{path});
|
|
return path.len;
|
|
}
|
|
|
|
// =============================================================================
|
|
// Session path configuration
|
|
// =============================================================================
|
|
|
|
/// Core path setup: sanitize names, create directories, resolve all log paths.
|
|
/// Called from enterWorldDetour when the player clicks Enter World.
|
|
fn configureSession(char_span: []const u8, realm_span: []const u8) void {
|
|
// 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("[logsessions] 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("[logsessions] 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;
|
|
}
|
|
|
|
// =============================================================================
|
|
// HandleCharacterSelection hook — set up paths before world loading
|
|
// =============================================================================
|
|
|
|
var enter_world_hook: hook.Detour(fn () callconv(sc) void) = .{};
|
|
|
|
/// Fires when the player clicks Enter World on the character select screen.
|
|
/// Character name and realm are available from the select screen data.
|
|
/// Sets up paths BEFORE the world loading sequence calls InitializeLogBuffer.
|
|
fn enterWorldDetour() callconv(sc) void {
|
|
asm volatile ("" ::: .{ .esi = true, .edi = true, .ebx = true });
|
|
|
|
if (!g_paths_configured) {
|
|
const char_name = getCharSelectName();
|
|
const realm_name = getRealmName();
|
|
|
|
if (char_name != null and realm_name != null) {
|
|
configureSession(std.mem.span(char_name.?), std.mem.span(realm_name.?));
|
|
} else {
|
|
con.print("[logsessions] enter world: char/realm not available\n");
|
|
}
|
|
}
|
|
|
|
enter_world_hook.callOriginal(.{});
|
|
}
|
|
|
|
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 });
|
|
|
|
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("[logsessions] 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")) {
|
|
g_raw_combat_handle_addr = handle_out; // capture SuperWoW's handle address
|
|
con.fmt("[logsessions] 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("[logsessions] 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 markers on first write per log
|
|
// =============================================================================
|
|
|
|
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 });
|
|
|
|
if (g_session_char_len > 0) {
|
|
if (!g_combat_marker_written) {
|
|
const combat_handle = hook.readMem(u32, o.COMBAT_LOG_HANDLE);
|
|
if (handle == combat_handle and combat_handle != 0) {
|
|
writeSessionMarker(handle, "COMBATLOG_SESSION: %s");
|
|
g_combat_marker_written = true;
|
|
}
|
|
}
|
|
|
|
if (!g_chat_marker_written) {
|
|
const chat_handle = hook.readMem(u32, o.CHAT_LOG_HANDLE);
|
|
if (handle == chat_handle and chat_handle != 0) {
|
|
writeSessionMarker(handle, "CHAT_SESSION: %s");
|
|
g_chat_marker_written = true;
|
|
}
|
|
}
|
|
|
|
if (!g_raw_marker_written and g_raw_combat_handle_addr != 0) {
|
|
const raw_handle = hook.readMem(u32, g_raw_combat_handle_addr);
|
|
if (handle == raw_handle and raw_handle != 0) {
|
|
writeSessionMarker(handle, "COMBATLOG_SESSION: %s");
|
|
g_raw_marker_written = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
write_log_hook.callOriginal(.{ handle, fmt, va_list });
|
|
}
|
|
|
|
/// Write a session marker line using stored character + realm names.
|
|
fn writeSessionMarker(handle: u32, fmt_str: [*:0]const u8) void {
|
|
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;
|
|
|
|
const marker_ptr: u32 = @intFromPtr(&marker_buf);
|
|
write_log_hook.callOriginal(.{
|
|
handle,
|
|
@intFromPtr(fmt_str),
|
|
@intFromPtr(&marker_ptr),
|
|
});
|
|
|
|
con.fmt("[logsessions] marker: {s}\n", .{marker_str});
|
|
}
|
|
|
|
// =============================================================================
|
|
// 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_paths_configured) {
|
|
con.print("[logsessions] session reset\n");
|
|
}
|
|
g_paths_configured = false;
|
|
g_combat_marker_written = false;
|
|
g_chat_marker_written = false;
|
|
g_raw_marker_written = false;
|
|
g_raw_combat_handle_addr = 0;
|
|
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();
|
|
}
|
|
|
|
// =============================================================================
|
|
// Lua API — log path accessors
|
|
// =============================================================================
|
|
|
|
/// GetCombatLogPath() → string or nil
|
|
pub fn luaGetCombatLogPath(L: lua.State) callconv(.c) u32 {
|
|
if (g_combat_path_len > 0) {
|
|
lua.pushstring(L, @ptrCast(g_combat_path[0..g_combat_path_len :0]));
|
|
} else {
|
|
lua.pushnil(L);
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
/// GetChatLogPath() → string or nil
|
|
pub fn luaGetChatLogPath(L: lua.State) callconv(.c) u32 {
|
|
if (g_chat_path_len > 0) {
|
|
lua.pushstring(L, @ptrCast(g_chat_path[0..g_chat_path_len :0]));
|
|
} else {
|
|
lua.pushnil(L);
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
// =============================================================================
|
|
// Install / remove hooks
|
|
// =============================================================================
|
|
|
|
pub fn installHooks() void {
|
|
con.print("[logsessions] Module loaded\n");
|
|
|
|
// Multi-DLL safety: only one instance per process should hook
|
|
var mutex_name_buf: [64]u8 = undefined;
|
|
const mutex_name = std.fmt.bufPrint(&mutex_name_buf, "Local\\WeirdUtils_LogSessionsHook_{d}", .{GetCurrentProcessId()}) catch return;
|
|
mutex_name_buf[mutex_name.len] = 0;
|
|
|
|
g_mutex = CreateMutexA(null, 1, @ptrCast(mutex_name_buf[0..mutex_name.len :0]));
|
|
if (g_mutex == null) return;
|
|
|
|
if (GetLastError() == ERROR_ALREADY_EXISTS) {
|
|
_ = CloseHandle(g_mutex.?);
|
|
g_mutex = null;
|
|
g_is_hook_owner = false;
|
|
con.print("[logsessions] Another DLL owns hooks (mutex taken), skipping\n");
|
|
return;
|
|
}
|
|
g_is_hook_owner = true;
|
|
|
|
// Hook HandleCharacterSelection — sets up paths when player clicks Enter World,
|
|
// before the world loading sequence calls InitializeLogBuffer.
|
|
if (enter_world_hook.attach(o.FN_HANDLE_CHAR_SELECT, &enterWorldDetour) != .ok) {
|
|
con.print("[logsessions] FAILED to hook HandleCharacterSelection!\n");
|
|
} else {
|
|
con.print("[logsessions] hooked HandleCharacterSelection OK\n");
|
|
}
|
|
|
|
if (init_log_hook.attach(o.FN_INIT_LOG_BUFFER, &initLogDetour) != .ok) {
|
|
con.print("[logsessions] FAILED to hook InitializeLogBuffer!\n");
|
|
} else {
|
|
con.print("[logsessions] hooked InitializeLogBuffer OK\n");
|
|
}
|
|
|
|
if (write_log_hook.attach(o.FN_WRITE_FMT_LOG_MSG, &writeLogDetour) != .ok) {
|
|
con.print("[logsessions] FAILED to hook WriteFormattedLogMessage!\n");
|
|
} else {
|
|
con.print("[logsessions] hooked WriteFormattedLogMessage OK\n");
|
|
}
|
|
}
|
|
|
|
pub fn removeHooks() void {
|
|
if (g_is_hook_owner) {
|
|
write_log_hook.detach();
|
|
init_log_hook.detach();
|
|
enter_world_hook.detach();
|
|
restorePathPointers();
|
|
|
|
if (g_mutex) |m| {
|
|
_ = ReleaseMutex(m);
|
|
_ = CloseHandle(m);
|
|
g_mutex = null;
|
|
}
|
|
}
|
|
g_is_hook_owner = false;
|
|
}
|