From 5768c508686f31468277233facc6b339713efb2d Mon Sep 17 00:00:00 2001 From: MarcelineVQ Date: Wed, 4 Mar 2026 13:55:24 -0800 Subject: [PATCH] Hook HandleCharacterSelection to set up log paths before world loading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Read character name from select screen data and realm from CVar when the player clicks Enter World, before InitializeLogBuffer fires. Removes GUID/name-cache fallback — no more early passthrough to wrong paths. --- src/combatlog/combatlog.zig | 134 +++++++++++++++--------------------- src/combatlog/offsets.zig | 29 +++++--- 2 files changed, 75 insertions(+), 88 deletions(-) diff --git a/src/combatlog/combatlog.zig b/src/combatlog/combatlog.zig index 5962ce5..48184f3 100644 --- a/src/combatlog/combatlog.zig +++ b/src/combatlog/combatlog.zig @@ -4,7 +4,8 @@ //! directories: `Logs\\\__.txt`. //! //! Features: -//! - Lazy path setup: resolves character/realm on first InitializeLogBuffer call +//! - Early path setup: hooks HandleCharacterSelection to resolve character/realm +//! from the select screen data before world loading begins //! - Session continuation: reuses files modified < 30 min ago //! - Session marker: writes `COMBATLOG_SESSION: ` on first combat write //! @@ -105,56 +106,11 @@ var g_session_marker_written: bool = false; var g_original_combat_path_ptr: u32 = 0; var g_original_chat_path_ptr: u32 = 0; -/// 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) +// Character / realm identity // ============================================================================= -/// Get local player GUID via GetPlayerGUID (0x468550). -/// __fastcall(), no params, returns EAX(low):EDX(high). -fn getPlayerGUID() u64 { - var lo: u32 = undefined; - var hi: u32 = undefined; - asm volatile ("call *%[func]" - : [_] "={eax}" (lo), - [_] "={edx}" (hi), - : [func] "r" (o.FN_GET_PLAYER_GUID), - : .{ .ecx = true, .memory = true, .cc = true }); - 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]) - \\ 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)), - : .{ .ecx = true, .edx = true, .memory = true, .cc = true }); - 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); @@ -164,6 +120,20 @@ fn getRealmName() ?[*:0]const u8 { 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 // ============================================================================= @@ -302,34 +272,12 @@ fn resolveLogPath(prefix: []const u8, result_buf: *[260]u8) usize { } // ============================================================================= -// Lazy session path setup +// Session path configuration // ============================================================================= -/// 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); - +/// 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); @@ -369,6 +317,32 @@ fn setupSessionPaths() void { 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("[combatlog] 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); @@ -391,11 +365,6 @@ 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); @@ -510,7 +479,13 @@ pub fn installHooks() void { } g_is_hook_owner = true; - // Paths are set up lazily on first InitializeLogBuffer call (after player login) + // 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("[combatlog] FAILED to hook HandleCharacterSelection!\n"); + } else { + con.print("[combatlog] hooked HandleCharacterSelection OK\n"); + } if (init_log_hook.attach(o.FN_INIT_LOG_BUFFER, &initLogDetour) != .ok) { con.print("[combatlog] FAILED to hook InitializeLogBuffer!\n"); @@ -529,6 +504,7 @@ 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| { diff --git a/src/combatlog/offsets.zig b/src/combatlog/offsets.zig index 1e3729e..faa51f0 100644 --- a/src/combatlog/offsets.zig +++ b/src/combatlog/offsets.zig @@ -43,16 +43,27 @@ pub const FN_INIT_LOG_BUFFER: usize = 0x0065a0c0; pub const FN_WRITE_FMT_LOG_MSG: usize = 0x0065ac20; // ============================================================================= -// Player identity +// Character select / enter world // ============================================================================= -/// GetPlayerGUID — __fastcall(), no params, returns EAX(low):EDX(high). -pub const FN_GET_PLAYER_GUID: usize = 0x00468550; +/// HandleCharacterSelection — void(void), called by Lua EnterWorld(). +/// Fires when the player clicks "Enter World" on the character select screen, +/// before world loading and log buffer initialization. +pub const FN_HANDLE_CHAR_SELECT: usize = 0x0046b500; -/// RetrieveNPCDataFromCache — __thiscall(ECX=cache_obj), 6 stack params, RET 0x18. -/// (guid_low, guid_high, name_buf_ptr, 0, 0, 0) → char* name or NULL in EAX. -/// Resolves player/NPC names from the name cache — available before the object manager. -pub const FN_NAME_CACHE_LOOKUP: usize = 0x0055f080; +/// Selected character index (int) on the character select screen. +pub const CHAR_SELECT_INDEX: usize = 0x0083856c; + +/// Character list base pointer. Each entry is 0x120 bytes. +/// Character name (C string) is at entry + 0x08. +pub const CHAR_LIST_BASE: usize = 0x00b42144; + +/// Number of characters in the character list. +pub const CHAR_LIST_COUNT: usize = 0x00b42140; + +/// Size of each character entry in the character list. +pub const CHAR_ENTRY_SIZE: usize = 0x120; + +/// Offset of character name within a character entry. +pub const CHAR_NAME_OFFSET: usize = 0x08; -/// Name cache object — static instance passed as ECX (this) to RetrieveNPCDataFromCache. -pub const NAME_CACHE_OBJ: usize = 0x00c0e228;