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:
+1
-1
@@ -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.
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 (0–9, 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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user