diff --git a/src/guidcache/guidcache.zig b/src/guidcache/guidcache.zig deleted file mode 100644 index 29e0625..0000000 --- a/src/guidcache/guidcache.zig +++ /dev/null @@ -1,84 +0,0 @@ -//! Standalone GUID cache module for isolated testing. -//! Hooks FindObjectByGUID (0x464890) with a per-frame-flushed direct-mapped cache. - -const hook = @import("zhook"); -const logging = @import("../logging.zig"); -const mod_mutex = @import("../mutex.zig"); - -pub const module_name: [*:0]const u8 = "guidcache"; - -var g_mutex: ?*anyopaque = null; -var g_is_hook_owner: bool = false; -var log: logging.Logger = .{}; - -const FindGuidFn = fn (u32, u32, u32, u32) callconv(hook.cc.fastcall) ?*anyopaque; -var findguid_hook: hook.Detour(FindGuidFn) = .{}; - -const WorldUpdateFn = fn (u32) callconv(hook.cc.fastcall) void; -var world_update_hook: hook.Detour(WorldUpdateFn) = .{}; - -var destroy_objmgr_hook: hook.Detour(fn () callconv(hook.cc.stdcall) void) = .{}; - -const GUID_CACHE_BITS = 12; -const GUID_CACHE_SIZE = 1 << GUID_CACHE_BITS; -const GUID_CACHE_MASK = GUID_CACHE_SIZE - 1; -const GuidCacheEntry = struct { guid_lo: u32 = 0, guid_hi: u32 = 0, result: u32 = 0 }; -var guid_cache: [GUID_CACHE_SIZE]GuidCacheEntry = [_]GuidCacheEntry{.{}} ** GUID_CACHE_SIZE; - -fn findguidDetour(a: u32, b: u32, c: u32, d: u32) callconv(hook.cc.fastcall) ?*anyopaque { - const idx = (c ^ d) & GUID_CACHE_MASK; - const entry = &guid_cache[idx]; - - if (entry.guid_lo == c and entry.guid_hi == d and entry.result != 0) { - return @ptrFromInt(entry.result); - } - - const ret = findguid_hook.callOriginal(.{ a, b, c, d }); - const result = @intFromPtr(ret); - if (result != 0) { - entry.* = .{ .guid_lo = c, .guid_hi = d, .result = result }; - } else { - entry.* = .{}; - } - return ret; -} - -fn worldUpdateDetour(frame_count: u32) callconv(hook.cc.fastcall) void { - guid_cache = [_]GuidCacheEntry{.{}} ** GUID_CACHE_SIZE; - world_update_hook.callOriginal(.{frame_count}); -} - -fn destroyObjMgrDetour() callconv(hook.cc.stdcall) void { - guid_cache = [_]GuidCacheEntry{.{}} ** GUID_CACHE_SIZE; - destroy_objmgr_hook.callOriginal(.{}); -} - -pub fn isActive() bool { - return g_is_hook_owner; -} - -pub fn installHooks() void { - const result = mod_mutex.acquire(module_name); - g_mutex = result.handle; - g_is_hook_owner = result.is_owner; - if (!g_is_hook_owner) return; - - log = logging.Logger.open(module_name, .both); - _ = findguid_hook.attach(0x464890, &findguidDetour); - _ = world_update_hook.attach(0x482EA0, &worldUpdateDetour); - _ = destroy_objmgr_hook.attach(0x467700, &destroyObjMgrDetour); - log.print("guidcache: GUID lookup cache active\n"); -} - -pub fn removeHooks() void { - if (g_is_hook_owner) { - findguid_hook.detach(); - world_update_hook.detach(); - destroy_objmgr_hook.detach(); - log.close(); - mod_mutex.release(&g_mutex); - } - g_is_hook_owner = false; -} - -pub fn onShutdown() void {} diff --git a/src/logging.zig b/src/logging.zig index 7f31319..b0ddfee 100644 --- a/src/logging.zig +++ b/src/logging.zig @@ -67,6 +67,9 @@ const win = if (debug) struct { const GENERIC_WRITE: u32 = 0x40000000; const FILE_SHARE_READ: u32 = 0x00000001; const CREATE_ALWAYS: u32 = 2; + const OPEN_ALWAYS: u32 = 4; + const FILE_END: u32 = 2; + extern "kernel32" fn SetFilePointer(hFile: *anyopaque, lDistanceToMove: i32, lpDistanceToMoveHigh: ?*i32, dwMoveMethod: u32) callconv(WINAPI) u32; const FILE_ATTRIBUTE_NORMAL: u32 = 0x80; var console_handle: ?*anyopaque = null; @@ -100,6 +103,7 @@ pub fn deinit() void { // ============================================================================= pub const Output = enum { console, file, both }; +pub const FileMode = enum { truncate, append }; // ============================================================================= // Logger @@ -108,6 +112,7 @@ pub const Output = enum { console, file, both }; pub const Logger = struct { file_handle: if (debug) ?*anyopaque else void = if (debug) null else {}, default_dest: if (debug) Output else void = if (debug) .console else {}, + file_mode: if (debug) FileMode else void = if (debug) .truncate else {}, /// Module name for lazy file creation. Null = no file output. file_name: if (debug) ?[*:0]const u8 else void = if (debug) null else {}, /// Auto-prefix: "[name] " prepended to every log line. @@ -118,8 +123,16 @@ pub const Logger = struct { /// first write, so modules that never log to file produce no `.log` file. /// Pass null for no file (console-only even when dest is .both/.file). pub fn open(module_name: ?[*:0]const u8, default: Output) Logger { + return openWithMode(module_name, default, .truncate); + } + + pub fn openAppend(module_name: ?[*:0]const u8, default: Output) Logger { + return openWithMode(module_name, default, .append); + } + + pub fn openWithMode(module_name: ?[*:0]const u8, default: Output, mode: FileMode) Logger { if (debug) { - var l: Logger = .{ .default_dest = default, .file_name = module_name }; + var l: Logger = .{ .default_dest = default, .file_name = module_name, .file_mode = mode }; if (module_name) |name| { const span = std.mem.span(name); if (span.len + 3 <= l.prefix.len) { @@ -217,17 +230,21 @@ pub const Logger = struct { @memcpy(buf[0..span.len], span); @memcpy(buf[span.len..][0..4], ".log"); buf[span.len + 4] = 0; + const disposition = if (self.file_mode == .append) win.OPEN_ALWAYS else win.CREATE_ALWAYS; const fh = win.CreateFileA( @ptrCast(buf[0 .. span.len + 4 :0]), win.GENERIC_WRITE, win.FILE_SHARE_READ, null, - win.CREATE_ALWAYS, + disposition, win.FILE_ATTRIBUTE_NORMAL, null, ); if (fh) |h| { if (@intFromPtr(h) != 0xFFFFFFFF) { + if (self.file_mode == .append) { + _ = win.SetFilePointer(h, 0, null, win.FILE_END); + } self.file_handle = h; return h; } diff --git a/src/logsessions/logsessions.zig b/src/logsessions/logsessions.zig index 6e20865..d926d1f 100644 --- a/src/logsessions/logsessions.zig +++ b/src/logsessions/logsessions.zig @@ -1,12 +1,13 @@ //! Log session rotation with per-character directories. //! //! Redirects combat log, raw combat log, and chat log to per-character -//! directories: `Logs\\\__.txt`. +//! directories: `Logs\\\WoWLog_YYYY_MM_DD.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 +//! - Daily rotation: one log file per character per day, no timestamps +//! - Session continuation: appends to today's file on each login (same character) //! - Session marker: writes `COMBATLOG_SESSION: ` on first combat write //! //! All DLL-side - no Lua addon needed. @@ -118,10 +119,6 @@ var g_raw_marker_written: bool = false; /// 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 // ============================================================================= @@ -196,88 +193,60 @@ fn setupSessionDir(realm: []const u8, char_name: []const u8) bool { } // ============================================================================= -// Session continuation - find recent file to reuse +// Daily log file lookup // ============================================================================= -/// Scan directory for files matching `_*.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 { +/// Scan directory for today's daily log file matching `_YYYY_MM_DD.txt`. +/// Returns full path if found, null if not. +fn findDailyFile(prefix: []const u8, result_buf: *[260]u8) ?usize { if (g_dir_path_len == 0) return null; - // Build search pattern: _*.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], + var st: SYSTEMTIME = undefined; + GetLocalTime(&st); + + // Build exact filename: _YYYY_MM_DD.txt + var filename_buf: [260]u8 = undefined; + const filename = std.fmt.bufPrint(&filename_buf, "{s}_{d:0>4}_{d:0>2}_{d:0>2}.txt", .{ prefix, + st.wYear, + st.wMonth, + st.wDay, }) catch return null; - search_buf[pattern.len] = 0; + // Build full path: + const total_len = g_dir_path_len + filename.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], filename); + result_buf[total_len] = 0; + + // Check if file exists var find_data: WIN32_FIND_DATAA = undefined; - const find_handle = FindFirstFileA(@ptrCast(search_buf[0..pattern.len :0]), &find_data); + const find_handle = FindFirstFileA(@ptrCast(result_buf[0..total_len :0]), &find_data); if (find_handle == INVALID_HANDLE_VALUE) return null; - defer _ = FindClose(find_handle); + _ = 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; + return total_len; } -/// 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| { +/// Resolve a log file path: reuse today's daily file or create new one. +fn resolveDailyLogPath(prefix: []const u8, result_buf: *[260]u8) usize { + // Try to find today's daily file + if (findDailyFile(prefix, result_buf)) |len| { log.fmt("reusing: {s}\n", .{result_buf[0..len]}); return len; } - // Generate new timestamped filename + // Generate new daily filename: _YYYY_MM_DD.txt 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", .{ + const path = std.fmt.bufPrint(result_buf, "{s}{s}_{d:0>4}_{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; log.fmt("new: {s}\n", .{path}); @@ -310,22 +279,9 @@ fn configureSession(char_span: []const u8, realm_span: []const u8) void { } // 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_combat_path_len = resolveDailyLogPath("WoWCombatLog", &g_combat_path); + g_raw_combat_path_len = resolveDailyLogPath("WoWRawCombatLog", &g_raw_combat_path); + g_chat_path_len = resolveDailyLogPath("WoWChatLog", &g_chat_path); g_paths_configured = true; } @@ -356,19 +312,6 @@ fn enterWorldDetour() callconv(hook.cc.stdcall) void { 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 // ============================================================================= @@ -473,14 +416,9 @@ pub fn onShutdown() void { 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; + // Keep paths set so initLogDetour keeps redirecting on next login g_session_char_len = 0; g_session_realm_len = 0; - // Restore original path pointers so next session starts clean - restorePathPointers(); } // ============================================================================= @@ -512,7 +450,6 @@ pub fn luaGetChatLogPath(L: lua.State) callconv(.c) u32 { // ============================================================================= pub fn installHooks() void { - const result = mod_mutex.acquire(module_name); g_mutex = result.handle; g_is_hook_owner = result.is_owner; @@ -546,7 +483,6 @@ pub fn removeHooks() void { write_log_hook.detach(); init_log_hook.detach(); enter_world_hook.detach(); - restorePathPointers(); log.close(); mod_mutex.release(&g_mutex); } diff --git a/src/minimapicons/minimapicons.zig b/src/minimapicons/minimapicons.zig index db32885..7cc16db 100644 --- a/src/minimapicons/minimapicons.zig +++ b/src/minimapicons/minimapicons.zig @@ -971,6 +971,11 @@ fn renderObjectBlipsDetour(thisptr: u32, _edx: u32, dn_info: u32) callconv(hook. drawMinimapBlip(blips[i].pos, blips[i].blip.scale); } } + + // Clear texture binding so downstream renders don't use our stale texture. + // Silicon's d3d9 translation layer does not reset this between render passes. + const gxRsSet: *const fn (u32, u32) callconv(hook.cc.fastcall) void = @ptrFromInt(ADDR.GxRsSet); + gxRsSet(23, 0); } // ClntObjMgrEnumVisibleObjects: __fastcall(callback ECX, context EDX) diff --git a/src/weirdperformance/weirdperformance.zig b/src/weirdperformance/weirdperformance.zig index 3a646ed..5dd9c5b 100644 --- a/src/weirdperformance/weirdperformance.zig +++ b/src/weirdperformance/weirdperformance.zig @@ -327,7 +327,7 @@ pub fn installHooks() void { if (inflate_hook.install()) installed += 1; // Lua slab allocator replacement - // installed += luaalloc.install(); // disabled for testing + installed += luaalloc.install(); // GC phase profiler installed += luagc.install(); diff --git a/src/worldmarkers/offsets.zig b/src/worldmarkers/offsets.zig index 89be697..40b5cef 100644 --- a/src/worldmarkers/offsets.zig +++ b/src/worldmarkers/offsets.zig @@ -100,3 +100,10 @@ pub const LEADER_GUID: usize = 0x00bc75f8; pub const RAID_ROSTER_ARRAY: usize = 0x00b712a8; pub const RAID_MEMBER_COUNT: usize = 0x00b713e0; pub const ROSTER_ENTRY_RANK: usize = 0x0C; + +// ============================================================================= +// Transform and position (additional) +// ============================================================================= + +/// SetUnitPositionAndOrientation - __fastcall(ECX=entity, EDX=pos), 1 stack param (facing). +pub const FN_SET_UNIT_POSITION: usize = 0x00698e20; diff --git a/src/worldmarkers/worldmarkers.zig b/src/worldmarkers/worldmarkers.zig index 1c8fa2d..106fa2a 100644 --- a/src/worldmarkers/worldmarkers.zig +++ b/src/worldmarkers/worldmarkers.zig @@ -11,12 +11,12 @@ //! WorldMarker(index) - place marker at cursor terrain position //! ClearWorldMarker(index) - remove specific marker (1-5) //! ClearWorldMarker() - remove all markers -//! CanSetWorldMarkers() - returns 1 if leader/assist, nil otherwise +//! GetWorldMarker(index) - returns x, y, z, areaId or nil +//! CanSetWorldMarker() - returns 1 if leader/assist, nil otherwise //! //! Lua API (WorldMarkers table - internal, used by addon): -//! WorldMarkers.SetMarkerDef(i, x, y, z, area, sender) -//! WorldMarkers.ClearMarkerDef([index,] sender) -//! WorldMarkers.GetMarkerDef(index) - returns x, y, z, areaId or nil +//! WorldMarkers.SetMarkerSync(i, x, y, z, area, sender) +//! WorldMarkers.ClearMarkerSync([index,] sender) const std = @import("std"); const hook = @import("zhook"); @@ -388,7 +388,7 @@ fn spawnEntity(index: usize, pos: Vec3) bool { /// SetUnitPositionAndOrientation - __fastcall(ECX=positionData, EDX=pos), 1 stack param. fn setUnitPositionAndOrientation(entity: *anyopaque, pos: *[3]f32, facing: f32) void { - hook.call(fn (*anyopaque, *[3]f32, f32) callconv(hook.cc.fastcall) void, 0x698e20, .{ entity, pos, facing }); + hook.call(fn (*anyopaque, *[3]f32, f32) callconv(hook.cc.fastcall) void, o.FN_SET_UNIT_POSITION, .{ entity, pos, facing }); } /// Remove only the live entity for a marker slot (def untouched). @@ -504,9 +504,7 @@ pub fn luaClearWorldMarker(L: lua.State) callconv(.c) u32 { } const nargs = lua.gettop(L); - const lua_type: *const fn (lua.State, i32) callconv(hook.cc.fastcall) i32 = @ptrFromInt(0x6F3400); - - if (nargs == 0 or lua_type(L, 1) == 0) { + if (nargs == 0 or lua.typeOf(L, 1) == 0) { // No args or nil - clear all clearAllMarkers(); lua.pushnumber(L, 1.0);