const std = @import("std"); const hook = @import("zhook"); const logging = @import("logging.zig"); var log: logging.Logger = .{}; // Build options for conditional module compilation const build_opts = struct { const screenshot = @import("build_options").enable_pngscreenshots; const interact = @import("build_options").enable_interact; const outline = @import("build_options").enable_outline; const worldmarkers = @import("build_options").enable_worldmarkers; const framecrash = @import("build_options").enable_framecrash; const logsessions = @import("build_options").enable_logsessions; const minimapicons = @import("build_options").enable_minimapicons; const transmogfix = @import("build_options").enable_transmogfix; const customassets = @import("build_options").enable_customassets; const healtextfix = @import("build_options").enable_healtextfix; const bigcursor = @import("build_options").enable_bigcursor; const clickthrough = @import("build_options").enable_clickthrough; const dpslog = @import("build_options").enable_dpslog; const transform44 = @import("build_options").enable_transform44; const addonperf = @import("build_options").enable_addonperf; const ssemaths = @import("build_options").enable_ssemaths; const silicon = @import("build_options").enable_silicon; const weirdperformance = @import("build_options").enable_weirdperformance; const superweirdo = @import("build_options").enable_superweirdo; const luagc = @import("build_options").enable_luagc; }; // Conditional module imports const screenshot = if (build_opts.screenshot) @import("screenshot/screenshot.zig") else struct {}; const interact = if (build_opts.interact) @import("interact/interact.zig") else struct {}; const outline = if (build_opts.outline) @import("outline/outline.zig") else struct {}; const markers = if (build_opts.worldmarkers) @import("worldmarkers/worldmarkers.zig") else struct {}; const framecrash = if (build_opts.framecrash) @import("framecrash/framecrash.zig") else struct {}; const logsessions = if (build_opts.logsessions) @import("logsessions/logsessions.zig") else struct {}; 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 customassets = if (build_opts.customassets) @import("customassets/customassets.zig") else struct {}; const healtextfix = if (build_opts.healtextfix) @import("healtextfix/healtextfix.zig") else struct {}; const bigcursor = if (build_opts.bigcursor) @import("bigcursor/bigcursor.zig") else struct {}; const clickthrough = if (build_opts.clickthrough) @import("clickthrough/clickthrough.zig") else struct {}; const dpslog = if (build_opts.dpslog) @import("dpslog/dpslog.zig") else struct {}; const transform44 = if (build_opts.transform44) @import("transform44/transform44.zig") else struct {}; const addonperf = if (build_opts.addonperf) @import("addonperf/addonperf.zig") else struct {}; const ssemaths = if (build_opts.ssemaths) @import("ssemaths/ssemaths.zig") else struct {}; const silicon = if (build_opts.silicon) @import("silicon/silicon.zig") else struct {}; const weirdperformance = if (build_opts.weirdperformance) @import("weirdperformance/weirdperformance.zig") else struct {}; const superweirdo = if (build_opts.superweirdo) @import("superweirdo/superweirdo.zig") else struct {}; const luagc_mod = if (build_opts.luagc) @import("luagc/luagc.zig") else struct {}; const module_active = @import("module_active.zig"); const WINAPI = std.builtin.CallingConvention.winapi; const safe_variant_core = @import("build_options").safe_variant_core; // Standalone compatibility helper. // Older WeirdUtils cores can replace these two File_FindInArchive branches // with NOP NOP. Safe standalone variants restore the original bytes only when // that exact legacy patch is present. fn restoreLegacyFileFindArchiveGates() void { const gate1: usize = 0x654B5C; const gate2: usize = 0x654B6A; const original1 = [2]u8{ 0x74, 0x25 }; const original2 = [2]u8{ 0x75, 0x17 }; if (hook.readMem(u8, gate1) == 0x90 and hook.readMem(u8, gate1 + 1) == 0x90) { hook.writeProtected(gate1, &original1); log.print("compat: restored File_FindInArchive gate 1\n"); } if (hook.readMem(u8, gate2) == 0x90 and hook.readMem(u8, gate2 + 1) == 0x90) { hook.writeProtected(gate2, &original2); log.print("compat: restored File_FindInArchive gate 2\n"); } } // ============================================================================= // Lua Protection Bypass // ============================================================================= var protection_hook: hook.Detour(fn () callconv(hook.cc.stdcall) void) = .{}; fn luaProtectionDetour() callconv(hook.cc.stdcall) void {} // ============================================================================= // Lua C API wrappers (WoW 1.12.1 - all __fastcall, L in ECX) // ============================================================================= pub const lua = @import("lua.zig"); // ============================================================================= // Game function wrappers // ============================================================================= noinline fn registerFunction(name: [*:0]const u8, func_addr: usize) void { // FrameScript_RegisterFunction is __fastcall: // ECX = const char *name // EDX = lua_CFunction func // // Do not route this through hook.call here. With the current Zig/zhook // combination that path was emitted as two stack pushes, so the engine // consumed stale ECX/EDX values left by the previous hook in the chain. // Force the verified WoW 1.12.1 ABI explicitly. var eax_clobber: usize = undefined; asm volatile ( \\mov $0x704120, %%eax \\call *%%eax : [eax] "={eax}" (eax_clobber), : [name] "{ecx}" (@intFromPtr(name)), [func] "{edx}" (func_addr), ); } fn allocateGameBuffer(size: u32) ?[*]u8 { return hook.call(fn (u32, u32, u32, u32) callconv(hook.cc.stdcall) ?[*]u8, 0x6462E0, .{ size, @intFromPtr(@as([*:0]const u8, "weirdutils")), 0, 0, }); } // ============================================================================= // Custom C functions (callable from Lua) // ============================================================================= fn registerLuaFunctions() void { // Conditional module functions if (build_opts.interact) { registerFunction("InteractNearest", @intFromPtr(&interact.interactNearest)); registerFunction("LootAllCorpses", @intFromPtr(&interact.lootAllCorpses)); } if (build_opts.outline) { registerFunction("OutlineCommand", @intFromPtr(&outline.outlineCommand)); registerFunction("OutlineSyncTarget", @intFromPtr(&outline.outlineSyncTarget)); registerFunction("OutlineDebug", @intFromPtr(&outline.outlineDebug)); } if (build_opts.logsessions) { registerFunction("GetCombatLogPath", @intFromPtr(&logsessions.luaGetCombatLogPath)); registerFunction("GetChatLogPath", @intFromPtr(&logsessions.luaGetChatLogPath)); } if (build_opts.bigcursor) { registerFunction("SetCursorScale", @intFromPtr(&bigcursor.luaSetCursorScale)); registerFunction("GetCursorScale", @intFromPtr(&bigcursor.luaGetCursorScale)); } if (build_opts.minimapicons and minimapicons.isActive()) { const lib = [_]lua.LuaReg{ .{ .name = "SetObjectTypeBlip", .func = @intFromPtr(&minimapicons.luaSetObjectTypeBlip) }, .{ .name = "SetMinimapCityToggle", .func = @intFromPtr(&minimapicons.luaSetCityToggle) }, .{ .name = null, .func = 0 }, }; lua.openlib(lua.getContext(), "WeirdUtils", &lib, 0); } if (build_opts.transform44) { registerFunction("SetWeatherOverride", @intFromPtr(&transform44.luaSetWeatherOverride)); } if (build_opts.dpslog) { registerFunction("GetSpellInfo", @intFromPtr(&dpslog.luaGetSpellInfo)); registerFunction("CombatLogGetCurrentEventInfo", @intFromPtr(&dpslog.luaCombatLogGetCurrentEventInfo)); registerFunction("UnitCastingInfo", @intFromPtr(&dpslog.luaUnitCastingInfo)); registerFunction("UnitChannelInfo", @intFromPtr(&dpslog.luaUnitChannelInfo)); } if (build_opts.addonperf) { registerFunction("GetAddOnMemoryUsage", @intFromPtr(&addonperf.luaGetAddOnMemoryUsage)); registerFunction("UpdateAddOnMemoryUsage", @intFromPtr(&addonperf.luaUpdateAddOnMemoryUsage)); registerFunction("GetAddOnCPUUsage", @intFromPtr(&addonperf.luaGetAddOnCPUUsage)); registerFunction("UpdateAddOnCPUUsage", @intFromPtr(&addonperf.luaUpdateAddOnCPUUsage)); registerFunction("ResetAddOnCPUUsage", @intFromPtr(&addonperf.luaResetAddOnCPUUsage)); registerFunction("GetScriptCPUUsage", @intFromPtr(&addonperf.luaGetScriptCPUUsage)); } if (build_opts.luagc and luagc_mod.isActive()) { registerFunction("ZGCStats", @intFromPtr(&luagc_mod.luaZGCStats)); registerFunction("ZGCTune", @intFromPtr(&luagc_mod.luaZGCTune)); } if (build_opts.worldmarkers and markers.isActive()) { // User-facing functions stay global registerFunction("WorldMarker", @intFromPtr(&markers.luaWorldMarker)); registerFunction("ClearWorldMarker", @intFromPtr(&markers.luaClearWorldMarker)); registerFunction("GetWorldMarker", @intFromPtr(&markers.luaGetWorldMarker)); registerFunction("CanSetWorldMarker", @intFromPtr(&markers.luaCanSetMarkers)); // Sync functions in WorldMarkers table (used by addon message handler) const lib = [_]lua.LuaReg{ .{ .name = "SetMarkerSync", .func = @intFromPtr(&markers.luaSetMarkerDef) }, .{ .name = "ClearMarkerSync", .func = @intFromPtr(&markers.luaClearMarkerDef) }, .{ .name = null, .func = 0 }, // sentinel }; lua.openlib(lua.getContext(), "WorldMarkers", &lib, 0); } } // ============================================================================= // Module version registry — GetWeirdUtilsVersion(name?) from Lua // ============================================================================= // // Populates a global "WeirdUtils" table with { module_name = "version", ... } // for each enabled module. If the table already exists (from another DLL), // new entries are merged in additively. // // GetWeirdUtilsVersion() → returns the full WeirdUtils table // GetWeirdUtilsVersion("name") → returns version string for that module, or nil const all_module_names = @import("build_options").all_module_names; const all_module_versions = @import("build_options").all_module_versions; fn registerModuleVersions() void { const L = lua.getContext(); // Get or create the WeirdUtils global table lua.getglobal(L, "WeirdUtils"); if (lua.typeOf(L, -1) != 5) { // 5 = LUA_TTABLE lua.pop(L, 1); lua.newtable(L); } // For each enabled module, set WeirdUtils[name] = version inline for (all_module_names, 0..) |name, i| { const enabled = @field(@import("build_options"), "enable_" ++ name); if (enabled) { lua.pushstring(L, @ptrCast(name.ptr)); lua.pushstring(L, @ptrCast(all_module_versions[i].ptr)); lua.settable(L, -3); } } // Set as global lua.setglobal(L, "WeirdUtils"); // Register query function registerFunction("GetWeirdUtilsVersion", @intFromPtr(&luaGetWeirdUtilsVersion)); } fn luaGetWeirdUtilsVersion(L_ecx: usize) callconv(hook.cc.fastcall) u32 { const L: lua.State = @ptrFromInt(L_ecx); const nargs = lua.gettop(L); if (nargs >= 1 and lua.isstring(L, 1)) { // GetWeirdUtilsVersion("name") → return version or nil const name = lua.tostring(L, 1) orelse { lua.pushnil(L); return 1; }; lua.getglobal(L, "WeirdUtils"); if (lua.typeOf(L, -1) != 5) { lua.pop(L, 1); lua.pushnil(L); return 1; } lua.pushstring(L, name); lua.gettable(L, -2); lua.remove(L, -2); // remove WeirdUtils table, leave value return 1; } // GetWeirdUtilsVersion() → return the whole table lua.getglobal(L, "WeirdUtils"); return 1; } // ============================================================================= // Embedded addon files // ============================================================================= const build_options = @import("build_options"); const addons = @import("addons.zig"); const findEmbeddedFile = addons.findEmbeddedFile; // ============================================================================= // Hook: LoadFileWithTextureResourceFallback (0x648620) // ============================================================================= const LoadFileFn = fn (u32, [*:0]const u8, *?[*]u8, ?*u32, u32, u32, u32) callconv(hook.cc.stdcall) u32; var file_hook: hook.Detour(LoadFileFn) = .{}; fn loadFileDetour( unk: u32, path: [*:0]const u8, buf_out: *?[*]u8, size_out: ?*u32, extra_alloc: u32, flags: u32, async_ptr: u32, ) callconv(hook.cc.stdcall) u32 { if (findEmbeddedFile(path)) |entry| { const data_len: u32 = @intCast(entry.data.len); const total = data_len + extra_alloc; const buf = allocateGameBuffer(total) orelse return 0; @memcpy(buf[0..entry.data.len], entry.data); if (extra_alloc > 0) { @memset(buf[entry.data.len..][0..extra_alloc], 0); } buf_out.* = buf; if (size_out) |s| s.* = data_len; log.fmt("served embedded: {s} ({d} bytes)\n", .{ std.mem.span(path), data_len }); return 1; } return file_hook.callOriginal(.{ unk, path, buf_out, size_out, extra_alloc, flags, async_ptr }); } // ============================================================================= // In-memory file serving hooks (Storm file I/O layer) // ============================================================================= // // M2 model loading bypasses our LoadFileWithTextureResourceFallback hook. // These hooks intercept at the lower Storm file I/O level (openFileWithOptions // and friends) to create fake file contexts for embedded files and serve data // from memory. // // Fake context detection: type==0, handle(+0x04)==NULL, embedded_ptr(+0x30)!=0 const OpenFileFn = fn (u32, [*:0]const u8, u32, *u32) callconv(hook.cc.stdcall) u32; var open_file_hook: hook.Detour(OpenFileFn) = .{}; const GetFileSizeFn = fn (u32, ?*u32) callconv(hook.cc.stdcall) u32; var get_file_size_hook: hook.Detour(GetFileSizeFn) = .{}; const ReadFileFn = fn (u32, [*]u8, u32, ?*u32, u32, u32) callconv(hook.cc.stdcall) u32; var read_file_hook: hook.Detour(ReadFileFn) = .{}; const CleanupFileFn = fn (u32) callconv(hook.cc.stdcall) void; var cleanup_file_handle_hook: hook.Detour(CleanupFileFn) = .{}; const ProcessAsyncFn = fn (u32) callconv(hook.cc.fastcall) void; var process_async_hook: hook.Detour(ProcessAsyncFn) = .{}; const LoadModelFn = fn (u32, u32, u32) callconv(hook.cc.thiscall) u32; var model_load_hook: hook.Detour(LoadModelFn) = .{}; // Windows API imports for async handling extern "kernel32" fn EnterCriticalSection(lpCriticalSection: *anyopaque) callconv(WINAPI) void; extern "kernel32" fn LeaveCriticalSection(lpCriticalSection: *anyopaque) callconv(WINAPI) void; extern "kernel32" fn SetEvent(hEvent: *anyopaque) callconv(WINAPI) i32; /// Check if a file context is one of our fakes fn isFakeFileContext(ctx_addr: u32) bool { if (ctx_addr < 0x10000) return false; return hook.readMem(u32, ctx_addr) == 0 and // type == 0 (disk) hook.readMem(u32, ctx_addr + 0x04) == 0 and // handle == NULL hook.readMem(u32, ctx_addr + 0x30) != 0; // embedded ptr set } /// Call initializeFileContext (0x647290) - __thiscall(ECX=ctx, type) fn callInitFileContext(ctx: [*]u8, file_type: u32) void { hook.call(fn (u32, u32) callconv(hook.cc.thiscall) void, 0x647290, .{ @intFromPtr(ctx), file_type }); } /// Call cleanupFileContext (0x6472d0) - __thiscall(ECX=ctx) fn callCleanupFileContext(ctx: [*]u8) void { hook.call(fn (u32) callconv(hook.cc.thiscall) void, 0x6472d0, .{@intFromPtr(ctx)}); } /// Free a buffer via FreeMemory/SMemFree (0x646430) - __stdcall(ptr, src, flags) fn freeGameBuffer(ptr: [*]u8) void { hook.call(fn (u32, u32, u32) callconv(hook.cc.stdcall) void, 0x646430, .{ @intFromPtr(ptr), @intFromPtr(@as([*:0]const u8, "weirdutils")), 0xffffffff, }); } // --- Hook 1: openFileWithOptions (0x6477c0) --- fn openFileDetour( archive_ptr: u32, path: [*:0]const u8, flags: u32, handle_out: *u32, ) callconv(hook.cc.stdcall) u32 { if (findEmbeddedFile(path)) |entry| { // Allocate and zero-fill 0x60-byte file context const ctx = allocateGameBuffer(0x60) orelse { handle_out.* = 0; return 0; }; @memset(ctx[0..0x60], 0); // Initialize critical section and set type=0 callInitFileContext(ctx, 0); // Store embedded data pointer and size in unused fields @as(*align(1) u32, @ptrFromInt(@intFromPtr(ctx) + 0x30)).* = @intCast(@intFromPtr(entry.data.ptr)); @as(*align(1) u32, @ptrFromInt(@intFromPtr(ctx) + 0x34)).* = @intCast(entry.data.len); // Duplicate path string at +0x0C for game's use const path_span = std.mem.span(path); const path_len: u32 = @intCast(path_span.len + 1); if (allocateGameBuffer(path_len)) |path_buf| { @memcpy(path_buf[0..path_span.len], path_span); path_buf[path_span.len] = 0; @as(*align(1) u32, @ptrFromInt(@intFromPtr(ctx) + 0x0C)).* = @intFromPtr(path_buf); } handle_out.* = @intFromPtr(ctx); log.fmt("fake ctx @0x{x}: {s} ({d} bytes)\n", .{ @intFromPtr(ctx), path_span, entry.data.len }); return 2; // success (non-zero type code) } return open_file_hook.callOriginal(.{ archive_ptr, path, flags, handle_out }); } // --- Hook 2: GetFileSizeFromHandle (0x6487f0) --- fn getFileSizeDetour( file_ctx: u32, high_size_out: ?*u32, ) callconv(hook.cc.stdcall) u32 { if (isFakeFileContext(file_ctx)) { if (high_size_out) |h| h.* = 0; const size = hook.readMem(u32, file_ctx + 0x34); log.fmt("getFileSize fake @0x{x} = {d}\n", .{ file_ctx, size }); return size; } return get_file_size_hook.callOriginal(.{ file_ctx, high_size_out }); } // --- Hook 3: ReadFileFromMultipleSources (0x648460) --- fn readFileDetour( ctx: u32, buffer: [*]u8, size: u32, bytes_read_out: ?*u32, async_ptr: u32, param6: u32, ) callconv(hook.cc.stdcall) u32 { if (isFakeFileContext(ctx)) { const data_ptr = hook.readMem(u32, ctx + 0x30); const data_size = hook.readMem(u32, ctx + 0x34); const read_size = @min(size, data_size); log.fmt("readFile fake @0x{x} size={d}/{d} async=0x{x}\n", .{ ctx, read_size, data_size, async_ptr }); const src: [*]const u8 = @ptrFromInt(data_ptr); @memcpy(buffer[0..read_size], src[0..read_size]); if (bytes_read_out) |out| out.* = read_size; // If async, signal the completion event immediately if (async_ptr != 0) { const event_handle = hook.readMem(u32, async_ptr + 4); if (event_handle != 0) { _ = SetEvent(@ptrFromInt(event_handle)); } } return 1; // success } return read_file_hook.callOriginal(.{ ctx, buffer, size, bytes_read_out, async_ptr, param6 }); } // --- Hook 4: processAsyncFileOperation (0x647350) --- fn processAsyncDetour(param1: u32) callconv(hook.cc.fastcall) void { const ctx_addr = hook.readMem(u32, param1 + 0x08); if (isFakeFileContext(ctx_addr)) { // Enter critical section (same as original prologue) EnterCriticalSection(@ptrFromInt(ctx_addr + 0x24)); // Read request fields const dest = hook.readMem(u32, param1 + 0x0C); const req_size = hook.readMem(u32, param1 + 0x10); const data_ptr = hook.readMem(u32, ctx_addr + 0x30); const data_size = hook.readMem(u32, ctx_addr + 0x34); const read_size = @min(req_size, data_size); // Copy embedded data to destination buffer const src: [*]const u8 = @ptrFromInt(data_ptr); const dst: [*]u8 = @ptrFromInt(dest); @memcpy(dst[0..read_size], src[0..read_size]); // Replicate cleanup epilogue from original function: // 1. Decrement refcount at *(ctx + 0x5c) const rc_addr = ctx_addr + 0x5c; const cur_rc = hook.readMem(i32, rc_addr); @as(*align(1) i32, @ptrFromInt(rc_addr)).* = cur_rc - 1; // 2. Leave critical section LeaveCriticalSection(@ptrFromInt(ctx_addr + 0x24)); // 3. Signal completion event: *(*(param1 + 0x14) + 4) const seek_struct = hook.readMem(u32, param1 + 0x14); if (seek_struct != 0) { const event_handle = hook.readMem(u32, seek_struct + 4); if (event_handle != 0) { _ = SetEvent(@ptrFromInt(event_handle)); } } // 4. If close-after-read flag at *(ctx + 0x58) is set, clean up const close_flag = hook.readMem(u32, ctx_addr + 0x58); if (close_flag != 0) { cleanupFileHandleDetour(ctx_addr); } return; } // Not our fake - call original process_async_hook.callOriginal(.{param1}); } // --- Hook 6: CleanupFileHandleResources (0x648730) --- fn cleanupFileHandleDetour(file_ctx: u32) callconv(hook.cc.stdcall) void { const fake = isFakeFileContext(file_ctx); if (fake) { const path_ptr = hook.readMem(u32, file_ctx + 0x0C); if (path_ptr != 0) { const path: [*:0]const u8 = @ptrFromInt(path_ptr); log.fmt("cleanup FAKE @0x{x}: {s}\n", .{ file_ctx, std.mem.span(path) }); } else { log.fmt("cleanup FAKE @0x{x}: (no path)\n", .{file_ctx}); } } // Always use original CleanupFileHandleResources - it handles fake contexts correctly // (NULL-safe checks on +0x04/+0x3C/+0x40/+0x08, then cleanupFileContext + FreeMemory). cleanup_file_handle_hook.callOriginal(.{file_ctx}); if (fake) log.fmt("cleanup FAKE @0x{x} done\n", .{file_ctx}); } // --- Hook 5: loadModelFromFileAsync (0x71d4e0) --- fn loadModelAsyncDetour(model: u32, file_handle: u32, should_use_callback: u32) callconv(hook.cc.thiscall) u32 { // file_handle IS the file context address directly (Ghidra shows pointer* but // the assembly pushes it directly to GetFileSizeFromHandle - no dereference) if (isFakeFileContext(file_handle)) { log.fmt("loadModelAsync: model=0x{x} fh=0x{x} cb={d}\n", .{ model, file_handle, should_use_callback }); const data_ptr = hook.readMem(u32, file_handle + 0x30); const data_size = hook.readMem(u32, file_handle + 0x34); log.fmt(" embed_ptr=0x{x} embed_size={d}\n", .{ data_ptr, data_size }); // Toggle callback flag (bit 1 of model+8) based on shouldUseCallback const flags = hook.readMem(u32, model + 0x08); if (should_use_callback != 0) { @as(*align(1) u32, @ptrFromInt(model + 0x08)).* = flags | 2; } else { @as(*align(1) u32, @ptrFromInt(model + 0x08)).* = flags & ~@as(u32, 2); } // Store size in model first (original does this before allocation) @as(*align(1) u32, @ptrFromInt(model + 0x134)).* = data_size; // Allocate buffer via setCullMode (0x71f9a0) - same as original path // setCullMode is __fastcall(ECX=size), returns buffer pointer const buffer_addr = hook.call(fn (u32) callconv(hook.cc.fastcall) u32, 0x71f9a0, .{data_size}); if (buffer_addr == 0) { log.print(" setCullMode alloc failed\n"); return 0; } log.fmt(" buffer=0x{x}\n", .{buffer_addr}); // Store buffer in model object @as(*align(1) u32, @ptrFromInt(model + 0x130)).* = buffer_addr; // Copy embedded data into the allocated buffer const buffer: [*]u8 = @ptrFromInt(buffer_addr); const src: [*]const u8 = @ptrFromInt(data_ptr); @memcpy(buffer[0..data_size], src[0..data_size]); log.print(" memcpy done\n"); // No async task - set task pointer to NULL @as(*align(1) u32, @ptrFromInt(model + 0x0c)).* = 0; log.print(" task=0 set\n"); // Match onModelLoadComplete ordering: clean up file handle BEFORE processing. // The original async flow does: CleanupFileHandleResources → ReturnAsyncTaskToPool // → model+0x0c=0 → processLoadedModelData. We must close the file context before // processLoadedModelData runs, because initializeModelResources creates texture // async tasks that interact with the file I/O system. // Call original CleanupFileHandleResources through the trampoline (bypasses our // detour). Must clean up file context before processLoadedModelData runs. log.fmt(" cleanup via trampoline fh=0x{x}\n", .{file_handle}); cleanup_file_handle_hook.callOriginal(.{file_handle}); log.print(" cleanup done\n"); // Dump model fields before processLoadedModelData log.fmt(" PRE model+0x0c=0x{x} +0x130=0x{x} +0x134=0x{x} +0x138=0x{x}\n", .{ hook.readMem(u32, model + 0x0c), hook.readMem(u32, model + 0x130), hook.readMem(u32, model + 0x134), hook.readMem(u32, model + 0x138), }); // Call processLoadedModelData directly - __fastcall(ECX=model) log.fmt(" calling processLoadedModelData(0x{x})...\n", .{model}); const result = hook.call(fn (u32) callconv(hook.cc.fastcall) u32, 0x71d640, .{model}); log.print(" processLoadedModelData returned\n"); log.fmt(" result=0x{x}\n", .{result}); // Dump model fields after processLoadedModelData - check if texture async task was created log.print(" POST dump:\n"); log.fmt(" POST model+0x0c=0x{x} +0x130=0x{x} +0x134=0x{x} +0x138=0x{x}\n", .{ hook.readMem(u32, model + 0x0c), hook.readMem(u32, model + 0x130), hook.readMem(u32, model + 0x134), hook.readMem(u32, model + 0x138), }); log.fmt(" sync loaded {d} bytes, returning 1\n", .{data_size}); log.print(" === loadModelAsyncDetour EXIT ===\n"); return 1; } // Not our fake - call original return model_load_hook.callOriginal(.{ model, file_handle, should_use_callback }); } // --- Hook 6: CheckFileExistence (0x654DD0) --- // __fastcall(ECX=filename, EDX=flags, stack=outputBuffer) → EAX (bool) // Tells the game whether a file exists in the VFS. Without this, embedded // files are invisible to preloadFileWithFlags, so LoadAddonRecursive skips // Bindings.xml (and SavedVariables) for our addons. const CheckFileExistenceFn = fn (u32, u32, u32) callconv(hook.cc.fastcall) u32; var cfe_hook: hook.Detour(CheckFileExistenceFn) = .{}; fn checkFileExistenceDetour(filename_ptr: u32, flags: u32, output_buffer_ptr: u32) callconv(hook.cc.fastcall) u32 { if (filename_ptr != 0) { // Embedded DLL files const path: [*:0]const u8 = @ptrFromInt(filename_ptr); if (findEmbeddedFile(path) != null) return 1; // Loose disk files (customassets module) if (build_opts.customassets and customassets.isActive()) { if (customassets.looseFilesLookup(filename_ptr, output_buffer_ptr)) return 1; } } return cfe_hook.callOriginal(.{ filename_ptr, flags, output_buffer_ptr }); } // --- Hook 7: File_FindInArchive (0x6549a0) — archive cache short-circuit --- // Intercepts the core MPQ file lookup to skip both chain walk and hash lookup // on repeat opens. Caches {outer_archive, inner_archive, block_entry} per file. // // --- Install/remove in-memory file hooks --- fn installFileHooks() void { _ = open_file_hook.attach(0x6477c0, &openFileDetour); _ = get_file_size_hook.attach(0x6487f0, &getFileSizeDetour); _ = read_file_hook.attach(0x648460, &readFileDetour); _ = cleanup_file_handle_hook.attach(0x648730, &cleanupFileHandleDetour); _ = model_load_hook.attach(0x71d4e0, &loadModelAsyncDetour); _ = cfe_hook.attach(0x654DD0, &checkFileExistenceDetour); // NOP two gates in File_FindInArchive that prevent CheckFileExistence // from running. Without this, preloadFileWithFlags never reaches our // hook and the game skips Bindings.xml for embedded addons. // (customassets also does this for loose files; the patches are idempotent.) nopFileExistenceGates(); log.print("in-memory file hooks installed\n"); } // File_FindInArchive (0x6549a0) has two conditional jumps that skip the // call to CheckFileExistence (0x654DD0). NOP them so our hook always runs. var old_gate1: [2]u8 = undefined; var gate1_patched: bool = false; var old_gate2: [2]u8 = undefined; var gate2_patched: bool = false; fn nopFileExistenceGates() void { const nops = [2]u8{ 0x90, 0x90 }; // Gate 1: JZ at 0x654b5c (74 25) -- skips CheckFileExistence if (hook.readMem(u8, 0x654b5c) == 0x74 and hook.readMem(u8, 0x654b5d) == 0x25) { old_gate1 = .{ 0x74, 0x25 }; hook.writeProtected(0x654b5c, &nops); gate1_patched = true; } // Gate 2: JNZ at 0x654b6a (75 17) -- skips CheckFileExistence if (hook.readMem(u8, 0x654b6a) == 0x75 and hook.readMem(u8, 0x654b6b) == 0x17) { old_gate2 = .{ 0x75, 0x17 }; hook.writeProtected(0x654b6a, &nops); gate2_patched = true; } } fn revertFileExistenceGates() void { if (gate1_patched) { hook.writeProtected(0x654b5c, &old_gate1); gate1_patched = false; } if (gate2_patched) { hook.writeProtected(0x654b6a, &old_gate2); gate2_patched = false; } } fn removeFileHooks() void { revertFileExistenceGates(); cfe_hook.detach(); model_load_hook.detach(); process_async_hook.detach(); cleanup_file_handle_hook.detach(); read_file_hook.detach(); get_file_size_hook.detach(); open_file_hook.detach(); } // ============================================================================= // Hook: LoadScriptFunctions (0x490250) // ============================================================================= var register_commands_hook: hook.Detour(fn () callconv(hook.cc.stdcall) void) = .{}; var glue_commands_hook: hook.Detour(fn () callconv(hook.cc.stdcall) void) = .{}; /// Hook for Player_LoadScriptFunctions (0x490250). /// Fires after login/reload — registers gameplay Lua functions + version table. fn registerAllSystemCommandsDetour() callconv(hook.cc.stdcall) void { register_commands_hook.callOriginal(.{}); registerLuaFunctions(); registerModuleVersions(); } // Safe standalone Outline only needs its Lua command to be re-registered // after login/reload. It deliberately does not touch the shared WeirdUtils // version table, avoiding nested Lua-table mutations across multiple DLLs. fn registerOutlineCommandsDetour() callconv(hook.cc.stdcall) void { register_commands_hook.callOriginal(.{}); if (build_opts.outline) { registerLuaFunctions(); } } /// Hook for Glue_LoadScriptFunctions (0x46ABB0). /// Fires at the login/glue screen — registers version table so addons can query early. fn glueLoadScriptFunctionsDetour() callconv(hook.cc.stdcall) void { glue_commands_hook.callOriginal(.{}); registerModuleVersions(); } // ============================================================================= // Hook: GameEngine_MainInitialize (0x46a400) // ============================================================================= var engine_init_hook: hook.Detour(fn () callconv(hook.cc.stdcall) void) = .{}; fn engineInitDetour() callconv(hook.cc.stdcall) void { engine_init_hook.callOriginal(.{}); if (safe_variant_core and build_opts.outline) { restoreLegacyFileFindArchiveGates(); } if (build_opts.screenshot) { screenshot.installHook(); } if (build_opts.outline) { _ = outline.init(); } if (build_opts.healtextfix) { healtextfix.lateInit(); } if (build_opts.bigcursor) { bigcursor.lateInit(); } if (build_opts.transform44) { transform44.lateInit(); } if (build_opts.ssemaths) { ssemaths.lateInit(); } if (build_opts.silicon) { silicon.lateInit(); } if (build_opts.weirdperformance) { weirdperformance.lateInit(); } } // ============================================================================= // Hook: World_HandleLogoutCleanup (0x491180) // Fires on real character logout/disconnect only - NOT on /reload or map change. // ============================================================================= var logout_hook: hook.Detour(fn () callconv(hook.cc.stdcall) void) = .{}; fn logoutDetour() callconv(hook.cc.stdcall) void { log.print("World_HandleLogoutCleanup -- player logout\n"); // Reset per-session state - only on real logout/disconnect, not /reload. if (build_opts.worldmarkers) markers.onShutdown(); if (build_opts.minimapicons) minimapicons.onShutdown(); if (build_opts.clickthrough) clickthrough.onShutdown(); if (build_opts.logsessions) logsessions.onShutdown(); // Clean up world objects BEFORE game teardown - modules with // remove_on_shutdown must destroy while game systems are alive. 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(); } } logout_hook.callOriginal(.{}); } // ============================================================================= // Hook: CGGameUI_Shutdown (0x490BD0) // ============================================================================= var shutdown_hook: hook.Detour(fn () callconv(hook.cc.stdcall) void) = .{}; // ============================================================================= // Module lifecycle - single table drives install, shutdown, and uninstall. // Adding a module here guarantees all three phases are handled. // ============================================================================= const ModuleHooks = struct { name: ?[*:0]const u8 = null, install: ?*const fn () void = null, remove: ?*const fn () void = null, is_active: ?*const fn () bool = 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.customassets) .{ .name = customassets.module_name, .install = customassets.installHooks, .remove = customassets.removeHooks, .is_active = customassets.isActive } else .{}, if (build_opts.framecrash) .{ .name = framecrash.module_name, .install = framecrash.installHooks, .remove = framecrash.removeHooks, .is_active = framecrash.isActive } else .{}, if (build_opts.logsessions) .{ .name = logsessions.module_name, .install = logsessions.installHooks, .remove = logsessions.removeHooks, .is_active = logsessions.isActive } else .{}, if (build_opts.transmogfix) .{ .name = transmogfix.module_name, .install = transmogfix.installHooks, .remove = transmogfix.removeHooks, .is_active = transmogfix.isActive } else .{}, if (build_opts.minimapicons) .{ .name = minimapicons.module_name, .install = minimapicons.installHooks, .remove = minimapicons.removeHooks, .is_active = minimapicons.isActive } else .{}, if (build_opts.healtextfix) .{ .name = healtextfix.module_name, .install = healtextfix.installHooks, .remove = healtextfix.removeHooks, .is_active = healtextfix.isActive } else .{}, if (build_opts.bigcursor) .{ .name = bigcursor.module_name, .install = bigcursor.installHooks, .remove = bigcursor.removeHooks, .is_active = bigcursor.isActive } else .{}, if (build_opts.clickthrough) .{ .name = clickthrough.module_name, .install = clickthrough.installHooks, .remove = clickthrough.removeHooks, .is_active = clickthrough.isActive } else .{}, if (build_opts.dpslog) .{ .name = dpslog.module_name, .install = dpslog.installHooks, .remove = dpslog.removeHooks, .is_active = dpslog.isActive } else .{}, if (build_opts.transform44) .{ .name = transform44.module_name, .install = transform44.installHooks, .remove = transform44.removeHooks, .is_active = transform44.isActive } else .{}, if (build_opts.addonperf) .{ .name = addonperf.module_name, .install = addonperf.installHooks, .remove = addonperf.removeHooks, .is_active = addonperf.isActive } else .{}, if (build_opts.ssemaths) .{ .name = ssemaths.module_name, .install = ssemaths.installHooks, .remove = ssemaths.removeHooks, .is_active = ssemaths.isActive } else .{}, if (build_opts.worldmarkers) .{ .name = markers.module_name, .install = markers.installHooks, .remove = markers.removeHooks, .is_active = markers.isActive } else .{}, if (build_opts.interact) .{ .name = interact.module_name, .install = interact.installHooks, .remove = interact.removeHooks, .is_active = interact.isActive } else .{}, if (build_opts.outline) .{ .name = outline.module_name, .remove = outline.cleanup, .is_active = outline.isActive } else .{}, if (build_opts.screenshot) .{ .name = screenshot.module_name, .remove = screenshot.removeHook, .is_active = screenshot.isActive } else .{}, if (build_opts.silicon) .{ .name = silicon.module_name, .install = silicon.installHooks, .remove = silicon.removeHooks, .is_active = silicon.isActive } else .{}, if (build_opts.weirdperformance) .{ .name = weirdperformance.module_name, .install = weirdperformance.installHooks, .remove = weirdperformance.removeHooks, .is_active = weirdperformance.isActive, .remove_on_shutdown = true } else .{}, if (build_opts.superweirdo) .{ .name = superweirdo.module_name, .install = superweirdo.installHooks, .remove = superweirdo.removeHooks, .is_active = superweirdo.isActive } else .{}, if (build_opts.luagc) .{ .name = luagc_mod.module_name, .install = luagc_mod.installHooks, .remove = luagc_mod.removeHooks, .is_active = luagc_mod.isActive } else .{}, }; fn shutdownDetour() callconv(hook.cc.stdcall) void { log.print("CGGameUI_Shutdown\n"); // Per-session resets and remove_on_shutdown cleanup live in logoutDetour // (World_HandleLogoutCleanup) - fires on real logout/disconnect only, not /reload. shutdown_hook.callOriginal(.{}); } // ============================================================================= // Init / Cleanup // ============================================================================= fn install() void { logging.init(); log = logging.Logger.open("weirdutils", .console); if (safe_variant_core) { log.print("Installing minimal standalone core\n"); // customassets needs only CheckFileExistence for loose Data files. // Do not install embedded-file hooks and do not NOP File_FindInArchive. if (build_opts.customassets) { _ = cfe_hook.attach(0x654DD0, &checkFileExistenceDetour); } // Outline needs one Lua registration hook so /outline survives /reload, // plus the engine-init hook for its deferred renderer initialization. if (build_opts.outline) { _ = register_commands_hook.attach(0x490250, ®isterOutlineCommandsDetour); _ = engine_init_hook.attach(0x46a400, &engineInitDetour); } // Install only the actual module hooks. inline for (modules) |m| { if (m.install) |inst| inst(); if (m.name) |name| { if (m.is_active) |active_fn| module_active.register(name, active_fn); } } // The user's original WeirdPerformance loads before these variants. // Repair only its legacy MPQ gate NOPs; leave WeirdPerformance itself untouched. if (build_opts.customassets or build_opts.outline) { restoreLegacyFileFindArchiveGates(); } // No embedded addon registration in minimal variants. // Outline's addon is supplied normally under Interface\\AddOns. return; } // Core hooks chain safely across multiple DLLs via zhook's E9-detect path: // each DLL's trampoline JMPs to the previous DLL's detour, forming a LIFO // call chain. Callbacks are additive (Lua registration, file serving) or // guarded by per-module mutexes (lateInit, onShutdown). DLL_PROCESS_DETACH // fires in reverse load order, so detach unwinds the chain correctly. log.print("Installing core hooks\n"); _ = protection_hook.attach(0x42a320, &luaProtectionDetour); installFileHooks(); _ = file_hook.attach(0x648620, &loadFileDetour); _ = register_commands_hook.attach(0x490250, ®isterAllSystemCommandsDetour); _ = glue_commands_hook.attach(0x46ABB0, &glueLoadScriptFunctionsDetour); _ = engine_init_hook.attach(0x46a400, &engineInitDetour); _ = logout_hook.attach(0x491180, &logoutDetour); _ = shutdown_hook.attach(0x490BD0, &shutdownDetour); // Module hooks always run (each module has its own mutex) inline for (modules) |m| { if (m.install) |inst| inst(); if (m.name) |name| { if (m.is_active) |f| module_active.register(name, f); } } // Addon registration runs for every DLL (LoadAddonTOC is idempotent). // Each DLL only registers its own compiled-in module addons. addons.install(); } fn uninstall() void { if (safe_variant_core) { comptime var j = modules.len; inline while (j > 0) { j -= 1; if (modules[j].remove) |rm| rm(); } if (build_opts.outline) { engine_init_hook.detach(); register_commands_hook.detach(); } if (build_opts.customassets) { cfe_hook.detach(); } logging.deinit(); return; } // Remove modules in reverse order comptime var i = modules.len; inline while (i > 0) { i -= 1; if (modules[i].remove) |rm| rm(); } addons.uninstall(); shutdown_hook.detach(); logout_hook.detach(); engine_init_hook.detach(); register_commands_hook.detach(); glue_commands_hook.detach(); file_hook.detach(); removeFileHooks(); protection_hook.detach(); logging.deinit(); } // ============================================================================= // Runtime Module Control API - exported for other DLLs // ============================================================================= fn asciiEqlIgnoreCase(a: [*:0]const u8, b: [*:0]const u8) bool { var i: usize = 0; while (true) : (i += 1) { const ca = a[i]; const cb = b[i]; if (ca == 0 and cb == 0) return true; if (ca == 0 or cb == 0) return false; const la = if (ca >= 'A' and ca <= 'Z') ca + 32 else ca; const lb = if (cb >= 'A' and cb <= 'Z') cb + 32 else cb; if (la != lb) return false; } } /// Returns 1 if the named module is compiled in AND currently active, 0 otherwise. fn isModuleActive(name: [*:0]const u8) callconv(.c) i32 { inline for (modules) |m| { if (m.name) |mod_name| { if (asciiEqlIgnoreCase(name, mod_name)) { if (m.is_active) |active_fn| { return if (active_fn()) 1 else 0; } return 0; } } } return 0; } /// Disables the named module by calling its remove function. /// Returns 1 if found and removed, 0 if not found or not compiled in. fn disableModule(name: [*:0]const u8) callconv(.c) i32 { inline for (modules) |m| { if (m.name) |mod_name| { if (asciiEqlIgnoreCase(name, mod_name)) { if (m.remove) |rm| { rm(); addons.pruneInactivePrefixes(); return 1; } return 0; } } } return 0; } /// Disables all modules in reverse order. /// Core hooks are left intact since other DLLs may depend on them. /// Returns the number of modules that were disabled. fn disableAll() callconv(.c) i32 { var count: i32 = 0; comptime var i = modules.len; inline while (i > 0) { i -= 1; if (modules[i].remove) |rm| { rm(); count += 1; } } addons.pruneInactivePrefixes(); return count; } comptime { @export(&isModuleActive, .{ .name = "WeirdUtils_IsModuleActive" }); @export(&disableModule, .{ .name = "WeirdUtils_DisableModule" }); @export(&disableAll, .{ .name = "WeirdUtils_DisableAll" }); } // ============================================================================= // DLL entry point // ============================================================================= pub export fn DllMain( _: ?*anyopaque, reason: u32, _: ?*anyopaque, ) callconv(WINAPI) std.os.windows.BOOL { switch (reason) { 1 => install(), 0 => uninstall(), else => {}, } return @enumFromInt(1); }