multi-module cleanup: daily logs, luaalloc enable, minimap texture fix

- logsessions: switch from timestamped per-session files to daily rotation
  (WoWCombatLog_YYYY_MM_DD.txt), remove path pointer overwrites, persist
  paths across logout so redirects keep working
- logging: add append file mode (openAppend) for daily log rotation
- minimapicons: clear texture binding after blip render to fix stale
  texture with Silicon d3d9 layer
- weirdperformance: enable luaalloc slab allocator (was disabled for testing)
- worldmarkers: extract FN_SET_UNIT_POSITION to offsets, use lua.typeOf
  helper, update doc comments for renamed API
- delete unused guidcache standalone test module
This commit is contained in:
MarcelineVQ
2026-04-06 17:51:29 -07:00
parent 49f3f2452c
commit d6711901df
7 changed files with 75 additions and 196 deletions
-84
View File
@@ -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 {}
+19 -2
View File
@@ -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;
}
+37 -101
View File
@@ -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\<realm>\<character>\<Type>_<timestamp>_<PID>.txt`.
//! directories: `Logs\<realm>\<character>\WoW<Type>Log_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: <char> <realm>` 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 `<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 {
/// Scan directory for today's daily log file matching `<prefix>_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: <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],
var st: SYSTEMTIME = undefined;
GetLocalTime(&st);
// Build exact filename: <dir><prefix>_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: <dir><filename>
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(&current_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: <dir><prefix>_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);
}
+5
View File
@@ -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)
+1 -1
View File
@@ -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();
+7
View File
@@ -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;
+6 -8
View File
@@ -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);