Add logging module with auto-prefix, route all output through Logger

Replace console.zig with logging.zig: per-module Logger with auto
[name] prefix, optional file output, and destination routing. Convert
all modules from manual [name] prefixes and global con.print to Logger
instances. Remove redundant "Module loaded" lines. Replace
OutputDebugStringA in outline/tracker with Logger. Add dpslog module
with structured combat log events (SPELL_DMG, PERIODIC, HEAL, MELEE).
Add clickthrough module and bigcursor D3D9 cursor scaling.
This commit is contained in:
MarcelineVQ
2026-03-10 13:26:45 -07:00
parent e403ee18f2
commit 0f14213b0b
21 changed files with 1058 additions and 298 deletions
+1 -1
View File
@@ -130,7 +130,7 @@ Makes interactable Objects and NPCs clickable through players and units.
- Players blocking interactable NPCs (vendors, trainers, flight masters, bankers, etc.) or Objects (mailboxes, summoning portals, soulwells) become transparent to clicks
- Units (pets, NPCs) blocking interactable Objects become transparent to clicks
- Non-interactable objects (other players' pets, random mobs) are not affected, this solely helps with player dogpiles
- PvP objects and Non-interactable objects (other players' pets, random mobs) are not affected, this solely helps with player dogpiles
No configuration needed, install and forget.
+1
View File
@@ -24,6 +24,7 @@ const module_list = [_]ModuleDesc{
.{ .name = "customassets", .desc = "Enable loose file loading & permissive patch glob" },
.{ .name = "healtextfix", .desc = "Enable SuperWoW heal text fix" },
.{ .name = "bigcursor", .desc = "Enable big cursor module" },
.{ .name = "clickthrough", .desc = "Enable GO click-through (enlarge GO model bounds)" },
.{ .name = "dpslog", .desc = "Enable structured combat log events for addons", .default = false },
};
+6 -5
View File
@@ -13,10 +13,10 @@
const std = @import("std");
const hook = @import("zhook");
const con = @import("console.zig");
const logging = @import("logging.zig");
var log: logging.Logger = .{};
const build_options = @import("build_options");
// Build option convenience aliases
const build_opts = struct {
const interact = build_options.enable_interact;
@@ -310,7 +310,7 @@ fn setupAddonsDetour(mgr_ptr: u32) callconv(hook.cc.fastcall) void {
const active = if (mod.is_active) |f| f() else true;
if (active) {
const name: [*:0]const u8 = comptime (mod.addon_name.? ++ "\x00").ptr;
con.fmt("[addons] registering embedded addon: {s}\n", .{name});
log.fmt("registering embedded addon: {s}\n", .{name});
callLoadAddonTOC(name);
if (mod.hidden) {
@@ -332,12 +332,12 @@ fn hideAddonFromList(name: [*:0]const u8) void {
const node_name = hook.readMem([*:0]const u8, node + 0x14);
if (std.mem.orderZ(u8, node_name, name) == .eq) {
hook.writeMem(node + 0x29, &[_]u8{1});
con.fmt("[addons] hidden addon {s} excluded from list (+0x29=1)\n", .{name});
log.fmt("hidden addon {s} excluded from list (+0x29=1)\n", .{name});
return;
}
node = hook.readMem(u32, list_base + 4 + node);
}
con.fmt("[addons] WARNING: could not find {s} in addon list\n", .{name});
log.fmt("WARNING: could not find {s} in addon list\n", .{name});
}
fn callLoadAddonTOC(addon_name: [*:0]const u8) void {
@@ -359,6 +359,7 @@ const has_addons = blk: {
pub fn install() void {
if (!has_addons) return;
log = logging.Logger.open("addons", .console);
_ = setup_addons_hook.attach(0x51C740, &setupAddonsDetour);
}
+10 -49
View File
@@ -9,7 +9,7 @@
const std = @import("std");
const hook = @import("zhook");
const con = @import("../console.zig");
const logging = @import("../logging.zig");
const mod_mutex = @import("../mutex.zig");
const WINAPI = std.builtin.CallingConvention.winapi;
@@ -18,49 +18,10 @@ pub const module_name: [*:0]const u8 = "bigcursor";
var g_mutex: ?*anyopaque = null;
var g_is_hook_owner: bool = false;
var log: logging.Logger = .{};
// =============================================================================
// File logging (survives crashes — console closes too fast)
// =============================================================================
extern "kernel32" fn CreateFileA(name: [*:0]const u8, access: u32, share: u32, sa: ?*anyopaque, disp: u32, flags: u32, template: ?*anyopaque) callconv(WINAPI) ?*anyopaque;
extern "kernel32" fn WriteFile(handle: *anyopaque, buf: [*]const u8, len: u32, written: ?*u32, overlapped: ?*anyopaque) callconv(WINAPI) i32;
extern "kernel32" fn FlushFileBuffers(handle: *anyopaque) callconv(WINAPI) i32;
extern "kernel32" fn CloseHandle(handle: *anyopaque) callconv(WINAPI) i32;
const INVALID_HANDLE: usize = 0xFFFFFFFF;
var g_logfile: ?*anyopaque = null;
fn logInit() void {
const h = CreateFileA("bigcursor_debug.log", 0x40000000, 1, null, 2, 0x80, null); // GENERIC_WRITE, FILE_SHARE_READ, CREATE_ALWAYS, NORMAL
if (h) |handle| {
if (@intFromPtr(handle) != INVALID_HANDLE) {
g_logfile = handle;
}
}
}
fn logDeinit() void {
if (g_logfile) |h| {
_ = CloseHandle(h);
g_logfile = null;
}
}
fn log(msg: []const u8) void {
con.print(msg);
if (g_logfile) |h| {
_ = WriteFile(h, msg.ptr, @intCast(msg.len), null, null);
_ = FlushFileBuffers(h);
}
}
fn logFmt(comptime f: []const u8, args: anytype) void {
var buf: [512]u8 = undefined;
const msg = std.fmt.bufPrint(&buf, f, args) catch return;
log(msg);
}
// =============================================================================
// Scale2x — edge-aware pixel-art 2x upscaler (EPX/AdvMAME2x)
//
@@ -567,7 +528,7 @@ pub fn luaSetCursorScale(L: *anyopaque) callconv(.c) u32 {
g_scale_f = clamped;
cacheClear();
g_hcursor = null;
logFmt("[bigcursor] scale set to {d:.2}\n", .{g_scale_f});
log.fmt("scale set to {d:.2}\n", .{g_scale_f});
}
return 0;
}
@@ -618,17 +579,17 @@ pub fn lateInit() void {
if (hooks_installed) return;
const vt = getD3D9VTable() orelse {
con.print("[bigcursor] D3D9 device not available yet\n");
log.print("D3D9 device not available yet\n");
return;
};
d3d9_vtable = vt;
if (!patchVtableEntry(vt, VT_SetCursorProperties, @intFromPtr(&hkSetCursorProperties), &orig_set_cursor_props)) {
con.print("[bigcursor] failed to hook SetCursorProperties\n");
log.print("failed to hook SetCursorProperties\n");
return;
}
if (!patchVtableEntry(vt, VT_ShowCursor, @intFromPtr(&hkShowCursor), &orig_show_cursor)) {
con.print("[bigcursor] failed to hook ShowCursor\n");
log.print("failed to hook ShowCursor\n");
restoreVtableEntry(vt, VT_SetCursorProperties, orig_set_cursor_props);
return;
}
@@ -638,7 +599,7 @@ pub fn lateInit() void {
// Register CVar for persistence (tenths: 15 = 1.5x, 20 = 2.0x)
_ = registerCVar(CVAR_NAME, 0, 0, "12", 0, 1, 0, 0);
g_scale_f = readCVarScaleInit();
logFmt("[bigcursor] D3D9 hooks installed (scale={d:.1}x)\n", .{g_scale_f});
log.fmt("D3D9 hooks installed (scale={d:.1}x)\n", .{g_scale_f});
}
// =============================================================================
@@ -650,13 +611,13 @@ pub fn isActive() bool {
}
pub fn installHooks() void {
logInit();
log("[bigcursor] Module loaded\n");
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, .console);
}
pub fn removeHooks() void {
@@ -672,8 +633,8 @@ pub fn removeHooks() void {
g_hcursor = null;
if (g_is_hook_owner) {
log.close();
mod_mutex.release(&g_mutex);
}
g_is_hook_owner = false;
logDeinit();
}
+34 -4
View File
@@ -22,6 +22,14 @@ pub const module_name: [*:0]const u8 = "clickthrough";
const ADDR_WorldIntersectionTest: usize = 0x480DF0;
const ADDR_GetObjectByGUID: usize = 0x464870;
// Map identification (Map.dbc lookup)
const OBJECT_MANAGER_PTR: usize = 0x00B41414;
const OBJMGR_MAP_ID_OFFSET: usize = 0xCC;
const MAP_DBC_DATA: usize = 0x00C0DAA8;
const MAP_DBC_MAX: usize = 0x00C0DAAC;
const MAP_DBC_MAP_TYPE_OFFSET: usize = 0x08;
const MAP_TYPE_BATTLEGROUND: u32 = 3;
// =============================================================================
// HitTestResult layout
// =============================================================================
@@ -34,6 +42,8 @@ const HIT_RESULT_SIZE: usize = 0x34;
const OBJ_DESCRIPTOR: usize = 0x08;
const OBJ_TYPE_MASK_OFFSET: usize = 0x08; // at *(*(obj+8)+8)
// Object type + descriptor field offsets
// Type masks
const TYPE_UNIT: u32 = 0x09;
const TYPE_PLAYER: u32 = 0x19;
@@ -49,14 +59,34 @@ const OBJ_TYPE: usize = 0x14;
const OBJ_TYPE_UNIT: u32 = 3;
const OBJ_TYPE_GO: u32 = 5;
const DESC_NPC_FLAGS: usize = 0x93 * 4; // UNIT_NPC_FLAGS = OBJECT_END(0x06) + 0x8D = 0x93
const DESC_ENTRY: usize = 0x03 * 4; // OBJECT_FIELD_ENTRY
const ADDR_CallSpellCastHandler: usize = 0x5F8800;
// =============================================================================
// Battleground detection
// =============================================================================
/// Check if the current map is a battleground by reading Map.dbc mapType.
fn isInBattleground() bool {
const obj_mgr = hook.readMem(u32, OBJECT_MANAGER_PTR);
if (obj_mgr == 0) return false;
const map_id = hook.readMem(u32, obj_mgr + OBJMGR_MAP_ID_OFFSET);
const dbc_max = hook.readMem(u32, MAP_DBC_MAX);
if (map_id > dbc_max) return false;
const table_base = hook.readMem(u32, MAP_DBC_DATA);
if (table_base == 0) return false;
const row = hook.readMem(u32, table_base + map_id * 4);
if (row == 0) return false;
const map_type = hook.readMem(u32, row + MAP_DBC_MAP_TYPE_OFFSET);
return map_type == MAP_TYPE_BATTLEGROUND;
}
fn getObjectByGUID(guid_lo: u32, guid_hi: u32) u32 {
if (guid_lo == 0 and guid_hi == 0) return 0;
return hook.call(fn (u32, u32) callconv(hook.cc.stdcall) u32, ADDR_GetObjectByGUID, .{ guid_lo, guid_hi });
}
/// Check if the GUID refers to an interactable GO (mailbox, soulwell, etc.)
/// Check if the GUID refers to an interactable GO.
fn isInteractableGO(guid_lo: u32, guid_hi: u32) bool {
const obj = getObjectByGUID(guid_lo, guid_hi);
if (obj == 0) return false;
@@ -109,7 +139,8 @@ fn worldIntersectDetour(world_frame: u32, ray_start: u32, ray_end: u32, flags: u
// Call original with caller's flags
const hit_type = wit_hook.callOriginal(.{ world_frame, ray_start, ray_end, flags, hit_result });
if (!g_is_hook_owner or hit_result == 0 or hit_type != 2) return hit_type;
// No click-through in battlegrounds
if (!g_is_hook_owner or hit_result == 0 or hit_type != 2 or isInBattleground()) return hit_type;
// hitType 2 = object hit. Check if it's a unit/player.
const buf_lo = hook.readMem(u32, hit_result + HIT_GUID_LO);
@@ -161,7 +192,6 @@ pub fn isActive() bool {
}
pub fn installHooks() void {
logging.print("[clickthrough] Module loaded\n");
const result = mod_mutex.acquire(module_name);
g_mutex = result.handle;
@@ -171,7 +201,7 @@ pub fn installHooks() void {
log = logging.Logger.open(module_name, .both);
_ = wit_hook.attach(ADDR_WorldIntersectionTest, &worldIntersectDetour);
log.fmt("[clickthrough] WorldIntersectionTest hooked at 0x{x}\n", .{ADDR_WorldIntersectionTest});
log.fmt("WorldIntersectionTest hooked at 0x{x}\n", .{ADDR_WorldIntersectionTest});
}
pub fn removeHooks() void {
-65
View File
@@ -1,65 +0,0 @@
//! Debug console - compiles out entirely in non-Debug builds.
//!
//! Usage from any module:
//! const con = @import("../console.zig"); // or appropriate relative path
//! con.print("[markers] loaded\n");
//! con.fmt("[markers] pos = {d}, {d}, {d}\n", .{ x, y, z });
const std = @import("std");
const builtin = @import("builtin");
const debug = builtin.mode == .Debug;
const WINAPI = std.builtin.CallingConvention.winapi;
const win = if (debug) struct {
extern "kernel32" fn AllocConsole() callconv(WINAPI) i32;
extern "kernel32" fn FreeConsole() callconv(WINAPI) i32;
extern "kernel32" fn SetConsoleTitleA(title: [*:0]const u8) callconv(WINAPI) i32;
extern "kernel32" fn GetStdHandle(nStdHandle: u32) callconv(WINAPI) ?*anyopaque;
extern "kernel32" fn WriteConsoleA(
hOut: *anyopaque,
buf: [*]const u8,
len: u32,
written: ?*u32,
reserved: ?*anyopaque,
) callconv(WINAPI) i32;
const STD_OUTPUT_HANDLE: u32 = 0xFFFFFFF5;
var handle: ?*anyopaque = null;
} else void;
pub fn init() void {
if (debug) {
_ = win.AllocConsole();
_ = win.SetConsoleTitleA("weirdutils");
win.handle = win.GetStdHandle(win.STD_OUTPUT_HANDLE);
print("[weirdutils] Console attached\n");
}
}
pub fn deinit() void {
if (debug) {
if (win.handle != null) {
print("[weirdutils] Detaching\n");
_ = win.FreeConsole();
win.handle = null;
}
}
}
pub fn print(msg: []const u8) void {
if (debug) {
if (win.handle) |h| {
_ = win.WriteConsoleA(h, msg.ptr, @intCast(msg.len), null, null);
}
}
}
pub fn fmt(comptime f: []const u8, args: anytype) void {
if (debug) {
var buf: [512]u8 = undefined;
const msg = std.fmt.bufPrint(&buf, f, args) catch return;
print(msg);
}
}
+6 -3
View File
@@ -12,7 +12,7 @@
const std = @import("std");
const hook = @import("zhook");
const con = @import("../console.zig");
const logging = @import("../logging.zig");
// =============================================================================
// Windows API (project-specific - not in hook lib)
@@ -176,7 +176,7 @@ pub fn looseFilesLookup(game_path_ptr: u32, output_buffer_ptr: u32) bool {
const disk_path = loose_files.get(norm_buf[0..path.len]) orelse return false;
con.fmt("[customassets] loose hit: \"{s}\"\n", .{path});
log.fmt("loose hit: \"{s}\"\n", .{path});
if (output_buffer_ptr != 0) {
const disk_len = cStrLen(disk_path.ptr);
@@ -260,19 +260,21 @@ pub const module_name: [*:0]const u8 = "customassets";
var installed: bool = false;
var g_mutex: ?*anyopaque = null;
var g_is_hook_owner: bool = false;
var log: logging.Logger = .{};
pub fn isActive() bool {
return g_is_hook_owner;
}
pub fn installHooks() void {
con.print("[customassets] Module loaded\n");
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, .console);
applyGlobPatch();
applyLooseFilePatches();
looseFilesInit();
@@ -288,6 +290,7 @@ pub fn removeHooks() void {
}
if (g_is_hook_owner) {
log.close();
mod_mutex.release(&g_mutex);
}
g_is_hook_owner = false;
+542 -6
View File
@@ -1,33 +1,569 @@
//! DPS log module.
//! DPS log module — structured combat log events.
//!
//! Provides structured Lua objects for combat log events so addons can read
//! parsed fields directly instead of re-parsing the combat log string.
//! Hooks packet handlers for SMSG_SPELLNONMELEEDAMAGELOG,
//! SMSG_PERIODICAURALOG, SMSG_SPELLHEALLOG, and SMSG_ATTACKERSTATEUPDATE
//! to fire custom Lua events with structured numeric/string args, so addons
//! can skip string parsing.
//!
//! TODO: Hook combat log event dispatch, build Lua tables per event type.
//! Custom events (registered in unused event table slots):
//! COMBAT_LOG_SPELL_DMG (551) — spell damage (direct)
//! COMBAT_LOG_PERIODIC (552) — periodic damage/heal ticks
//! COMBAT_LOG_HEAL (553) — direct heals
//! COMBAT_LOG_MELEE (554) — melee damage (auto-attack + special)
//!
//! Lua args for COMBAT_LOG_SPELL_DMG:
//! arg1=targetGUID, arg2=casterGUID, arg3=spellId, arg4=damage,
//! arg5=school, arg6=absorb, arg7=resist, arg8=blocked, arg9=hitInfo
//!
//! Lua args for COMBAT_LOG_PERIODIC:
//! arg1=targetGUID, arg2=casterGUID, arg3=spellId, arg4=amount,
//! arg5=school, arg6=absorb, arg7=resist, arg8=auraType, arg9=powerType
//!
//! Lua args for COMBAT_LOG_HEAL:
//! arg1=targetGUID, arg2=casterGUID, arg3=spellId, arg4=healAmount,
//! arg5=isCrit
//!
//! Lua args for COMBAT_LOG_MELEE:
//! arg1=targetGUID, arg2=attackerGUID, arg3=totalDamage, arg4=school,
//! arg5=absorb, arg6=resist, arg7=blocked, arg8=hitInfo, arg9=victimState
const con = @import("../console.zig");
const std = @import("std");
const hook = @import("zhook");
const logging = @import("../logging.zig");
const mod_mutex = @import("../mutex.zig");
pub const module_name: [*:0]const u8 = "dpslog";
var g_mutex: ?*anyopaque = null;
var g_is_hook_owner: bool = false;
var log: logging.Logger = .{};
pub fn isActive() bool {
return g_is_hook_owner;
}
// =============================================================================
// Custom event IDs & names
// =============================================================================
/// We register events at slots 551..554 (beyond nampower's 549/550).
/// The event name string pointer array has 4-byte stride per event ID.
/// Nampower: 549→0xBE1A2C, 550→0xBE1A30 → base = 0xBE1198.
/// 551: 0xBE1A34 552: 0xBE1A38 553: 0xBE1A3C 554: 0xBE1A40
const EVENT_SPELL_DMG: u32 = 551;
const EVENT_PERIODIC: u32 = 552;
const EVENT_HEAL: u32 = 553;
const EVENT_MELEE: u32 = 554;
/// Derived from nampower: event 549 string ptr is at 0xBE1A2C → base = 0xBE1198.
const EVENT_STR_PTR_BASE: u32 = 0xBE1198;
/// Static event name strings — must outlive the process.
const event_name_spell_dmg: [*:0]const u8 = "COMBAT_LOG_SPELL_DMG";
const event_name_periodic: [*:0]const u8 = "COMBAT_LOG_PERIODIC";
const event_name_heal: [*:0]const u8 = "COMBAT_LOG_HEAL";
const event_name_melee: [*:0]const u8 = "COMBAT_LOG_MELEE";
// TODO: Detect nampower via GetModuleHandleA("nampower.dll") and skip
// overlapping SpellNonMeleeDmgLog/PeriodicAuraLog hooks if loaded (nampower
// fires SPELL_DAMAGE_EVENT_SELF/OTHER for those). Low priority — most users
// won't run both DLLs simultaneously.
// =============================================================================
// CDataStore helpers — direct memory reads from packet buffer
// =============================================================================
//
// CDataStore layout (from nampower cdatastore.hpp):
// +0x00: vtable
// +0x04: m_buffer (u8 pointer)
// +0x08: m_base
// +0x0C: m_alloc
// +0x10: m_size
// +0x14: m_read (current read position)
//
// NOTE: the nampower header shows m_buffer at +0x00 after vtable, but
// CDataStore is a virtual class — vtable is +0x00, members follow.
// Let's match nampower's actual memory layout which treats it as:
// m_buffer at offset after vtable. In the C++ class, the first member
// after the vtable IS m_buffer. So:
// +0x00: vtable ptr
// +0x04: m_buffer
// +0x08: m_base
// +0x0C: m_alloc
// +0x10: m_size
// +0x14: m_read
const CDS_BUFFER = 0x04;
const CDS_BASE = 0x08;
const CDS_SIZE = 0x10;
const CDS_READ = 0x14;
fn cdsGetRead(cds: u32) u32 {
return hook.readMem(u32, cds + CDS_READ);
}
fn cdsSetRead(cds: u32, pos: u32) void {
@as(*align(1) u32, @ptrFromInt(cds + CDS_READ)).* = pos;
}
fn cdsBuffer(cds: u32) u32 {
return hook.readMem(u32, cds + CDS_BUFFER);
}
fn cdsBase(cds: u32) u32 {
return hook.readMem(u32, cds + CDS_BASE);
}
fn cdsSize(cds: u32) u32 {
return hook.readMem(u32, cds + CDS_SIZE);
}
/// Read a T from the CDataStore at the current read position, advancing m_read.
fn cdsGet(comptime T: type, cds: u32) ?T {
const rpos = cdsGetRead(cds);
const end = rpos + @sizeOf(T);
if (end > cdsSize(cds)) return null;
const buf = cdsBuffer(cds);
const base = cdsBase(cds);
const val = @as(*align(1) const T, @ptrFromInt(buf -% base + rpos)).*;
cdsSetRead(cds, end);
return val;
}
/// Read a packed GUID from the CDataStore. Returns null on read error.
/// Packed GUID format: 1-byte mask, then one byte per set bit in the mask.
fn cdsGetPackedGuid(cds: u32) ?u64 {
const mask = cdsGet(u8, cds) orelse return null;
var guid: u64 = 0;
inline for (0..8) |i| {
if (mask & (@as(u8, 1) << @intCast(i)) != 0) {
const b = cdsGet(u8, cds) orelse return null;
guid |= @as(u64, b) << @intCast(i * 8);
}
}
return guid;
}
// =============================================================================
// GUID → hex string conversion
// =============================================================================
/// Convert a u64 GUID to a 0x-prefixed hex string. Returns pointer to static buffer.
/// Uses two alternating buffers so we can format target + caster in one call.
var guid_bufs: [4][20]u8 = undefined;
var guid_buf_idx: u2 = 0;
fn guidToString(guid: u64) [*:0]const u8 {
const idx = guid_buf_idx;
guid_buf_idx +%= 1;
const buf = &guid_bufs[idx];
buf[0] = '0';
buf[1] = 'x';
const hex = "0123456789ABCDEF";
inline for (0..16) |i| {
const shift: u6 = @intCast((15 - i) * 4);
buf[2 + i] = hex[@intCast((guid >> shift) & 0xF)];
}
buf[18] = 0;
return @ptrCast(buf[0..18 :0]);
}
// =============================================================================
// SignalEventParam — fire a Lua event with structured args
// =============================================================================
//
// SignalEventParam (0x703F50): __cdecl(eventId, fmtStr, ...)
// The format string uses %s/%d to push args as Lua event args (arg1, arg2, ...).
// We use typed wrapper functions since Zig can't express variadic cdecl generically.
/// Fire spell damage event: "%s%s%d%d%d%d%d%d%d"
/// args: targetGuid, casterGuid, spellId, damage, school, absorb, resist, blocked, hitInfo
fn fireSpellDamageEvent(event_id: u32, target: [*:0]const u8, caster: [*:0]const u8, spell_id: u32, damage: u32, school: u32, absorb: u32, resist: i32, blocked: u32, hit_info: u32) void {
const SignalFn = fn (u32, [*:0]const u8, [*:0]const u8, [*:0]const u8, u32, u32, u32, u32, i32, u32, u32) callconv(hook.cc.cdecl) void;
const f: *const SignalFn = @ptrFromInt(0x703F50);
// Format: targetGuid(s), casterGuid(s), spellId(d), damage(d), school(d), absorb(d), resist(d), blocked(d), hitInfo(d)
@call(.auto, f, .{ event_id, "%s%s%d%d%d%d%d%d%d", target, caster, spell_id, damage, school, absorb, resist, blocked, hit_info });
}
/// Fire periodic event: "%s%s%d%d%d%d%d%d%d"
/// Same format as spell damage but with auraType instead of hitInfo.
fn firePeriodicEvent(event_id: u32, target: [*:0]const u8, caster: [*:0]const u8, spell_id: u32, amount: u32, school: u32, absorb: u32, resist: i32, aura_type: u32, power_type: u32) void {
const SignalFn = fn (u32, [*:0]const u8, [*:0]const u8, [*:0]const u8, u32, u32, u32, u32, i32, u32, u32) callconv(hook.cc.cdecl) void;
const f: *const SignalFn = @ptrFromInt(0x703F50);
@call(.auto, f, .{ event_id, "%s%s%d%d%d%d%d%d%d", target, caster, spell_id, amount, school, absorb, resist, aura_type, power_type });
}
/// Fire heal event: "%s%s%d%d%d"
/// args: targetGuid, casterGuid, spellId, healAmount, isCrit
fn fireHealEvent(event_id: u32, target: [*:0]const u8, caster: [*:0]const u8, spell_id: u32, amount: u32, is_crit: u32) void {
const SignalFn = fn (u32, [*:0]const u8, [*:0]const u8, [*:0]const u8, u32, u32, u32) callconv(hook.cc.cdecl) void;
const f: *const SignalFn = @ptrFromInt(0x703F50);
@call(.auto, f, .{ event_id, "%s%s%d%d%d", target, caster, spell_id, amount, is_crit });
}
// =============================================================================
// Hook: FrameScript_CreateEvents (0x703D90)
// Expands max event count to accommodate our custom event slots.
// =============================================================================
/// FrameScript_CreateEvents signature: __fastcall(ECX=param1, EDX=maxEventId)
/// Nampower hooks this as (int param_1, uint32_t maxEventId) — the first two
/// args in fastcall go to ECX and EDX.
var create_events_hook: hook.Detour(fn (u32, u32) callconv(hook.cc.fastcall) void) = .{};
fn createEventsDetour(param1: u32, max_event_id: u32) callconv(hook.cc.fastcall) void {
// Expand to at least 556 (we use slots 551..554, need maxId > 554)
var new_max = max_event_id;
if (new_max < 556) {
new_max = 556;
log.fmt("FrameScript_CreateEvents: expanded maxEventId {d} -> {d}\n", .{ max_event_id, new_max });
}
create_events_hook.callOriginal(.{ param1, new_max });
}
// =============================================================================
// Hook: SpellNonMeleeDmgLogHandler (0x5E85E0)
// Packet: SMSG_SPELLNONMELEEDAMAGELOG
// Convention: FastCall(unk, opCode, unk2, CDataStore*)
// =============================================================================
const FastCallPacketHandlerFn = fn (u32, u32, u32, u32) callconv(hook.cc.fastcall) u32;
var spell_dmg_hook: hook.Detour(FastCallPacketHandlerFn) = .{};
fn spellNonMeleeDmgLogDetour(unk: u32, opcode: u32, unk2: u32, cds: u32) callconv(hook.cc.fastcall) u32 {
asm volatile ("" ::: .{ .esi = true, .edi = true, .ebx = true });
// Save read position
const saved_read = cdsGetRead(cds);
// Parse SMSG_SPELLNONMELEEDAMAGELOG packet
const target_guid = cdsGetPackedGuid(cds);
const caster_guid = cdsGetPackedGuid(cds);
const spell_id = cdsGet(u32, cds);
const damage = cdsGet(u32, cds);
const school = cdsGet(u8, cds);
const absorb = cdsGet(u32, cds);
const resist = cdsGet(i32, cds);
const _periodic_log = cdsGet(u8, cds);
const _unused = cdsGet(u8, cds);
const blocked = cdsGet(u32, cds);
const hit_info = cdsGet(u32, cds);
// Restore read position before calling original
cdsSetRead(cds, saved_read);
// Fire event if all fields parsed successfully
if (target_guid != null and caster_guid != null and spell_id != null and
damage != null and school != null and absorb != null and resist != null and
blocked != null and hit_info != null)
{
const target_str = guidToString(target_guid.?);
const caster_str = guidToString(caster_guid.?);
fireSpellDamageEvent(
EVENT_SPELL_DMG,
target_str,
caster_str,
spell_id.?,
damage.?,
@as(u32, school.?),
absorb.?,
resist.?,
blocked.?,
hit_info.?,
);
}
_ = _periodic_log;
_ = _unused;
return spell_dmg_hook.callOriginal(.{ unk, opcode, unk2, cds });
}
// =============================================================================
// Hook: PeriodicAuraLogHandler (0x626DD0)
// Packet: SMSG_PERIODICAURALOG
// Convention: FastCall(unk, opCode, unk2, CDataStore*)
// =============================================================================
var periodic_hook: hook.Detour(FastCallPacketHandlerFn) = .{};
fn periodicAuraLogDetour(unk: u32, opcode: u32, unk2: u32, cds: u32) callconv(hook.cc.fastcall) u32 {
asm volatile ("" ::: .{ .esi = true, .edi = true, .ebx = true });
const saved_read = cdsGetRead(cds);
// Parse SMSG_PERIODICAURALOG packet
const target_guid = cdsGetPackedGuid(cds);
const caster_guid = cdsGetPackedGuid(cds);
const spell_id = cdsGet(u32, cds);
const count = cdsGet(u32, cds);
if (target_guid != null and caster_guid != null and spell_id != null and count != null) {
const target_str = guidToString(target_guid.?);
const caster_str = guidToString(caster_guid.?);
// Process first aura entry (count is almost always 1)
const aura_type = cdsGet(u32, cds);
if (aura_type) |at| {
switch (at) {
3, 89 => {
// PERIODIC_DAMAGE / PERIODIC_DAMAGE_PERCENT
const amount = cdsGet(u32, cds) orelse 0;
const spell_school = cdsGet(u32, cds) orelse 0;
const absorb = cdsGet(u32, cds) orelse 0;
const resist = cdsGet(i32, cds) orelse 0;
firePeriodicEvent(EVENT_PERIODIC, target_str, caster_str, spell_id.?, amount, spell_school, absorb, resist, at, 0);
},
8, 20 => {
// PERIODIC_HEAL / OBS_MOD_HEALTH
const amount = cdsGet(u32, cds) orelse 0;
firePeriodicEvent(EVENT_PERIODIC, target_str, caster_str, spell_id.?, amount, 0, 0, 0, at, 0);
},
21, 24 => {
// OBS_MOD_MANA / PERIODIC_ENERGIZE
const power_type = cdsGet(u32, cds) orelse 0;
const amount = cdsGet(u32, cds) orelse 0;
firePeriodicEvent(EVENT_PERIODIC, target_str, caster_str, spell_id.?, amount, 0, 0, 0, at, power_type);
},
64 => {
// PERIODIC_MANA_LEECH
const power_type = cdsGet(u32, cds) orelse 0;
const amount = cdsGet(u32, cds) orelse 0;
_ = cdsGet(u32, cds); // multiplier (skip)
firePeriodicEvent(EVENT_PERIODIC, target_str, caster_str, spell_id.?, amount, 0, 0, 0, at, power_type);
},
else => {},
}
}
}
// Restore read position
cdsSetRead(cds, saved_read);
return periodic_hook.callOriginal(.{ unk, opcode, unk2, cds });
}
// =============================================================================
// Hook: SpellHealLogHandler (0x5E89C0)
// Packet: SMSG_SPELLHEALLOG (opcode 0x150)
// Convention: FastCall(unk, opCode, unk2, CDataStore*)
// Packet format: targetGuid(PackedGuid), casterGuid(PackedGuid),
// spellId(u32), healAmount(u32), isCrit(u8)
// =============================================================================
var heal_hook: hook.Detour(FastCallPacketHandlerFn) = .{};
fn spellHealLogDetour(unk: u32, opcode: u32, unk2: u32, cds: u32) callconv(hook.cc.fastcall) u32 {
asm volatile ("" ::: .{ .esi = true, .edi = true, .ebx = true });
const saved_read = cdsGetRead(cds);
// Parse SMSG_SPELLHEALLOG packet
const target_guid = cdsGetPackedGuid(cds);
const caster_guid = cdsGetPackedGuid(cds);
const spell_id = cdsGet(u32, cds);
const heal_amount = cdsGet(u32, cds);
const is_crit_raw = cdsGet(u8, cds);
cdsSetRead(cds, saved_read);
if (target_guid != null and caster_guid != null and spell_id != null and
heal_amount != null and is_crit_raw != null)
{
const target_str = guidToString(target_guid.?);
const caster_str = guidToString(caster_guid.?);
const is_crit: u32 = if (is_crit_raw.? != 0) 1 else 0;
fireHealEvent(EVENT_HEAL, target_str, caster_str, spell_id.?, heal_amount.?, is_crit);
}
return heal_hook.callOriginal(.{ unk, opcode, unk2, cds });
}
// =============================================================================
// Hook: MeleeDispatcher (0x6255B0)
// Shared packet handler for opcodes 0x143-0x14A. We filter for 0x14A only.
// Packet: SMSG_ATTACKERSTATEUPDATE (opcode 0x14A)
// Convention: FastCall(unk, opCode, unk2, CDataStore*)
//
// Packet format:
// hitInfo(u32), attackerGuid(PackedGuid), targetGuid(PackedGuid),
// totalDamage(u32), subDamageCount(u8),
// per sub: school(u32), damageFP(f32), damage(u32), absorb(u32), resist(u32),
// victimState(u32), unknown1(u32), unknown2(u32), spellId(u32),
// if hitInfo & 0x1: blocked(u32)
// =============================================================================
const OPCODE_ATTACKERSTATEUPDATE: u32 = 0x14A;
var melee_hook: hook.Detour(FastCallPacketHandlerFn) = .{};
fn meleeDispatcherDetour(unk: u32, opcode: u32, unk2: u32, cds: u32) callconv(hook.cc.fastcall) u32 {
asm volatile ("" ::: .{ .esi = true, .edi = true, .ebx = true });
// Only intercept SMSG_ATTACKERSTATEUPDATE; pass all other opcodes through
if (opcode == OPCODE_ATTACKERSTATEUPDATE) {
parseMeleePacket(cds);
}
return melee_hook.callOriginal(.{ unk, opcode, unk2, cds });
}
fn parseMeleePacket(cds: u32) void {
const saved_read = cdsGetRead(cds);
defer cdsSetRead(cds, saved_read);
const hit_info = cdsGet(u32, cds) orelse return;
const attacker_guid = cdsGetPackedGuid(cds) orelse return;
const target_guid = cdsGetPackedGuid(cds) orelse return;
const total_damage = cdsGet(u32, cds) orelse return;
const sub_count = cdsGet(u8, cds) orelse return;
// Read first sub-damage entry for school/absorb/resist
var school: u32 = 0;
var absorb: u32 = 0;
var resist: u32 = 0;
if (sub_count > 0) {
school = cdsGet(u32, cds) orelse 0;
_ = cdsGet(f32, cds); // damageFP (skip)
_ = cdsGet(u32, cds); // damage (skip, use totalDamage)
absorb = cdsGet(u32, cds) orelse 0;
resist = cdsGet(u32, cds) orelse 0;
// Skip remaining sub-damage entries
var i: u8 = 1;
while (i < sub_count) : (i += 1) {
_ = cdsGet(u32, cds); // school
_ = cdsGet(f32, cds); // damageFP
_ = cdsGet(u32, cds); // damage
_ = cdsGet(u32, cds); // absorb
_ = cdsGet(u32, cds); // resist
}
}
const victim_state = cdsGet(u32, cds) orelse return;
_ = cdsGet(u32, cds); // unknown1
_ = cdsGet(u32, cds); // unknown2
_ = cdsGet(u32, cds); // spellId (0 for melee, nonzero for special attacks)
var blocked: u32 = 0;
if (hit_info & 0x1 != 0) {
blocked = cdsGet(u32, cds) orelse 0;
}
const target_str = guidToString(target_guid);
const attacker_str = guidToString(attacker_guid);
// Fire melee event: same 9-arg format as spell damage
// arg1=target, arg2=attacker, arg3=totalDamage, arg4=school,
// arg5=absorb, arg6=resist, arg7=blocked, arg8=hitInfo, arg9=victimState
fireMeleeEvent(EVENT_MELEE, target_str, attacker_str, total_damage, school, absorb, resist, blocked, hit_info, victim_state);
}
/// Fire melee event: "%s%s%d%d%d%d%d%d%d"
/// args: targetGuid, attackerGuid, totalDamage, school, absorb, resist, blocked, hitInfo, victimState
fn fireMeleeEvent(event_id: u32, target: [*:0]const u8, attacker: [*:0]const u8, damage: u32, school: u32, absorb: u32, resist: u32, blocked: u32, hit_info: u32, victim_state: u32) void {
const SignalFn = fn (u32, [*:0]const u8, [*:0]const u8, [*:0]const u8, u32, u32, u32, u32, u32, u32, u32) callconv(hook.cc.cdecl) void;
const f: *const SignalFn = @ptrFromInt(0x703F50);
@call(.auto, f, .{ event_id, "%s%s%d%d%d%d%d%d%d", target, attacker, damage, school, absorb, resist, blocked, hit_info, victim_state });
}
// =============================================================================
// Custom event registration — write event name ptrs to unused table slots
// =============================================================================
fn registerCustomEvents() void {
// Each event slot's string name pointer is at EVENT_STR_PTR_BASE + eventId * 4
// These are in .data (RW), so writeProtected is needed since the event name
// array is in .rdata/.data boundary area.
const spell_dmg_addr = EVENT_STR_PTR_BASE + EVENT_SPELL_DMG * 4;
const periodic_addr = EVENT_STR_PTR_BASE + EVENT_PERIODIC * 4;
const heal_addr = EVENT_STR_PTR_BASE + EVENT_HEAL * 4;
const melee_addr = EVENT_STR_PTR_BASE + EVENT_MELEE * 4;
const spell_dmg_ptr: u32 = @intFromPtr(event_name_spell_dmg);
const periodic_ptr: u32 = @intFromPtr(event_name_periodic);
const heal_ptr: u32 = @intFromPtr(event_name_heal);
const melee_ptr: u32 = @intFromPtr(event_name_melee);
hook.writeProtected(spell_dmg_addr, std.mem.asBytes(&spell_dmg_ptr));
hook.writeProtected(periodic_addr, std.mem.asBytes(&periodic_ptr));
hook.writeProtected(heal_addr, std.mem.asBytes(&heal_ptr));
hook.writeProtected(melee_addr, std.mem.asBytes(&melee_ptr));
log.fmt("Registered events: {d}={s}, {d}={s}, {d}={s}, {d}={s}\n", .{
EVENT_SPELL_DMG, std.mem.span(event_name_spell_dmg),
EVENT_PERIODIC, std.mem.span(event_name_periodic),
EVENT_HEAL, std.mem.span(event_name_heal),
EVENT_MELEE, std.mem.span(event_name_melee),
});
}
// =============================================================================
// Install / remove
// =============================================================================
pub fn installHooks() void {
con.print("[dpslog] Module loaded (stub)\n");
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, .console);
// Register custom events (overwrite event name string pointers)
registerCustomEvents();
// Hook FrameScript_CreateEvents to expand event count
if (create_events_hook.attach(0x703D90, &createEventsDetour) != .ok) {
log.print("FAILED to hook FrameScript_CreateEvents\n");
} else {
log.print("Hooked FrameScript_CreateEvents\n");
}
if (spell_dmg_hook.attach(0x5E85E0, &spellNonMeleeDmgLogDetour) != .ok) {
log.print("FAILED to hook SpellNonMeleeDmgLogHandler\n");
} else {
log.print("Hooked SpellNonMeleeDmgLogHandler\n");
}
if (periodic_hook.attach(0x626DD0, &periodicAuraLogDetour) != .ok) {
log.print("FAILED to hook PeriodicAuraLogHandler\n");
} else {
log.print("Hooked PeriodicAuraLogHandler\n");
}
if (heal_hook.attach(0x5E89C0, &spellHealLogDetour) != .ok) {
log.print("FAILED to hook SpellHealLogHandler\n");
} else {
log.print("Hooked SpellHealLogHandler\n");
}
if (melee_hook.attach(0x6255B0, &meleeDispatcherDetour) != .ok) {
log.print("FAILED to hook MeleeDispatcher\n");
} else {
log.print("Hooked MeleeDispatcher\n");
}
}
pub fn removeHooks() void {
if (g_is_hook_owner) {
melee_hook.detach();
heal_hook.detach();
periodic_hook.detach();
spell_dmg_hook.detach();
create_events_hook.detach();
log.close();
mod_mutex.release(&g_mutex);
}
g_is_hook_owner = false;
+26 -22
View File
@@ -21,7 +21,7 @@
const std = @import("std");
const hook = @import("zhook");
const con = @import("../console.zig");
const logging = @import("../logging.zig");
const WINAPI = std.builtin.CallingConvention.winapi;
@@ -39,6 +39,7 @@ pub const module_name: [*:0]const u8 = "framecrash";
var g_mutex: ?*anyopaque = null;
var g_is_hook_owner: bool = false;
var log: logging.Logger = .{};
pub fn isActive() bool {
return g_is_hook_owner;
@@ -345,12 +346,12 @@ fn setAnimOrderDetour(frame: u32, point_enum: u32, relativeTo: u32, rel_point: u
const live_uiparent = getLiveUIParent();
if (live_uiparent != 0 and live_uiparent != relativeTo) {
con.fmt("[framecrash] FIX: SetAnimOrder dead relativeTo=0x{x:0>8} -> UIParent=0x{x:0>8}, owner=\"{s}\" point={d}\n", .{
log.fmt("FIX: SetAnimOrder dead relativeTo=0x{x:0>8} -> UIParent=0x{x:0>8}, owner=\"{s}\" point={d}\n", .{
relativeTo, live_uiparent, fmtFrameName(frame), point_enum,
});
fixed_relativeTo = live_uiparent;
} else {
con.fmt("[framecrash] RACE: SetAnimOrder dead relativeTo=0x{x:0>8}, no live UIParent! owner=\"{s}\" point={d}\n", .{
log.fmt("RACE: SetAnimOrder dead relativeTo=0x{x:0>8}, no live UIParent! owner=\"{s}\" point={d}\n", .{
relativeTo, fmtFrameName(frame), point_enum,
});
}
@@ -436,7 +437,7 @@ fn cleanupReverseDependencies(dying_frame: u32) void {
}
if (cleaned > 0) {
con.fmt("[framecrash] Nulled {d} stale relativeTo ptr(s) referencing dying frame 0x{x:0>8}\n", .{ cleaned, dying_frame });
log.fmt("Nulled {d} stale relativeTo ptr(s) referencing dying frame 0x{x:0>8}\n", .{ cleaned, dying_frame });
}
}
@@ -504,7 +505,7 @@ fn tryFixStaleRelativeTo(anchor: u32, stale: u32) bool {
const field: *align(1) u32 = @ptrFromInt(anchor + 0x0C);
field.* = live;
con.fmt("[framecrash] HEALED: anchor 0x{x:0>8} relativeTo 0x{x:0>8} -> UIParent 0x{x:0>8}\n", .{
log.fmt("HEALED: anchor 0x{x:0>8} relativeTo 0x{x:0>8} -> UIParent 0x{x:0>8}\n", .{
anchor, stale, live,
});
return true;
@@ -636,29 +637,31 @@ pub fn installHooks() void {
g_is_hook_owner = result.is_owner;
if (!g_is_hook_owner) return;
log = logging.Logger.open(module_name, .console);
// Root cause fix #1: detour cleanup_linked_list_structures to clean up
// reverse anchor references before the frame is destroyed.
if (cleanup_hook.attach(CLEANUP_TARGET, &cleanupDetour) != .ok) {
con.print("[framecrash] ERROR: Failed to install frame cleanup detour\n");
log.print("ERROR: Failed to install frame cleanup detour\n");
} else {
con.print("[framecrash] Frame cleanup detour installed\n");
log.print("Frame cleanup detour installed\n");
}
// Root cause fix #2: detour destroyUIElement - the second destruction path
// used by cleanupGraphicsResources during UI teardown/reload. This path
// frees frames without walking the dependency list.
if (destroy_ui_hook.attach(DESTROY_UI_TARGET, &destroyUIDetour) != .ok) {
con.print("[framecrash] ERROR: Failed to install destroyUIElement detour\n");
log.print("ERROR: Failed to install destroyUIElement detour\n");
} else {
con.print("[framecrash] destroyUIElement detour installed\n");
log.print("destroyUIElement detour installed\n");
}
// Root cause fix #3: detour ProcessUIUpdateEvent - virtual function that
// calls CleanupUIElement + FreeMemory without layout cleanup.
if (process_ui_hook.attach(PROCESS_UI_TARGET, &processUIDetour) != .ok) {
con.print("[framecrash] ERROR: Failed to install ProcessUIUpdateEvent detour\n");
log.print("ERROR: Failed to install ProcessUIUpdateEvent detour\n");
} else {
con.print("[framecrash] ProcessUIUpdateEvent detour installed\n");
log.print("ProcessUIUpdateEvent detour installed\n");
}
// Defense-in-depth: patch anchor vtable[1]/[2]/[3] to validate relativeTo
@@ -666,22 +669,22 @@ pub fn installHooks() void {
patchVtableSlot(GET_WIDTH_SLOT, @intFromPtr(&getWidthHook), &orig_get_width);
patchVtableSlot(GET_HEIGHT_SLOT, @intFromPtr(&getHeightHook), &orig_get_height);
patchVtableSlot(GET_RELATIVE_TO_SLOT, @intFromPtr(&getRelativeToHook), &orig_get_relative_to);
con.print("[framecrash] Anchor vtable hooks installed (GetWidth/GetHeight/GetRelativeTo)\n");
log.print("Anchor vtable hooks installed (GetWidth/GetHeight/GetRelativeTo)\n");
// Diagnostic: hook PauseAnimationGroup to track dependency registrations.
// Answers: "was a dependency ever registered for this stale address?"
if (pause_anim_hook.attach(PAUSE_ANIM_TARGET, &pauseAnimDetour) != .ok) {
con.print("[framecrash] ERROR: Failed to install PauseAnimationGroup detour\n");
log.print("ERROR: Failed to install PauseAnimationGroup detour\n");
} else {
con.print("[framecrash] PauseAnimationGroup detour installed\n");
log.print("PauseAnimationGroup detour installed\n");
}
// Diagnostic: hook SetAnimationOrder to detect race conditions.
// Validates relativeTo param BEFORE anchor creation.
if (set_anim_hook.attach(SET_ANIM_TARGET, &setAnimOrderDetour) != .ok) {
con.print("[framecrash] ERROR: Failed to install SetAnimationOrder detour\n");
log.print("ERROR: Failed to install SetAnimationOrder detour\n");
} else {
con.print("[framecrash] SetAnimationOrder detour installed\n");
log.print("SetAnimationOrder detour installed\n");
}
}
@@ -689,28 +692,29 @@ pub fn removeHooks() void {
if (g_is_hook_owner) {
// Remove diagnostic hooks first (reverse install order)
set_anim_hook.detach();
con.print("[framecrash] SetAnimationOrder detour removed\n");
log.print("SetAnimationOrder detour removed\n");
pause_anim_hook.detach();
con.print("[framecrash] PauseAnimationGroup detour removed\n");
log.print("PauseAnimationGroup detour removed\n");
// Restore original vtable pointers (reverse order)
restoreVtableSlot(GET_RELATIVE_TO_SLOT, &orig_get_relative_to);
restoreVtableSlot(GET_HEIGHT_SLOT, &orig_get_height);
restoreVtableSlot(GET_WIDTH_SLOT, &orig_get_width);
con.print("[framecrash] Anchor vtable hooks removed\n");
log.print("Anchor vtable hooks removed\n");
process_ui_hook.detach();
con.print("[framecrash] ProcessUIUpdateEvent detour removed\n");
log.print("ProcessUIUpdateEvent detour removed\n");
destroy_ui_hook.detach();
con.print("[framecrash] destroyUIElement detour removed\n");
log.print("destroyUIElement detour removed\n");
cleanup_hook.detach();
con.print("[framecrash] Frame cleanup detour removed\n");
log.print("Frame cleanup detour removed\n");
}
if (g_is_hook_owner) {
log.close();
mod_mutex.release(&g_mutex);
}
g_is_hook_owner = false;
+20 -17
View File
@@ -38,7 +38,7 @@
const std = @import("std");
const hook = @import("zhook");
const con = @import("../console.zig");
const logging = @import("../logging.zig");
const WINAPI = std.builtin.CallingConvention.winapi;
extern "kernel32" fn GetModuleHandleA(lpModuleName: ?[*:0]const u8) callconv(WINAPI) ?*anyopaque;
@@ -86,11 +86,11 @@ const patch_sets = [_]PatchSet{
};
fn printHex(prefix: []const u8, bytes: []const u8) void {
con.print(prefix);
log.print(prefix);
for (bytes) |b| {
con.fmt("{x:0>2} ", .{b});
log.fmt("{x:0>2} ", .{b});
}
con.print("\n");
log.print("\n");
}
/// Convert a file offset to a virtual address by walking PE section headers.
@@ -131,6 +131,7 @@ pub const module_name: [*:0]const u8 = "healtextfix";
var g_mutex: ?*anyopaque = null;
var g_is_hook_owner: bool = false;
var log: logging.Logger = .{};
var g_applied_set: ?*const PatchSet = null;
pub fn isActive() bool {
@@ -152,11 +153,11 @@ fn detectVersion(base: [*]const u8) ?[]const u8 {
}
pub fn installHooks() void {
con.print("[healtextfix] Module loaded\n");
const result = mod_mutex.acquire(module_name);
g_mutex = result.handle;
g_is_hook_owner = result.is_owner;
if (g_is_hook_owner) log = logging.Logger.open(module_name, .console);
}
/// Called from engineInitDetour (GameEngine_MainInitialize hook) - late enough
@@ -165,33 +166,33 @@ pub fn lateInit() void {
if (!g_is_hook_owner) return;
const superwow_base = GetModuleHandleA("SuperWoWhook.dll");
if (superwow_base == null) {
con.print("[healtextfix] SuperWoWhook.dll not found, skipping\n");
log.print("SuperWoWhook.dll not found, skipping\n");
return;
}
const base: [*]const u8 = @ptrCast(superwow_base.?);
con.fmt("[healtextfix] SuperWoWhook.dll at 0x{x}\n", .{@intFromPtr(base)});
log.fmt("SuperWoWhook.dll at 0x{x}\n", .{@intFromPtr(base)});
// Detect SuperWoW version
const version = detectVersion(base) orelse {
con.print("[healtextfix] Could not detect SuperWoW version, skipping\n");
log.print("Could not detect SuperWoW version, skipping\n");
return;
};
con.fmt("[healtextfix] Detected SuperWoW version: {s}\n", .{version});
log.fmt("Detected SuperWoW version: {s}\n", .{version});
// Find matching patch set
const set: *const PatchSet = blk: {
for (&patch_sets) |*ps| {
if (std.mem.eql(u8, ps.version, version)) break :blk ps;
}
con.fmt("[healtextfix] No patches for version \"{s}\", skipping\n", .{version});
log.fmt("No patches for version \"{s}\", skipping\n", .{version});
return;
};
var applied: u32 = 0;
for (set.patches, 0..) |patch, idx| {
const va = fileOffsetToVA(base, patch.file_offset) orelse {
con.fmt("[healtextfix] Patch {d}: failed to resolve file offset 0x{x}\n", .{ idx, patch.file_offset });
log.fmt("Patch {d}: failed to resolve file offset 0x{x}\n", .{ idx, patch.file_offset });
continue;
};
@@ -217,10 +218,10 @@ pub fn lateInit() void {
}
}
if (already) {
con.fmt("[healtextfix] Patch {d}: already applied\n", .{idx});
log.fmt("Patch {d}: already applied\n", .{idx});
applied += 1;
} else {
con.fmt("[healtextfix] Patch {d}: unexpected bytes at VA 0x{x}\n", .{ idx, @intFromPtr(target) });
log.fmt("Patch {d}: unexpected bytes at VA 0x{x}\n", .{ idx, @intFromPtr(target) });
printHex("[healtextfix] expected: ", patch.old);
printHex("[healtextfix] found: ", target[0..patch.old.len]);
}
@@ -230,17 +231,18 @@ pub fn lateInit() void {
// Apply patch
hook.writeProtected(@intFromPtr(target), patch.new);
applied += 1;
con.fmt("[healtextfix] Patch {d}: applied at VA 0x{x}\n", .{ idx, @intFromPtr(target) });
log.fmt("Patch {d}: applied at VA 0x{x}\n", .{ idx, @intFromPtr(target) });
}
if (applied > 0) g_applied_set = set;
con.fmt("[healtextfix] {d}/{d} patches applied\n", .{ applied, set.patches.len });
log.fmt("{d}/{d} patches applied\n", .{ applied, set.patches.len });
}
pub fn removeHooks() void {
if (!g_is_hook_owner) return;
const set = g_applied_set orelse {
log.close();
mod_mutex.release(&g_mutex);
g_is_hook_owner = false;
return;
@@ -267,12 +269,13 @@ pub fn removeHooks() void {
if (is_patched) {
hook.writeProtected(@intFromPtr(target), patch.old);
con.fmt("[healtextfix] Patch {d}: restored\n", .{idx});
log.fmt("Patch {d}: restored\n", .{idx});
}
}
g_applied_set = null;
log.print("All patches restored\n");
log.close();
mod_mutex.release(&g_mutex);
g_is_hook_owner = false;
con.print("[healtextfix] All patches restored\n");
}
+4 -4
View File
@@ -1,6 +1,6 @@
const std = @import("std");
const hook = @import("zhook");
const con = @import("../console.zig");
const logging = @import("../logging.zig");
const WINAPI = std.builtin.CallingConvention.winapi;
extern "kernel32" fn GetTickCount() callconv(WINAPI) u32;
@@ -55,7 +55,6 @@ const C3Vector = struct {
// Calling Conventions
// =============================================================================
// =============================================================================
// Game API
// =============================================================================
@@ -249,6 +248,7 @@ pub const module_name: [*:0]const u8 = "interact";
var g_mutex: ?*anyopaque = null;
var g_is_hook_owner: bool = false;
var log: logging.Logger = .{};
pub fn isActive() bool {
return g_is_hook_owner;
@@ -378,20 +378,20 @@ fn hookSceneEnd(device: u32) callconv(hook.cc.thiscall) void {
// =============================================================================
pub fn installHooks() void {
con.print("[interact] Module loaded\n");
const result = mod_mutex.acquire(module_name);
g_mutex = result.handle;
g_is_hook_owner = result.is_owner;
if (!g_is_hook_owner) return;
// SceneEnd - per-frame loot queue processing
log = logging.Logger.open(module_name, .console);
_ = scene_end_hook.attach(Offsets.ADDR_SceneEnd, &hookSceneEnd);
}
pub fn removeHooks() void {
if (g_is_hook_owner) {
scene_end_hook.detach();
log.close();
mod_mutex.release(&g_mutex);
}
g_is_hook_owner = false;
+258
View File
@@ -0,0 +1,258 @@
//! Debug logging - compiles out entirely in non-Debug builds.
//!
//! Per-module Logger with optional file output and per-call destination routing.
//! Log files are created lazily on first write -- modules that never log to
//! file produce no `.log` file.
//!
//! const logging = @import("../logging.zig");
//! var log: logging.Logger = .{};
//!
//! // In installHooks:
//! log = logging.Logger.open("mymodule", .console); // console only (file created on demand)
//! log = logging.Logger.open("mymodule", .both); // console + file by default
//! log = logging.Logger.open(null, .console); // no file at all
//!
//! // Default destination (auto-prefixed with [mymodule]):
//! log.print("loaded\n"); // -> [mymodule] loaded
//! log.fmt("val={d}\n", .{v}); // -> [mymodule] val=42
//!
//! // Override for specific calls:
//! log.to(.file).fmt("noisy={d}\n", .{n}); // file only
//! log.to(.both).print("important event\n"); // console + file
//! log.to(.console).print("console only\n"); // console only
//!
//! Global console-only convenience (no auto-prefix):
//! logging.print("[addons] loaded\n");
//! logging.fmt("[addons] count={d}\n", .{n});
const std = @import("std");
const builtin = @import("builtin");
const debug = builtin.mode == .Debug;
const WINAPI = std.builtin.CallingConvention.winapi;
const win = if (debug) struct {
extern "kernel32" fn AllocConsole() callconv(WINAPI) i32;
extern "kernel32" fn FreeConsole() callconv(WINAPI) i32;
extern "kernel32" fn SetConsoleTitleA(title: [*:0]const u8) callconv(WINAPI) i32;
extern "kernel32" fn GetStdHandle(nStdHandle: u32) callconv(WINAPI) ?*anyopaque;
extern "kernel32" fn WriteConsoleA(
hOut: *anyopaque,
buf: [*]const u8,
len: u32,
written: ?*u32,
reserved: ?*anyopaque,
) callconv(WINAPI) i32;
extern "kernel32" fn CreateFileA(
lpFileName: [*:0]const u8,
dwDesiredAccess: u32,
dwShareMode: u32,
lpSecurityAttributes: ?*anyopaque,
dwCreationDisposition: u32,
dwFlagsAndAttributes: u32,
hTemplateFile: ?*anyopaque,
) callconv(WINAPI) ?*anyopaque;
extern "kernel32" fn WriteFile(
hFile: *anyopaque,
lpBuffer: [*]const u8,
nNumberOfBytesToWrite: u32,
lpNumberOfBytesWritten: ?*u32,
lpOverlapped: ?*anyopaque,
) callconv(WINAPI) i32;
extern "kernel32" fn CloseHandle(hObject: *anyopaque) callconv(WINAPI) i32;
const STD_OUTPUT_HANDLE: u32 = 0xFFFFFFF5;
const GENERIC_WRITE: u32 = 0x40000000;
const FILE_SHARE_READ: u32 = 0x00000001;
const CREATE_ALWAYS: u32 = 2;
const FILE_ATTRIBUTE_NORMAL: u32 = 0x80;
var console_handle: ?*anyopaque = null;
} else void;
// =============================================================================
// Console management (called from main.zig install/uninstall)
// =============================================================================
pub fn init() void {
if (debug) {
_ = win.AllocConsole();
_ = win.SetConsoleTitleA("weirdutils");
win.console_handle = win.GetStdHandle(win.STD_OUTPUT_HANDLE);
print("[weirdutils] Console attached\n");
}
}
pub fn deinit() void {
if (debug) {
if (win.console_handle != null) {
print("[weirdutils] Detaching\n");
_ = win.FreeConsole();
win.console_handle = null;
}
}
}
// =============================================================================
// Output destination
// =============================================================================
pub const Output = enum { console, file, both };
// =============================================================================
// Logger
// =============================================================================
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 {},
/// 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.
prefix: if (debug) [32]u8 else void = if (debug) .{0} ** 32 else {},
prefix_len: if (debug) u8 else void = if (debug) 0 else {},
/// Open a logger with optional file output. File is created lazily on
/// 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 {
if (debug) {
var l: Logger = .{ .default_dest = default, .file_name = module_name };
if (module_name) |name| {
const span = std.mem.span(name);
if (span.len + 3 <= l.prefix.len) {
l.prefix[0] = '[';
@memcpy(l.prefix[1..][0..span.len], span);
l.prefix[1 + span.len] = ']';
l.prefix[2 + span.len] = ' ';
l.prefix_len = @intCast(3 + span.len);
}
}
return l;
}
return .{};
}
pub fn close(self: *Logger) void {
if (debug) {
if (self.file_handle) |h| {
_ = win.CloseHandle(h);
self.file_handle = null;
}
}
}
/// Print using the default destination.
pub fn print(self: *Logger, msg: []const u8) void {
if (debug) self.dispatch(self.default_dest, msg);
}
/// Format and print using the default destination.
pub fn fmt(self: *Logger, comptime f: []const u8, args: anytype) void {
if (debug) {
var buf: [512]u8 = undefined;
const msg = std.fmt.bufPrint(&buf, f, args) catch return;
self.dispatch(self.default_dest, msg);
}
}
/// Override the destination for a single call.
pub fn to(self: *Logger, dest: Output) Writer {
return .{ .logger = self, .dest = dest };
}
pub const Writer = struct {
logger: *Logger,
dest: Output,
pub fn print(self: Writer, msg: []const u8) void {
if (debug) self.logger.dispatch(self.dest, msg);
}
pub fn fmt(self: Writer, comptime f: []const u8, args: anytype) void {
if (debug) {
var buf: [512]u8 = undefined;
const msg = std.fmt.bufPrint(&buf, f, args) catch return;
self.logger.dispatch(self.dest, msg);
}
}
};
fn dispatch(self: *Logger, dest: Output, msg: []const u8) void {
const pfx = self.prefix[0..self.prefix_len];
switch (dest) {
.console => {
if (pfx.len > 0) writeConsole(pfx);
writeConsole(msg);
},
.file => {
if (pfx.len > 0) self.writeFile(pfx);
self.writeFile(msg);
},
.both => {
if (pfx.len > 0) {
writeConsole(pfx);
self.writeFile(pfx);
}
writeConsole(msg);
self.writeFile(msg);
},
}
}
fn writeFile(self: *Logger, msg: []const u8) void {
const h = self.ensureFileOpen() orelse return;
_ = win.WriteFile(h, msg.ptr, @intCast(msg.len), null, null);
}
/// Lazily create the log file on first write.
fn ensureFileOpen(self: *Logger) ?*anyopaque {
if (self.file_handle) |h| return h;
const name = self.file_name orelse return null;
const span = std.mem.span(name);
var buf: [64]u8 = undefined;
if (span.len + 4 >= buf.len) return null;
@memcpy(buf[0..span.len], span);
@memcpy(buf[span.len..][0..4], ".log");
buf[span.len + 4] = 0;
const fh = win.CreateFileA(
@ptrCast(buf[0 .. span.len + 4 :0]),
win.GENERIC_WRITE,
win.FILE_SHARE_READ,
null,
win.CREATE_ALWAYS,
win.FILE_ATTRIBUTE_NORMAL,
null,
);
if (fh) |h| {
if (@intFromPtr(h) != 0xFFFFFFFF) {
self.file_handle = h;
return h;
}
}
return null;
}
};
// =============================================================================
// Global console-only convenience (for utility files and main.zig)
// =============================================================================
pub fn print(msg: []const u8) void {
if (debug) writeConsole(msg);
}
pub fn fmt(comptime f: []const u8, args: anytype) void {
if (debug) {
var buf: [512]u8 = undefined;
const msg = std.fmt.bufPrint(&buf, f, args) catch return;
writeConsole(msg);
}
}
fn writeConsole(msg: []const u8) void {
if (win.console_handle) |h| {
_ = win.WriteConsoleA(h, msg.ptr, @intCast(msg.len), null, null);
}
}
+22 -19
View File
@@ -13,7 +13,7 @@
const std = @import("std");
const hook = @import("zhook");
const con = @import("../console.zig");
const logging = @import("../logging.zig");
const lua = @import("../lua.zig");
const o = @import("offsets.zig");
@@ -77,6 +77,7 @@ pub const module_name: [*:0]const u8 = "logsessions";
var g_mutex: ?*anyopaque = null;
var g_is_hook_owner: bool = false;
var log: logging.Logger = .{};
pub fn isActive() bool {
return g_is_hook_owner;
@@ -190,7 +191,7 @@ fn setupSessionDir(realm: []const u8, char_name: []const u8) bool {
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]});
log.fmt("dir: {s}\n", .{g_dir_path[0..g_dir_path_len]});
return true;
}
@@ -260,7 +261,7 @@ fn findRecentFile(prefix: []const u8, result_buf: *[260]u8) ?usize {
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]});
log.fmt("reusing: {s}\n", .{result_buf[0..len]});
return len;
}
@@ -279,7 +280,7 @@ fn resolveLogPath(prefix: []const u8, result_buf: *[260]u8) usize {
st.wSecond,
}) catch return 0;
result_buf[path.len] = 0;
con.fmt("[logsessions] new: {s}\n", .{path});
log.fmt("new: {s}\n", .{path});
return path.len;
}
@@ -294,7 +295,7 @@ fn configureSession(char_span: []const u8, realm_span: []const u8) void {
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", .{
log.fmt("session: {s} on {s}\n", .{
g_session_char[0..g_session_char_len],
g_session_realm[0..g_session_realm_len],
});
@@ -304,7 +305,7 @@ fn configureSession(char_span: []const u8, realm_span: []const u8) void {
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");
log.print("setup: failed to create directories\n");
return;
}
@@ -348,7 +349,7 @@ fn enterWorldDetour() callconv(hook.cc.stdcall) void {
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");
log.print("enter world: char/realm not available\n");
}
}
@@ -382,18 +383,18 @@ fn initLogDetour(file_path: u32, flags: u32, handle_out: u32) callconv(hook.cc.s
// 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] });
log.fmt("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] });
log.fmt("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] });
log.fmt("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 });
}
@@ -454,7 +455,7 @@ fn writeSessionMarker(handle: u32, fmt_str: [*:0]const u8) void {
@intFromPtr(&marker_ptr),
});
con.fmt("[logsessions] marker: {s}\n", .{marker_str});
log.fmt("marker: {s}\n", .{marker_str});
}
// =============================================================================
@@ -465,7 +466,7 @@ fn writeSessionMarker(handle: u32, fmt_str: [*:0]const u8) void {
/// Called from logoutDetour/shutdownDetour in main.zig.
pub fn onShutdown() void {
if (g_paths_configured) {
con.print("[logsessions] session reset\n");
log.print("session reset\n");
}
g_paths_configured = false;
g_combat_marker_written = false;
@@ -511,31 +512,32 @@ pub fn luaGetChatLogPath(L: lua.State) callconv(.c) u32 {
// =============================================================================
pub fn installHooks() void {
con.print("[logsessions] Module loaded\n");
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, .console);
// 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");
log.print("FAILED to hook HandleCharacterSelection!\n");
} else {
con.print("[logsessions] hooked HandleCharacterSelection OK\n");
log.print("hooked HandleCharacterSelection OK\n");
}
if (init_log_hook.attach(o.FN_INIT_LOG_BUFFER, &initLogDetour) != .ok) {
con.print("[logsessions] FAILED to hook InitializeLogBuffer!\n");
log.print("FAILED to hook InitializeLogBuffer!\n");
} else {
con.print("[logsessions] hooked InitializeLogBuffer OK\n");
log.print("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");
log.print("FAILED to hook WriteFormattedLogMessage!\n");
} else {
con.print("[logsessions] hooked WriteFormattedLogMessage OK\n");
log.print("hooked WriteFormattedLogMessage OK\n");
}
}
@@ -545,6 +547,7 @@ pub fn removeHooks() void {
init_log_hook.detach();
enter_world_hook.detach();
restorePathPointers();
log.close();
mod_mutex.release(&g_mutex);
}
g_is_hook_owner = false;
+36 -31
View File
@@ -1,6 +1,7 @@
const std = @import("std");
const hook = @import("zhook");
pub const con = @import("console.zig");
const logging = @import("logging.zig");
var log: logging.Logger = .{};
// Build options for conditional module compilation
const build_opts = struct {
@@ -15,6 +16,7 @@ const build_opts = struct {
const customassets = @import("build_options").enable_customassets;
const healtextfix = @import("build_options").enable_healtextfix;
const bigcursor = @import("build_options").enable_bigcursor;
const clickthrough = @import("build_options").enable_clickthrough;
const dpslog = @import("build_options").enable_dpslog;
};
@@ -30,6 +32,7 @@ const transmogfix = if (build_opts.transmogfix) @import("transmogfix/transmogfix
const customassets = if (build_opts.customassets) @import("customassets/customassets.zig") else struct {};
const healtextfix = if (build_opts.healtextfix) @import("healtextfix/healtextfix.zig") else struct {};
const bigcursor = if (build_opts.bigcursor) @import("bigcursor/bigcursor.zig") else struct {};
const clickthrough = if (build_opts.clickthrough) @import("clickthrough/clickthrough.zig") else struct {};
const dpslog = if (build_opts.dpslog) @import("dpslog/dpslog.zig") else struct {};
const WINAPI = std.builtin.CallingConvention.winapi;
@@ -145,7 +148,7 @@ fn loadFileDetour(
buf_out.* = buf;
if (size_out) |s| s.* = data_len;
con.fmt("[file] served embedded: {s} ({d} bytes)\n", .{ std.mem.span(path), data_len });
log.fmt("served embedded: {s} ({d} bytes)\n", .{ std.mem.span(path), data_len });
return 1;
}
@@ -244,7 +247,7 @@ fn openFileDetour(
}
handle_out.* = @intFromPtr(ctx);
con.fmt("[file] fake ctx @0x{x}: {s} ({d} bytes)\n", .{ @intFromPtr(ctx), path_span, entry.data.len });
log.fmt("fake ctx @0x{x}: {s} ({d} bytes)\n", .{ @intFromPtr(ctx), path_span, entry.data.len });
return 2; // success (non-zero type code)
}
@@ -260,7 +263,7 @@ fn getFileSizeDetour(
if (isFakeFileContext(file_ctx)) {
if (high_size_out) |h| h.* = 0;
const size = hook.readMem(u32, file_ctx + 0x34);
con.fmt("[file] getFileSize fake @0x{x} = {d}\n", .{ file_ctx, size });
log.fmt("getFileSize fake @0x{x} = {d}\n", .{ file_ctx, size });
return size;
}
@@ -282,7 +285,7 @@ fn readFileDetour(
const data_size = hook.readMem(u32, ctx + 0x34);
const read_size = @min(size, data_size);
con.fmt("[file] readFile fake @0x{x} size={d}/{d} async=0x{x}\n", .{ ctx, read_size, data_size, async_ptr });
log.fmt("readFile fake @0x{x} size={d}/{d} async=0x{x}\n", .{ ctx, read_size, data_size, async_ptr });
const src: [*]const u8 = @ptrFromInt(data_ptr);
@memcpy(buffer[0..read_size], src[0..read_size]);
@@ -363,9 +366,9 @@ fn cleanupFileHandleDetour(file_ctx: u32) callconv(hook.cc.stdcall) void {
const path_ptr = hook.readMem(u32, file_ctx + 0x0C);
if (path_ptr != 0) {
const path: [*:0]const u8 = @ptrFromInt(path_ptr);
con.fmt("[file] cleanup FAKE @0x{x}: {s}\n", .{ file_ctx, std.mem.span(path) });
log.fmt("cleanup FAKE @0x{x}: {s}\n", .{ file_ctx, std.mem.span(path) });
} else {
con.fmt("[file] cleanup FAKE @0x{x}: (no path)\n", .{file_ctx});
log.fmt("cleanup FAKE @0x{x}: (no path)\n", .{file_ctx});
}
}
@@ -373,7 +376,7 @@ fn cleanupFileHandleDetour(file_ctx: u32) callconv(hook.cc.stdcall) void {
// (NULL-safe checks on +0x04/+0x3C/+0x40/+0x08, then cleanupFileContext + FreeMemory).
cleanup_file_handle_hook.callOriginal(.{file_ctx});
if (fake) con.fmt("[file] cleanup FAKE @0x{x} done\n", .{file_ctx});
if (fake) log.fmt("cleanup FAKE @0x{x} done\n", .{file_ctx});
}
// --- Hook 5: loadModelFromFileAsync (0x71d4e0) ---
@@ -383,10 +386,10 @@ fn loadModelAsyncDetour(model: u32, file_handle: u32, should_use_callback: u32)
// file_handle IS the file context address directly (Ghidra shows pointer* but
// the assembly pushes it directly to GetFileSizeFromHandle - no dereference)
if (isFakeFileContext(file_handle)) {
con.fmt("[file] loadModelAsync: model=0x{x} fh=0x{x} cb={d}\n", .{ model, file_handle, should_use_callback });
log.fmt("loadModelAsync: model=0x{x} fh=0x{x} cb={d}\n", .{ model, file_handle, should_use_callback });
const data_ptr = hook.readMem(u32, file_handle + 0x30);
const data_size = hook.readMem(u32, file_handle + 0x34);
con.fmt("[file] embed_ptr=0x{x} embed_size={d}\n", .{ data_ptr, data_size });
log.fmt(" embed_ptr=0x{x} embed_size={d}\n", .{ data_ptr, data_size });
// Toggle callback flag (bit 1 of model+8) based on shouldUseCallback
const flags = hook.readMem(u32, model + 0x08);
@@ -403,10 +406,10 @@ fn loadModelAsyncDetour(model: u32, file_handle: u32, should_use_callback: u32)
// setCullMode is __fastcall(ECX=size), returns buffer pointer
const buffer_addr = hook.call(fn (u32) callconv(hook.cc.fastcall) u32, 0x71f9a0, .{data_size});
if (buffer_addr == 0) {
con.print("[file] setCullMode alloc failed\n");
log.print(" setCullMode alloc failed\n");
return 0;
}
con.fmt("[file] buffer=0x{x}\n", .{buffer_addr});
log.fmt(" buffer=0x{x}\n", .{buffer_addr});
// Store buffer in model object
@as(*align(1) u32, @ptrFromInt(model + 0x130)).* = buffer_addr;
@@ -415,11 +418,11 @@ fn loadModelAsyncDetour(model: u32, file_handle: u32, should_use_callback: u32)
const buffer: [*]u8 = @ptrFromInt(buffer_addr);
const src: [*]const u8 = @ptrFromInt(data_ptr);
@memcpy(buffer[0..data_size], src[0..data_size]);
con.print("[file] memcpy done\n");
log.print(" memcpy done\n");
// No async task - set task pointer to NULL
@as(*align(1) u32, @ptrFromInt(model + 0x0c)).* = 0;
con.print("[file] task=0 set\n");
log.print(" task=0 set\n");
// Match onModelLoadComplete ordering: clean up file handle BEFORE processing.
// The original async flow does: CleanupFileHandleResources → ReturnAsyncTaskToPool
@@ -428,12 +431,12 @@ fn loadModelAsyncDetour(model: u32, file_handle: u32, should_use_callback: u32)
// async tasks that interact with the file I/O system.
// Call original CleanupFileHandleResources through the trampoline (bypasses our
// detour). Must clean up file context before processLoadedModelData runs.
con.fmt("[file] cleanup via trampoline fh=0x{x}\n", .{file_handle});
log.fmt(" cleanup via trampoline fh=0x{x}\n", .{file_handle});
cleanup_file_handle_hook.callOriginal(.{file_handle});
con.print("[file] cleanup done\n");
log.print(" cleanup done\n");
// Dump model fields before processLoadedModelData
con.fmt("[file] PRE model+0x0c=0x{x} +0x130=0x{x} +0x134=0x{x} +0x138=0x{x}\n", .{
log.fmt(" PRE model+0x0c=0x{x} +0x130=0x{x} +0x134=0x{x} +0x138=0x{x}\n", .{
hook.readMem(u32, model + 0x0c),
hook.readMem(u32, model + 0x130),
hook.readMem(u32, model + 0x134),
@@ -441,22 +444,22 @@ fn loadModelAsyncDetour(model: u32, file_handle: u32, should_use_callback: u32)
});
// Call processLoadedModelData directly - __fastcall(ECX=model)
con.fmt("[file] calling processLoadedModelData(0x{x})...\n", .{model});
log.fmt(" calling processLoadedModelData(0x{x})...\n", .{model});
const result = hook.call(fn (u32) callconv(hook.cc.fastcall) u32, 0x71d640, .{model});
con.print("[file] processLoadedModelData returned\n");
con.fmt("[file] result=0x{x}\n", .{result});
log.print(" processLoadedModelData returned\n");
log.fmt(" result=0x{x}\n", .{result});
// Dump model fields after processLoadedModelData - check if texture async task was created
con.print("[file] POST dump:\n");
con.fmt("[file] POST model+0x0c=0x{x} +0x130=0x{x} +0x134=0x{x} +0x138=0x{x}\n", .{
log.print(" POST dump:\n");
log.fmt(" POST model+0x0c=0x{x} +0x130=0x{x} +0x134=0x{x} +0x138=0x{x}\n", .{
hook.readMem(u32, model + 0x0c),
hook.readMem(u32, model + 0x130),
hook.readMem(u32, model + 0x134),
hook.readMem(u32, model + 0x138),
});
con.fmt("[file] sync loaded {d} bytes, returning 1\n", .{data_size});
con.print("[file] === loadModelAsyncDetour EXIT ===\n");
log.fmt(" sync loaded {d} bytes, returning 1\n", .{data_size});
log.print(" === loadModelAsyncDetour EXIT ===\n");
return 1;
}
@@ -496,7 +499,7 @@ fn installFileHooks() void {
_ = cleanup_file_handle_hook.attach(0x648730, &cleanupFileHandleDetour);
_ = model_load_hook.attach(0x71d4e0, &loadModelAsyncDetour);
_ = cfe_hook.attach(0x654DD0, &checkFileExistenceDetour);
con.print("[file] in-memory file hooks installed\n");
log.print("in-memory file hooks installed\n");
}
fn removeFileHooks() void {
@@ -520,7 +523,6 @@ fn loadScriptFunctionsDetour() callconv(hook.cc.stdcall) void {
registerLuaFunctions();
}
// =============================================================================
// Hook: GameEngine_MainInitialize (0x46a400)
// =============================================================================
@@ -552,11 +554,12 @@ fn engineInitDetour() callconv(hook.cc.stdcall) void {
var logout_hook: hook.Detour(fn () callconv(hook.cc.stdcall) void) = .{};
fn logoutDetour() callconv(hook.cc.stdcall) void {
con.print("[weirdutils] World_HandleLogoutCleanup -- player logout\n");
log.print("World_HandleLogoutCleanup -- player logout\n");
// Reset per-session state - only on real logout/disconnect, not /reload.
if (build_opts.worldmarkers) markers.onShutdown();
if (build_opts.minimapicons) minimapicons.onShutdown();
if (build_opts.clickthrough) clickthrough.onShutdown();
if (build_opts.logsessions) logsessions.onShutdown();
// Clean up world objects BEFORE game teardown - modules with
@@ -605,6 +608,7 @@ const modules = [_]ModuleHooks{
if (build_opts.minimapicons) .{ .name = minimapicons.module_name, .install = minimapicons.installHooks, .remove = minimapicons.removeHooks, .is_active = minimapicons.isActive } else .{},
if (build_opts.healtextfix) .{ .name = healtextfix.module_name, .install = healtextfix.installHooks, .remove = healtextfix.removeHooks, .is_active = healtextfix.isActive } else .{},
if (build_opts.bigcursor) .{ .name = bigcursor.module_name, .install = bigcursor.installHooks, .remove = bigcursor.removeHooks, .is_active = bigcursor.isActive } else .{},
if (build_opts.clickthrough) .{ .name = clickthrough.module_name, .install = clickthrough.installHooks, .remove = clickthrough.removeHooks, .is_active = clickthrough.isActive } else .{},
if (build_opts.dpslog) .{ .name = dpslog.module_name, .install = dpslog.installHooks, .remove = dpslog.removeHooks, .is_active = dpslog.isActive } else .{},
if (build_opts.worldmarkers) .{ .name = markers.module_name, .install = markers.installHooks, .remove = markers.removeHooks, .is_active = markers.isActive } else .{},
if (build_opts.interact) .{ .name = interact.module_name, .install = interact.installHooks, .remove = interact.removeHooks, .is_active = interact.isActive } else .{},
@@ -613,7 +617,7 @@ const modules = [_]ModuleHooks{
};
fn shutdownDetour() callconv(hook.cc.stdcall) void {
con.print("[weirdutils] CGGameUI_Shutdown\n");
log.print("CGGameUI_Shutdown\n");
// Per-session resets and remove_on_shutdown cleanup live in logoutDetour
// (World_HandleLogoutCleanup) - fires on real logout/disconnect only, not /reload.
shutdown_hook.callOriginal(.{});
@@ -624,8 +628,9 @@ fn shutdownDetour() callconv(hook.cc.stdcall) void {
// =============================================================================
fn install() void {
con.init();
con.print("[weirdutils] Installing hooks\n");
logging.init();
log = logging.Logger.open("weirdutils", .console);
log.print("Installing hooks\n");
_ = protection_hook.attach(0x42a320, &luaProtectionDetour);
installFileHooks();
_ = file_hook.attach(0x648620, &loadFileDetour);
@@ -658,7 +663,7 @@ fn uninstall() void {
file_hook.detach();
removeFileHooks();
protection_hook.detach();
con.deinit();
logging.deinit();
}
// =============================================================================
+29 -30
View File
@@ -276,7 +276,7 @@ fn getCursorTerrainPosition() ?Vec3 {
const y = hook.readMem(f32, world_frame + o.WF_HIT_TERRAIN_Y);
const z = hook.readMem(f32, world_frame + o.WF_HIT_TERRAIN_Z);
log.fmt("[worldmarkers] hitTest: type={d} pos={d:.1},{d:.1},{d:.1}\n", .{ hit_type, x, y, z });
log.fmt("hitTest: type={d} pos={d:.1},{d:.1},{d:.1}\n", .{ hit_type, x, y, z });
if (hit_type == 2) {
// Object hit — the intersection point is unreliable (can be at camera).
@@ -289,7 +289,7 @@ fn getCursorTerrainPosition() ?Vec3 {
if (obj != 0) {
const pos = getUnitPosition(obj);
if (pos.x != 0 or pos.y != 0 or pos.z != 0) {
log.fmt("[worldmarkers] object hit, using unit pos: {d:.1},{d:.1},{d:.1}\n", .{ pos.x, pos.y, pos.z });
log.fmt("object hit, using unit pos: {d:.1},{d:.1},{d:.1}\n", .{ pos.x, pos.y, pos.z });
return pos;
}
}
@@ -405,7 +405,7 @@ fn spawnEntity(index: usize, pos: Vec3) bool {
var position = [3]f32{ pos.x, pos.y, pos.z + MARKER_Z_OFFSET };
const obj = createEntityInstance(MODEL_PATHS[index], &position, 0.0, 0, 1) orelse {
log.fmt("[worldmarkers] failed to create marker {d}\n", .{index + 1});
log.fmt("failed to create marker {d}\n", .{index + 1});
return false;
};
@@ -419,7 +419,7 @@ fn spawnEntity(index: usize, pos: Vec3) bool {
marker_created_tick[index] = GetTickCount();
hold_queued[index] = false;
log.fmt("[worldmarkers] marker {d} spawned at {d:.1}, {d:.1}, {d:.1} @0x{x}\n", .{
log.fmt("marker {d} spawned at {d:.1}, {d:.1}, {d:.1} @0x{x}\n", .{
index + 1, pos.x, pos.y, pos.z, @intFromPtr(obj),
});
return true;
@@ -461,7 +461,7 @@ fn clearAllMarkers() void {
}
marker_defs[i] = EMPTY_DEF;
}
if (any) log.print("[worldmarkers] all markers cleared\n");
if (any) log.print("all markers cleared\n");
}
// =============================================================================
@@ -476,21 +476,21 @@ pub fn luaWorldMarker(L: lua.State) callconv(.c) u32 {
return 1;
}
if (!canSetMarkers()) {
log.print("[worldmarkers] WorldMarker: no permission\n");
log.print("WorldMarker: no permission\n");
return 0; // nil - addon shows permission message
}
const nargs = lua.gettop(L);
if (nargs < 1 or !lua.isnumber(L, 1)) {
log.print("[worldmarkers] WorldMarker: expected index (1-5)\n");
log.print("WorldMarker: expected index (1-5)\n");
lua.pushnumber(L, -1.0);
return 1;
}
const raw_index = @as(i32, @intFromFloat(lua.tonumber(L, 1)));
if (raw_index < 1 or raw_index > NUM_MARKERS) {
log.print("[worldmarkers] WorldMarker: index must be 1-5\n");
log.print("WorldMarker: index must be 1-5\n");
lua.pushnumber(L, -1.0);
return 1;
}
@@ -506,12 +506,12 @@ pub fn luaWorldMarker(L: lua.State) callconv(.c) u32 {
}
} else if (nargs >= 2 and lua.isstring(L, 2)) {
const unit_id = lua.tostring(L, 2) orelse {
log.print("[worldmarkers] WorldMarker: invalid unit string\n");
log.print("WorldMarker: invalid unit string\n");
lua.pushnumber(L, -1.0);
return 1;
};
const pos = resolveUnitPosition(unit_id) orelse {
log.fmt("[worldmarkers] WorldMarker: unit '{s}' not found\n", .{std.mem.span(unit_id)});
log.fmt("WorldMarker: unit '{s}' not found\n", .{std.mem.span(unit_id)});
lua.pushnumber(L, -1.0);
return 1;
};
@@ -521,7 +521,7 @@ pub fn luaWorldMarker(L: lua.State) callconv(.c) u32 {
}
} else {
const pos = getCursorTerrainPosition() orelse {
log.print("[worldmarkers] no terrain under cursor\n");
log.print("no terrain under cursor\n");
lua.pushnumber(L, -1.0);
return 1;
};
@@ -538,7 +538,7 @@ pub fn luaWorldMarker(L: lua.State) callconv(.c) u32 {
/// Returns 1 on success, nil on permission denied.
pub fn luaClearWorldMarker(L: lua.State) callconv(.c) u32 {
if (!canSetMarkers()) {
log.print("[worldmarkers] ClearWorldMarker: no permission\n");
log.print("ClearWorldMarker: no permission\n");
return 0;
}
@@ -553,13 +553,13 @@ pub fn luaClearWorldMarker(L: lua.State) callconv(.c) u32 {
}
if (!lua.isnumber(L, 1)) {
log.print("[worldmarkers] ClearWorldMarker: expected index (1-5) or nil\n");
log.print("ClearWorldMarker: expected index (1-5) or nil\n");
return 0;
}
const raw_index = @as(i32, @intFromFloat(lua.tonumber(L, 1)));
if (raw_index < 1 or raw_index > NUM_MARKERS) {
log.print("[worldmarkers] ClearWorldMarker: index must be 1-5\n");
log.print("ClearWorldMarker: index must be 1-5\n");
return 0;
}
@@ -603,7 +603,7 @@ fn tickAnimations() void {
const addr = @intFromPtr(entity);
const refcount = hook.readMem(u16, addr + 0x0E);
if (refcount <= 1) {
log.fmt("[worldmarkers] zombie detected [{d}] @0x{x} rc={d}, destroying\n", .{ i + 1, addr, refcount });
log.fmt("zombie detected [{d}] @0x{x} rc={d}, destroying\n", .{ i + 1, addr, refcount });
cleanupEntity(entity);
marker_entities[i] = null;
hold_queued[i] = false;
@@ -631,7 +631,7 @@ fn tickAnimations() void {
const dist_sq = dx * dx + dy * dy + dz * dz;
if (dist_sq < RESPAWN_DISTANCE_SQ) {
log.fmt("[worldmarkers] respawning [{d}] dist={d:.0}\n", .{ i + 1, @sqrt(dist_sq) });
log.fmt("respawning [{d}] dist={d:.0}\n", .{ i + 1, @sqrt(dist_sq) });
_ = spawnEntity(i, marker_defs[i].pos);
}
}
@@ -652,7 +652,7 @@ pub fn luaSetMarkerDef(L: lua.State) callconv(.c) u32 {
const sender = lua.tostring(L, 6) orelse return 0;
if (!senderHasPermission(sender)) {
log.fmt("[worldmarkers] SetMarkerDef: sender '{s}' denied\n", .{std.mem.span(sender)});
log.fmt("SetMarkerDef: sender '{s}' denied\n", .{std.mem.span(sender)});
return 0;
}
@@ -674,7 +674,7 @@ pub fn luaSetMarkerDef(L: lua.State) callconv(.c) u32 {
.active = true,
};
log.fmt("[worldmarkers] SetMarkerDef [{d}] at {d:.1},{d:.1},{d:.1} area={d}\n", .{ index + 1, x, y, z, area_id });
log.fmt("SetMarkerDef [{d}] at {d:.1},{d:.1},{d:.1} area={d}\n", .{ index + 1, x, y, z, area_id });
return 0;
}
@@ -689,7 +689,7 @@ pub fn luaClearMarkerDef(L: lua.State) callconv(.c) u32 {
// ClearMarkerDef(senderName) - clear all
const sender = lua.tostring(L, 1) orelse return 0;
if (!senderHasPermission(sender)) {
log.fmt("[worldmarkers] ClearMarkerDef: sender '{s}' denied\n", .{std.mem.span(sender)});
log.fmt("ClearMarkerDef: sender '{s}' denied\n", .{std.mem.span(sender)});
return 0;
}
clearAllMarkers();
@@ -700,7 +700,7 @@ pub fn luaClearMarkerDef(L: lua.State) callconv(.c) u32 {
// ClearMarkerDef(index, senderName) - clear one
const sender = lua.tostring(L, 2) orelse return 0;
if (!senderHasPermission(sender)) {
log.fmt("[worldmarkers] ClearMarkerDef: sender '{s}' denied\n", .{std.mem.span(sender)});
log.fmt("ClearMarkerDef: sender '{s}' denied\n", .{std.mem.span(sender)});
return 0;
}
const raw_index = @as(i32, @intFromFloat(lua.tonumber(L, 1)));
@@ -773,7 +773,7 @@ fn worldUpdateDetour(frame: u32) callconv(hook.cc.fastcall) void {
/// processCinematicExit, DestroyPathObjectIfPresent). This unlinks them from
/// the WDOODADDEF hash table so the atexit handler never touches freed memory.
fn worldCleanupDetour() callconv(hook.cc.stdcall) void {
log.print("[worldmarkers] >>> worldCleanupDetour FIRING <<<\n");
log.print(">>> worldCleanupDetour FIRING <<<\n");
destroyAllEntities();
world_cleanup_hook.callOriginal(.{});
}
@@ -786,7 +786,7 @@ fn destroyAllEntities() void {
for (&marker_entities, 0..) |*slot, i| {
if (slot.*) |existing| {
const addr = @intFromPtr(existing);
log.fmt("[worldmarkers] destroying marker[{d}] @0x{x}\n", .{ i, addr });
log.fmt("destroying marker[{d}] @0x{x}\n", .{ i, addr });
cleanupEntity(existing);
slot.* = null;
count += 1;
@@ -798,7 +798,7 @@ fn destroyAllEntities() void {
for (&despawning, 0..) |*slot, i| {
if (slot.*) |d| {
const addr = @intFromPtr(d.entity);
log.fmt("[worldmarkers] destroying despawn[{d}] @0x{x}\n", .{ i, addr });
log.fmt("destroying despawn[{d}] @0x{x}\n", .{ i, addr });
cleanupEntity(d.entity);
slot.* = null;
count += 1;
@@ -806,7 +806,7 @@ fn destroyAllEntities() void {
}
if (count > 0) {
log.fmt("[worldmarkers] world cleanup: destroyed {d} entities\n", .{count});
log.fmt("world cleanup: destroyed {d} entities\n", .{count});
}
}
@@ -815,7 +815,6 @@ fn destroyAllEntities() void {
// =============================================================================
pub fn installHooks() void {
logging.print("[worldmarkers] Module loaded\n");
const result = mod_mutex.acquire(module_name);
g_mutex = result.handle;
@@ -825,17 +824,17 @@ pub fn installHooks() void {
// Hook OnWorldUpdate for per-frame animation tick (runs every frame while world is active).
if (world_update_hook.attach(o.FN_ON_WORLD_UPDATE, &worldUpdateDetour) != .ok) {
log.print("[worldmarkers] FAILED to hook OnWorldUpdate!\n");
log.print("FAILED to hook OnWorldUpdate!\n");
} else {
log.print("[worldmarkers] hooked OnWorldUpdate OK\n");
log.print("hooked OnWorldUpdate OK\n");
}
// Hook CleanupWorldAndEntities to destroy our entities before world teardown.
// This fires on map change, logout, AND exit - before heaps are destroyed.
if (world_cleanup_hook.attach(o.FN_CLEANUP_WORLD_AND_ENTITIES, &worldCleanupDetour) != .ok) {
log.print("[worldmarkers] FAILED to hook CleanupWorldAndEntities!\n");
log.print("FAILED to hook CleanupWorldAndEntities!\n");
} else {
log.print("[worldmarkers] hooked CleanupWorldAndEntities OK\n");
log.print("hooked CleanupWorldAndEntities OK\n");
}
}
@@ -844,7 +843,7 @@ pub fn installHooks() void {
/// worldCleanupDetour which fires after shutdown.
pub fn onShutdown() void {
for (&marker_defs) |*d| d.* = EMPTY_DEF;
log.print("[worldmarkers] defs cleared (shutdown)\n");
log.print("defs cleared (shutdown)\n");
}
pub fn removeHooks() void {
+26 -13
View File
@@ -3,6 +3,11 @@
//! Adds NPC type tracking to the minimap (flight masters, innkeepers, mailboxes, etc.)
//! by hooking the minimap's object enumeration and blip rendering pipeline.
//!
//! Uses the minimap's own ObjectEnumProc callback rather than the rendering pipeline's
//! CGObjectIsDisabled/CGUnitShouldRender hooks (used by clickthrough/perfboost) because
//! we need access to the minimap info struct for blip positioning and must be in the
//! minimap drawing pipeline to render custom blip textures via RenderObjectBlips.
//!
//! Hooks:
//! ObjectEnumProc (0x4EAA90) — intercepts per-object minimap callback, checks NPC flags
//! RenderObjectBlips (0x4EBC00) — draws custom blip textures after default blips
@@ -16,10 +21,9 @@
const std = @import("std");
const hook = @import("zhook");
const lua = @import("../lua.zig");
const con = @import("../console.zig");
const logging = @import("../logging.zig");
const mod_mutex = @import("../mutex.zig");
pub const module_name: [*:0]const u8 = "minimapicons";
// =============================================================================
@@ -343,6 +347,7 @@ var g_local_player_guid: u64 = 0; // cached per enumeration cycle
var g_mutex: ?*anyopaque = null;
var g_is_hook_owner: bool = false;
var log: logging.Logger = .{};
pub fn isActive() bool {
return g_is_hook_owner;
@@ -483,7 +488,7 @@ fn isUnitAllowed(obj: u32, local_player: u32) bool {
// Check hostility via UnitReaction
const reaction = unitReaction(local_player, obj);
if (reaction < 4) {
con.fmt("[minimapicons] unit 0x{x} rejected: hostile (reaction={d}, race={d})\n", .{ obj, reaction, getRace(obj) });
log.fmt("unit 0x{x} rejected: hostile (reaction={d}, race={d})\n", .{ obj, reaction, getRace(obj) });
return false;
}
@@ -499,7 +504,7 @@ fn isUnitAllowed(obj: u32, local_player: u32) bool {
if (summoner != 0 and isValidPtr(summoner)) {
const our_race = getRace(local_player);
const their_race = getRace(summoner);
con.fmt("[minimapicons] unit 0x{x} summoner 0x{x}: our race={d} their race={d}\n", .{ obj, summoner, our_race, their_race });
// log.fmt("unit 0x{x} summoner 0x{x}: our race={d} their race={d}\n", .{ obj, summoner, our_race, their_race });
if (!isSameFaction(our_race, their_race))
return false;
}
@@ -513,7 +518,7 @@ fn isUnitAllowed(obj: u32, local_player: u32) bool {
fn isGoInteractable(obj: u32) bool {
const result: u8 = @truncate(hook.call(fn (u32) callconv(hook.cc.fastcall) u32, ADDR.CallSpellCastHandler, .{obj}));
if (result == 0) {
con.fmt("[minimapicons] GO 0x{x} rejected: not interactable (entry={d})\n", .{ obj, getObjectEntry(obj) });
log.fmt("GO 0x{x} rejected: not interactable (entry={d})\n", .{ obj, getObjectEntry(obj) });
}
return result != 0;
}
@@ -623,7 +628,7 @@ fn loadTexture(path: [*:0]const u8) u32 {
});
if (!status.ok() or texture == 0) {
con.print("[minimapicons] Failed to load texture\n");
log.print("Failed to load texture\n");
return 0;
}
@@ -853,6 +858,13 @@ fn trackObject(info: u32, guid_lo: u32, guid_hi: u32, blip: Blip) void {
};
}
// Skip objects more than 55y above or below the player (filters multi-level cities and large dungeons like kara40)
if (g_local_player != 0) {
if (getObjectPosition(g_local_player)) |player_pos| {
if (@abs(pos.z - player_pos.z) > 55.0) return;
}
}
var minimap_pos: C2Vector = undefined;
worldPosToMinimapCoords(&minimap_pos, g_minimap_info.cur, g_minimap_info.radius, pos.x, pos.y, g_minimap_info.layout_scale, g_minimap_info.unk_scale);
@@ -980,7 +992,7 @@ fn enumVisibleObjectsDetour(callback: u32, context: u32) callconv(hook.cc.fastca
else
0;
if (g_local_player != prev) {
con.fmt("[minimapicons] local player: 0x{x} race={d}\n", .{
log.fmt("local player: 0x{x} race={d}\n", .{
g_local_player,
if (g_local_player != 0) @as(u32, getRace(g_local_player)) else @as(u32, 0),
});
@@ -1177,36 +1189,36 @@ fn strEqlInsensitive(a: [*:0]const u8, b: []const u8) bool {
// =============================================================================
pub fn installHooks() void {
con.print("[minimapicons] Module loaded\n");
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, .console);
if (enum_proc_hook.attach(ADDR.ObjectEnumProc, &objectEnumProcDetour) != .ok) {
con.print("[minimapicons] Failed to hook ObjectEnumProc\n");
log.print("Failed to hook ObjectEnumProc\n");
return;
}
if (render_blips_hook.attach(ADDR.RenderObjectBlips, &renderObjectBlipsDetour) != .ok) {
con.print("[minimapicons] Failed to hook RenderObjectBlips\n");
log.print("Failed to hook RenderObjectBlips\n");
enum_proc_hook.detach();
return;
}
if (enum_vis_hook.attach(ADDR.EnumVisibleObjects, &enumVisibleObjectsDetour) != .ok) {
con.print("[minimapicons] Failed to hook EnumVisibleObjects\n");
log.print("Failed to hook EnumVisibleObjects\n");
render_blips_hook.detach();
enum_proc_hook.detach();
return;
}
con.print("[minimapicons] Hooks installed\n");
log.print("Hooks installed\n");
}
/// Called from CGGameUI_Shutdown (logout/exit) — frees heap-allocated filter strings.
pub fn onShutdown() void {
for (&g_flag_tracking) |*entry| entry.clear();
con.print("[minimapicons] filters freed (shutdown)\n");
log.print("filters freed (shutdown)\n");
}
pub fn removeHooks() void {
@@ -1220,6 +1232,7 @@ pub fn removeHooks() void {
for (&g_go_id_tracking) |*entry| entry.active = false;
g_blip_count = 0;
log.close();
mod_mutex.release(&g_mutex);
}
g_is_hook_owner = false;
+1 -1
View File
@@ -1,5 +1,5 @@
const std = @import("std");
const con = @import("console.zig");
const con = @import("logging.zig");
const WINAPI = std.builtin.CallingConvention.winapi;
extern "kernel32" fn CreateMutexA(lpMutexAttributes: ?*anyopaque, bInitialOwner: i32, lpName: ?[*:0]const u8) callconv(WINAPI) ?*anyopaque;
+5 -2
View File
@@ -5,7 +5,7 @@
const std = @import("std");
const hook = @import("zhook");
const con = @import("../console.zig");
const logging = @import("../logging.zig");
const tracker = @import("tracker.zig");
const model_hook = @import("model_hook.zig");
const d3d9_hook = @import("d3d9_hook.zig");
@@ -14,6 +14,7 @@ const WINAPI = std.builtin.CallingConvention.winapi;
const mod_mutex = @import("../mutex.zig");
pub const module_name: [*:0]const u8 = "outline";
var log: logging.Logger = .{};
var g_mutex: ?*anyopaque = null;
var g_is_hook_owner: bool = false;
@@ -27,13 +28,14 @@ pub fn isActive() bool {
/// dummy D3D9 device during engine init corrupts the d3d9 proxy's state and
/// causes model rendering to stutter at ~10fps.
pub fn init() bool {
con.print("[outline] Module loaded\n");
const result = mod_mutex.acquire(module_name);
g_mutex = result.handle;
g_is_hook_owner = result.is_owner;
if (!g_is_hook_owner) return true;
log = logging.Logger.open(module_name, .console);
tracker.initLogger();
if (!model_hook.installHooks()) return false;
return true;
}
@@ -50,6 +52,7 @@ pub fn cleanup() void {
if (g_is_hook_owner) {
d3d9_hook.removeHooks();
model_hook.removeHooks();
log.close();
mod_mutex.release(&g_mutex);
}
g_is_hook_owner = false;
+8 -7
View File
@@ -12,6 +12,7 @@
const std = @import("std");
const hook = @import("zhook");
const logging = @import("../logging.zig");
const wow = @import("wow.zig");
const o = @import("offsets.zig");
const types = @import("types.zig");
@@ -307,11 +308,13 @@ pub const Diag = struct {
};
pub var diag: Diag = .{};
var log: logging.Logger = .{};
const WINAPI = std.builtin.CallingConvention.winapi;
extern "kernel32" fn OutputDebugStringA(lpOutputString: [*:0]const u8) callconv(WINAPI) void;
pub fn initLogger() void {
log = logging.Logger.open("outline", .console);
}
/// Log diagnostic counters via OutputDebugStringA. Called from EndScene.
/// Log diagnostic counters. Called from EndScene.
/// Only logs when outline activity is detected, limited to first 20 events.
pub fn logDiagnostics(cached_draw_count: u32) void {
const has_activity = diag.scan_targets > 0 or diag.scan_raid_marks > 0 or
@@ -322,8 +325,7 @@ pub fn logDiagnostics(cached_draw_count: u32) void {
if (!has_activity or diag.log_count >= 20) return;
diag.log_count += 1;
var buf: [256]u8 = undefined;
const msg = std.fmt.bufPrint(&buf, "[Outline] scan t={d} r={d} d={d} | classify t={d} r={d} d={d} | cached={d}\x00", .{
log.fmt("scan t={d} r={d} d={d} | classify t={d} r={d} d={d} | cached={d}\n", .{
diag.scan_targets,
diag.scan_raid_marks,
diag.scan_dead_players,
@@ -331,8 +333,7 @@ pub fn logDiagnostics(cached_draw_count: u32) void {
diag.classify_raid_mark,
diag.classify_dead_player,
cached_draw_count,
}) catch return;
OutputDebugStringA(@ptrCast(msg.ptr));
});
}
/// Reset per-frame diagnostic counters. Called at start of scanObjects.
+5 -3
View File
@@ -1,6 +1,6 @@
const std = @import("std");
const hook = @import("zhook");
const con = @import("../console.zig");
const logging = @import("../logging.zig");
const png = @import("png.zig");
const WINAPI = std.builtin.CallingConvention.winapi;
@@ -53,7 +53,6 @@ const ERROR_ALREADY_EXISTS: u32 = 183;
// State
// =============================================================================
// CVar for compression level persistence (09, default 6)
const CVAR_NAME = "screenshotQuality";
const CVAR_LOOKUP: usize = 0x0063DEC0;
@@ -100,6 +99,7 @@ pub const module_name: [*:0]const u8 = "pngscreenshots";
var g_mutex: ?HANDLE = null;
var g_is_hook_owner: bool = false;
var log: logging.Logger = .{};
pub fn isActive() bool {
return g_is_hook_owner;
@@ -296,13 +296,14 @@ fn writePng(path: [*:0]const u8, pixels: [*]const u8, width: u16, height: u16, l
// =============================================================================
pub fn installHook() void {
con.print("[screenshot] Module loaded\n");
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, .console);
// Register CVar for compression quality persistence (saved to config.wtf)
_ = registerCVar(CVAR_NAME, 0, 0, "6", 0, 1, 0, 0);
@@ -320,6 +321,7 @@ pub fn installHook() void {
pub fn removeHook() void {
if (g_is_hook_owner) {
tga_hook.detach();
log.close();
mod_mutex.release(&g_mutex);
}
g_is_hook_owner = false;
+18 -16
View File
@@ -16,7 +16,7 @@
const std = @import("std");
const hook = @import("zhook");
const con = @import("../console.zig");
const logging = @import("../logging.zig");
const WINAPI = std.builtin.CallingConvention.winapi;
@@ -143,6 +143,7 @@ pub const module_name: [*:0]const u8 = "transmogfix";
var g_enabled: bool = true;
var g_initialized: bool = false;
var g_is_hook_owner: bool = false;
var log: logging.Logger = .{};
var g_mutex: ?*anyopaque = null;
pub fn isActive() bool {
@@ -401,7 +402,7 @@ fn processTimeouts(now: u32) void {
// Re-resolve the GUID to a live object pointer - the cached
// unit_ptr may be stale if the player despawned since capture.
const unit = getObjectByGUID(g_other_pending[i].guid);
con.fmt("[other] TIMEOUT slot={d:2} guid=0x{X:0>16} {d}ms unit=0x{X:0>8}\n", .{ g_other_pending[i].slot, g_other_pending[i].guid, elapsed, unit });
log.fmt("TIMEOUT slot={d:2} guid=0x{X:0>16} {d}ms unit=0x{X:0>8}\n", .{ g_other_pending[i].slot, g_other_pending[i].guid, elapsed, unit });
if (unit != 0 and (unit & 1) == 0) {
const field_index = PLAYER_VISIBLE_ITEM_1_0 + (@as(u32, @intCast(g_other_pending[i].slot)) * VISIBLE_ITEM_STRIDE);
_ = callOriginalSetBlock(unit, field_index, 0);
@@ -451,14 +452,14 @@ fn processTimeouts(now: u32) void {
const dest: *u32 = @ptrFromInt(unit + UNIT_CACHED_MODELDATA_OFFSET);
dest.* = box_model_data;
con.fmt("[other] REFRESH unit=0x{X:0>8} table=0x{X:0>8} boxModel=0x{X:0>8}\n", .{ unit, display_table, box_model_data });
log.fmt("REFRESH unit=0x{X:0>8} table=0x{X:0>8} boxModel=0x{X:0>8}\n", .{ unit, display_table, box_model_data });
refreshEquipmentDisplay(unit);
continue;
}
}
// Fallback to RefreshVisualAppearance
con.fmt("[other] REFRESH fallback unit=0x{X:0>8} table=0x{X:0>8}\n", .{ unit, display_table });
log.fmt("REFRESH fallback unit=0x{X:0>8} table=0x{X:0>8}\n", .{ unit, display_table });
if (refresh_hook.inner.trampoline != 0) {
callOriginalRefresh(unit, 0, 0, 1);
}
@@ -503,7 +504,7 @@ fn hookSetBlock(obj: u32, index: u32, value: u32) callconv(hook.cc.thiscall) u32
g_local_pending[slot].timestamp = now;
g_local_pending[slot].active = true;
g_local_pending[slot].has_durability = false;
con.fmt("[local] BLOCK clear slot={d:2} item=0x{X:0>8}\n", .{ slot, g_cached_visible_item[slot] });
log.fmt("BLOCK clear slot={d:2} item=0x{X:0>8}\n", .{ slot, g_cached_visible_item[slot] });
return 1; // Block the clear
} else if (val != 0 and g_local_pending[slot].active) {
if (val == g_local_pending[slot].original_visible_item) {
@@ -513,10 +514,10 @@ fn hookSetBlock(obj: u32, index: u32, value: u32) callconv(hook.cc.thiscall) u32
const dur = g_local_pending[slot].captured_durability;
if (dur != 0) {
writeItemDurabilityDirect(slot, dur);
con.fmt("[local] APPLY dur slot={d:2} dur={d}\n", .{ slot, dur });
log.fmt("APPLY dur slot={d:2} dur={d}\n", .{ slot, dur });
} else {
// Don't block - broken items need visual update
con.fmt("[local] PASS broken slot={d:2}\n", .{slot});
log.fmt("PASS broken slot={d:2}\n", .{slot});
g_local_pending[slot].active = false;
g_local_pending[slot].has_durability = false;
g_local_pending_count -= 1;
@@ -529,7 +530,7 @@ fn hookSetBlock(obj: u32, index: u32, value: u32) callconv(hook.cc.thiscall) u32
updateInventoryAlertStates();
con.fmt("[local] BLOCK restore slot={d:2} item=0x{X:0>8} -- coalesced!\n", .{ slot, val });
log.fmt("BLOCK restore slot={d:2} item=0x{X:0>8} -- coalesced!\n", .{ slot, val });
return 1; // Block the restore
} else {
// Different item value - real gear change
@@ -564,7 +565,7 @@ fn hookSetBlock(obj: u32, index: u32, value: u32) callconv(hook.cc.thiscall) u32
g_other_pending[ni].timestamp = now;
g_other_pending[ni].unit_ptr = obj;
g_other_pending[ni].active = true;
con.fmt("[other] BLOCK clear slot={d:2} guid=0x{X:0>16}\n", .{ slot, guid });
log.fmt("BLOCK clear slot={d:2} guid=0x{X:0>16}\n", .{ slot, guid });
return 1; // Block the clear
}
}
@@ -576,7 +577,7 @@ fn hookSetBlock(obj: u32, index: u32, value: u32) callconv(hook.cc.thiscall) u32
if (elapsed < OTHER_PLAYER_TIMEOUT_MS and val == current_val) {
g_other_pending[ui].active = false;
g_other_pending_count -= 1;
con.fmt("[other] BLOCK restore slot={d:2} guid=0x{X:0>16} {d}ms -- coalesced!\n", .{ slot, guid, elapsed });
log.fmt("BLOCK restore slot={d:2} guid=0x{X:0>16} {d}ms -- coalesced!\n", .{ slot, guid, elapsed });
return 1; // Block the restore
} else {
g_other_pending[ui].active = false;
@@ -597,7 +598,7 @@ fn hookSetBlock(obj: u32, index: u32, value: u32) callconv(hook.cc.thiscall) u32
g_local_pending[s].captured_durability = val;
g_local_pending[s].has_durability = true;
g_local_pending[s].timestamp = GetTickCount();
con.fmt("[local] CATCH dur slot={d:2} dur={d}\n", .{ s, val });
log.fmt("CATCH dur slot={d:2} dur={d}\n", .{ s, val });
return 1; // Block - captured
}
}
@@ -626,7 +627,7 @@ fn hookSetBlock(obj: u32, index: u32, value: u32) callconv(hook.cc.thiscall) u32
// Low word clear with pending VISIBLE_ITEM block = REAL UNEQUIP
if (is_low_word and val == 0 and g_local_pending[es].active) {
con.fmt("[local] REAL UNEQUIP slot={d:2} -- replaying blocked clear\n", .{es});
log.fmt("REAL UNEQUIP slot={d:2} -- replaying blocked clear\n", .{es});
const field_index = PLAYER_VISIBLE_ITEM_1_0 + (@as(u32, @intCast(es)) * VISIBLE_ITEM_STRIDE);
_ = callOriginalSetBlock(obj, field_index, 0);
@@ -661,7 +662,7 @@ fn hookRefreshVisualAppearance(unit: u32, event_data: u32, extra_data: u32, forc
// LOCAL PLAYER: If we have pending SetBlock blocks, skip expensive refresh
if (g_cache.valid and guid == g_cache.local_guid and g_local_pending_count > 0) {
con.fmt("[local] SKIP RefreshVisualAppearance (pending={d})\n", .{g_local_pending_count});
log.fmt("SKIP RefreshVisualAppearance (pending={d})\n", .{g_local_pending_count});
refreshAppearanceAndEquipment(unit);
const flags1: *u32 = @ptrFromInt(unit + 0xccc);
const flags2: *u32 = @ptrFromInt(unit + 0xcd0);
@@ -731,7 +732,7 @@ fn hookRefreshVisualAppearance(unit: u32, event_data: u32, extra_data: u32, forc
const should_skip = (restored_slots > 0) and (cleared_slots == 0) and all_restores_within_timeout;
if (should_skip) {
con.fmt("[other] SKIP RefreshVisualAppearance restored={d} guid=0x{X:0>16}\n", .{ restored_slots, guid });
log.fmt("SKIP RefreshVisualAppearance restored={d} guid=0x{X:0>16}\n", .{ restored_slots, guid });
refreshAppearanceAndEquipment(unit);
const flags1: *u32 = @ptrFromInt(unit + 0xccc);
const flags2: *u32 = @ptrFromInt(unit + 0xcd0);
@@ -764,7 +765,6 @@ fn hookSceneEnd(device: u32) callconv(hook.cc.thiscall) void {
// =============================================================================
pub fn installHooks() void {
con.print("[transmogfix] Module loaded\n");
// Legacy mutex name - this DLL existed in the wild before the naming convention
const result = mod_mutex.acquireLegacy("TransmogCoalesceHook", module_name);
@@ -774,6 +774,7 @@ pub fn installHooks() void {
g_initialized = true;
return;
}
log = logging.Logger.open(module_name, .console);
// Initialize state (already zero-initialized by Zig defaults)
g_local_pending = [1]LocalPending{.{}} ** 19;
@@ -801,7 +802,7 @@ pub fn installHooks() void {
}
g_initialized = true;
con.print("[transmogfix] All 3 hooks installed\n");
log.print("All 3 hooks installed\n");
}
pub fn removeHooks() void {
@@ -809,6 +810,7 @@ pub fn removeHooks() void {
scene_end_hook.detach();
refresh_hook.detach();
set_block_hook.detach();
log.close();
mod_mutex.release(&g_mutex);
}