Fix marker crash on logout and add module lifecycle table
The marker cleanup hook on CleanupWorldAndEntities was never installed — markers.installHooks() was missing from install() in main.zig. Entities created via WorldMarker were never cleaned up before the game's atexit handler iterated the hash table over freed heap memory. Key changes: - Add markers.installHooks() call (the actual crash fix) - Replace manual install/uninstall/shutdown lists with a single modules table that drives all three phases — prevents this class of bug - Gate marker Lua functions, addon, and keybindings behind isActive() so they're skipped when another DLL owns the hooks - Add world_cleanup_hook.detach() to removeHooks() (was missing) - Migrate from vendored libs/hook to external zhook dependency - Unify installHooks return types to void across all modules - Add diagnostic logging to marker cleanup (temporary, for testing)
This commit is contained in:
@@ -2,3 +2,5 @@
|
||||
zig-out/
|
||||
reference/
|
||||
research/
|
||||
docs/
|
||||
ideas/
|
||||
|
||||
@@ -155,7 +155,7 @@ cd /media/storage/projects/zig/weirdutils
|
||||
zig build
|
||||
```
|
||||
|
||||
Target: x86-windows-msvc (32-bit DLL), Zig 0.15.
|
||||
Target: x86-windows-msvc (32-bit DLL), Zig 0.16 (patched: fastcall inreg fix).
|
||||
Host: Linux (Arch), game runs via Wine/DXVK.
|
||||
|
||||
## Hook Installation Order
|
||||
|
||||
@@ -13,11 +13,12 @@ pub fn build(b: *std.Build) void {
|
||||
const enable_interact = b.option(bool, "interact", "Enable interact module") orelse true;
|
||||
const enable_outline = b.option(bool, "outline", "Enable outline module") orelse true;
|
||||
const enable_markers = b.option(bool, "markers", "Enable markers module") orelse true;
|
||||
const enable_framecrash = b.option(bool, "framecrash", "Enable framecrash fix") orelse true;
|
||||
const enable_framecrash = b.option(bool, "framecrash", "Enable framecrash fix") orelse false;
|
||||
const enable_combatlog = b.option(bool, "combatlog", "Enable combat log freshness") orelse true;
|
||||
const enable_minimapicons = b.option(bool, "minimapicons", "Enable custom minimap icons") orelse true;
|
||||
const enable_transmogfix = b.option(bool, "transmogfix", "Enable transmog update coalescing") orelse true;
|
||||
const enable_assetfix = b.option(bool, "assetfix", "Enable loose file loading & permissive patch glob") orelse true;
|
||||
const enable_healtextfix = b.option(bool, "healtextfix", "Enable SuperWoW heal text fix") orelse true;
|
||||
|
||||
// Create build options module
|
||||
const build_options = b.addOptions();
|
||||
@@ -30,12 +31,14 @@ pub fn build(b: *std.Build) void {
|
||||
build_options.addOption(bool, "enable_minimapicons", enable_minimapicons);
|
||||
build_options.addOption(bool, "enable_transmogfix", enable_transmogfix);
|
||||
build_options.addOption(bool, "enable_assetfix", enable_assetfix);
|
||||
build_options.addOption(bool, "enable_healtextfix", enable_healtextfix);
|
||||
const build_options_module = build_options.createModule();
|
||||
|
||||
const hook_mod = b.dependency("hook", .{
|
||||
const zhook_dep = b.dependency("zhook", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
}).module("hook");
|
||||
});
|
||||
const zhook_mod = zhook_dep.module("zhook");
|
||||
|
||||
const lib = b.addLibrary(.{
|
||||
.name = "weirdutils",
|
||||
@@ -45,7 +48,7 @@ pub fn build(b: *std.Build) void {
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.imports = &.{
|
||||
.{ .name = "hook", .module = hook_mod },
|
||||
.{ .name = "zhook", .module = zhook_mod },
|
||||
.{ .name = "build_options", .module = build_options_module },
|
||||
},
|
||||
}),
|
||||
@@ -57,18 +60,19 @@ pub fn build(b: *std.Build) void {
|
||||
const build_all_step = b.step("all-variants", "Build all DLL variants");
|
||||
|
||||
// Helper to create a single-module build
|
||||
const Variant = struct { name: []const u8, screenshot: bool, interact: bool, outline: bool, markers: bool, framecrash: bool, combatlog: bool, minimapicons: bool, transmogfix: bool, assetfix: bool };
|
||||
const Variant = struct { name: []const u8, screenshot: bool, interact: bool, outline: bool, markers: bool, framecrash: bool, combatlog: bool, minimapicons: bool, transmogfix: bool, assetfix: bool, healtextfix: bool };
|
||||
inline for (&[_]Variant{
|
||||
.{ .name = "full", .screenshot = true, .interact = true, .outline = true, .markers = true, .framecrash = true, .combatlog = true, .minimapicons = true, .transmogfix = true, .assetfix = true },
|
||||
.{ .name = "screenshot", .screenshot = true, .interact = false, .outline = false, .markers = false, .framecrash = true, .combatlog = true, .minimapicons = false, .transmogfix = false, .assetfix = false },
|
||||
.{ .name = "interact", .screenshot = false, .interact = true, .outline = false, .markers = false, .framecrash = true, .combatlog = true, .minimapicons = false, .transmogfix = false, .assetfix = false },
|
||||
.{ .name = "outline", .screenshot = false, .interact = false, .outline = true, .markers = false, .framecrash = true, .combatlog = true, .minimapicons = false, .transmogfix = false, .assetfix = false },
|
||||
.{ .name = "markers", .screenshot = false, .interact = false, .outline = false, .markers = true, .framecrash = true, .combatlog = true, .minimapicons = false, .transmogfix = false, .assetfix = false },
|
||||
.{ .name = "framecrash", .screenshot = false, .interact = false, .outline = false, .markers = false, .framecrash = true, .combatlog = false, .minimapicons = false, .transmogfix = false, .assetfix = false },
|
||||
.{ .name = "combatlog", .screenshot = false, .interact = false, .outline = false, .markers = false, .framecrash = false, .combatlog = true, .minimapicons = false, .transmogfix = false, .assetfix = false },
|
||||
.{ .name = "minimapicons", .screenshot = false, .interact = false, .outline = false, .markers = false, .framecrash = true, .combatlog = false, .minimapicons = true, .transmogfix = false, .assetfix = false },
|
||||
.{ .name = "transmogfix", .screenshot = false, .interact = false, .outline = false, .markers = false, .framecrash = false, .combatlog = false, .minimapicons = false, .transmogfix = true, .assetfix = false },
|
||||
.{ .name = "assetfix", .screenshot = false, .interact = false, .outline = false, .markers = false, .framecrash = false, .combatlog = false, .minimapicons = false, .transmogfix = false, .assetfix = true },
|
||||
.{ .name = "full", .screenshot = true, .interact = true, .outline = true, .markers = true, .framecrash = true, .combatlog = true, .minimapicons = true, .transmogfix = true, .assetfix = true, .healtextfix = true },
|
||||
.{ .name = "screenshot", .screenshot = true, .interact = false, .outline = false, .markers = false, .framecrash = true, .combatlog = true, .minimapicons = false, .transmogfix = false, .assetfix = false, .healtextfix = false },
|
||||
.{ .name = "interact", .screenshot = false, .interact = true, .outline = false, .markers = false, .framecrash = true, .combatlog = true, .minimapicons = false, .transmogfix = false, .assetfix = false, .healtextfix = false },
|
||||
.{ .name = "outline", .screenshot = false, .interact = false, .outline = true, .markers = false, .framecrash = true, .combatlog = true, .minimapicons = false, .transmogfix = false, .assetfix = false, .healtextfix = false },
|
||||
.{ .name = "markers", .screenshot = false, .interact = false, .outline = false, .markers = true, .framecrash = true, .combatlog = true, .minimapicons = false, .transmogfix = false, .assetfix = false, .healtextfix = false },
|
||||
.{ .name = "framecrash", .screenshot = false, .interact = false, .outline = false, .markers = false, .framecrash = true, .combatlog = false, .minimapicons = false, .transmogfix = false, .assetfix = false, .healtextfix = false },
|
||||
.{ .name = "combatlog", .screenshot = false, .interact = false, .outline = false, .markers = false, .framecrash = false, .combatlog = true, .minimapicons = false, .transmogfix = false, .assetfix = false, .healtextfix = false },
|
||||
.{ .name = "minimapicons", .screenshot = false, .interact = false, .outline = false, .markers = false, .framecrash = true, .combatlog = false, .minimapicons = true, .transmogfix = false, .assetfix = false, .healtextfix = false },
|
||||
.{ .name = "transmogfix", .screenshot = false, .interact = false, .outline = false, .markers = false, .framecrash = false, .combatlog = false, .minimapicons = false, .transmogfix = true, .assetfix = false, .healtextfix = false },
|
||||
.{ .name = "assetfix", .screenshot = false, .interact = false, .outline = false, .markers = false, .framecrash = false, .combatlog = false, .minimapicons = false, .transmogfix = false, .assetfix = true, .healtextfix = false },
|
||||
.{ .name = "healtextfix", .screenshot = false, .interact = false, .outline = false, .markers = false, .framecrash = false, .combatlog = false, .minimapicons = false, .transmogfix = false, .assetfix = false, .healtextfix = true },
|
||||
}) |variant| {
|
||||
const opts = b.addOptions();
|
||||
opts.addOption(bool, "enable_screenshot", variant.screenshot);
|
||||
@@ -80,6 +84,7 @@ pub fn build(b: *std.Build) void {
|
||||
opts.addOption(bool, "enable_minimapicons", variant.minimapicons);
|
||||
opts.addOption(bool, "enable_transmogfix", variant.transmogfix);
|
||||
opts.addOption(bool, "enable_assetfix", variant.assetfix);
|
||||
opts.addOption(bool, "enable_healtextfix", variant.healtextfix);
|
||||
|
||||
const variant_lib = b.addLibrary(.{
|
||||
.name = variant.name,
|
||||
@@ -89,7 +94,7 @@ pub fn build(b: *std.Build) void {
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.imports = &.{
|
||||
.{ .name = "hook", .module = hook_mod },
|
||||
.{ .name = "zhook", .module = zhook_mod },
|
||||
.{ .name = "build_options", .module = opts.createModule() },
|
||||
},
|
||||
}),
|
||||
|
||||
+2
-2
@@ -3,8 +3,8 @@
|
||||
.version = "0.1.0",
|
||||
.fingerprint = 0x54f0a9542562d318,
|
||||
.dependencies = .{
|
||||
.hook = .{
|
||||
.path = "libs/hook",
|
||||
.zhook = .{
|
||||
.path = "../zhook",
|
||||
},
|
||||
},
|
||||
.paths = .{
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub fn build(b: *std.Build) void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
const hwbp = b.option(bool, "hwbp", "Use hardware breakpoint hooks instead of inline patching") orelse false;
|
||||
|
||||
const options = b.addOptions();
|
||||
options.addOption(bool, "use_hwbp", hwbp);
|
||||
|
||||
const hook_mod = b.addModule("hook", .{
|
||||
.root_source_file = b.path("src/hook.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
hook_mod.addOptions("config", options);
|
||||
|
||||
// x86 length disassembler — standalone, no dependencies
|
||||
const x86dis_mod = b.addModule("x86dis", .{
|
||||
.root_source_file = b.path("src/x86dis.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
// Generic hook — uses x86dis + hook for auto-sizing trampolines
|
||||
const generic_hook_mod = b.addModule("generic_hook", .{
|
||||
.root_source_file = b.path("src/generic_hook.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
generic_hook_mod.addImport("x86dis", x86dis_mod);
|
||||
generic_hook_mod.addImport("hook.zig", hook_mod);
|
||||
generic_hook_mod.addOptions("config", options);
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
.{
|
||||
.name = .hook,
|
||||
.version = "0.1.0",
|
||||
.fingerprint = 0xa4584355bc807307,
|
||||
.paths = .{
|
||||
"build.zig",
|
||||
"build.zig.zon",
|
||||
"src",
|
||||
},
|
||||
}
|
||||
@@ -1,279 +0,0 @@
|
||||
//! Generic x86 inline hook — no manual prologue size or fixup lists needed.
|
||||
//!
|
||||
//! Uses the x86 length disassembler (HDE32 port) to automatically determine
|
||||
//! how many prologue bytes to steal, and relocates all relative instructions.
|
||||
//!
|
||||
//! Combines MinHook's compact disassembler with HadesMem's type-safe approach:
|
||||
//! declare the function signature once at comptime, get a correctly-typed
|
||||
//! trampoline and detour with zero manual casting.
|
||||
//!
|
||||
//! ## Low-level API (GenericHook)
|
||||
//!
|
||||
//! ```zig
|
||||
//! var my_hook: GenericHook = .{};
|
||||
//! if (my_hook.install(0x401000, @intFromPtr(&myDetour)) == .ok) {
|
||||
//! const orig = my_hook.getTrampoline(OrigFnType);
|
||||
//! _ = orig();
|
||||
//! }
|
||||
//! my_hook.remove();
|
||||
//! ```
|
||||
//!
|
||||
//! ## Type-safe API (Detour) — HadesMem-inspired
|
||||
//!
|
||||
//! ```zig
|
||||
//! const MyHook = Detour(fn (u32, u32) callconv(.{ .x86_stdcall = .{} }) u32);
|
||||
//! var hook: MyHook = .{};
|
||||
//! hook.attach(0x401000, myDetour);
|
||||
//! // Inside detour: hook.callOriginal(.{arg1, arg2});
|
||||
//! ```
|
||||
|
||||
const std = @import("std");
|
||||
const x86dis = @import("x86dis");
|
||||
const hook_base = @import("hook.zig");
|
||||
|
||||
const VirtualAlloc = hook_base.VirtualAlloc;
|
||||
const VirtualFree = hook_base.VirtualFree;
|
||||
const PAGE_EXECUTE_READWRITE = hook_base.PAGE_EXECUTE_READWRITE;
|
||||
const MEM_COMMIT = hook_base.MEM_COMMIT;
|
||||
const MEM_RELEASE = hook_base.MEM_RELEASE;
|
||||
const writeProtected = hook_base.writeProtected;
|
||||
|
||||
const JMP_SIZE: usize = 5; // E9 + rel32
|
||||
const MAX_STOLEN: usize = 32;
|
||||
const TRAMPOLINE_BUF: usize = 64;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// GenericHook — low-level auto-sizing hook
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
pub const GenericHook = struct {
|
||||
mem: ?[*]u8 = null,
|
||||
trampoline: usize = 0,
|
||||
target: usize = 0,
|
||||
stolen_size: usize = 0,
|
||||
saved_bytes: [MAX_STOLEN]u8 = undefined,
|
||||
|
||||
pub const Error = enum {
|
||||
ok,
|
||||
alloc_failed,
|
||||
disasm_error,
|
||||
prologue_too_short,
|
||||
unsupported_relocation,
|
||||
};
|
||||
|
||||
/// Analyse target, build trampoline, patch target → detour. One call.
|
||||
pub fn install(self: *GenericHook, target: usize, detour_addr: usize) Error {
|
||||
const err = self.prepare(target);
|
||||
if (err != .ok) return err;
|
||||
self.activate(detour_addr);
|
||||
return .ok;
|
||||
}
|
||||
|
||||
/// Phase 1: disassemble prologue, allocate trampoline, copy + relocate.
|
||||
pub fn prepare(self: *GenericHook, target: usize) Error {
|
||||
if (self.mem != null) return .ok;
|
||||
|
||||
const src: [*]const u8 = @ptrFromInt(target);
|
||||
|
||||
// ── determine how many bytes to steal ──
|
||||
var stolen: usize = 0;
|
||||
while (stolen < JMP_SIZE) {
|
||||
const insn = x86dis.decode(src + stolen);
|
||||
if (insn.flags & x86dis.F_ERROR != 0) return .disasm_error;
|
||||
if (insn.len == 0) return .disasm_error;
|
||||
stolen += insn.len;
|
||||
if (stolen > MAX_STOLEN) return .prologue_too_short;
|
||||
}
|
||||
|
||||
// ── allocate ──
|
||||
const mem = VirtualAlloc(null, TRAMPOLINE_BUF, MEM_COMMIT, PAGE_EXECUTE_READWRITE) orelse return .alloc_failed;
|
||||
|
||||
self.mem = mem;
|
||||
self.target = target;
|
||||
self.stolen_size = stolen;
|
||||
self.trampoline = @intFromPtr(mem);
|
||||
|
||||
@memcpy(self.saved_bytes[0..stolen], src[0..stolen]);
|
||||
|
||||
// ── check if already hooked (E9 at target) — chain through ──
|
||||
if (src[0] == 0xE9) {
|
||||
const other_detour = hook_base.rel32Target(target);
|
||||
mem[0] = 0xE9;
|
||||
hook_base.writeRel32(mem + 1, self.trampoline + 1, other_detour);
|
||||
return .ok;
|
||||
}
|
||||
|
||||
// ── build trampoline: copy + relocate ──
|
||||
var t_pos: usize = 0;
|
||||
var s_pos: usize = 0;
|
||||
|
||||
while (s_pos < stolen) {
|
||||
const insn = x86dis.decode(src + s_pos);
|
||||
const op = insn.opcode;
|
||||
const src_addr = target + s_pos;
|
||||
const dst_addr = self.trampoline + t_pos;
|
||||
|
||||
if (insn.flags & x86dis.F_RELATIVE != 0) {
|
||||
if (op == 0xE8 or op == 0xE9) {
|
||||
// CALL/JMP rel32
|
||||
const abs = hook_base.rel32Target(src_addr);
|
||||
mem[t_pos] = op;
|
||||
hook_base.writeRel32(mem + t_pos + 1, dst_addr + 1, abs);
|
||||
t_pos += 5;
|
||||
} else if (op == 0x0F and insn.opcode2 >= 0x80 and insn.opcode2 <= 0x8F) {
|
||||
// Jcc rel32 (0F 80-8F)
|
||||
const abs = jcc32Target(src_addr);
|
||||
mem[t_pos] = 0x0F;
|
||||
mem[t_pos + 1] = insn.opcode2;
|
||||
hook_base.writeRel32(mem + t_pos + 2, dst_addr + 2, abs);
|
||||
t_pos += 6;
|
||||
} else if (op >= 0x70 and op <= 0x7F) {
|
||||
// Short Jcc → expand to near Jcc (0F 8x)
|
||||
const offset = @as(i8, @bitCast(src[s_pos + 1]));
|
||||
const abs: usize = @bitCast(@as(isize, @intCast(src_addr + 2)) + offset);
|
||||
mem[t_pos] = 0x0F;
|
||||
mem[t_pos + 1] = op + 0x10;
|
||||
hook_base.writeRel32(mem + t_pos + 2, dst_addr + 2, abs);
|
||||
t_pos += 6;
|
||||
} else if (op == 0xEB) {
|
||||
// Short JMP → expand to near JMP (E9)
|
||||
const offset = @as(i8, @bitCast(src[s_pos + 1]));
|
||||
const abs: usize = @bitCast(@as(isize, @intCast(src_addr + 2)) + offset);
|
||||
mem[t_pos] = 0xE9;
|
||||
hook_base.writeRel32(mem + t_pos + 1, dst_addr + 1, abs);
|
||||
t_pos += 5;
|
||||
} else {
|
||||
// LOOP/JECXZ or unknown — cannot trivially expand
|
||||
self.cleanup();
|
||||
return .unsupported_relocation;
|
||||
}
|
||||
} else {
|
||||
// Non-relative — copy verbatim
|
||||
@memcpy(mem[t_pos .. t_pos + insn.len], src[s_pos .. s_pos + insn.len]);
|
||||
t_pos += insn.len;
|
||||
}
|
||||
s_pos += insn.len;
|
||||
}
|
||||
|
||||
// ── JMP back to original code after stolen bytes ──
|
||||
mem[t_pos] = 0xE9;
|
||||
hook_base.writeRel32(
|
||||
mem + t_pos + 1,
|
||||
self.trampoline + t_pos + 1,
|
||||
target + stolen,
|
||||
);
|
||||
|
||||
return .ok;
|
||||
}
|
||||
|
||||
/// Phase 2: write the E9 JMP patch at the target.
|
||||
pub fn activate(self: *GenericHook, detour_addr: usize) void {
|
||||
var patch: [MAX_STOLEN]u8 = .{0x90} ** MAX_STOLEN;
|
||||
patch[0] = 0xE9;
|
||||
hook_base.writeRel32(patch[1..5], self.target + 1, detour_addr);
|
||||
writeProtected(self.target, patch[0..self.stolen_size]);
|
||||
}
|
||||
|
||||
/// Restore original bytes and free trampoline memory.
|
||||
pub fn remove(self: *GenericHook) void {
|
||||
if (self.mem == null) return;
|
||||
writeProtected(self.target, self.saved_bytes[0..self.stolen_size]);
|
||||
_ = VirtualFree(@ptrFromInt(@intFromPtr(self.mem.?)), 0, MEM_RELEASE);
|
||||
self.mem = null;
|
||||
}
|
||||
|
||||
/// Get trampoline as a typed function pointer.
|
||||
pub fn getTrampoline(self: *const GenericHook, comptime T: type) T {
|
||||
return @ptrFromInt(self.trampoline);
|
||||
}
|
||||
|
||||
fn cleanup(self: *GenericHook) void {
|
||||
if (self.mem) |m| {
|
||||
_ = VirtualFree(@ptrFromInt(@intFromPtr(m)), 0, MEM_RELEASE);
|
||||
self.mem = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// Detour(FnType) — HadesMem-style type-safe generic hook
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Comptime-generic typed detour. Declare the target function's type once;
|
||||
/// get type-checked attach/callOriginal with no manual pointer casts.
|
||||
///
|
||||
/// ```zig
|
||||
/// const StdcallU32x2 = fn (u32, u32) callconv(.{ .x86_stdcall = .{} }) u32;
|
||||
/// const MyHook = generic_hook.Detour(StdcallU32x2);
|
||||
/// var hook: MyHook = .{};
|
||||
/// hook.attach(0x401000, &myDetour);
|
||||
/// // in detour: hook.callOriginal(.{ a, b });
|
||||
/// hook.detach();
|
||||
/// ```
|
||||
pub fn Detour(comptime FnType: type) type {
|
||||
const FnInfo = @typeInfo(FnType).@"fn";
|
||||
const FnPtr = *const FnType;
|
||||
const ReturnType = FnInfo.return_type orelse void;
|
||||
const ParamTypes = FnInfo.params;
|
||||
|
||||
return struct {
|
||||
inner: GenericHook = .{},
|
||||
|
||||
const Self = @This();
|
||||
|
||||
/// Hook the function at `target` to redirect to `detour`.
|
||||
pub fn attach(self: *Self, target: usize, detour: FnPtr) GenericHook.Error {
|
||||
return self.inner.install(target, @intFromPtr(detour));
|
||||
}
|
||||
|
||||
/// Call the original (pre-hook) function through the trampoline.
|
||||
pub fn callOriginal(self: *const Self, args: anytype) ReturnType {
|
||||
const orig: FnPtr = @ptrFromInt(self.inner.trampoline);
|
||||
return @call(.auto, orig, coerceArgs(ParamTypes, args));
|
||||
}
|
||||
|
||||
/// Unhook: restore original bytes, free trampoline.
|
||||
pub fn detach(self: *Self) void {
|
||||
self.inner.remove();
|
||||
}
|
||||
|
||||
/// Get the trampoline as the correctly-typed function pointer.
|
||||
pub fn original(self: *const Self) FnPtr {
|
||||
return @ptrFromInt(self.inner.trampoline);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Coerce a tuple of args into the exact parameter types expected.
|
||||
fn coerceArgs(comptime params: anytype, args: anytype) CoercedTuple(params) {
|
||||
var result: CoercedTuple(params) = undefined;
|
||||
inline for (0..params.len) |idx| {
|
||||
@field(result, std.fmt.comptimePrint("{d}", .{idx})) = args[idx];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
fn CoercedTuple(comptime params: anytype) type {
|
||||
var fields: [params.len]std.builtin.Type.StructField = undefined;
|
||||
inline for (0..params.len) |idx| {
|
||||
fields[idx] = .{
|
||||
.name = std.fmt.comptimePrint("{d}", .{idx}),
|
||||
.type = params[idx].type.?,
|
||||
.default_value_ptr = null,
|
||||
.is_comptime = false,
|
||||
.alignment = 0,
|
||||
};
|
||||
}
|
||||
return @Type(.{ .@"struct" = .{
|
||||
.layout = .auto,
|
||||
.fields = &fields,
|
||||
.decls = &.{},
|
||||
.is_tuple = true,
|
||||
} });
|
||||
}
|
||||
|
||||
/// Resolve absolute target of a Jcc rel32 (0F 8x xx xx xx xx) — 6 byte insn.
|
||||
fn jcc32Target(addr: usize) usize {
|
||||
const disp: u32 = @bitCast(@as(*align(1) const i32, @ptrFromInt(addr + 2)).*);
|
||||
return (addr + 6) +% disp;
|
||||
}
|
||||
@@ -1,470 +0,0 @@
|
||||
//! x86 inline hooking library for Windows DLL injection.
|
||||
//!
|
||||
//! Provides a `Hook` struct for patching function prologues with JMP detours,
|
||||
//! building trampolines to call the original, and chaining with other hooks.
|
||||
//! Also includes memory read/write helpers, rel32 arithmetic, a generic
|
||||
//! `fastcall` caller, and a fastcall-to-cdecl thunk builder.
|
||||
//!
|
||||
//! ## Quick start
|
||||
//!
|
||||
//! ```zig
|
||||
//! const hook = @import("hook.zig");
|
||||
//!
|
||||
//! var my_hook = hook.Hook{};
|
||||
//!
|
||||
//! // One-shot: prepare trampoline + patch in one call.
|
||||
//! // The last arg is a slice of opcode offsets within the prologue that
|
||||
//! // contain E8/E9 (CALL/JMP rel32) instructions needing fixup.
|
||||
//! _ = my_hook.install(target_addr, prologue_size, @intFromPtr(&detour), &.{1});
|
||||
//!
|
||||
//! // Two-phase (when you need the alloc block before patching, e.g. for a thunk):
|
||||
//! _ = my_hook.prepare(target_addr, prologue_size, &.{});
|
||||
//! const thunk_buf = my_hook.mem.? + 32;
|
||||
//! _ = hook.buildFastcallToCdeclThunk(thunk_buf, @intFromPtr(&hookImpl), 1);
|
||||
//! my_hook.activate(@intFromPtr(thunk_buf));
|
||||
//!
|
||||
//! // Call the original from inside the detour:
|
||||
//! const orig = my_hook.getTrampoline(*const fn () callconv(.{ .x86_stdcall = .{} }) void);
|
||||
//! orig();
|
||||
//!
|
||||
//! // Unhook (restore original bytes, free memory):
|
||||
//! my_hook.remove();
|
||||
//! ```
|
||||
|
||||
const std = @import("std");
|
||||
const use_hwbp = @import("config").use_hwbp;
|
||||
|
||||
// =============================================================================
|
||||
// Windows API
|
||||
// =============================================================================
|
||||
|
||||
const WINAPI = std.builtin.CallingConvention.winapi;
|
||||
|
||||
pub const PAGE_EXECUTE_READWRITE: u32 = 0x40;
|
||||
pub const MEM_COMMIT: u32 = 0x1000;
|
||||
pub const MEM_RELEASE: u32 = 0x8000;
|
||||
|
||||
extern "kernel32" fn VirtualProtect(
|
||||
lpAddress: *anyopaque,
|
||||
dwSize: usize,
|
||||
flNewProtect: u32,
|
||||
lpflOldProtect: *u32,
|
||||
) callconv(WINAPI) i32;
|
||||
|
||||
extern "kernel32" fn VirtualAlloc(
|
||||
lpAddress: ?*anyopaque,
|
||||
dwSize: usize,
|
||||
flAllocationType: u32,
|
||||
flProtect: u32,
|
||||
) callconv(WINAPI) ?[*]u8;
|
||||
|
||||
extern "kernel32" fn VirtualFree(
|
||||
lpAddress: *anyopaque,
|
||||
dwSize: usize,
|
||||
dwFreeType: u32,
|
||||
) callconv(WINAPI) i32;
|
||||
|
||||
// =============================================================================
|
||||
// Hardware breakpoint support (DR0-DR3 + Vectored Exception Handler)
|
||||
// =============================================================================
|
||||
|
||||
const CONTEXT_DEBUG_REGISTERS: u32 = 0x00010010;
|
||||
const EXCEPTION_SINGLE_STEP: u32 = 0x80000004;
|
||||
const EXCEPTION_CONTINUE_EXECUTION: i32 = -1;
|
||||
const EXCEPTION_CONTINUE_SEARCH: i32 = 0;
|
||||
|
||||
extern "kernel32" fn GetCurrentThread() callconv(WINAPI) *anyopaque;
|
||||
|
||||
extern "kernel32" fn GetThreadContext(
|
||||
hThread: *anyopaque,
|
||||
lpContext: *CONTEXT,
|
||||
) callconv(WINAPI) i32;
|
||||
|
||||
extern "kernel32" fn SetThreadContext(
|
||||
hThread: *anyopaque,
|
||||
lpContext: *const CONTEXT,
|
||||
) callconv(WINAPI) i32;
|
||||
|
||||
extern "kernel32" fn AddVectoredExceptionHandler(
|
||||
First: u32,
|
||||
Handler: *const fn (*EXCEPTION_POINTERS) callconv(WINAPI) i32,
|
||||
) callconv(WINAPI) ?*anyopaque;
|
||||
|
||||
extern "kernel32" fn RemoveVectoredExceptionHandler(
|
||||
Handle: *anyopaque,
|
||||
) callconv(WINAPI) u32;
|
||||
|
||||
const CONTEXT = extern struct {
|
||||
ContextFlags: u32,
|
||||
Dr0: u32,
|
||||
Dr1: u32,
|
||||
Dr2: u32,
|
||||
Dr3: u32,
|
||||
Dr6: u32,
|
||||
Dr7: u32,
|
||||
FloatSave: [112]u8,
|
||||
SegGs: u32,
|
||||
SegFs: u32,
|
||||
SegEs: u32,
|
||||
SegDs: u32,
|
||||
Edi: u32,
|
||||
Esi: u32,
|
||||
Ebx: u32,
|
||||
Edx: u32,
|
||||
Ecx: u32,
|
||||
Eax: u32,
|
||||
Ebp: u32,
|
||||
Eip: u32,
|
||||
SegCs: u32,
|
||||
EFlags: u32,
|
||||
Esp: u32,
|
||||
SegSs: u32,
|
||||
ExtendedRegisters: [512]u8,
|
||||
};
|
||||
|
||||
const EXCEPTION_RECORD = extern struct {
|
||||
ExceptionCode: u32,
|
||||
ExceptionFlags: u32,
|
||||
ExceptionRecord: ?*EXCEPTION_RECORD,
|
||||
ExceptionAddress: ?*anyopaque,
|
||||
NumberParameters: u32,
|
||||
ExceptionInformation: [15]usize,
|
||||
};
|
||||
|
||||
const EXCEPTION_POINTERS = extern struct {
|
||||
ExceptionRecord: *EXCEPTION_RECORD,
|
||||
ContextRecord: *CONTEXT,
|
||||
};
|
||||
|
||||
var hwbp_slots: [4]?*Hook = .{ null, null, null, null };
|
||||
var hwbp_detours: [4]usize = .{ 0, 0, 0, 0 };
|
||||
var veh_handle: ?*anyopaque = null;
|
||||
|
||||
fn vehHandler(info: *EXCEPTION_POINTERS) callconv(WINAPI) i32 {
|
||||
if (info.ExceptionRecord.ExceptionCode != EXCEPTION_SINGLE_STEP)
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
|
||||
const eip = info.ContextRecord.Eip;
|
||||
for (0..4) |i| {
|
||||
if (hwbp_slots[i]) |h| {
|
||||
if (h.target == eip) {
|
||||
info.ContextRecord.Eip = @intCast(hwbp_detours[i]);
|
||||
return EXCEPTION_CONTINUE_EXECUTION;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Memory helpers
|
||||
// =============================================================================
|
||||
|
||||
/// Read a value of type `T` from an arbitrary memory address (unaligned).
|
||||
pub fn readMem(comptime T: type, addr: usize) T {
|
||||
return @as(*align(1) const T, @ptrFromInt(addr)).*;
|
||||
}
|
||||
|
||||
/// Write raw bytes to an arbitrary memory address. No protection change —
|
||||
/// caller must ensure the page is writable (or use `writeProtected`).
|
||||
pub fn writeMem(addr: usize, bytes: []const u8) void {
|
||||
const dest: [*]u8 = @ptrFromInt(addr);
|
||||
for (bytes, 0..) |b, i| {
|
||||
dest[i] = b;
|
||||
}
|
||||
}
|
||||
|
||||
/// Write bytes to a potentially read-only/executable page. Temporarily sets
|
||||
/// PAGE_EXECUTE_READWRITE, writes, then restores the original protection.
|
||||
pub fn writeProtected(addr: usize, bytes: []const u8) void {
|
||||
var old: u32 = 0;
|
||||
_ = VirtualProtect(@ptrFromInt(addr), bytes.len, PAGE_EXECUTE_READWRITE, &old);
|
||||
writeMem(addr, bytes);
|
||||
_ = VirtualProtect(@ptrFromInt(addr), bytes.len, old, &old);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Rel32 helpers
|
||||
// =============================================================================
|
||||
|
||||
/// Resolve the absolute target of an E8 (CALL) or E9 (JMP) at `addr`.
|
||||
/// Reads the signed rel32 operand at addr+1 and computes addr+5+offset.
|
||||
pub fn rel32Target(addr: usize) usize {
|
||||
const offset: u32 = @bitCast(@as(*align(1) const i32, @ptrFromInt(addr + 1)).*);
|
||||
return (addr + 5) +% offset;
|
||||
}
|
||||
|
||||
/// Write a rel32 displacement into dest[0..4] such that a JMP/CALL
|
||||
/// from address `from` reaches `to`. Displacement = to - (from + 4).
|
||||
pub fn writeRel32(dest: [*]u8, from: usize, to: usize) void {
|
||||
std.mem.writeInt(u32, dest[0..4], to -% (from + 4), .little);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Calling convention helper
|
||||
// =============================================================================
|
||||
|
||||
/// Call a __fastcall function at `addr` with ECX and EDX arguments.
|
||||
/// Dispatches on return type: void, f64 (x87 ST0), or integer/pointer (EAX).
|
||||
pub fn fastcall(comptime R: type, addr: usize, ecx: anytype, edx: anytype) R {
|
||||
if (R == f64) {
|
||||
return asm volatile ("call *%[func]"
|
||||
: [ret] "={st}" (-> f64),
|
||||
: [_] "{ecx}" (ecx), [_] "{edx}" (edx), [func] "r" (addr),
|
||||
: .{ .eax = true, .memory = true, .cc = true }
|
||||
);
|
||||
} else if (R == void) {
|
||||
asm volatile ("call *%[func]"
|
||||
:
|
||||
: [_] "{ecx}" (ecx), [_] "{edx}" (edx), [func] "r" (addr),
|
||||
: .{ .eax = true, .memory = true, .cc = true }
|
||||
);
|
||||
} else {
|
||||
return asm volatile ("call *%[func]"
|
||||
: [ret] "={eax}" (-> R),
|
||||
: [_] "{ecx}" (ecx), [_] "{edx}" (edx), [func] "r" (addr),
|
||||
: .{ .memory = true, .cc = true }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Hook struct
|
||||
// =============================================================================
|
||||
|
||||
const ALLOC_SIZE: usize = 64;
|
||||
const TRAMPOLINE_RESERVE: usize = 32;
|
||||
const MAX_PROLOGUE: usize = 16;
|
||||
|
||||
pub const Hook = struct {
|
||||
mem: ?[*]u8 = null,
|
||||
trampoline: usize = 0,
|
||||
target: usize = 0,
|
||||
prologue_size: usize = 0,
|
||||
saved_bytes: [MAX_PROLOGUE]u8 = undefined,
|
||||
|
||||
/// Allocate executable memory, save current bytes at target, and build the
|
||||
/// trampoline. Does NOT patch the target yet — call activate() after.
|
||||
/// `rel32_fixups` is a slice of opcode offsets within the prologue that
|
||||
/// contain E8/E9 instructions needing rel32 adjustment.
|
||||
pub fn prepare(
|
||||
self: *Hook,
|
||||
target: usize,
|
||||
prologue_size: usize,
|
||||
rel32_fixups: []const usize,
|
||||
) bool {
|
||||
if (self.mem != null) return true;
|
||||
|
||||
const mem = VirtualAlloc(null, ALLOC_SIZE, MEM_COMMIT, PAGE_EXECUTE_READWRITE) orelse return false;
|
||||
self.mem = mem;
|
||||
self.target = target;
|
||||
self.prologue_size = prologue_size;
|
||||
|
||||
// Save current bytes for remove()
|
||||
const src: [*]const u8 = @ptrFromInt(target);
|
||||
@memcpy(self.saved_bytes[0..prologue_size], src[0..prologue_size]);
|
||||
|
||||
// Build trampoline
|
||||
self.trampoline = @intFromPtr(mem);
|
||||
|
||||
if (src[0] == 0xE9) {
|
||||
// Another DLL already hooked — resolve their JMP and chain through it
|
||||
const other_detour = rel32Target(target);
|
||||
mem[0] = 0xE9;
|
||||
writeRel32(mem + 1, self.trampoline + 1, other_detour);
|
||||
} else {
|
||||
// Original prologue — copy bytes, fix up any relative instructions, JMP back
|
||||
@memcpy(mem[0..prologue_size], src[0..prologue_size]);
|
||||
|
||||
for (rel32_fixups) |opcode_offset| {
|
||||
const abs_target = rel32Target(target + opcode_offset);
|
||||
const tramp_operand = self.trampoline + opcode_offset + 1;
|
||||
writeRel32(mem + opcode_offset + 1, tramp_operand, abs_target);
|
||||
}
|
||||
|
||||
mem[prologue_size] = 0xE9;
|
||||
writeRel32(
|
||||
mem + prologue_size + 1,
|
||||
self.trampoline + prologue_size + 1,
|
||||
target + prologue_size,
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Write the E9 JMP patch (inline mode) or set a hardware breakpoint
|
||||
/// (HWBP mode) to redirect target → detour_addr.
|
||||
pub fn activate(self: *Hook, detour_addr: usize) void {
|
||||
if (use_hwbp) {
|
||||
// Register VEH on first use
|
||||
if (veh_handle == null) {
|
||||
veh_handle = AddVectoredExceptionHandler(1, &vehHandler);
|
||||
}
|
||||
// Find free DR slot
|
||||
const slot: u2 = for (0..4) |i| {
|
||||
if (hwbp_slots[i] == null) break @as(u2, @intCast(i));
|
||||
} else return; // all 4 slots occupied
|
||||
|
||||
hwbp_slots[slot] = self;
|
||||
hwbp_detours[slot] = detour_addr;
|
||||
|
||||
const thread = GetCurrentThread();
|
||||
var ctx: CONTEXT = std.mem.zeroes(CONTEXT);
|
||||
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
|
||||
_ = GetThreadContext(thread, &ctx);
|
||||
|
||||
// Set DRn to target address
|
||||
switch (slot) {
|
||||
0 => { ctx.Dr0 = @intCast(self.target); },
|
||||
1 => { ctx.Dr1 = @intCast(self.target); },
|
||||
2 => { ctx.Dr2 = @intCast(self.target); },
|
||||
3 => { ctx.Dr3 = @intCast(self.target); },
|
||||
}
|
||||
|
||||
// Enable local execute breakpoint in DR7
|
||||
const s: u5 = slot;
|
||||
ctx.Dr7 |= @as(u32, 1) << (s * 2);
|
||||
ctx.Dr7 &= ~(@as(u32, 0xF) << (16 + s * 4));
|
||||
|
||||
_ = SetThreadContext(thread, &ctx);
|
||||
} else {
|
||||
var patch: [MAX_PROLOGUE]u8 = .{0x90} ** MAX_PROLOGUE;
|
||||
patch[0] = 0xE9;
|
||||
writeRel32(patch[1..5], self.target + 1, detour_addr);
|
||||
writeProtected(self.target, patch[0..self.prologue_size]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience: prepare + activate in one call.
|
||||
pub fn install(
|
||||
self: *Hook,
|
||||
target: usize,
|
||||
prologue_size: usize,
|
||||
detour_addr: usize,
|
||||
rel32_fixups: []const usize,
|
||||
) bool {
|
||||
if (!self.prepare(target, prologue_size, rel32_fixups)) return false;
|
||||
self.activate(detour_addr);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Restore original bytes (inline) or clear the DR slot (HWBP), then free
|
||||
/// the trampoline memory.
|
||||
pub fn remove(self: *Hook) void {
|
||||
if (self.mem == null) return;
|
||||
|
||||
if (use_hwbp) {
|
||||
for (0..4) |i| {
|
||||
if (hwbp_slots[i]) |h| {
|
||||
if (h == self) {
|
||||
const thread = GetCurrentThread();
|
||||
var ctx: CONTEXT = std.mem.zeroes(CONTEXT);
|
||||
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
|
||||
_ = GetThreadContext(thread, &ctx);
|
||||
|
||||
switch (@as(u2, @intCast(i))) {
|
||||
0 => { ctx.Dr0 = 0; },
|
||||
1 => { ctx.Dr1 = 0; },
|
||||
2 => { ctx.Dr2 = 0; },
|
||||
3 => { ctx.Dr3 = 0; },
|
||||
}
|
||||
const s: u5 = @intCast(i);
|
||||
ctx.Dr7 &= ~(@as(u32, 1) << (s * 2));
|
||||
ctx.Dr7 &= ~(@as(u32, 0xF) << (16 + s * 4));
|
||||
|
||||
_ = SetThreadContext(thread, &ctx);
|
||||
|
||||
hwbp_slots[i] = null;
|
||||
hwbp_detours[i] = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
writeProtected(self.target, self.saved_bytes[0..self.prologue_size]);
|
||||
}
|
||||
|
||||
_ = VirtualFree(@ptrFromInt(@intFromPtr(self.mem.?)), 0, MEM_RELEASE);
|
||||
self.mem = null;
|
||||
}
|
||||
|
||||
/// Cast trampoline address to a typed function pointer for calling the original.
|
||||
pub fn getTrampoline(self: *const Hook, comptime T: type) T {
|
||||
return @ptrFromInt(self.trampoline);
|
||||
}
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Thunk builder: fastcall(ECX, EDX, stack...) → cdecl(stack, stack, stack...)
|
||||
// =============================================================================
|
||||
|
||||
/// Build a fastcall-to-cdecl bridge thunk in `buf`.
|
||||
/// `cdecl_fn` is the address of the cdecl target function.
|
||||
/// `stack_arg_count` is the number of extra stack arguments beyond ECX/EDX.
|
||||
/// Returns the total thunk size in bytes.
|
||||
///
|
||||
/// Generated code:
|
||||
/// push dword [esp + 4*N] ; for each stack arg, right-to-left
|
||||
/// ...
|
||||
/// push edx ; arg 2
|
||||
/// push ecx ; arg 1
|
||||
/// mov eax, <cdecl_fn>
|
||||
/// call eax
|
||||
/// add esp, (2 + stack_arg_count) * 4
|
||||
/// ret (stack_arg_count * 4)
|
||||
pub fn buildFastcallToCdeclThunk(buf: [*]u8, cdecl_fn: usize, stack_arg_count: u8) usize {
|
||||
var pos: usize = 0;
|
||||
|
||||
// Push stack args right-to-left. At entry, [esp] = return addr,
|
||||
// [esp+4] = first stack arg, [esp+8] = second, etc.
|
||||
// But each push shifts esp, so we always read from [esp + 4 * stack_arg_count]
|
||||
// (the offset stays constant because we push the same number of times as the depth grows).
|
||||
var i: u8 = stack_arg_count;
|
||||
while (i > 0) : (i -= 1) {
|
||||
// push dword ptr [esp + 4 * stack_arg_count]
|
||||
buf[pos] = 0xFF;
|
||||
buf[pos + 1] = 0x74;
|
||||
buf[pos + 2] = 0x24;
|
||||
buf[pos + 3] = stack_arg_count * 4;
|
||||
pos += 4;
|
||||
}
|
||||
|
||||
// push edx (arg 2)
|
||||
buf[pos] = 0x52;
|
||||
pos += 1;
|
||||
|
||||
// push ecx (arg 1)
|
||||
buf[pos] = 0x51;
|
||||
pos += 1;
|
||||
|
||||
// mov eax, <cdecl_fn>
|
||||
buf[pos] = 0xB8;
|
||||
std.mem.writeInt(u32, buf[pos + 1 ..][0..4], @intCast(cdecl_fn), .little);
|
||||
pos += 5;
|
||||
|
||||
// call eax
|
||||
buf[pos] = 0xFF;
|
||||
buf[pos + 1] = 0xD0;
|
||||
pos += 2;
|
||||
|
||||
// add esp, (2 + stack_arg_count) * 4 (cdecl caller cleanup)
|
||||
const cleanup: u8 = (2 + stack_arg_count) * 4;
|
||||
buf[pos] = 0x83;
|
||||
buf[pos + 1] = 0xC4;
|
||||
buf[pos + 2] = cleanup;
|
||||
pos += 3;
|
||||
|
||||
// ret (stack_arg_count * 4) (fastcall callee cleans stack args)
|
||||
if (stack_arg_count == 0) {
|
||||
buf[pos] = 0xC3; // ret
|
||||
pos += 1;
|
||||
} else {
|
||||
buf[pos] = 0xC2; // ret imm16
|
||||
std.mem.writeInt(u16, buf[pos + 1 ..][0..2], @as(u16, stack_arg_count) * 4, .little);
|
||||
pos += 3;
|
||||
}
|
||||
|
||||
return pos;
|
||||
}
|
||||
@@ -1,402 +0,0 @@
|
||||
//! Minimal x86 (32-bit) length disassembler.
|
||||
//!
|
||||
//! Faithful port of Vyacheslav Patkov's Hacker Disassembler Engine 32 (HDE32),
|
||||
//! used by MinHook. Only computes instruction length + flags needed for
|
||||
//! relocation (F_RELATIVE). ~470 bytes of table data, compiles to ~1-2 KB.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
// ── public flags ───────────────────────────────────────────────────────
|
||||
pub const F_MODRM: u32 = 0x00000001;
|
||||
pub const F_SIB: u32 = 0x00000002;
|
||||
pub const F_IMM8: u32 = 0x00000004;
|
||||
pub const F_IMM16: u32 = 0x00000008;
|
||||
pub const F_IMM32: u32 = 0x00000010;
|
||||
pub const F_DISP8: u32 = 0x00000020;
|
||||
pub const F_DISP16: u32 = 0x00000040;
|
||||
pub const F_DISP32: u32 = 0x00000080;
|
||||
pub const F_RELATIVE: u32 = 0x00000100;
|
||||
pub const F_ERROR: u32 = 0x00001000;
|
||||
|
||||
// ── internal cflags ────────────────────────────────────────────────────
|
||||
const C_MODRM: u8 = 0x01;
|
||||
const C_IMM8: u8 = 0x02;
|
||||
const C_IMM16: u8 = 0x04;
|
||||
const C_IMM_P66: u8 = 0x10;
|
||||
const C_REL8: u8 = 0x20;
|
||||
const C_REL32: u8 = 0x40;
|
||||
const C_GROUP: u8 = 0x80;
|
||||
const C_ERROR: u8 = 0xff;
|
||||
|
||||
const PRE_NONE: u8 = 0x01;
|
||||
const PRE_66: u8 = 0x08;
|
||||
const PRE_67: u8 = 0x10;
|
||||
|
||||
const DELTA_OPCODES: usize = 0x4a;
|
||||
|
||||
// HDE32 opcode table — verbatim from table32.h
|
||||
const hde32_table = [_]u8{
|
||||
0xa3, 0xa8, 0xa3, 0xa8, 0xa3, 0xa8, 0xa3, 0xa8, 0xa3, 0xa8, 0xa3, 0xa8, 0xa3, 0xa8, 0xa3,
|
||||
0xa8, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xac, 0xaa, 0xb2, 0xaa, 0x9f, 0x9f,
|
||||
0x9f, 0x9f, 0xb5, 0xa3, 0xa3, 0xa4, 0xaa, 0xaa, 0xba, 0xaa, 0x96, 0xaa, 0xa8, 0xaa, 0xc3,
|
||||
0xc3, 0x96, 0x96, 0xb7, 0xae, 0xd6, 0xbd, 0xa3, 0xc5, 0xa3, 0xa3, 0x9f, 0xc3, 0x9c, 0xaa,
|
||||
0xaa, 0xac, 0xaa, 0xbf, 0x03, 0x7f, 0x11, 0x7f, 0x01, 0x7f, 0x01, 0x3f, 0x01, 0x01, 0x90,
|
||||
0x82, 0x7d, 0x97, 0x59, 0x59, 0x59, 0x59, 0x59, 0x7f, 0x59, 0x59, 0x60, 0x7d, 0x7f, 0x7f,
|
||||
0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x9a, 0x88, 0x7d,
|
||||
0x59, 0x50, 0x50, 0x50, 0x50, 0x59, 0x59, 0x59, 0x59, 0x61, 0x94, 0x61, 0x9e, 0x59, 0x59,
|
||||
0x85, 0x59, 0x92, 0xa3, 0x60, 0x60, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59,
|
||||
0x59, 0x59, 0x9f, 0x01, 0x03, 0x01, 0x04, 0x03, 0xd5, 0x03, 0xcc, 0x01, 0xbc, 0x03, 0xf0,
|
||||
0x10, 0x10, 0x10, 0x10, 0x50, 0x50, 0x50, 0x50, 0x14, 0x20, 0x20, 0x20, 0x20, 0x01, 0x01,
|
||||
0x01, 0x01, 0xc4, 0x02, 0x10, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0xc0, 0xc2, 0x10, 0x11,
|
||||
0x02, 0x03, 0x11, 0x03, 0x03, 0x04, 0x00, 0x00, 0x14, 0x00, 0x02, 0x00, 0x00, 0xc6, 0xc8,
|
||||
0x02, 0x02, 0x02, 0x02, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0xff, 0xca,
|
||||
0x01, 0x01, 0x01, 0x00, 0x06, 0x00, 0x04, 0x00, 0xc0, 0xc2, 0x01, 0x01, 0x03, 0x01, 0xff,
|
||||
0xff, 0x01, 0x00, 0x03, 0xc4, 0xc4, 0xc6, 0x03, 0x01, 0x01, 0x01, 0xff, 0x03, 0x03, 0x03,
|
||||
0xc8, 0x40, 0x00, 0x0a, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x33, 0x01, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xbf, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00,
|
||||
0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0xbf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x7f, 0x00, 0x00, 0xff, 0x4a, 0x4a, 0x4a, 0x4a, 0x4b, 0x52, 0x4a, 0x4a, 0x4a, 0x4a, 0x4f,
|
||||
0x4c, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x55, 0x45, 0x40, 0x4a, 0x4a, 0x4a,
|
||||
0x45, 0x59, 0x4d, 0x46, 0x4a, 0x5d, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a,
|
||||
0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x61, 0x63, 0x67, 0x4e, 0x4a, 0x4a, 0x6b, 0x6d, 0x4a, 0x4a,
|
||||
0x45, 0x6d, 0x4a, 0x4a, 0x44, 0x45, 0x4a, 0x4a, 0x00, 0x00, 0x00, 0x02, 0x0d, 0x06, 0x06,
|
||||
0x06, 0x06, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x06, 0x06, 0x06, 0x00, 0x06, 0x06, 0x02, 0x06,
|
||||
0x00, 0x0a, 0x0a, 0x07, 0x07, 0x06, 0x02, 0x05, 0x05, 0x02, 0x02, 0x00, 0x00, 0x04, 0x04,
|
||||
0x04, 0x04, 0x00, 0x00, 0x00, 0x0e, 0x05, 0x06, 0x06, 0x06, 0x01, 0x06, 0x00, 0x00, 0x08,
|
||||
0x00, 0x10, 0x00, 0x18, 0x00, 0x20, 0x00, 0x28, 0x00, 0x30, 0x00, 0x80, 0x01, 0x82, 0x01,
|
||||
0x86, 0x00, 0xf6, 0xcf, 0xfe, 0x3f, 0xab, 0x00, 0xb0, 0x00, 0xb1, 0x00, 0xb3, 0x00, 0xba,
|
||||
0xf8, 0xbb, 0x00, 0xc0, 0x00, 0xc1, 0x00, 0xc7, 0xbf, 0x62, 0xff, 0x00, 0x8d, 0xff, 0x00,
|
||||
0xc4, 0xff, 0x00, 0xc5, 0xff, 0x00,
|
||||
};
|
||||
|
||||
pub const Insn = struct {
|
||||
len: u8,
|
||||
flags: u32,
|
||||
opcode: u8,
|
||||
opcode2: u8,
|
||||
};
|
||||
|
||||
/// Decode the instruction at `code`, returning its length and flags.
|
||||
pub fn decode(code: [*]const u8) Insn {
|
||||
var result = Insn{ .len = 0, .flags = 0, .opcode = 0, .opcode2 = 0 };
|
||||
|
||||
var p: usize = 0;
|
||||
var pref: u8 = 0;
|
||||
var disp_size: u8 = 0;
|
||||
|
||||
// ── prefixes ──
|
||||
var prefix_count: u8 = 16;
|
||||
prefix_loop: while (prefix_count > 0) : (prefix_count -= 1) {
|
||||
switch (code[p]) {
|
||||
0xf3, 0xf2 => pref |= if (code[p] == 0xf3) 0x04 else 0x02,
|
||||
0xf0 => pref |= 0x20, // PRE_LOCK
|
||||
0x26, 0x2e, 0x36, 0x3e, 0x64, 0x65 => pref |= 0x40, // PRE_SEG
|
||||
0x66 => pref |= PRE_66,
|
||||
0x67 => pref |= PRE_67,
|
||||
else => break :prefix_loop,
|
||||
}
|
||||
p += 1;
|
||||
}
|
||||
|
||||
result.flags = @as(u32, pref) << 23;
|
||||
|
||||
if (pref == 0) pref |= PRE_NONE;
|
||||
|
||||
// ── opcode ──
|
||||
var ht_base: usize = 0;
|
||||
var c = code[p];
|
||||
p += 1;
|
||||
result.opcode = c;
|
||||
|
||||
if (c == 0x0f) {
|
||||
// two-byte opcode
|
||||
result.opcode2 = code[p];
|
||||
c = code[p];
|
||||
p += 1;
|
||||
ht_base = DELTA_OPCODES;
|
||||
} else if (c >= 0xa0 and c <= 0xa3) {
|
||||
// MOV moffs — address-size prefix swaps operand-size behavior
|
||||
if (pref & PRE_67 != 0)
|
||||
pref |= PRE_66
|
||||
else
|
||||
pref &= ~PRE_66;
|
||||
}
|
||||
|
||||
const opcode = c;
|
||||
|
||||
// ── two-level table lookup: ht[ht[opcode/4] + (opcode%4)] ──
|
||||
var cflags: u8 = blk: {
|
||||
const idx1 = ht_base + @as(usize, opcode / 4);
|
||||
if (idx1 >= hde32_table.len) break :blk C_ERROR;
|
||||
const idx2 = ht_base + @as(usize, hde32_table[idx1]) + @as(usize, opcode % 4);
|
||||
if (idx2 >= hde32_table.len) break :blk C_ERROR;
|
||||
break :blk hde32_table[idx2];
|
||||
};
|
||||
|
||||
if (cflags == C_ERROR) {
|
||||
result.flags |= F_ERROR;
|
||||
cflags = 0;
|
||||
if ((opcode & 0xfd) == 0x24) // (opcode & -3) == 0x24
|
||||
cflags +%= 1;
|
||||
}
|
||||
|
||||
// ── group resolution ──
|
||||
var x: u8 = 0;
|
||||
if (cflags & C_GROUP != 0) {
|
||||
const group_idx = ht_base + @as(usize, cflags & 0x7f);
|
||||
if (group_idx + 1 < hde32_table.len) {
|
||||
const t = std.mem.readInt(u16, hde32_table[group_idx..][0..2], .little);
|
||||
cflags = @truncate(t);
|
||||
x = @truncate(t >> 8);
|
||||
}
|
||||
}
|
||||
|
||||
// ── modrm ──
|
||||
if (cflags & C_MODRM != 0) {
|
||||
result.flags |= F_MODRM;
|
||||
const modrm = code[p];
|
||||
p += 1;
|
||||
const m_mod = modrm >> 6;
|
||||
const m_rm: u8 = modrm & 7;
|
||||
const m_reg: u3 = @truncate((modrm & 0x3f) >> 3);
|
||||
|
||||
// F6 TEST imm8 / F7 TEST imm16/32
|
||||
if (m_reg <= 1) {
|
||||
if (opcode == 0xf6)
|
||||
cflags |= C_IMM8;
|
||||
if (opcode == 0xf7)
|
||||
cflags |= C_IMM_P66;
|
||||
}
|
||||
|
||||
// displacement
|
||||
switch (m_mod) {
|
||||
0 => {
|
||||
if (pref & PRE_67 != 0) {
|
||||
if (m_rm == 6) disp_size = 2;
|
||||
} else {
|
||||
if (m_rm == 5) disp_size = 4;
|
||||
}
|
||||
},
|
||||
1 => disp_size = 1,
|
||||
2 => {
|
||||
disp_size = 2;
|
||||
if (pref & PRE_67 == 0)
|
||||
disp_size = 4;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
|
||||
// SIB byte
|
||||
if (m_mod != 3 and m_rm == 4 and (pref & PRE_67 == 0)) {
|
||||
result.flags |= F_SIB;
|
||||
const sib = code[p];
|
||||
p += 1;
|
||||
if ((sib & 7) == 5 and (m_mod & 1) == 0)
|
||||
disp_size = 4;
|
||||
}
|
||||
|
||||
// displacement bytes
|
||||
switch (disp_size) {
|
||||
1 => {
|
||||
result.flags |= F_DISP8;
|
||||
p += 1;
|
||||
},
|
||||
2 => {
|
||||
result.flags |= F_DISP16;
|
||||
p += 2;
|
||||
},
|
||||
4 => {
|
||||
result.flags |= F_DISP32;
|
||||
p += 4;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
// ── immediates ──
|
||||
if (cflags & C_IMM_P66 != 0) {
|
||||
if (cflags & C_REL32 != 0) {
|
||||
if (pref & PRE_66 != 0) {
|
||||
result.flags |= F_IMM16 | F_RELATIVE;
|
||||
p += 2;
|
||||
// disasm_done — skip remaining immediate checks
|
||||
result.len = @intCast(p);
|
||||
if (result.len > 15) {
|
||||
result.flags |= F_ERROR;
|
||||
result.len = 15;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
// fall through to rel32_ok below
|
||||
} else {
|
||||
if (pref & PRE_66 != 0) {
|
||||
result.flags |= F_IMM16;
|
||||
p += 2;
|
||||
} else {
|
||||
result.flags |= F_IMM32;
|
||||
p += 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cflags & C_IMM16 != 0) {
|
||||
if (result.flags & F_IMM32 != 0) {
|
||||
result.flags |= F_IMM16;
|
||||
} else if (result.flags & F_IMM16 != 0) {
|
||||
// F_2IMM16
|
||||
} else {
|
||||
result.flags |= F_IMM16;
|
||||
}
|
||||
p += 2;
|
||||
}
|
||||
if (cflags & C_IMM8 != 0) {
|
||||
result.flags |= F_IMM8;
|
||||
p += 1;
|
||||
}
|
||||
|
||||
if (cflags & C_REL32 != 0) {
|
||||
result.flags |= F_IMM32 | F_RELATIVE;
|
||||
p += 4;
|
||||
} else if (cflags & C_REL8 != 0) {
|
||||
result.flags |= F_IMM8 | F_RELATIVE;
|
||||
p += 1;
|
||||
}
|
||||
|
||||
result.len = @intCast(p);
|
||||
if (result.len > 15) {
|
||||
result.flags |= F_ERROR;
|
||||
result.len = 15;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── tests ──────────────────────────────────────────────────────────────
|
||||
|
||||
test "push ebp" {
|
||||
const d = decode(&[_]u8{ 0x55, 0xCC });
|
||||
try std.testing.expectEqual(@as(u8, 1), d.len);
|
||||
}
|
||||
|
||||
test "mov ebp, esp" {
|
||||
// 8B EC (or 89 E5)
|
||||
const d = decode(&[_]u8{ 0x8B, 0xEC });
|
||||
try std.testing.expectEqual(@as(u8, 2), d.len);
|
||||
try std.testing.expect(d.flags & F_MODRM != 0);
|
||||
}
|
||||
|
||||
test "call rel32" {
|
||||
const d = decode(&[_]u8{ 0xE8, 0x78, 0x56, 0x34, 0x12 });
|
||||
try std.testing.expectEqual(@as(u8, 5), d.len);
|
||||
try std.testing.expect(d.flags & F_RELATIVE != 0);
|
||||
try std.testing.expect(d.flags & F_IMM32 != 0);
|
||||
}
|
||||
|
||||
test "jmp rel32" {
|
||||
const d = decode(&[_]u8{ 0xE9, 0x00, 0x00, 0x00, 0x00 });
|
||||
try std.testing.expectEqual(@as(u8, 5), d.len);
|
||||
try std.testing.expect(d.flags & F_RELATIVE != 0);
|
||||
}
|
||||
|
||||
test "sub esp, imm8" {
|
||||
// 83 EC 10
|
||||
const d = decode(&[_]u8{ 0x83, 0xEC, 0x10 });
|
||||
try std.testing.expectEqual(@as(u8, 3), d.len);
|
||||
try std.testing.expect(d.flags & F_MODRM != 0);
|
||||
try std.testing.expect(d.flags & F_IMM8 != 0);
|
||||
}
|
||||
|
||||
test "mov eax, [ebp+8]" {
|
||||
// 8B 45 08
|
||||
const d = decode(&[_]u8{ 0x8B, 0x45, 0x08 });
|
||||
try std.testing.expectEqual(@as(u8, 3), d.len);
|
||||
try std.testing.expect(d.flags & F_MODRM != 0);
|
||||
try std.testing.expect(d.flags & F_DISP8 != 0);
|
||||
}
|
||||
|
||||
test "jz rel32 (0F 84)" {
|
||||
const d = decode(&[_]u8{ 0x0F, 0x84, 0x10, 0x00, 0x00, 0x00 });
|
||||
try std.testing.expectEqual(@as(u8, 6), d.len);
|
||||
try std.testing.expect(d.flags & F_RELATIVE != 0);
|
||||
}
|
||||
|
||||
test "nop" {
|
||||
const d = decode(&[_]u8{0x90});
|
||||
try std.testing.expectEqual(@as(u8, 1), d.len);
|
||||
}
|
||||
|
||||
test "ret" {
|
||||
const d = decode(&[_]u8{0xC3});
|
||||
try std.testing.expectEqual(@as(u8, 1), d.len);
|
||||
}
|
||||
|
||||
test "short jmp EB" {
|
||||
const d = decode(&[_]u8{ 0xEB, 0x05 });
|
||||
try std.testing.expectEqual(@as(u8, 2), d.len);
|
||||
try std.testing.expect(d.flags & F_RELATIVE != 0);
|
||||
try std.testing.expect(d.flags & F_IMM8 != 0);
|
||||
}
|
||||
|
||||
test "short jcc 74 (jz rel8)" {
|
||||
const d = decode(&[_]u8{ 0x74, 0x0A });
|
||||
try std.testing.expectEqual(@as(u8, 2), d.len);
|
||||
try std.testing.expect(d.flags & F_RELATIVE != 0);
|
||||
}
|
||||
|
||||
test "mov eax, imm32" {
|
||||
const d = decode(&[_]u8{ 0xB8, 0x44, 0x33, 0x22, 0x11 });
|
||||
try std.testing.expectEqual(@as(u8, 5), d.len);
|
||||
}
|
||||
|
||||
test "push imm32" {
|
||||
const d = decode(&[_]u8{ 0x68, 0x44, 0x33, 0x22, 0x11 });
|
||||
try std.testing.expectEqual(@as(u8, 5), d.len);
|
||||
}
|
||||
|
||||
test "push imm8" {
|
||||
// 6A 01
|
||||
const d = decode(&[_]u8{ 0x6A, 0x01 });
|
||||
try std.testing.expectEqual(@as(u8, 2), d.len);
|
||||
}
|
||||
|
||||
test "mov [ebp-4], eax" {
|
||||
// 89 45 FC
|
||||
const d = decode(&[_]u8{ 0x89, 0x45, 0xFC });
|
||||
try std.testing.expectEqual(@as(u8, 3), d.len);
|
||||
try std.testing.expect(d.flags & F_MODRM != 0);
|
||||
try std.testing.expect(d.flags & F_DISP8 != 0);
|
||||
}
|
||||
|
||||
test "lea eax, [ecx+edx*4+8]" {
|
||||
// 8D 44 91 08
|
||||
const d = decode(&[_]u8{ 0x8D, 0x44, 0x91, 0x08 });
|
||||
try std.testing.expectEqual(@as(u8, 4), d.len);
|
||||
try std.testing.expect(d.flags & F_MODRM != 0);
|
||||
try std.testing.expect(d.flags & F_SIB != 0);
|
||||
try std.testing.expect(d.flags & F_DISP8 != 0);
|
||||
}
|
||||
|
||||
test "mov [disp32], eax" {
|
||||
// A3 xx xx xx xx
|
||||
const d = decode(&[_]u8{ 0xA3, 0x00, 0x10, 0x40, 0x00 });
|
||||
try std.testing.expectEqual(@as(u8, 5), d.len);
|
||||
}
|
||||
|
||||
test "sub esp, imm32" {
|
||||
// 81 EC 00 01 00 00
|
||||
const d = decode(&[_]u8{ 0x81, 0xEC, 0x00, 0x01, 0x00, 0x00 });
|
||||
try std.testing.expectEqual(@as(u8, 6), d.len);
|
||||
try std.testing.expect(d.flags & F_MODRM != 0);
|
||||
}
|
||||
|
||||
test "test eax, imm32 (F7 C0)" {
|
||||
// F7 C0 FF 00 00 00 = test eax, 0xFF
|
||||
const d = decode(&[_]u8{ 0xF7, 0xC0, 0xFF, 0x00, 0x00, 0x00 });
|
||||
try std.testing.expectEqual(@as(u8, 6), d.len);
|
||||
}
|
||||
|
||||
test "ret imm16" {
|
||||
// C2 04 00
|
||||
const d = decode(&[_]u8{ 0xC2, 0x04, 0x00 });
|
||||
try std.testing.expectEqual(@as(u8, 3), d.len);
|
||||
}
|
||||
+13
-41
@@ -11,7 +11,7 @@
|
||||
// =============================================================================
|
||||
|
||||
const std = @import("std");
|
||||
const hook = @import("hook");
|
||||
const hook = @import("zhook");
|
||||
const con = @import("../console.zig");
|
||||
|
||||
// =============================================================================
|
||||
@@ -182,25 +182,18 @@ fn looseFilesLookup(game_path_ptr: u32) ?[*]const u8 {
|
||||
// Hook: CheckFileExistence (0x654DD0)
|
||||
// =============================================================================
|
||||
// __fastcall(ECX=filename, EDX=flags, stack=outputBuffer) → EAX
|
||||
// Prologue: 9 bytes (push ebp; mov ebp, esp; sub esp, 0x104) — no rel32 fixups
|
||||
|
||||
const CHECK_FILE_EXISTENCE: usize = 0x654DD0;
|
||||
const fc: std.builtin.CallingConvention = .{ .x86_fastcall = .{} };
|
||||
const CheckFileExistenceFn = fn (u32, u32, u32) callconv(fc) u32;
|
||||
|
||||
var cfe_hook = hook.Hook{};
|
||||
var cfe_hook: hook.Detour(CheckFileExistenceFn) = .{};
|
||||
|
||||
fn hookImpl(filename_ptr: u32, flags: u32, output_buffer_ptr: u32) callconv(.c) u32 {
|
||||
fn checkFileExistenceDetour(filename_ptr: u32, flags: u32, output_buffer_ptr: u32) callconv(fc) u32 {
|
||||
if (filename_ptr != 0) {
|
||||
if (looseFilesLookup(filename_ptr)) |disk_path| {
|
||||
const raw: [*]const u8 = @ptrFromInt(filename_ptr);
|
||||
con.fmt("[assetfix] loose hit: \"{s}\"\n", .{raw[0..cStrLen(raw)]});
|
||||
|
||||
// Write disk path (e.g. "Data\Character\...") to output buffer.
|
||||
// The caller (File_FindInArchive) uses this to open the file from disk.
|
||||
// Game paths contain '\' which makes CheckFileExistence's bit-0 handler
|
||||
// skip BuildFilePath and use the raw path as-is — failing because the
|
||||
// game-relative path has no "Data\" prefix. By writing the correct
|
||||
// disk-relative path to the output buffer ourselves, we bypass that
|
||||
// bug without transforming the filename argument (preserving chaining).
|
||||
if (output_buffer_ptr != 0) {
|
||||
const disk_len = cStrLen(disk_path);
|
||||
const out: [*]u8 = @ptrFromInt(output_buffer_ptr);
|
||||
@@ -212,31 +205,11 @@ fn hookImpl(filename_ptr: u32, flags: u32, output_buffer_ptr: u32) callconv(.c)
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return callOriginal(filename_ptr, flags, output_buffer_ptr);
|
||||
}
|
||||
|
||||
fn callOriginal(filename: u32, flags: u32, output_buffer: u32) u32 {
|
||||
// __fastcall: ECX=filename, EDX=flags, push outputBuffer, callee cleans 4
|
||||
return asm volatile (
|
||||
\\push %[output]
|
||||
\\call *%[func]
|
||||
: [ret] "={eax}" (-> u32),
|
||||
: [_] "{ecx}" (filename),
|
||||
[_] "{edx}" (flags),
|
||||
[output] "r" (output_buffer),
|
||||
[func] "r" (cfe_hook.trampoline),
|
||||
: .{ .memory = true, .cc = true });
|
||||
return cfe_hook.callOriginal(.{ filename_ptr, flags, output_buffer_ptr });
|
||||
}
|
||||
|
||||
fn installHook() bool {
|
||||
if (!cfe_hook.prepare(CHECK_FILE_EXISTENCE, 9, &.{})) return false;
|
||||
|
||||
// Build fastcall→cdecl thunk in the hook's alloc block (after trampoline)
|
||||
const thunk_buf = cfe_hook.mem.? + 32;
|
||||
_ = hook.buildFastcallToCdeclThunk(thunk_buf, @intFromPtr(&hookImpl), 1);
|
||||
|
||||
cfe_hook.activate(@intFromPtr(thunk_buf));
|
||||
return true;
|
||||
return cfe_hook.attach(0x654DD0, &checkFileExistenceDetour) == .ok;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -307,37 +280,36 @@ var installed: bool = false;
|
||||
var g_mutex: ?*anyopaque = null;
|
||||
var g_is_hook_owner: bool = false;
|
||||
|
||||
pub fn installHooks() bool {
|
||||
pub fn installHooks() void {
|
||||
con.print("[assetfix] Module loaded\n");
|
||||
|
||||
// Multi-DLL safety: only one instance per process should hook
|
||||
var mutex_name_buf: [64]u8 = undefined;
|
||||
const mutex_name = std.fmt.bufPrint(&mutex_name_buf, "Local\\AssetfixHook_{d}", .{GetCurrentProcessId()}) catch return false;
|
||||
const mutex_name = std.fmt.bufPrint(&mutex_name_buf, "Local\\AssetfixHook_{d}", .{GetCurrentProcessId()}) catch return;
|
||||
mutex_name_buf[mutex_name.len] = 0;
|
||||
|
||||
g_mutex = CreateMutexA(null, 1, @ptrCast(mutex_name_buf[0..mutex_name.len :0]));
|
||||
if (g_mutex == null) return false;
|
||||
if (g_mutex == null) return;
|
||||
|
||||
if (GetLastError() == ERROR_ALREADY_EXISTS) {
|
||||
_ = CloseHandle(g_mutex.?);
|
||||
g_mutex = null;
|
||||
g_is_hook_owner = false;
|
||||
con.print("[assetfix] Another DLL owns hooks (mutex taken), skipping\n");
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
g_is_hook_owner = true;
|
||||
|
||||
applyGlobPatch();
|
||||
applyLooseFilePatches();
|
||||
looseFilesInit();
|
||||
if (!installHook()) return false;
|
||||
if (!installHook()) return;
|
||||
installed = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn removeHooks() void {
|
||||
if (g_is_hook_owner and installed) {
|
||||
cfe_hook.remove();
|
||||
cfe_hook.detach();
|
||||
revertLooseFilePatches();
|
||||
revertGlobPatch();
|
||||
looseFilesCleanup();
|
||||
|
||||
@@ -573,3 +573,266 @@ The existing GetRelativeTo vtable hook can be kept as defense-in-depth.
|
||||
0x76772b: E8 F0 16 00 00 CALL SetAnimationOrigin ; 5 bytes
|
||||
```
|
||||
First 5 bytes (53 56 8B F1 57) can be replaced with JMP rel32 for a detour.
|
||||
|
||||
---
|
||||
|
||||
## Stale UIParent Pointer — The Persistent Unknown Destruction Path
|
||||
|
||||
### Discovery
|
||||
|
||||
One persistent stale pointer escapes ALL hooked destruction paths. Pattern:
|
||||
- Address always ends in `X008` (e.g., `0x17f20008`, `0x17fb4008`, `0x03bc0008`)
|
||||
- CFrame base = addr - 0x24 = `XXXXffe4` — crosses page boundary
|
||||
- First page (containing CFrame base, vtable) is DECOMMITTED
|
||||
- Second page (containing CLayoutFrame inner at +0x24) survives
|
||||
- Frame name at CFrame+0x98 reads garbage (`"t%Ç"`) from residual second-page data
|
||||
- NOT in destruction history ring buffer — never went through any hooked detour
|
||||
|
||||
### Diagnostic Hooks Added
|
||||
|
||||
**PauseAnimationGroup (0x767ee0)** — dependency registration tracker:
|
||||
- `__thiscall(ECX=relativeTo_frame, owner_frame, bitmask)`, RET 0x8
|
||||
- Prologue: `55 8B EC 53 8B D9` (6 bytes)
|
||||
- Silently records every registration to a 2048-entry ring buffer
|
||||
- Queried by vtable hooks when stale pointer detected
|
||||
|
||||
**SetAnimationOrder (0x767c70)** — anchor creation validator:
|
||||
- `__thiscall(ECX=frame, point_enum, relativeTo, relPoint, xOfs, yOfs, param_6)`, RET 0x18
|
||||
- Prologue: `55 8B EC 8B 45 0C` (6 bytes)
|
||||
- Validates relativeTo AND CFrame base (relativeTo - 0x24) with IsBadReadPtr
|
||||
- Catches race condition: relativeTo already dead when anchor created
|
||||
|
||||
### Key Finding: The Stale Frame is UIParent
|
||||
|
||||
**Confirmed by SetAnimationOrder RACE detection.** The following frames all call
|
||||
SetAnimOrder with the dead relativeTo address:
|
||||
|
||||
| Owner Frame | Point | Context |
|
||||
|------------|-------|---------|
|
||||
| `ScriptErrors` | 4 | Blizzard UI |
|
||||
| `GroupLootDropDown` | 0 | Blizzard UI |
|
||||
| `GroupLootFrame1` | 7 | Blizzard UI |
|
||||
| `PlayerFrame` | 0 | Blizzard UI |
|
||||
| `TargetFrame` | 0 | Blizzard UI |
|
||||
| `WorldMapFrame` | 4 | Blizzard UI |
|
||||
| `GuildBankFrame` | 0 | Blizzard UI |
|
||||
| `TransmogFrame` | 0 | Addon UI |
|
||||
| `NewTransmogAlertFrame` | 0 | Addon UI |
|
||||
| `TWTMain` | 0 | Addon UI |
|
||||
| `TWTMainSettings` | 0 | Addon UI |
|
||||
| `TWTMainTankModeWindow` | 0 | Addon UI |
|
||||
| `TWTWithAddonList` | 0 | Addon UI |
|
||||
|
||||
**Every top-level frame** anchors to this address → it's `UIParent`.
|
||||
|
||||
### Race Condition Confirmed
|
||||
|
||||
- `DEP REGISTERED` — PauseAnimationGroup WAS called for the address (dependency existed)
|
||||
- But name at registration time was `"t%Ç"` (garbage) — the frame was ALREADY DEAD
|
||||
when PauseAnimationGroup ran
|
||||
- `SetAnimOrder` RACE check: `IsBadReadPtr(relativeTo - 0x24)` FAILS (first page
|
||||
decommitted), but `IsBadReadPtr(relativeTo)` passes (second page survives)
|
||||
- The original game code doesn't validate relativeTo at all — just stores the raw pointer
|
||||
|
||||
### Symptom: Black Screen
|
||||
|
||||
When vtable hooks NULL all stale relativeTo pointers, every top-level frame loses its
|
||||
anchor to UIParent → nothing can lay out → full black screen. The crash is prevented
|
||||
but the UI is broken.
|
||||
|
||||
### Theory
|
||||
|
||||
UIParent is destroyed through an unknown path during a UI reload/transition (character
|
||||
select → world, or loading screen). The destruction does NOT go through:
|
||||
- `cleanup_linked_list_structures` (0x767720) — hooked, not triggered
|
||||
- `destroyUIElement` (0x7645a0) — hooked, not triggered
|
||||
- `ProcessUIUpdateEvent` (0x772ec0) — hooked, not triggered
|
||||
|
||||
A new UIParent is created at a different address, but addon/Blizzard initialization
|
||||
code passes the OLD (now dead) address to SetPoint/SetAnimOrder.
|
||||
|
||||
### Next Steps
|
||||
|
||||
1. Find UIParent global pointer in WoW binary (Ghidra)
|
||||
2. Determine when/how UIParent is destroyed and recreated
|
||||
3. Consider: hook SetAnimOrder to substitute live UIParent address when dead one detected
|
||||
4. Alternative: find the destruction path that frees UIParent without our hooks firing
|
||||
|
||||
---
|
||||
|
||||
## Ghidra RE: UIParent Resolution Mechanism
|
||||
|
||||
### UIParent String
|
||||
|
||||
- Address: `0x00842f14` (DATA, type=string, value="UIParent")
|
||||
- **Only 1 xref**: from `InitializeGameInterface` at `0x00490065`
|
||||
|
||||
### GetFrameFromLua (0x76c760)
|
||||
|
||||
Resolves a named frame from the Lua global table at runtime. Called from
|
||||
`InitializeGameInterface` to populate `PTR_00b4b44c`.
|
||||
|
||||
**Calling convention**: `__fastcall(ECX=name_string, EDX=typeID)`, returns `CFrame*`.
|
||||
Bare `RET` (no stack cleanup -- 0 stack args).
|
||||
|
||||
**Prologue**: `53 56 57 8B DA 8B F9` (7 bytes)
|
||||
|
||||
**Internal call chain**:
|
||||
```
|
||||
0x76c767: CALL 0x7040d0 -- lua.getContext() -> ESI = L
|
||||
0x76c772: CALL 0x6f3890 -- lua_pushstring(L, name)
|
||||
0x76c77e: CALL 0x6f3a40 -- lua_gettable(L, LUA_GLOBALSINDEX)
|
||||
0x76c788: CALL 0x6f3400 -- lua_type(L, -1)
|
||||
CMP EAX, ... -- type check (userdata? table?)
|
||||
JZ ... -- branch on type
|
||||
0x76c794: MOV EDX, ... -- extract frame pointer from Lua value
|
||||
```
|
||||
|
||||
Epilogue: two RET paths at `0x76c7a3` and `0x76c7db` (both bare `RET`).
|
||||
|
||||
**Usage in InitializeGameInterface (0x48fbf0)**:
|
||||
```c
|
||||
g_ParentFrameTypeID = g_NextTypeID + 1; // if not already set
|
||||
PTR_00b4b44c = GetFrameFromLua("UIParent", g_ParentFrameTypeID);
|
||||
PTR_00b4b3c4 = GetFrameFromLua("GameTooltip", another_type_id);
|
||||
```
|
||||
|
||||
**Key insight**: This function does a live Lua global lookup every time it's called.
|
||||
We can call it from our hooks to get the CURRENT UIParent, not a cached stale pointer.
|
||||
Just need `g_ParentFrameTypeID` from `0x00cf0c10` (runtime .bss value).
|
||||
|
||||
### g_ParentFrameTypeID (0xcf0c10)
|
||||
|
||||
Runtime type ID for the parent frame type. Set once during initialization, stable
|
||||
for the lifetime of the process. Read from `.bss` at runtime.
|
||||
|
||||
### Other Relevant Lookup Functions (found but not yet decompiled)
|
||||
|
||||
| Address | Name | Notes |
|
||||
|---------|------|-------|
|
||||
| 0x4b3250 | `FindUIElementByName` | Alternative name-based lookup |
|
||||
| 0x4c3c50 | `FrameScript_GetListOffset` | Frame list traversal helper |
|
||||
| 0x4c3c80 | `FrameScript_GetListNodeAt` | Frame list node access |
|
||||
|
||||
### Lua API Functions (confirmed addresses)
|
||||
|
||||
| Address | Function | Ghidra Name | Notes |
|
||||
|---------|----------|-------------|-------|
|
||||
| 0x6f36e0 | `lua_tolstring` | lua_tolstring | Confirmed |
|
||||
| 0x6f3740 | `lua_touserdata` | lua_objlen (WRONG) | See below |
|
||||
| 0x6f3770 | `lua_objlen` | lua_get_userdata_size | Actual objlen |
|
||||
|
||||
### lua_touserdata (0x6f3740) -- CONFIRMED
|
||||
|
||||
Ghidra mislabels this as `lua_objlen`. Disassembly confirms it's `lua_touserdata`:
|
||||
```asm
|
||||
0x6f374a: MOV ECX, [EAX] ; type tag
|
||||
0x6f374c: SUB ECX, 0x2 ; LUA_TLIGHTUSERDATA = 2
|
||||
0x6f374f: JZ 0x6f3760 ; -> return value[2] directly
|
||||
0x6f3751: SUB ECX, 0x5 ; LUA_TUSERDATA = 7 (2+5)
|
||||
0x6f3754: JZ 0x6f3759 ; -> return value[2] + 0x10 (skip Udata header)
|
||||
0x6f3756: XOR EAX, EAX ; else return NULL
|
||||
0x6f3758: RET
|
||||
0x6f3759: MOV EAX, [EAX+8] ; full userdata data ptr
|
||||
0x6f375c: ADD EAX, 0x10 ; skip 16-byte Udata header
|
||||
0x6f375f: RET
|
||||
0x6f3760: MOV EAX, [EAX+8] ; lightuserdata ptr
|
||||
0x6f3763: RET
|
||||
```
|
||||
|
||||
### GetFrameFromLua Full Flow (CONFIRMED)
|
||||
|
||||
From decompilation and byte-level verification:
|
||||
|
||||
```c
|
||||
CFrame* __fastcall GetFrameFromLua(ECX=name, EDX=typeID) {
|
||||
L = getContext(); // 0x7040d0
|
||||
lua_pushstring(L, name); // push "UIParent"
|
||||
lua_gettable(L, LUA_GLOBALSINDEX); // EDX=0xffffd8ef (-10001)
|
||||
type = lua_type(L, -1); // 0x6f3400
|
||||
if (type != 5) { // 5 = LUA_TTABLE
|
||||
lua_settop(L, -2); // pop, not a table
|
||||
return NULL;
|
||||
}
|
||||
lua_rawgeti(L, -1, 0); // push table[0] (CFrame* as userdata)
|
||||
ptr = lua_touserdata(L, -1); // extract C pointer (0x6f3740)
|
||||
lua_settop(L, -3); // pop table + userdata
|
||||
if (ptr == NULL) return NULL;
|
||||
if (!ptr->vtable[4](typeID)) return NULL; // validate frame type
|
||||
return ptr; // CFrame base pointer
|
||||
}
|
||||
```
|
||||
|
||||
Frame Lua representation: named frame globals are Lua **tables** with the CFrame
|
||||
pointer stored as userdata at `table[0]`. `GetFrameFromLua` extracts this, validates
|
||||
the type via vtable dispatch, and returns the raw CFrame pointer.
|
||||
|
||||
### Heal Implementation -- STATUS: CRASHES
|
||||
|
||||
Replaced the stale C++ global approach (`0x00B4B44C`) with a live Lua lookup via
|
||||
`hook.fastcall(u32, 0x76c760, name_ptr, type_id)`. The heal log message ("HEALED")
|
||||
appears in the console, confirming `GetFrameFromLua` returns a valid pointer. But
|
||||
the game segfaults shortly after with NO WoW crash log (bypasses the exception handler).
|
||||
|
||||
**Current code** (in framecrash.zig):
|
||||
```zig
|
||||
fn getLiveUIParent() u32 {
|
||||
const type_id = readAligned(G_PARENT_FRAME_TYPE_ID); // 0xcf0c10
|
||||
if (type_id == 0) return 0;
|
||||
const cframe_base = hook.fastcall(u32, GET_FRAME_FROM_LUA,
|
||||
@intFromPtr(@as([*:0]const u8, "UIParent")), type_id);
|
||||
if (cframe_base == 0) return 0;
|
||||
// validate + name check, return CLayoutFrame inner (+ 0x24)
|
||||
}
|
||||
```
|
||||
|
||||
Called from:
|
||||
- `setAnimOrderDetour` -- substitutes dead relativeTo BEFORE anchor creation
|
||||
- `tryFixStaleRelativeTo` -- heals existing anchors in vtable hooks
|
||||
- Vtable hooks (GetWidth/GetHeight/GetRelativeTo) -- defense-in-depth
|
||||
|
||||
**Suspected crash causes** (not yet verified):
|
||||
|
||||
1. **`hook.fastcall` clobber list incomplete** -- the inline asm for fastcall does
|
||||
NOT declare ECX/EDX as clobbered after `call`. This could cause the Zig compiler
|
||||
to assume those registers are preserved, leading to register corruption in the
|
||||
calling detour function. Fix: rewrite getLiveUIParent to call Lua API functions
|
||||
directly via typed function pointers (same pattern as main.zig's lua struct),
|
||||
avoiding hook.fastcall entirely.
|
||||
|
||||
2. **Reentrancy** -- vtable hooks (GetWidth/GetHeight) fire during layout calculation,
|
||||
which can happen many times per frame. Each call to getLiveUIParent does a full
|
||||
Lua stack push/pop cycle. If layout triggers a metamethod or callback that
|
||||
reenters layout, the Lua stack could be corrupted.
|
||||
|
||||
3. **Dependency list inconsistency** -- vtable hook HEAL path writes the new UIParent
|
||||
address directly into `anchor+0x0C`, but the PauseAnimationGroup dependency was
|
||||
registered on the OLD (dead) address. The dependency list on the new UIParent
|
||||
doesn't know about these anchors. This could cause issues when the new UIParent
|
||||
is later destroyed.
|
||||
|
||||
### Next Steps for Heal Fix
|
||||
|
||||
1. **Rewrite getLiveUIParent with direct Lua calls** -- use typed function pointers
|
||||
for each Lua API function instead of hook.fastcall into GetFrameFromLua. This
|
||||
eliminates the clobber list risk and allows per-step error checking. Key addresses:
|
||||
- `getContext` (0x7040d0): `fn() callconv(fc) u32`
|
||||
- `lua_pushstring` (0x6f3890): `fn(u32, [*:0]const u8) callconv(fc) void`
|
||||
- `lua_gettable` (0x6f3a40): `fn(u32, i32) callconv(fc) void`
|
||||
- `lua_type` (0x6f3400): `fn(u32, i32) callconv(fc) i32`
|
||||
- `lua_settop` (0x6f3080): `fn(u32, i32) callconv(fc) void`
|
||||
- `lua_rawgeti` (0x6f3bc0): `fn(u32, i32, i32) callconv(fc) void`
|
||||
- `lua_touserdata` (0x6f3740): `fn(u32, i32) callconv(fc) u32`
|
||||
|
||||
2. **Add reentrancy guard** -- static bool to prevent recursive getLiveUIParent calls.
|
||||
|
||||
3. **Cache result** -- call GetFrameFromLua once per stale-pointer batch, reuse for
|
||||
all heals in the same layout pass. Invalidate on next frame/event.
|
||||
|
||||
4. **Fix dependency list** -- after healing an anchor's relativeTo, call
|
||||
PauseAnimationGroup on the new UIParent to register the dependency. Without
|
||||
this, the new UIParent's destruction won't clean up these anchors.
|
||||
|
||||
### Module currently DISABLED by default
|
||||
|
||||
Build flag changed to `orelse false` in build.zig. Enable with `-Dframecrash=true`.
|
||||
|
||||
+153
-203
@@ -20,11 +20,12 @@
|
||||
//! See RESEARCH.md for full reverse engineering notes.
|
||||
|
||||
const std = @import("std");
|
||||
const hook = @import("hook");
|
||||
const hook = @import("zhook");
|
||||
const con = @import("../console.zig");
|
||||
|
||||
const WINAPI = std.builtin.CallingConvention.winapi;
|
||||
const THISCALL = std.builtin.CallingConvention{ .x86_thiscall = .{} };
|
||||
const tc: std.builtin.CallingConvention = .{ .x86_thiscall = .{} };
|
||||
const fc: std.builtin.CallingConvention = .{ .x86_fastcall = .{} };
|
||||
|
||||
extern "kernel32" fn IsBadReadPtr(lp: ?*const anyopaque, ucb: usize) callconv(WINAPI) i32;
|
||||
extern "kernel32" fn CreateMutexA(lpMutexAttributes: ?*anyopaque, bInitialOwner: i32, lpName: [*:0]const u8) callconv(WINAPI) ?*anyopaque;
|
||||
@@ -56,6 +57,14 @@ var g_is_hook_owner: bool = false;
|
||||
const ANCHOR_VTABLE_ADDR: usize = 0x0081c44c;
|
||||
const GET_RELATIVE_TO_SLOT: usize = ANCHOR_VTABLE_ADDR + 0x0C; // vtable[3]
|
||||
|
||||
// GetFrameFromLua (0x76c760): __fastcall(ECX=name_ptr, EDX=typeID) -> CFrame* or NULL.
|
||||
// Does a live Lua global lookup: pushstring(name) -> gettable(GLOBALS) -> rawgeti(0)
|
||||
// -> touserdata -> validate type -> return. Stack-neutral (pops what it pushes).
|
||||
const GET_FRAME_FROM_LUA: usize = 0x76c760;
|
||||
|
||||
// g_ParentFrameTypeID: runtime type ID for parent frame type, set once during init.
|
||||
const G_PARENT_FRAME_TYPE_ID: usize = 0x00cf0c10;
|
||||
|
||||
// =============================================================================
|
||||
// Root cause fix: hook frame destruction to clean up reverse anchor references
|
||||
//
|
||||
@@ -76,9 +85,9 @@ const GET_RELATIVE_TO_SLOT: usize = ANCHOR_VTABLE_ADDR + 0x0C; // vtable[3]
|
||||
// =============================================================================
|
||||
|
||||
const CLEANUP_TARGET: usize = 0x767720;
|
||||
const CLEANUP_PROLOGUE_SIZE: usize = 5;
|
||||
|
||||
var cleanup_hook: hook.Hook = .{};
|
||||
const CleanupFn = fn (u32) callconv(tc) void;
|
||||
var cleanup_hook: hook.Detour(CleanupFn) = .{};
|
||||
|
||||
// =============================================================================
|
||||
// Second destruction path: destroyUIElement (0x7645a0)
|
||||
@@ -93,9 +102,9 @@ var cleanup_hook: hook.Hook = .{};
|
||||
// =============================================================================
|
||||
|
||||
const DESTROY_UI_TARGET: usize = 0x7645a0;
|
||||
const DESTROY_UI_PROLOGUE_SIZE: usize = 6;
|
||||
|
||||
var destroy_ui_hook: hook.Hook = .{};
|
||||
const DestroyUIFn = fn (u32, u32) callconv(tc) u32;
|
||||
var destroy_ui_hook: hook.Detour(DestroyUIFn) = .{};
|
||||
|
||||
// =============================================================================
|
||||
// Third destruction path: ProcessUIUpdateEvent (0x772ec0)
|
||||
@@ -107,9 +116,9 @@ var destroy_ui_hook: hook.Hook = .{};
|
||||
// =============================================================================
|
||||
|
||||
const PROCESS_UI_TARGET: usize = 0x772ec0;
|
||||
const PROCESS_UI_PROLOGUE_SIZE: usize = 6;
|
||||
|
||||
var process_ui_hook: hook.Hook = .{};
|
||||
const ProcessUIFn = fn (u32, u32) callconv(tc) u32;
|
||||
var process_ui_hook: hook.Detour(ProcessUIFn) = .{};
|
||||
|
||||
// =============================================================================
|
||||
// Priority 1: Hook PauseAnimationGroup (0x767ee0) — dependency registration
|
||||
@@ -125,9 +134,9 @@ var process_ui_hook: hook.Hook = .{};
|
||||
// =============================================================================
|
||||
|
||||
const PAUSE_ANIM_TARGET: usize = 0x767ee0;
|
||||
const PAUSE_ANIM_PROLOGUE_SIZE: usize = 6;
|
||||
|
||||
var pause_anim_hook: hook.Hook = .{};
|
||||
const PauseAnimFn = fn (u32, u32, u32) callconv(tc) void;
|
||||
var pause_anim_hook: hook.Detour(PauseAnimFn) = .{};
|
||||
|
||||
// =============================================================================
|
||||
// Priority 2: Hook SetAnimationOrder (0x767c70) — anchor creation validation
|
||||
@@ -144,9 +153,9 @@ var pause_anim_hook: hook.Hook = .{};
|
||||
// =============================================================================
|
||||
|
||||
const SET_ANIM_TARGET: usize = 0x767c70;
|
||||
const SET_ANIM_PROLOGUE_SIZE: usize = 6;
|
||||
|
||||
var set_anim_hook: hook.Hook = .{};
|
||||
const SetAnimFn = fn (u32, u32, u32, u32, u32, u32, u32) callconv(tc) void;
|
||||
var set_anim_hook: hook.Detour(SetAnimFn) = .{};
|
||||
|
||||
// =============================================================================
|
||||
// Dependency registration ring buffer — track PauseAnimationGroup calls
|
||||
@@ -162,26 +171,32 @@ const DepRegistration = struct {
|
||||
owner: u32 = 0, // the frame that owns the anchor
|
||||
bitmask: u32 = 0, // which anchor slots (OR of 1<<point_enum)
|
||||
seq: u32 = 0, // monotonic sequence number for ordering
|
||||
name: [31:0]u8 = @splat(0), // name of relativeTo frame at registration time
|
||||
};
|
||||
|
||||
var reg_history: [REG_HISTORY_SIZE]DepRegistration = @splat(.{});
|
||||
var reg_idx: u32 = 0;
|
||||
|
||||
fn recordRegistration(relativeTo: u32, owner: u32, bitmask: u32) void {
|
||||
const seq = reg_idx;
|
||||
reg_history[reg_idx % REG_HISTORY_SIZE] = .{
|
||||
var entry = DepRegistration{
|
||||
.relativeTo = relativeTo,
|
||||
.owner = owner,
|
||||
.bitmask = bitmask,
|
||||
.seq = seq,
|
||||
.seq = reg_idx,
|
||||
};
|
||||
// Capture the frame name while it's still alive
|
||||
if (getFrameName(relativeTo)) |fname| {
|
||||
const span = std.mem.span(fname);
|
||||
const len = @min(span.len, 31);
|
||||
@memcpy(entry.name[0..len], span[0..len]);
|
||||
}
|
||||
reg_history[reg_idx % REG_HISTORY_SIZE] = entry;
|
||||
reg_idx +%= 1;
|
||||
}
|
||||
|
||||
/// Look up whether PauseAnimationGroup was ever called for a given relativeTo address.
|
||||
/// Returns the most recent registration entry if found, null otherwise.
|
||||
fn lookupRegistration(relativeTo: u32) ?DepRegistration {
|
||||
// Search backwards from most recent for best chance of finding it
|
||||
var best: ?DepRegistration = null;
|
||||
for (®_history) |*entry| {
|
||||
if (entry.relativeTo == relativeTo) {
|
||||
@@ -202,6 +217,15 @@ fn countRegistrations(relativeTo: u32) u32 {
|
||||
return count;
|
||||
}
|
||||
|
||||
/// Get the name stored at registration time for a relativeTo address.
|
||||
fn getRegisteredName(relativeTo: u32) []const u8 {
|
||||
if (lookupRegistration(relativeTo)) |reg| {
|
||||
const span = std.mem.sliceTo(®.name, 0);
|
||||
if (span.len > 0) return span;
|
||||
}
|
||||
return "(unknown)";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Destruction history ring buffer — correlate stale pointers with frame names
|
||||
// =============================================================================
|
||||
@@ -249,169 +273,85 @@ fn fmtStaleInfo(relativeTo: u32) struct { name: []const u8, saw_destroy: bool }
|
||||
return .{ .name = fmtFrameName(relativeTo), .saw_destroy = false };
|
||||
}
|
||||
|
||||
/// Log registration status for a stale relativeTo address.
|
||||
fn logRegistrationStatus(relativeTo: u32) void {
|
||||
const reg_count = countRegistrations(relativeTo);
|
||||
if (lookupRegistration(relativeTo)) |reg| {
|
||||
con.fmt("[framecrash] DEP REGISTERED: PauseAnimGroup was called {d}x for 0x{x:0>8}, last owner=0x{x:0>8} mask=0x{x}\n", .{
|
||||
reg_count, relativeTo, reg.owner, reg.bitmask,
|
||||
});
|
||||
} else {
|
||||
con.fmt("[framecrash] DEP NEVER REGISTERED: PauseAnimGroup was NEVER called for 0x{x:0>8} (in {d}-entry buffer)\n", .{
|
||||
relativeTo, REG_HISTORY_SIZE,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Dump diagnostic info for a stale relativeTo pointer not seen in our detour.
|
||||
fn dumpStaleContext(relativeTo: u32, anchor: u32) void {
|
||||
// Anchor relPoint enum at +0x10
|
||||
if (IsBadReadPtr(@ptrFromInt(anchor + 0x10), 4) != 0) return;
|
||||
const rel_point = readAligned(anchor + 0x10);
|
||||
|
||||
// Derive owner frame: anchor lives at owner_frame + relPoint*4 + 4
|
||||
const owner_layout = anchor -% (rel_point * 4 + 4);
|
||||
const owner_name = fmtFrameName(owner_layout);
|
||||
con.fmt("[framecrash] owner=\"{s}\" (0x{x:0>8}), relPoint={d}, stale=0x{x:0>8}\n", .{
|
||||
owner_name, owner_layout, rel_point, relativeTo,
|
||||
});
|
||||
}
|
||||
// logRegistrationStatus and dumpStaleContext removed — verbose diagnostic logging
|
||||
// superseded by HEAL/FIX/RACE messages. Ring buffers still used by tryFixStaleRelativeTo.
|
||||
|
||||
/// Detour for cleanup_linked_list_structures. Runs before the original to
|
||||
/// walk the dying frame's dependency list and destroy referencing anchors.
|
||||
fn cleanupDetour(frame: u32) callconv(THISCALL) void {
|
||||
// Record this frame in the destruction history before anything changes
|
||||
fn cleanupDetour(frame: u32) callconv(tc) void {
|
||||
recordDestruction(frame);
|
||||
|
||||
// Count reverse dependencies for logging
|
||||
const dep_count = countReverseDependencies(frame);
|
||||
if (dep_count > 0) {
|
||||
con.fmt("[framecrash] Destroying frame \"{s}\" (0x{x:0>8}), {d} reverse dependencies\n", .{
|
||||
fmtFrameName(frame),
|
||||
frame,
|
||||
dep_count,
|
||||
});
|
||||
}
|
||||
|
||||
cleanupReverseDependencies(frame);
|
||||
|
||||
// Call original cleanup_linked_list_structures via trampoline
|
||||
const orig = cleanup_hook.getTrampoline(*const fn (u32) callconv(THISCALL) void);
|
||||
orig(frame);
|
||||
cleanup_hook.callOriginal(.{frame});
|
||||
}
|
||||
|
||||
/// Detour for destroyUIElement. This is the second frame destruction path,
|
||||
/// called from cleanupGraphicsResources during UI teardown/reload. The original
|
||||
/// frees frames without walking the dependency list, leaving stale anchors.
|
||||
/// Signature: void* __thiscall destroyUIElement(void* this, byte free_flag)
|
||||
fn destroyUIDetour(frame: u32, free_flag: u32) callconv(THISCALL) u32 {
|
||||
// Record both possible interpretations: frame as CLayoutFrame inner,
|
||||
// and frame+0x24 in case frame is actually a CFrame base.
|
||||
// Anchors store CLayoutFrame inner ptrs as relativeTo.
|
||||
fn destroyUIDetour(frame: u32, free_flag: u32) callconv(tc) u32 {
|
||||
recordDestruction(frame);
|
||||
if (IsBadReadPtr(@ptrFromInt(frame + 0x24), 4) == 0) {
|
||||
recordDestruction(frame + 0x24);
|
||||
}
|
||||
|
||||
// Try cleaning deps at both offsets. cleanupReverseDependencies is
|
||||
// guarded by IsBadReadPtr so the wrong offset safely no-ops.
|
||||
const dep_count_a = countReverseDependencies(frame);
|
||||
const dep_count_b = countReverseDependencies(frame + 0x24);
|
||||
|
||||
if (dep_count_a > 0) {
|
||||
con.fmt("[framecrash] destroyUIElement frame \"{s}\" (0x{x:0>8}), {d} reverse deps (layout)\n", .{
|
||||
fmtFrameName(frame), frame, dep_count_a,
|
||||
});
|
||||
cleanupReverseDependencies(frame);
|
||||
}
|
||||
if (dep_count_b > 0) {
|
||||
con.fmt("[framecrash] destroyUIElement frame \"{s}\" (0x{x:0>8}), {d} reverse deps (inner+0x24)\n", .{
|
||||
fmtFrameName(frame + 0x24), frame + 0x24, dep_count_b,
|
||||
});
|
||||
cleanupReverseDependencies(frame + 0x24);
|
||||
}
|
||||
|
||||
// Call original destroyUIElement via trampoline
|
||||
const orig: *const fn (u32, u32) callconv(THISCALL) u32 = @ptrFromInt(destroy_ui_hook.trampoline);
|
||||
return orig(frame, free_flag);
|
||||
cleanupReverseDependencies(frame);
|
||||
cleanupReverseDependencies(frame + 0x24);
|
||||
return destroy_ui_hook.callOriginal(.{ frame, free_flag });
|
||||
}
|
||||
|
||||
/// Detour for ProcessUIUpdateEvent — third destruction path, called via vtable.
|
||||
fn processUIDetour(frame: u32, free_flag: u32) callconv(THISCALL) u32 {
|
||||
fn processUIDetour(frame: u32, free_flag: u32) callconv(tc) u32 {
|
||||
recordDestruction(frame);
|
||||
if (IsBadReadPtr(@ptrFromInt(frame + 0x24), 4) == 0) {
|
||||
recordDestruction(frame + 0x24);
|
||||
}
|
||||
|
||||
const dep_count_a = countReverseDependencies(frame);
|
||||
const dep_count_b = countReverseDependencies(frame + 0x24);
|
||||
|
||||
if (dep_count_a > 0) {
|
||||
con.fmt("[framecrash] processUI frame \"{s}\" (0x{x:0>8}), {d} reverse deps (layout)\n", .{
|
||||
fmtFrameName(frame), frame, dep_count_a,
|
||||
});
|
||||
cleanupReverseDependencies(frame);
|
||||
}
|
||||
if (dep_count_b > 0) {
|
||||
con.fmt("[framecrash] processUI frame \"{s}\" (0x{x:0>8}), {d} reverse deps (inner+0x24)\n", .{
|
||||
fmtFrameName(frame + 0x24), frame + 0x24, dep_count_b,
|
||||
});
|
||||
cleanupReverseDependencies(frame + 0x24);
|
||||
}
|
||||
|
||||
const orig: *const fn (u32, u32) callconv(THISCALL) u32 = @ptrFromInt(process_ui_hook.trampoline);
|
||||
return orig(frame, free_flag);
|
||||
cleanupReverseDependencies(frame);
|
||||
cleanupReverseDependencies(frame + 0x24);
|
||||
return process_ui_hook.callOriginal(.{ frame, free_flag });
|
||||
}
|
||||
|
||||
/// Detour for PauseAnimationGroup — records every dependency registration.
|
||||
/// This tells us whether a stale relativeTo was ever registered through the
|
||||
/// normal dependency tracking system.
|
||||
/// Signature: void __thiscall PauseAnimationGroup(ECX=relativeTo_frame, owner_frame, bitmask)
|
||||
fn pauseAnimDetour(relativeTo_frame: u32, owner_frame: u32, bitmask: u32) callconv(THISCALL) void {
|
||||
// Record this registration
|
||||
fn pauseAnimDetour(relativeTo_frame: u32, owner_frame: u32, bitmask: u32) callconv(tc) void {
|
||||
// Silently record — queried later by vtable hooks via logRegistrationStatus()
|
||||
recordRegistration(relativeTo_frame, owner_frame, bitmask);
|
||||
|
||||
con.fmt("[framecrash] PauseAnimGroup: relativeTo=0x{x:0>8} \"{s}\", owner=0x{x:0>8} \"{s}\", mask=0x{x}\n", .{
|
||||
relativeTo_frame,
|
||||
fmtFrameName(relativeTo_frame),
|
||||
owner_frame,
|
||||
fmtFrameName(owner_frame),
|
||||
bitmask,
|
||||
});
|
||||
|
||||
// Call original
|
||||
const orig = pause_anim_hook.getTrampoline(*const fn (u32, u32, u32) callconv(THISCALL) void);
|
||||
orig(relativeTo_frame, owner_frame, bitmask);
|
||||
pause_anim_hook.callOriginal(.{ relativeTo_frame, owner_frame, bitmask });
|
||||
}
|
||||
|
||||
/// Detour for SetAnimationOrder — validates relativeTo param before anchor creation.
|
||||
/// Uses cdecl thunk bridge because the function has float params.
|
||||
/// cdecl args: (ecx=frame, edx=unused, point_enum, relativeTo, relPoint, xOfs_bits, yOfs_bits, param_6)
|
||||
fn setAnimOrderDetour(frame: u32, _edx: u32, point_enum: u32, relativeTo: u32, rel_point: u32, x_ofs: u32, y_ofs: u32, param_6: u32) callconv(.c) void {
|
||||
_ = _edx;
|
||||
/// Float params (xOfs, yOfs) are passed as raw u32 bit patterns on the stack.
|
||||
fn setAnimOrderDetour(frame: u32, point_enum: u32, relativeTo: u32, rel_point: u32, x_ofs: u32, y_ofs: u32, param_6: u32) callconv(tc) void {
|
||||
var fixed_relativeTo = relativeTo;
|
||||
|
||||
// Validate relativeTo BEFORE the original creates the anchor
|
||||
if (relativeTo != 0) {
|
||||
if (IsBadReadPtr(@ptrFromInt(relativeTo), 0x10) != 0) {
|
||||
con.fmt("[framecrash] RACE: SetAnimOrder creating anchor with INVALID relativeTo=0x{x:0>8}! frame=0x{x:0>8} \"{s}\", point={d}\n", .{
|
||||
relativeTo,
|
||||
frame,
|
||||
fmtFrameName(frame),
|
||||
point_enum,
|
||||
});
|
||||
} else if (relativeTo == frame) {
|
||||
// Self-reference — the original function rejects this, but log it
|
||||
con.fmt("[framecrash] SetAnimOrder: self-reference rejected, frame=0x{x:0>8}\n", .{frame});
|
||||
// Validate relativeTo BEFORE the original creates the anchor.
|
||||
// Check BOTH the CLayoutFrame inner (relativeTo) AND the CFrame base (relativeTo - 0x24).
|
||||
// The stale pointer pattern: CFrame base crosses a page boundary, first page is
|
||||
// decommitted but second page (containing the CLayoutFrame inner) survives.
|
||||
if (relativeTo != 0 and relativeTo != frame) {
|
||||
const frame_base = relativeTo -% 0x24;
|
||||
const inner_bad = IsBadReadPtr(@ptrFromInt(relativeTo), 0x10) != 0;
|
||||
const base_bad = relativeTo >= 0x24 and IsBadReadPtr(@ptrFromInt(frame_base), 0x10) != 0;
|
||||
|
||||
if (inner_bad or base_bad) {
|
||||
// Dead relativeTo detected. Do a live Lua lookup for UIParent.
|
||||
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", .{
|
||||
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", .{
|
||||
relativeTo, fmtFrameName(frame), point_enum,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Call original trampoline as __thiscall(ECX=frame, 6 stack args).
|
||||
// All args are u32 — float params (xOfs, yOfs) are passed as raw bit patterns
|
||||
// which the original function reads from the stack as floats. The bit layout
|
||||
// is identical because __thiscall pushes all non-this args onto the stack.
|
||||
const orig: *const fn (u32, u32, u32, u32, u32, u32, u32) callconv(THISCALL) void =
|
||||
@ptrFromInt(set_anim_hook.trampoline);
|
||||
orig(frame, point_enum, relativeTo, rel_point, x_ofs, y_ofs, param_6);
|
||||
set_anim_hook.callOriginal(.{ frame, point_enum, fixed_relativeTo, rel_point, x_ofs, y_ofs, param_6 });
|
||||
}
|
||||
|
||||
/// Count how many nodes are in the PauseAnimationGroup dependency list.
|
||||
@@ -525,6 +465,45 @@ fn fmtFrameName(layout_frame: u32) []const u8 {
|
||||
return "(unnamed)";
|
||||
}
|
||||
|
||||
/// Get the live UIParent CLayoutFrame inner pointer via Lua global lookup.
|
||||
/// Calls GetFrameFromLua("UIParent", g_ParentFrameTypeID) which does a live
|
||||
/// Lua table lookup, bypassing the stale C++ global at 0x00B4B44C.
|
||||
/// Returns CLayoutFrame inner (CFrame base + 0x24), or 0 if unavailable.
|
||||
fn getLiveUIParent() u32 {
|
||||
const type_id = readAligned(G_PARENT_FRAME_TYPE_ID);
|
||||
if (type_id == 0) return 0;
|
||||
|
||||
const cframe_base = hook.fastcall(u32, GET_FRAME_FROM_LUA, @intFromPtr(@as([*:0]const u8, "UIParent")), type_id);
|
||||
if (cframe_base == 0) return 0;
|
||||
|
||||
// Validate the returned pointer
|
||||
if (IsBadReadPtr(@ptrFromInt(cframe_base), 0xA0) != 0) return 0;
|
||||
const inner = cframe_base + 0x24;
|
||||
if (IsBadReadPtr(@ptrFromInt(inner), 0x40) != 0) return 0;
|
||||
|
||||
// Verify the name is actually "UIParent" (paranoia check)
|
||||
if (getFrameName(inner)) |name| {
|
||||
if (!std.mem.eql(u8, std.mem.span(name), "UIParent")) return 0;
|
||||
} else return 0;
|
||||
|
||||
return inner;
|
||||
}
|
||||
|
||||
/// Attempt to fix a stale relativeTo in an anchor by substituting live UIParent.
|
||||
/// Uses GetFrameFromLua for a live Lua lookup, not the stale C++ global.
|
||||
/// Returns true if the fix was applied, false if no valid substitute found.
|
||||
fn tryFixStaleRelativeTo(anchor: u32, stale: u32) bool {
|
||||
const live = getLiveUIParent();
|
||||
if (live == 0 or live == stale) return false;
|
||||
|
||||
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", .{
|
||||
anchor, stale, live,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Defense-in-depth: anchor vtable hooks
|
||||
//
|
||||
@@ -562,25 +541,19 @@ fn isRelativeToValid(relativeTo: u32) bool {
|
||||
|
||||
/// Hook for vtable[3] GetRelativeTo. Validates the stored pointer.
|
||||
/// If stale, NULLs anchor+0x0C and returns 0 (safe "no relativeTo" path).
|
||||
fn getRelativeToHook(this: u32) callconv(THISCALL) u32 {
|
||||
const orig: *const fn (u32) callconv(THISCALL) u32 = @ptrFromInt(orig_get_relative_to);
|
||||
fn getRelativeToHook(this: u32) callconv(tc) u32 {
|
||||
const orig: *const fn (u32) callconv(tc) u32 = @ptrFromInt(orig_get_relative_to);
|
||||
const result = orig(this);
|
||||
|
||||
if (result == 0) return 0;
|
||||
|
||||
if (!isRelativeToValid(result)) {
|
||||
const info = fmtStaleInfo(result);
|
||||
if (info.saw_destroy) {
|
||||
con.fmt("[framecrash] STALE: frame \"{s}\" (0x{x:0>8}) went through detour but dep list missed anchor 0x{x:0>8}, detected in GetRelativeTo\n", .{
|
||||
info.name, result, this,
|
||||
});
|
||||
} else {
|
||||
con.fmt("[framecrash] STALE: frame 0x{x:0>8} NOT seen in detour, anchor 0x{x:0>8}, detected in GetRelativeTo\n", .{
|
||||
result, this,
|
||||
});
|
||||
dumpStaleContext(result, this);
|
||||
// Try to substitute live UIParent before NULLing
|
||||
if (tryFixStaleRelativeTo(this, result)) {
|
||||
// Re-call original — it now reads the fixed pointer
|
||||
return orig(this);
|
||||
}
|
||||
logRegistrationStatus(result);
|
||||
// No substitute available — NULL it out
|
||||
const field: *align(1) u32 = @ptrFromInt(this + 0x0C);
|
||||
field.* = 0;
|
||||
return 0;
|
||||
@@ -592,58 +565,44 @@ fn getRelativeToHook(this: u32) callconv(THISCALL) u32 {
|
||||
/// Hook for vtable[1] GetWidth. Checks anchor+0x0C before calling original.
|
||||
/// Returns sentinel if relativeTo is NULL or dangling.
|
||||
/// Signature: f32 __thiscall GetWidth(this, u32 param) — callee cleans 1 stack arg.
|
||||
fn getWidthHook(this: u32, param: u32) callconv(THISCALL) f32 {
|
||||
fn getWidthHook(this: u32, param: u32) callconv(tc) f32 {
|
||||
const relativeTo: u32 = readAligned(this + 0x0C);
|
||||
if (!isRelativeToValid(relativeTo)) {
|
||||
// Self-heal if dangling (not just NULL)
|
||||
if (relativeTo != 0) {
|
||||
const info = fmtStaleInfo(relativeTo);
|
||||
if (info.saw_destroy) {
|
||||
con.fmt("[framecrash] STALE: frame \"{s}\" (0x{x:0>8}) went through detour but dep list missed anchor 0x{x:0>8}, detected in GetWidth\n", .{
|
||||
info.name, relativeTo, this,
|
||||
});
|
||||
} else {
|
||||
con.fmt("[framecrash] STALE: frame 0x{x:0>8} NOT seen in detour, anchor 0x{x:0>8}, detected in GetWidth\n", .{
|
||||
relativeTo, this,
|
||||
});
|
||||
dumpStaleContext(relativeTo, this);
|
||||
// Try to substitute live UIParent instead of NULLing
|
||||
if (tryFixStaleRelativeTo(this, relativeTo)) {
|
||||
// Fixed — call original with the healed pointer
|
||||
const orig: *const fn (u32, u32) callconv(tc) f32 = @ptrFromInt(orig_get_width);
|
||||
return orig(this, param);
|
||||
}
|
||||
logRegistrationStatus(relativeTo);
|
||||
const field: *align(1) u32 = @ptrFromInt(this + 0x0C);
|
||||
field.* = 0;
|
||||
}
|
||||
return @as(*align(1) const f32, @ptrFromInt(SENTINEL_ADDR)).*;
|
||||
}
|
||||
|
||||
const orig: *const fn (u32, u32) callconv(THISCALL) f32 = @ptrFromInt(orig_get_width);
|
||||
const orig: *const fn (u32, u32) callconv(tc) f32 = @ptrFromInt(orig_get_width);
|
||||
return orig(this, param);
|
||||
}
|
||||
|
||||
/// Hook for vtable[2] GetHeight. Same pattern as GetWidth.
|
||||
/// Signature: f32 __thiscall GetHeight(this, u32 param) — callee cleans 1 stack arg.
|
||||
fn getHeightHook(this: u32, param: u32) callconv(THISCALL) f32 {
|
||||
fn getHeightHook(this: u32, param: u32) callconv(tc) f32 {
|
||||
const relativeTo: u32 = readAligned(this + 0x0C);
|
||||
if (!isRelativeToValid(relativeTo)) {
|
||||
if (relativeTo != 0) {
|
||||
const info = fmtStaleInfo(relativeTo);
|
||||
if (info.saw_destroy) {
|
||||
con.fmt("[framecrash] STALE: frame \"{s}\" (0x{x:0>8}) went through detour but dep list missed anchor 0x{x:0>8}, detected in GetHeight\n", .{
|
||||
info.name, relativeTo, this,
|
||||
});
|
||||
} else {
|
||||
con.fmt("[framecrash] STALE: frame 0x{x:0>8} NOT seen in detour, anchor 0x{x:0>8}, detected in GetHeight\n", .{
|
||||
relativeTo, this,
|
||||
});
|
||||
dumpStaleContext(relativeTo, this);
|
||||
// Try to substitute live UIParent instead of NULLing
|
||||
if (tryFixStaleRelativeTo(this, relativeTo)) {
|
||||
const orig: *const fn (u32, u32) callconv(tc) f32 = @ptrFromInt(orig_get_height);
|
||||
return orig(this, param);
|
||||
}
|
||||
logRegistrationStatus(relativeTo);
|
||||
const field: *align(1) u32 = @ptrFromInt(this + 0x0C);
|
||||
field.* = 0;
|
||||
}
|
||||
return @as(*align(1) const f32, @ptrFromInt(SENTINEL_ADDR)).*;
|
||||
}
|
||||
|
||||
const orig: *const fn (u32, u32) callconv(THISCALL) f32 = @ptrFromInt(orig_get_height);
|
||||
const orig: *const fn (u32, u32) callconv(tc) f32 = @ptrFromInt(orig_get_height);
|
||||
return orig(this, param);
|
||||
}
|
||||
|
||||
@@ -685,8 +644,7 @@ pub fn installHooks() void {
|
||||
|
||||
// Root cause fix #1: detour cleanup_linked_list_structures to clean up
|
||||
// reverse anchor references before the frame is destroyed.
|
||||
// Prologue: 53 56 8B F1 57 (5 bytes, no rel32 fixups needed)
|
||||
if (!cleanup_hook.install(CLEANUP_TARGET, CLEANUP_PROLOGUE_SIZE, @intFromPtr(&cleanupDetour), &.{})) {
|
||||
if (cleanup_hook.attach(CLEANUP_TARGET, &cleanupDetour) != .ok) {
|
||||
con.print("[framecrash] ERROR: Failed to install frame cleanup detour\n");
|
||||
} else {
|
||||
con.print("[framecrash] Frame cleanup detour installed\n");
|
||||
@@ -695,8 +653,7 @@ pub fn installHooks() void {
|
||||
// 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.
|
||||
// Prologue: 55 8B EC 56 8B F1 (6 bytes, no rel32 fixups needed)
|
||||
if (!destroy_ui_hook.install(DESTROY_UI_TARGET, DESTROY_UI_PROLOGUE_SIZE, @intFromPtr(&destroyUIDetour), &.{})) {
|
||||
if (destroy_ui_hook.attach(DESTROY_UI_TARGET, &destroyUIDetour) != .ok) {
|
||||
con.print("[framecrash] ERROR: Failed to install destroyUIElement detour\n");
|
||||
} else {
|
||||
con.print("[framecrash] destroyUIElement detour installed\n");
|
||||
@@ -704,8 +661,7 @@ pub fn installHooks() void {
|
||||
|
||||
// Root cause fix #3: detour ProcessUIUpdateEvent — virtual function that
|
||||
// calls CleanupUIElement + FreeMemory without layout cleanup.
|
||||
// Prologue: 55 8B EC 56 8B F1 (6 bytes, no rel32 fixups needed)
|
||||
if (!process_ui_hook.install(PROCESS_UI_TARGET, PROCESS_UI_PROLOGUE_SIZE, @intFromPtr(&processUIDetour), &.{})) {
|
||||
if (process_ui_hook.attach(PROCESS_UI_TARGET, &processUIDetour) != .ok) {
|
||||
con.print("[framecrash] ERROR: Failed to install ProcessUIUpdateEvent detour\n");
|
||||
} else {
|
||||
con.print("[framecrash] ProcessUIUpdateEvent detour installed\n");
|
||||
@@ -720,8 +676,7 @@ pub fn installHooks() void {
|
||||
|
||||
// Diagnostic: hook PauseAnimationGroup to track dependency registrations.
|
||||
// Answers: "was a dependency ever registered for this stale address?"
|
||||
// Prologue: 55 8B EC 53 8B D9 (6 bytes, no rel32)
|
||||
if (!pause_anim_hook.install(PAUSE_ANIM_TARGET, PAUSE_ANIM_PROLOGUE_SIZE, @intFromPtr(&pauseAnimDetour), &.{})) {
|
||||
if (pause_anim_hook.attach(PAUSE_ANIM_TARGET, &pauseAnimDetour) != .ok) {
|
||||
con.print("[framecrash] ERROR: Failed to install PauseAnimationGroup detour\n");
|
||||
} else {
|
||||
con.print("[framecrash] PauseAnimationGroup detour installed\n");
|
||||
@@ -729,25 +684,20 @@ pub fn installHooks() void {
|
||||
|
||||
// Diagnostic: hook SetAnimationOrder to detect race conditions.
|
||||
// Validates relativeTo param BEFORE anchor creation.
|
||||
// Prologue: 55 8B EC 8B 45 0C (6 bytes, no rel32)
|
||||
// Uses fastcall-to-cdecl thunk because of float stack params.
|
||||
if (set_anim_hook.prepare(SET_ANIM_TARGET, SET_ANIM_PROLOGUE_SIZE, &.{})) {
|
||||
const thunk = set_anim_hook.mem.? + 32;
|
||||
_ = hook.buildFastcallToCdeclThunk(thunk, @intFromPtr(&setAnimOrderDetour), 6);
|
||||
set_anim_hook.activate(@intFromPtr(thunk));
|
||||
con.print("[framecrash] SetAnimationOrder detour installed\n");
|
||||
} else {
|
||||
if (set_anim_hook.attach(SET_ANIM_TARGET, &setAnimOrderDetour) != .ok) {
|
||||
con.print("[framecrash] ERROR: Failed to install SetAnimationOrder detour\n");
|
||||
} else {
|
||||
con.print("[framecrash] SetAnimationOrder detour installed\n");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn removeHooks() void {
|
||||
if (g_is_hook_owner) {
|
||||
// Remove diagnostic hooks first (reverse install order)
|
||||
set_anim_hook.remove();
|
||||
set_anim_hook.detach();
|
||||
con.print("[framecrash] SetAnimationOrder detour removed\n");
|
||||
|
||||
pause_anim_hook.remove();
|
||||
pause_anim_hook.detach();
|
||||
con.print("[framecrash] PauseAnimationGroup detour removed\n");
|
||||
|
||||
// Restore original vtable pointers (reverse order)
|
||||
@@ -756,13 +706,13 @@ pub fn removeHooks() void {
|
||||
restoreVtableSlot(GET_WIDTH_SLOT, &orig_get_width);
|
||||
con.print("[framecrash] Anchor vtable hooks removed\n");
|
||||
|
||||
process_ui_hook.remove();
|
||||
process_ui_hook.detach();
|
||||
con.print("[framecrash] ProcessUIUpdateEvent detour removed\n");
|
||||
|
||||
destroy_ui_hook.remove();
|
||||
destroy_ui_hook.detach();
|
||||
con.print("[framecrash] destroyUIElement detour removed\n");
|
||||
|
||||
cleanup_hook.remove();
|
||||
cleanup_hook.detach();
|
||||
con.print("[framecrash] Frame cleanup detour removed\n");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const std = @import("std");
|
||||
const hook = @import("hook");
|
||||
const hook = @import("zhook");
|
||||
const con = @import("../console.zig");
|
||||
|
||||
const WINAPI = std.builtin.CallingConvention.winapi;
|
||||
@@ -385,25 +385,16 @@ pub fn lootAllCorpses(_: *anyopaque) callconv(.c) u32 {
|
||||
// Per-frame hook for processing the loot queue.
|
||||
// =============================================================================
|
||||
|
||||
var scene_end_hook = hook.Hook{};
|
||||
|
||||
fn hookSceneEnd(device: u32, _edx: u32) callconv(.c) void {
|
||||
_ = _edx;
|
||||
const tc: std.builtin.CallingConvention = .{ .x86_thiscall = .{} };
|
||||
const SceneEndFn = fn (u32) callconv(tc) void;
|
||||
var scene_end_hook: hook.Detour(SceneEndFn) = .{};
|
||||
|
||||
fn hookSceneEnd(device: u32) callconv(tc) void {
|
||||
if (loot_active) {
|
||||
processLootQueue();
|
||||
}
|
||||
|
||||
callOriginalSceneEnd(device);
|
||||
}
|
||||
|
||||
fn callOriginalSceneEnd(device: u32) void {
|
||||
asm volatile (
|
||||
\\call *%[func]
|
||||
:
|
||||
: [_] "{ecx}" (device),
|
||||
[func] "r" (scene_end_hook.trampoline),
|
||||
: .{ .eax = true, .edx = true, .memory = true, .cc = true });
|
||||
scene_end_hook.callOriginal(.{device});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -431,17 +422,12 @@ pub fn installHooks() void {
|
||||
g_is_hook_owner = true;
|
||||
|
||||
// SceneEnd — per-frame loot queue processing
|
||||
// Uses thunk: __fastcall(ECX=device, EDX) → cdecl(device, edx)
|
||||
if (scene_end_hook.prepare(Offsets.ADDR_SceneEnd, 9, &.{})) {
|
||||
const thunk = scene_end_hook.mem.? + 32;
|
||||
_ = hook.buildFastcallToCdeclThunk(thunk, @intFromPtr(&hookSceneEnd), 0);
|
||||
scene_end_hook.activate(@intFromPtr(thunk));
|
||||
}
|
||||
_ = scene_end_hook.attach(Offsets.ADDR_SceneEnd, &hookSceneEnd);
|
||||
}
|
||||
|
||||
pub fn removeHooks() void {
|
||||
if (g_is_hook_owner) {
|
||||
scene_end_hook.remove();
|
||||
scene_end_hook.detach();
|
||||
}
|
||||
|
||||
if (g_is_hook_owner) {
|
||||
|
||||
+53
-52
@@ -13,6 +13,7 @@ const build_opts = struct {
|
||||
const minimapicons = @import("build_options").enable_minimapicons;
|
||||
const transmogfix = @import("build_options").enable_transmogfix;
|
||||
const assetfix = @import("build_options").enable_assetfix;
|
||||
const healtextfix = @import("build_options").enable_healtextfix;
|
||||
};
|
||||
|
||||
// Conditional module imports
|
||||
@@ -25,6 +26,7 @@ const combatlog = if (build_opts.combatlog) @import("combatlog/combatlog.zig") e
|
||||
const minimapicons = if (build_opts.minimapicons) @import("minimapicons/minimapicons.zig") else struct {};
|
||||
const transmogfix = if (build_opts.transmogfix) @import("transmogfix/transmogfix.zig") else struct {};
|
||||
const assetfix = if (build_opts.assetfix) @import("assetfix/assetfix.zig") else struct {};
|
||||
const healtextfix = if (build_opts.healtextfix) @import("healtextfix/healtextfix.zig") else struct {};
|
||||
|
||||
const WINAPI = std.builtin.CallingConvention.winapi;
|
||||
const fc: std.builtin.CallingConvention = .{ .x86_fastcall = .{} };
|
||||
@@ -256,10 +258,11 @@ fn registerLuaFunctions() void {
|
||||
if (build_opts.outline) {
|
||||
registerFunction("OutlineCommand", @intFromPtr(&outline.outlineCommand));
|
||||
}
|
||||
if (build_opts.markers) {
|
||||
if (build_opts.markers and markers.isActive()) {
|
||||
registerFunction("WorldMarker", @intFromPtr(&markers.luaWorldMarker));
|
||||
registerFunction("ClearWorldMarker", @intFromPtr(&markers.luaClearWorldMarker));
|
||||
registerFunction("GetPlayerPosition", @intFromPtr(&markers.luaGetPlayerPosition));
|
||||
registerFunction("ProcessMarkerAnimations", @intFromPtr(&markers.luaProcessAnimations));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -852,7 +855,7 @@ fn loadAddonsDetour(error_handler: u32) callconv(fc) void {
|
||||
error_handler,
|
||||
);
|
||||
}
|
||||
if (build_opts.markers) {
|
||||
if (build_opts.markers and markers.isActive()) {
|
||||
callLoadFileListWithIncludes(
|
||||
"Interface\\AddOns\\Markers\\Markers.toc",
|
||||
&md5ctx,
|
||||
@@ -911,6 +914,9 @@ fn engineInitDetour() callconv(sc) void {
|
||||
if (build_opts.outline) {
|
||||
_ = outline.init();
|
||||
}
|
||||
if (build_opts.healtextfix) {
|
||||
healtextfix.lateInit();
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -919,11 +925,45 @@ fn engineInitDetour() callconv(sc) void {
|
||||
|
||||
var shutdown_hook: hook.Detour(fn () callconv(sc) void) = .{};
|
||||
|
||||
// =============================================================================
|
||||
// Module lifecycle — single table drives install, shutdown, and uninstall.
|
||||
// Adding a module here guarantees all three phases are handled.
|
||||
// =============================================================================
|
||||
|
||||
const ModuleHooks = struct {
|
||||
install: ?*const fn () void = null,
|
||||
remove: ?*const fn () void = null,
|
||||
/// If true, remove is also called during CGGameUI_Shutdown (before game
|
||||
/// teardown), not just during DLL unload. Use for modules that create
|
||||
/// world objects which must be destroyed while game systems are alive.
|
||||
remove_on_shutdown: bool = false,
|
||||
};
|
||||
|
||||
/// Order matters: modules are installed top-to-bottom, removed bottom-to-top.
|
||||
/// Modules with remove_on_shutdown run their remove during shutdownDetour too.
|
||||
const modules = [_]ModuleHooks{
|
||||
if (build_opts.assetfix) .{ .install = assetfix.installHooks, .remove = assetfix.removeHooks } else .{},
|
||||
if (build_opts.framecrash) .{ .install = framecrash.installHooks, .remove = framecrash.removeHooks } else .{},
|
||||
if (build_opts.combatlog) .{ .install = combatlog.installHooks, .remove = combatlog.removeHooks } else .{},
|
||||
if (build_opts.transmogfix) .{ .install = transmogfix.installHooks, .remove = transmogfix.removeHooks } else .{},
|
||||
if (build_opts.minimapicons) .{ .install = minimapicons.installHooks, .remove = minimapicons.removeHooks } else .{},
|
||||
if (build_opts.healtextfix) .{ .install = healtextfix.installHooks, .remove = healtextfix.removeHooks } else .{},
|
||||
if (build_opts.markers) .{ .install = markers.installHooks, .remove = markers.removeHooks, .remove_on_shutdown = true } else .{},
|
||||
if (build_opts.interact) .{ .install = interact.installHooks, .remove = interact.removeHooks } else .{},
|
||||
if (build_opts.outline) .{ .remove = outline.cleanup } else .{},
|
||||
if (build_opts.screenshot) .{ .remove = screenshot.removeHook } else .{},
|
||||
};
|
||||
|
||||
fn shutdownDetour() callconv(sc) void {
|
||||
// Clean up world objects BEFORE game shutdown — atexit handlers run before DllMain
|
||||
// so we must destroy markers here, not in uninstall().
|
||||
if (build_opts.markers) {
|
||||
markers.removeHooks();
|
||||
// Clean up world objects BEFORE game shutdown — atexit handlers run before
|
||||
// DllMain so modules with remove_on_shutdown must destroy here.
|
||||
comptime var i = modules.len;
|
||||
inline while (i > 0) {
|
||||
i -= 1;
|
||||
const m = modules[i];
|
||||
if (m.remove_on_shutdown) {
|
||||
if (m.remove) |rm| rm();
|
||||
}
|
||||
}
|
||||
|
||||
shutdown_hook.callOriginal(.{});
|
||||
@@ -941,28 +981,11 @@ fn install() void {
|
||||
_ = file_hook.attach(0x648620, &loadFileDetour);
|
||||
_ = lsf_hook.attach(0x490250, &loadScriptFunctionsDetour);
|
||||
|
||||
if (build_opts.assetfix) {
|
||||
_ = assetfix.installHooks();
|
||||
}
|
||||
if (build_opts.framecrash) {
|
||||
framecrash.installHooks();
|
||||
}
|
||||
if (build_opts.combatlog) {
|
||||
combatlog.installHooks();
|
||||
}
|
||||
if (build_opts.transmogfix) {
|
||||
_ = transmogfix.installHooks();
|
||||
}
|
||||
if (build_opts.minimapicons) {
|
||||
minimapicons.installHooks();
|
||||
inline for (modules) |m| {
|
||||
if (m.install) |inst| inst();
|
||||
}
|
||||
|
||||
_ = load_addons_hook.attach(0x51F600, &loadAddonsDetour);
|
||||
|
||||
if (build_opts.interact) {
|
||||
interact.installHooks();
|
||||
}
|
||||
|
||||
_ = engine_init_hook.attach(0x46a400, &engineInitDetour);
|
||||
_ = shutdown_hook.attach(0x490BD0, &shutdownDetour);
|
||||
}
|
||||
@@ -971,33 +994,11 @@ fn uninstall() void {
|
||||
shutdown_hook.detach();
|
||||
engine_init_hook.detach();
|
||||
|
||||
// Markers must be cleaned up first — destroys world objects while game systems are still alive
|
||||
if (build_opts.markers) {
|
||||
markers.removeHooks();
|
||||
}
|
||||
if (build_opts.outline) {
|
||||
outline.cleanup();
|
||||
}
|
||||
if (build_opts.screenshot) {
|
||||
screenshot.removeHook();
|
||||
}
|
||||
if (build_opts.interact) {
|
||||
interact.removeHooks();
|
||||
}
|
||||
if (build_opts.framecrash) {
|
||||
framecrash.removeHooks();
|
||||
}
|
||||
if (build_opts.combatlog) {
|
||||
combatlog.removeHooks();
|
||||
}
|
||||
if (build_opts.minimapicons) {
|
||||
minimapicons.removeHooks();
|
||||
}
|
||||
if (build_opts.transmogfix) {
|
||||
transmogfix.removeHooks();
|
||||
}
|
||||
if (build_opts.assetfix) {
|
||||
assetfix.removeHooks();
|
||||
// Remove in reverse order
|
||||
comptime var i = modules.len;
|
||||
inline while (i > 0) {
|
||||
i -= 1;
|
||||
if (modules[i].remove) |rm| rm();
|
||||
}
|
||||
|
||||
load_addons_hook.detach();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,13 @@ frame:SetScript("OnEvent", function()
|
||||
end
|
||||
end)
|
||||
|
||||
-- Per-frame animation driver: queues Hold after Stand finishes on new
|
||||
-- markers and re-queues it periodically so it never falls back to Stand.
|
||||
local animFrame = CreateFrame("Frame")
|
||||
animFrame:SetScript("OnUpdate", function()
|
||||
ProcessMarkerAnimations()
|
||||
end)
|
||||
|
||||
SLASH_MARKERS1 = "/markers"
|
||||
SLASH_MARKERS2 = "/mark"
|
||||
SlashCmdList["MARKERS"] = function(msg)
|
||||
|
||||
+71
-8
@@ -28,6 +28,11 @@ const ERROR_ALREADY_EXISTS: u32 = 183;
|
||||
var g_mutex: ?*anyopaque = null;
|
||||
var g_is_hook_owner: bool = false;
|
||||
|
||||
/// True if this DLL instance owns the markers hooks and Lua API is safe to use.
|
||||
pub fn isActive() bool {
|
||||
return g_is_hook_owner;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Constants
|
||||
// =============================================================================
|
||||
@@ -83,6 +88,7 @@ var despawning: [MAX_DESPAWNING]?DespawningEntity = .{null} ** MAX_DESPAWNING;
|
||||
// =============================================================================
|
||||
|
||||
const fc = std.builtin.CallingConvention{ .x86_fastcall = .{} };
|
||||
const sc = std.builtin.CallingConvention{ .x86_stdcall = .{} };
|
||||
|
||||
const lapi = struct {
|
||||
fn gettop(L: u32) i32 {
|
||||
@@ -461,6 +467,58 @@ pub fn luaGetPlayerPosition(L: u32) callconv(.c) u32 {
|
||||
return 3;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// World teardown hook
|
||||
// =============================================================================
|
||||
|
||||
var world_cleanup_hook: hook.Detour(fn () callconv(sc) void) = .{};
|
||||
|
||||
/// Pre-hook on CleanupWorldAndEntities (0x66fc40).
|
||||
/// Destroys all our entities via CleanupEntity_ProcessAttachments before the
|
||||
/// game's teardown runs — the same pattern every native caller uses (e.g.
|
||||
/// processCinematicExit, DestroyPathObjectIfPresent). This unlinks them from
|
||||
/// the WDOODADDEF hash table so the atexit handler never touches freed memory.
|
||||
fn worldCleanupDetour() callconv(sc) void {
|
||||
con.print("[markers] >>> worldCleanupDetour FIRING <<<\n");
|
||||
destroyAllEntities();
|
||||
world_cleanup_hook.callOriginal(.{});
|
||||
}
|
||||
|
||||
/// Destroy all tracked entities (active markers + despawning).
|
||||
/// Idempotent — safe to call multiple times.
|
||||
fn destroyAllEntities() void {
|
||||
var count: u32 = 0;
|
||||
for (&marker_entities, 0..) |*slot, i| {
|
||||
if (slot.*) |existing| {
|
||||
const addr = @intFromPtr(existing);
|
||||
const flags = hook.readMem(u32, addr + 0x8);
|
||||
const refcount = hook.readMem(u16, addr + 0x0E);
|
||||
con.fmt("[markers] destroying marker[{d}] @0x{x} flags=0x{x} refcount={d}\n", .{ i, addr, flags, refcount });
|
||||
cleanupEntity(existing);
|
||||
slot.* = null;
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
for (&hold_queued) |*h| h.* = false;
|
||||
for (&marker_created_tick) |*t| t.* = 0;
|
||||
|
||||
for (&despawning, 0..) |*slot, i| {
|
||||
if (slot.*) |d| {
|
||||
const addr = @intFromPtr(d.entity);
|
||||
const flags = hook.readMem(u32, addr + 0x8);
|
||||
const refcount = hook.readMem(u16, addr + 0x0E);
|
||||
con.fmt("[markers] destroying despawn[{d}] @0x{x} flags=0x{x} refcount={d}\n", .{ i, addr, flags, refcount });
|
||||
cleanupEntity(d.entity);
|
||||
slot.* = null;
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (count > 0) {
|
||||
con.fmt("[markers] world cleanup: destroyed {d} entities\n", .{count});
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Install hooks
|
||||
// =============================================================================
|
||||
@@ -483,18 +541,23 @@ pub fn installHooks() void {
|
||||
return;
|
||||
}
|
||||
g_is_hook_owner = true;
|
||||
|
||||
// Hook CleanupWorldAndEntities to destroy our entities before world teardown.
|
||||
// This fires on map change, logout, AND exit — before heaps are destroyed.
|
||||
const hook_result = world_cleanup_hook.attach(o.FN_CLEANUP_WORLD_AND_ENTITIES, &worldCleanupDetour);
|
||||
if (hook_result != .ok) {
|
||||
con.print("[markers] FAILED to hook CleanupWorldAndEntities!\n");
|
||||
} else {
|
||||
con.print("[markers] hooked CleanupWorldAndEntities OK\n");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn removeHooks() void {
|
||||
if (g_is_hook_owner) {
|
||||
// Force-cleanup: no time for animations during shutdown
|
||||
for (&marker_entities) |*slot| {
|
||||
if (slot.*) |existing| {
|
||||
cleanupEntity(existing);
|
||||
slot.* = null;
|
||||
}
|
||||
}
|
||||
forceCleanupDespawning();
|
||||
// destroyAllEntities is idempotent — if worldCleanupDetour already ran,
|
||||
// all slots are null and this is a no-op.
|
||||
destroyAllEntities();
|
||||
world_cleanup_hook.detach();
|
||||
}
|
||||
|
||||
if (g_is_hook_owner) {
|
||||
|
||||
@@ -33,6 +33,17 @@ pub const MOVEMENT_POS_Z: usize = 0x18;
|
||||
/// Increments refcount at entity+0x0E.
|
||||
pub const FN_CREATE_ENTITY_INSTANCE: usize = 0x006707c0;
|
||||
|
||||
// =============================================================================
|
||||
// World teardown (map unload / logout / exit)
|
||||
// =============================================================================
|
||||
|
||||
/// CleanupWorldAndEntities — void(), no params, __stdcall.
|
||||
/// Top-level world teardown: calls CleanupEntityList_ProcessAll, then
|
||||
/// CleanupWorldAndReleaseResources (which iterates heaps and force-frees).
|
||||
/// Called from InitializeWorldScene (map change) and ShutdownClientSystems (exit).
|
||||
/// Hook this to destroy custom entities BEFORE the game's teardown begins.
|
||||
pub const FN_CLEANUP_WORLD_AND_ENTITIES: usize = 0x0066fc40;
|
||||
|
||||
// =============================================================================
|
||||
// World object lifecycle
|
||||
// =============================================================================
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
//! and a Lua C callback for `/wu outline` commands.
|
||||
|
||||
const std = @import("std");
|
||||
const hook = @import("hook");
|
||||
const hook = @import("zhook");
|
||||
const con = @import("../console.zig");
|
||||
const tracker = @import("tracker.zig");
|
||||
const model_hook = @import("model_hook.zig");
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
//! occluded by world/WMO/game objects but show through other players.
|
||||
|
||||
const std = @import("std");
|
||||
const hook = @import("hook");
|
||||
const hook = @import("zhook");
|
||||
const types = @import("types.zig");
|
||||
const tracker = @import("tracker.zig");
|
||||
const model_hook = @import("model_hook.zig");
|
||||
|
||||
+28
-94
@@ -13,7 +13,7 @@
|
||||
//! implementation function.
|
||||
|
||||
const std = @import("std");
|
||||
const hook = @import("hook");
|
||||
const hook = @import("zhook");
|
||||
const api = @import("api.zig");
|
||||
const o = @import("offsets.zig");
|
||||
const types = @import("types.zig");
|
||||
@@ -25,15 +25,19 @@ const wow = @import("wow.zig");
|
||||
// Calling convention constants
|
||||
// =============================================================================
|
||||
|
||||
const THISCALL = std.builtin.CallingConvention{ .x86_thiscall = .{} };
|
||||
const tc: std.builtin.CallingConvention = .{ .x86_thiscall = .{} };
|
||||
|
||||
// =============================================================================
|
||||
// Hook state
|
||||
// =============================================================================
|
||||
|
||||
var render_draw_hook: hook.Hook = .{};
|
||||
var manage_render_hook: hook.Hook = .{};
|
||||
var draw_batch_hook: hook.Hook = .{};
|
||||
const RenderDrawFn = fn (u32, u32, u32, u32, u32) callconv(tc) void;
|
||||
const ManageRenderFn = fn (u32, u32) callconv(tc) void;
|
||||
const DrawBatchFn = fn (u32) callconv(tc) void;
|
||||
|
||||
var render_draw_hook: hook.Detour(RenderDrawFn) = .{};
|
||||
var manage_render_hook: hook.Detour(ManageRenderFn) = .{};
|
||||
var draw_batch_hook: hook.Detour(DrawBatchFn) = .{};
|
||||
|
||||
/// D3D9 hooks are deferred until the first model hook fires, because creating
|
||||
/// a dummy D3D9 device during engine init corrupts the proxy's state.
|
||||
@@ -71,7 +75,7 @@ var reordered_indices: [MAX_REORDER]i32 = undefined;
|
||||
// through other players and gear (since those aren't in depth when stencil
|
||||
// is written). The outline composites on top of everything in EndScene.
|
||||
|
||||
fn renderDrawDetour(this: u32, view_matrix: u32, batch_data: u32, batch_indices: u32, batch_count: u32) callconv(THISCALL) void {
|
||||
fn renderDrawDetour(this: u32, view_matrix: u32, batch_data: u32, batch_indices: u32, batch_count: u32) callconv(tc) void {
|
||||
// One-time: install D3D9 hooks now that the game is actively rendering.
|
||||
if (d3d9_deferred_pending) {
|
||||
d3d9_deferred_pending = false;
|
||||
@@ -80,7 +84,7 @@ fn renderDrawDetour(this: u32, view_matrix: u32, batch_data: u32, batch_indices:
|
||||
|
||||
// Skip reordering if nothing to outline or too many batches
|
||||
if (!tracker.enabled or !tracker.hasTargets() or batch_count == 0 or batch_count > MAX_REORDER) {
|
||||
callOrigRenderDraw(this, view_matrix, batch_data, batch_indices, batch_count);
|
||||
render_draw_hook.callOriginal(.{ this, view_matrix, batch_data, batch_indices, batch_count });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -102,7 +106,7 @@ fn renderDrawDetour(this: u32, view_matrix: u32, batch_data: u32, batch_indices:
|
||||
}
|
||||
|
||||
if (outline_count == 0) {
|
||||
callOrigRenderDraw(this, view_matrix, batch_data, batch_indices, batch_count);
|
||||
render_draw_hook.callOriginal(.{ this, view_matrix, batch_data, batch_indices, batch_count });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -134,26 +138,7 @@ fn renderDrawDetour(this: u32, view_matrix: u32, batch_data: u32, batch_indices:
|
||||
indices[i] = reordered_indices[i];
|
||||
}
|
||||
|
||||
callOrigRenderDraw(this, view_matrix, batch_data, batch_indices, batch_count);
|
||||
}
|
||||
|
||||
fn callOrigRenderDraw(this: u32, view_matrix: u32, batch_data: u32, batch_indices: u32, batch_count: u32) void {
|
||||
// __thiscall: ECX = this, stack = viewMatrix, batchData, batchIndices, batchCount
|
||||
// Callee cleans 16 bytes (4 stack params).
|
||||
// Pack args into a struct so we only need one "r" register to address them.
|
||||
const args = [4]u32{ view_matrix, batch_data, batch_indices, batch_count };
|
||||
asm volatile (
|
||||
\\push 12(%[a])
|
||||
\\push 8(%[a])
|
||||
\\push 4(%[a])
|
||||
\\push (%[a])
|
||||
\\call *%[func]
|
||||
:
|
||||
: [_] "{ecx}" (this),
|
||||
[a] "r" (&args),
|
||||
[func] "r" (render_draw_hook.trampoline),
|
||||
: .{ .eax = true, .edx = true, .memory = true, .cc = true }
|
||||
);
|
||||
render_draw_hook.callOriginal(.{ this, view_matrix, batch_data, batch_indices, batch_count });
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -162,22 +147,13 @@ fn callOrigRenderDraw(this: u32, view_matrix: u32, batch_data: u32, batch_indice
|
||||
// __thiscall(model_ECX, addToList_stack)
|
||||
// Native thiscall detour — no thunk needed.
|
||||
|
||||
fn manageRenderDetour(model: u32, add_to_list: u32) callconv(THISCALL) void {
|
||||
fn manageRenderDetour(model: u32, add_to_list: u32) callconv(tc) void {
|
||||
// Classify the model when it's being ADDED to the render list
|
||||
if (add_to_list == 1 and model != 0 and tracker.enabled and tracker.hasTargets()) {
|
||||
tracker.classifyModel(model);
|
||||
}
|
||||
|
||||
// Call original: __thiscall(model_ECX, addToList_stack)
|
||||
asm volatile (
|
||||
\\push %[add]
|
||||
\\call *%[func]
|
||||
:
|
||||
: [_] "{ecx}" (model),
|
||||
[add] "r" (add_to_list),
|
||||
[func] "r" (manage_render_hook.trampoline),
|
||||
: .{ .eax = true, .edx = true, .memory = true, .cc = true }
|
||||
);
|
||||
manage_render_hook.callOriginal(.{ model, add_to_list });
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -189,26 +165,10 @@ fn manageRenderDetour(model: u32, add_to_list: u32) callconv(THISCALL) void {
|
||||
// wrong ret instructions for functions with ≤2 register params. The naked
|
||||
// function bridges fastcall → cdecl and calls the implementation function.
|
||||
|
||||
fn drawBatchProjEntry() callconv(.naked) void {
|
||||
// __fastcall(ECX): ECX = render context, 0 stack args.
|
||||
// Bridge to cdecl: push edx + ecx as args, call impl, cleanup, ret.
|
||||
asm volatile (
|
||||
\\push %%edx
|
||||
\\push %%ecx
|
||||
\\call *%%eax
|
||||
\\add $8, %%esp
|
||||
\\ret
|
||||
:
|
||||
: [_] "{eax}" (@intFromPtr(&drawBatchProjImpl))
|
||||
);
|
||||
}
|
||||
|
||||
fn drawBatchProjImpl(ctx: u32, _edx: u32) callconv(.c) void {
|
||||
_ = _edx;
|
||||
|
||||
fn drawBatchProjDetour(ctx: u32) callconv(tc) void {
|
||||
// Fast path: no tracking enabled or nothing tracked → just call original
|
||||
if (!tracker.enabled or !tracker.hasTargets()) {
|
||||
callOrigDrawBatch(ctx);
|
||||
draw_batch_hook.callOriginal(.{ctx});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -224,60 +184,34 @@ fn drawBatchProjImpl(ctx: u32, _edx: u32) callconv(.c) void {
|
||||
rendering_outline = true;
|
||||
current_model = model_ptr;
|
||||
|
||||
callOrigDrawBatch(ctx);
|
||||
draw_batch_hook.callOriginal(.{ctx});
|
||||
|
||||
rendering_outline = false;
|
||||
current_model = 0;
|
||||
} else {
|
||||
// Normal rendering — no special handling needed
|
||||
callOrigDrawBatch(ctx);
|
||||
draw_batch_hook.callOriginal(.{ctx});
|
||||
}
|
||||
}
|
||||
|
||||
fn callOrigDrawBatch(ctx: u32) void {
|
||||
asm volatile ("call *%[func]"
|
||||
:
|
||||
: [_] "{ecx}" (ctx),
|
||||
[func] "r" (draw_batch_hook.trampoline),
|
||||
: .{ .eax = true, .edx = true, .memory = true, .cc = true }
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Install / Remove
|
||||
// =============================================================================
|
||||
|
||||
pub fn installHooks() bool {
|
||||
// CM2SceneRenderDraw — native thiscall detour, no thunk needed.
|
||||
// Prologue is 9 bytes: PUSH EBP (1) + MOV EBP,ESP (2) + SUB ESP,0x80 (6).
|
||||
if (!render_draw_hook.install(
|
||||
o.FN_CM2SCENE_RENDER_DRAW,
|
||||
9,
|
||||
@intFromPtr(&renderDrawDetour),
|
||||
&.{},
|
||||
)) return false;
|
||||
if (render_draw_hook.attach(o.FN_CM2SCENE_RENDER_DRAW, &renderDrawDetour) != .ok)
|
||||
return false;
|
||||
|
||||
// ManageRenderListNode — native thiscall detour, no thunk needed.
|
||||
if (!manage_render_hook.install(
|
||||
o.FN_CM2MODEL_MANAGE_RENDER_LIST,
|
||||
6,
|
||||
@intFromPtr(&manageRenderDetour),
|
||||
&.{},
|
||||
)) return false;
|
||||
if (manage_render_hook.attach(o.FN_CM2MODEL_MANAGE_RENDER_LIST, &manageRenderDetour) != .ok)
|
||||
return false;
|
||||
|
||||
// DrawBatchProj — naked entry bridges fastcall → cdecl, no thunk needed.
|
||||
if (!draw_batch_hook.install(
|
||||
o.FN_DRAW_BATCH_PROJ,
|
||||
6,
|
||||
@intFromPtr(&drawBatchProjEntry),
|
||||
&.{},
|
||||
)) return false;
|
||||
if (draw_batch_hook.attach(o.FN_DRAW_BATCH_PROJ, &drawBatchProjDetour) != .ok)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn removeHooks() void {
|
||||
draw_batch_hook.remove();
|
||||
manage_render_hook.remove();
|
||||
render_draw_hook.remove();
|
||||
draw_batch_hook.detach();
|
||||
manage_render_hook.detach();
|
||||
render_draw_hook.detach();
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
//! comparison against the object manager's validated set.
|
||||
|
||||
const std = @import("std");
|
||||
const hook = @import("hook");
|
||||
const hook = @import("zhook");
|
||||
const wow = @import("wow.zig");
|
||||
const o = @import("offsets.zig");
|
||||
const types = @import("types.zig");
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
//! game functions (UnitGUID, GetObjectByGUID, UnitReaction).
|
||||
|
||||
const std = @import("std");
|
||||
const hook = @import("hook");
|
||||
const hook = @import("zhook");
|
||||
const o = @import("offsets.zig");
|
||||
const types = @import("types.zig");
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const std = @import("std");
|
||||
const hook = @import("hook");
|
||||
const hook = @import("zhook");
|
||||
const con = @import("../console.zig");
|
||||
const png = @import("png.zig");
|
||||
|
||||
@@ -55,7 +55,9 @@ const ERROR_ALREADY_EXISTS: u32 = 183;
|
||||
|
||||
var enabled: bool = true;
|
||||
var compression_level: i32 = 6; // user-facing 0–9, kept for Lua interface
|
||||
var tga_hook: hook.Hook = .{};
|
||||
const tc: std.builtin.CallingConvention = .{ .x86_thiscall = .{} };
|
||||
const TgaWriteFn = fn (u32, u32) callconv(tc) i32;
|
||||
var tga_hook: hook.Detour(TgaWriteFn) = .{};
|
||||
var screenshot_dir: [260]u8 = undefined;
|
||||
var screenshot_dir_len: usize = 0;
|
||||
var screenshot_counter: u8 = 0;
|
||||
@@ -79,7 +81,7 @@ var queue: [MAX_PENDING]PendingScreenshot = undefined;
|
||||
var queue_head: usize = 0;
|
||||
var queue_tail: usize = 0;
|
||||
var queue_count: usize = 0;
|
||||
var mutex: std.Thread.Mutex = .{};
|
||||
var mutex: std.atomic.Mutex = .unlocked;
|
||||
var worker_running: bool = false;
|
||||
var g_mutex: ?HANDLE = null;
|
||||
var g_is_hook_owner: bool = false;
|
||||
@@ -122,15 +124,7 @@ fn extractDir(filename_ptr: u32) void {
|
||||
// =============================================================================
|
||||
|
||||
fn callOriginal(self: u32, filename: u32) i32 {
|
||||
return asm volatile (
|
||||
\\push %[filename]
|
||||
\\call *%[func]
|
||||
: [ret] "={eax}" (-> i32),
|
||||
: [_] "{ecx}" (self),
|
||||
[filename] "r" (filename),
|
||||
[func] "r" (tga_hook.trampoline),
|
||||
: .{ .edx = true, .memory = true, .cc = true }
|
||||
);
|
||||
return tga_hook.callOriginal(.{ self, filename });
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -138,8 +132,7 @@ fn callOriginal(self: u32, filename: u32) i32 {
|
||||
// Thunked from __fastcall(self_ECX, _EDX, filename_stack) → cdecl
|
||||
// =============================================================================
|
||||
|
||||
fn tgaWriteDetour(self: u32, _edx: u32, filename: u32) callconv(.c) i32 {
|
||||
_ = _edx;
|
||||
fn tgaWriteDetour(self: u32, filename: u32) callconv(tc) i32 {
|
||||
|
||||
if (!enabled) return callOriginal(self, filename);
|
||||
|
||||
@@ -167,7 +160,7 @@ fn tgaWriteDetour(self: u32, _edx: u32, filename: u32) callconv(.c) i32 {
|
||||
@memcpy(buffer, src[0..size]);
|
||||
|
||||
// Enqueue for async processing
|
||||
mutex.lock();
|
||||
while (!mutex.tryLock()) {}
|
||||
defer mutex.unlock();
|
||||
|
||||
if (!enqueue(.{ .buffer = buffer.ptr, .width = width, .height = height, .size = size, .level = png.mapLevel(compression_level) })) {
|
||||
@@ -196,7 +189,7 @@ fn workerThread() void {
|
||||
while (true) {
|
||||
var shot: PendingScreenshot = undefined;
|
||||
{
|
||||
mutex.lock();
|
||||
while (!mutex.tryLock()) {}
|
||||
defer mutex.unlock();
|
||||
if (dequeue()) |s| {
|
||||
shot = s;
|
||||
@@ -368,24 +361,19 @@ pub fn installHook() void {
|
||||
g_is_hook_owner = true;
|
||||
|
||||
// CTgaFile::Write at 0x5a4810
|
||||
// __thiscall(self, filename) — prologue: 55 8B EC 83 EC 08 = 6 bytes, no fixups
|
||||
// Thunk: fastcall(ECX=self, EDX, stack: filename) → cdecl(self, edx, filename)
|
||||
// __thiscall(self, filename) ret 4
|
||||
//
|
||||
// Another DLL (UnitXP_SP3) hooks this same address during DLL_PROCESS_ATTACH,
|
||||
// replacing the prologue with an E9 JMP. Restore the original prologue first
|
||||
// so prepare() builds a trampoline to the real function rather than chaining
|
||||
// through UnitXP's detour.
|
||||
hook.writeProtected(0x5a4810, &.{ 0x55, 0x8B, 0xEC, 0x83, 0xEC, 0x08 });
|
||||
if (tga_hook.prepare(0x5a4810, 6, &.{})) {
|
||||
const thunk = tga_hook.mem.? + 32;
|
||||
_ = hook.buildFastcallToCdeclThunk(thunk, @intFromPtr(&tgaWriteDetour), 1);
|
||||
tga_hook.activate(@intFromPtr(thunk));
|
||||
}
|
||||
_ = tga_hook.attach(0x5a4810, &tgaWriteDetour);
|
||||
}
|
||||
|
||||
pub fn removeHook() void {
|
||||
if (g_is_hook_owner) {
|
||||
tga_hook.remove();
|
||||
tga_hook.detach();
|
||||
}
|
||||
|
||||
if (g_is_hook_owner) {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
// =============================================================================
|
||||
|
||||
const std = @import("std");
|
||||
const hook = @import("hook");
|
||||
const hook = @import("zhook");
|
||||
const con = @import("../console.zig");
|
||||
|
||||
const WINAPI = std.builtin.CallingConvention.winapi;
|
||||
@@ -165,9 +165,14 @@ var g_mutex: ?*anyopaque = null;
|
||||
// Hooks
|
||||
// =============================================================================
|
||||
|
||||
var set_block_hook = hook.Hook{};
|
||||
var refresh_hook = hook.Hook{};
|
||||
var scene_end_hook = hook.Hook{};
|
||||
const tc: std.builtin.CallingConvention = .{ .x86_thiscall = .{} };
|
||||
const SetBlockFn = fn (u32, u32, u32) callconv(tc) u32;
|
||||
const RefreshFn = fn (u32, u32, u32, u32) callconv(tc) void;
|
||||
const SceneEndFn = fn (u32) callconv(tc) void;
|
||||
|
||||
var set_block_hook: hook.Detour(SetBlockFn) = .{};
|
||||
var refresh_hook: hook.Detour(RefreshFn) = .{};
|
||||
var scene_end_hook: hook.Detour(SceneEndFn) = .{};
|
||||
|
||||
// =============================================================================
|
||||
// Object manager helpers
|
||||
@@ -358,55 +363,15 @@ fn findOtherPendingEntry(guid: u64, slot: i32) i32 {
|
||||
// =============================================================================
|
||||
|
||||
fn callOriginalSetBlock(obj: u32, index: u32, value: u32) u32 {
|
||||
// Save/restore ECX around the call: the callee overwrites ECX internally
|
||||
// (SetBlock does `mov ecx, [ebp+0xC]`), but we can't list ECX as a clobber
|
||||
// since it's already an input constraint. Without the save/restore, the
|
||||
// compiler may reuse the now-stale ECX for `obj` on a subsequent call.
|
||||
// The trampoline's `ret 8` cleans up the pushed index+value, leaving our
|
||||
// saved ECX on top for the pop.
|
||||
return asm volatile (
|
||||
\\push %%ecx
|
||||
\\push %[value]
|
||||
\\push %[index]
|
||||
\\call *%[func]
|
||||
\\pop %%ecx
|
||||
: [ret] "={eax}" (-> u32),
|
||||
: [_] "{ecx}" (obj),
|
||||
[index] "r" (index),
|
||||
[value] "r" (value),
|
||||
[func] "r" (set_block_hook.trampoline),
|
||||
: .{ .edx = true, .memory = true, .cc = true }
|
||||
);
|
||||
return set_block_hook.callOriginal(.{ obj, index, value });
|
||||
}
|
||||
|
||||
fn callOriginalRefresh(unit: u32, event_data: u32, extra_data: u32, force_update: u32) void {
|
||||
// __thiscall: ECX=this, stack args right-to-left. EDX is caller-saved scratch
|
||||
// (confirmed via Ghidra: __thiscall, EDX not part of calling convention).
|
||||
// Pin force_update to EDX to stay within 3 "r" registers (EBX/ESI/EDI)
|
||||
// since EBP is the frame pointer on x86.
|
||||
asm volatile (
|
||||
\\push %[force]
|
||||
\\push %[extra]
|
||||
\\push %[event]
|
||||
\\call *%[func]
|
||||
:
|
||||
: [_] "{ecx}" (unit),
|
||||
[force] "{edx}" (force_update),
|
||||
[event] "r" (event_data),
|
||||
[extra] "r" (extra_data),
|
||||
[func] "r" (refresh_hook.trampoline),
|
||||
: .{ .eax = true, .memory = true, .cc = true }
|
||||
);
|
||||
refresh_hook.callOriginal(.{ unit, event_data, extra_data, force_update });
|
||||
}
|
||||
|
||||
fn callOriginalSceneEnd(device: u32) void {
|
||||
asm volatile (
|
||||
\\call *%[func]
|
||||
:
|
||||
: [_] "{ecx}" (device),
|
||||
[func] "r" (scene_end_hook.trampoline),
|
||||
: .{ .eax = true, .edx = true, .memory = true, .cc = true }
|
||||
);
|
||||
scene_end_hook.callOriginal(.{device});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -429,7 +394,7 @@ fn processTimeouts(now: u32) void {
|
||||
}
|
||||
|
||||
// OTHER PLAYERS: Use timeout since we don't have their INV_SLOT info
|
||||
if (g_other_pending_count > 0 and set_block_hook.trampoline != 0) {
|
||||
if (g_other_pending_count > 0 and set_block_hook.inner.trampoline != 0) {
|
||||
const UnitSlots = struct {
|
||||
unit: u32 = 0,
|
||||
slots: [19]i32 = .{0} ** 19,
|
||||
@@ -507,7 +472,7 @@ fn processTimeouts(now: u32) void {
|
||||
|
||||
// Fallback to RefreshVisualAppearance
|
||||
con.fmt("[other] REFRESH fallback unit=0x{X:0>8} table=0x{X:0>8}\n", .{ unit, display_table });
|
||||
if (refresh_hook.trampoline != 0) {
|
||||
if (refresh_hook.inner.trampoline != 0) {
|
||||
callOriginalRefresh(unit, 0, 0, 1);
|
||||
}
|
||||
}
|
||||
@@ -518,8 +483,7 @@ fn processTimeouts(now: u32) void {
|
||||
// Hook 1: SetBlock (0x6142E0)
|
||||
// =============================================================================
|
||||
|
||||
fn hookSetBlock(obj: u32, _edx: u32, index: u32, value: u32) callconv(.c) u32 {
|
||||
_ = _edx;
|
||||
fn hookSetBlock(obj: u32, index: u32, value: u32) callconv(tc) u32 {
|
||||
const val = value;
|
||||
|
||||
// VISIBLE_ITEM writes
|
||||
@@ -694,11 +658,9 @@ fn hookSetBlock(obj: u32, _edx: u32, index: u32, value: u32) callconv(.c) u32 {
|
||||
// Hook 2: RefreshVisualAppearance (0x5fb880)
|
||||
// =============================================================================
|
||||
|
||||
fn hookRefreshVisualAppearance(unit: u32, _edx: u32, event_data: u32, extra_data: u32, force_update: u32) callconv(.c) void {
|
||||
_ = _edx;
|
||||
|
||||
if (!g_enabled or refresh_hook.trampoline == 0) {
|
||||
if (refresh_hook.trampoline != 0) {
|
||||
fn hookRefreshVisualAppearance(unit: u32, event_data: u32, extra_data: u32, force_update: u32) callconv(tc) void {
|
||||
if (!g_enabled or refresh_hook.inner.trampoline == 0) {
|
||||
if (refresh_hook.inner.trampoline != 0) {
|
||||
callOriginalRefresh(unit, event_data, extra_data, force_update);
|
||||
}
|
||||
return;
|
||||
@@ -802,8 +764,7 @@ fn hookRefreshVisualAppearance(unit: u32, _edx: u32, event_data: u32, extra_data
|
||||
// Hook 3: SceneEnd (0x5a17a0)
|
||||
// =============================================================================
|
||||
|
||||
fn hookSceneEnd(device: u32, _edx: u32) callconv(.c) void {
|
||||
_ = _edx;
|
||||
fn hookSceneEnd(device: u32) callconv(tc) void {
|
||||
|
||||
if (g_enabled and (g_local_pending_count > 0 or g_other_pending_count > 0)) {
|
||||
processTimeouts(GetTickCount());
|
||||
@@ -816,16 +777,16 @@ fn hookSceneEnd(device: u32, _edx: u32) callconv(.c) void {
|
||||
// Init / Cleanup
|
||||
// =============================================================================
|
||||
|
||||
pub fn installHooks() bool {
|
||||
pub fn installHooks() void {
|
||||
con.print("[transmogfix] Module loaded\n");
|
||||
|
||||
// Multi-DLL safety: only one instance per process should hook
|
||||
var mutex_name_buf: [64]u8 = undefined;
|
||||
const mutex_name = std.fmt.bufPrint(&mutex_name_buf, "Local\\TransmogCoalesceHook_{d}", .{GetCurrentProcessId()}) catch return false;
|
||||
const mutex_name = std.fmt.bufPrint(&mutex_name_buf, "Local\\TransmogCoalesceHook_{d}", .{GetCurrentProcessId()}) catch return;
|
||||
mutex_name_buf[mutex_name.len] = 0;
|
||||
|
||||
g_mutex = CreateMutexA(null, 1, @ptrCast(mutex_name_buf[0..mutex_name.len :0]));
|
||||
if (g_mutex == null) return false;
|
||||
if (g_mutex == null) return;
|
||||
|
||||
if (GetLastError() == ERROR_ALREADY_EXISTS) {
|
||||
_ = CloseHandle(g_mutex.?);
|
||||
@@ -833,7 +794,7 @@ pub fn installHooks() bool {
|
||||
g_is_hook_owner = false;
|
||||
g_initialized = true;
|
||||
con.print("[transmogfix] Another DLL owns hooks (mutex taken), skipping\n");
|
||||
return true; // Success but not owner — no hooks
|
||||
return;
|
||||
}
|
||||
g_is_hook_owner = true;
|
||||
|
||||
@@ -846,41 +807,31 @@ pub fn installHooks() bool {
|
||||
g_unit_cache = [1]UnitVisualState{.{}} ** UNIT_CACHE_SIZE;
|
||||
g_cached_visible_item = .{0} ** 19;
|
||||
|
||||
// Hook 1: SetBlock (6 bytes, no fixups)
|
||||
if (!set_block_hook.prepare(ADDR_SetBlock, 6, &.{})) return false;
|
||||
const sb_thunk = set_block_hook.mem.? + 32;
|
||||
_ = hook.buildFastcallToCdeclThunk(sb_thunk, @intFromPtr(&hookSetBlock), 2);
|
||||
set_block_hook.activate(@intFromPtr(sb_thunk));
|
||||
// Hook 1: SetBlock
|
||||
if (set_block_hook.attach(ADDR_SetBlock, &hookSetBlock) != .ok) return;
|
||||
|
||||
// Hook 2: RefreshVisualAppearance (6 bytes, no fixups)
|
||||
if (!refresh_hook.prepare(ADDR_RefreshVisualAppearance, 6, &.{})) {
|
||||
set_block_hook.remove();
|
||||
return false;
|
||||
// Hook 2: RefreshVisualAppearance
|
||||
if (refresh_hook.attach(ADDR_RefreshVisualAppearance, &hookRefreshVisualAppearance) != .ok) {
|
||||
set_block_hook.detach();
|
||||
return;
|
||||
}
|
||||
const rv_thunk = refresh_hook.mem.? + 32;
|
||||
_ = hook.buildFastcallToCdeclThunk(rv_thunk, @intFromPtr(&hookRefreshVisualAppearance), 3);
|
||||
refresh_hook.activate(@intFromPtr(rv_thunk));
|
||||
|
||||
// Hook 3: SceneEnd (9 bytes, no fixups)
|
||||
if (!scene_end_hook.prepare(ADDR_SceneEnd, 9, &.{})) {
|
||||
refresh_hook.remove();
|
||||
set_block_hook.remove();
|
||||
return false;
|
||||
// Hook 3: SceneEnd
|
||||
if (scene_end_hook.attach(ADDR_SceneEnd, &hookSceneEnd) != .ok) {
|
||||
refresh_hook.detach();
|
||||
set_block_hook.detach();
|
||||
return;
|
||||
}
|
||||
const se_thunk = scene_end_hook.mem.? + 32;
|
||||
_ = hook.buildFastcallToCdeclThunk(se_thunk, @intFromPtr(&hookSceneEnd), 0);
|
||||
scene_end_hook.activate(@intFromPtr(se_thunk));
|
||||
|
||||
g_initialized = true;
|
||||
con.print("[transmogfix] All 3 hooks installed\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn removeHooks() void {
|
||||
if (g_is_hook_owner) {
|
||||
scene_end_hook.remove();
|
||||
refresh_hook.remove();
|
||||
set_block_hook.remove();
|
||||
scene_end_hook.detach();
|
||||
refresh_hook.detach();
|
||||
set_block_hook.detach();
|
||||
}
|
||||
|
||||
if (g_is_hook_owner) {
|
||||
|
||||
Reference in New Issue
Block a user