From c04680788d003a1ebb021084cff25565fd1f4eeb Mon Sep 17 00:00:00 2001 From: MarcelineVQ Date: Mon, 2 Mar 2026 23:38:04 -0800 Subject: [PATCH] Fix release-mode crashes: inline asm register clobbers and fastcall ABI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: hook.fastcall used "r" constraints + explicit MOV to set ECX/EDX. LLVM can allocate "r" inputs to clobbered registers, causing cross-assignment (ecx_in→EDX, edx_in→ECX) or function address stomping when func lands in ECX/EDX. Debug works by luck (trivial regalloc); Release optimizes aggressively and hits the conflicts. Fix: explicit "{ecx}", "{edx}", "{eax}" register constraints in zhook fastcall — compiler places values directly, no MOV needed, no ambiguity. Also fix 9 inline asm blocks across main.zig, interact.zig, screenshot.zig, markers.zig missing ECX/EDX clobbers after CALL instructions. Without clobbers the optimizer assumes registers retain input values after the call — stale reuse in release builds. Other changes in this commit: - Rename markers→worldmarkers (build flag, DLL, Lua table) - Rename assetfix→looseassets - lua.zig: add .never_tail to pushcclosure, pcall, openlib, pushnumber - Move internal marker functions into WorldMarkers Lua table via openlib - Remove unused GetCurrentAreaId function - Fix cleanup_file_handle_hook.original() → .callOriginal() --- DLL_README.md | 23 +++++--- README.md | 23 +++++--- build.zig | 36 ++++++------ src/interact/interact.zig | 2 +- .../looseassets.zig} | 10 ++-- src/lua.zig | 9 +-- src/main.zig | 57 +++++++++++-------- src/markers/MPQ_FILESYSTEM_RESEARCH.md | 18 +++--- src/markers/addon/Markers.lua | 20 +++---- src/markers/markers.zig | 30 +++++----- src/screenshot/screenshot.zig | 2 +- 11 files changed, 126 insertions(+), 104 deletions(-) rename src/{assetfix/assetfix.zig => looseassets/looseassets.zig} (97%) diff --git a/DLL_README.md b/DLL_README.md index 45bbada..9f80d33 100644 --- a/DLL_README.md +++ b/DLL_README.md @@ -12,7 +12,7 @@ On Turtle WoW, place your chosen DLLs next to your `WoW.exe` and add them to you ### World Markers -Place up to 5 animated colored markers at any position in the world, useful for raid positioning, pull planning, or route marking. +Place up to 5 animated colored markers at any position in the world, useful for raid positioning, pull planning, or route marking. Requires party/raid leader or raid assist. - `/worldmarker 1` through `/worldmarker 5` (or `/wm 1`) -- place a marker where your cursor is pointing - `/worldmarker 1 target` -- place a marker on a unit (player, target, mouseover, etc.) @@ -21,14 +21,17 @@ Place up to 5 animated colored markers at any position in the world, useful for Keybindings for placing each marker and clearing all markers are available in the Key Bindings menu. +Markers automatically sync with group members who also have WeirdUtils installed. When a leader/assist places or clears a marker, all group members see it. Markers persist across zone transitions and respawn when you return to the area. + Lua API for addon developers: -- `WorldMarker(index)` -- place marker at cursor terrain position +- `WorldMarker(index)` -- place marker at cursor terrain position (returns 1 on success, nil if no permission) - `WorldMarker(index, "unit")` -- place marker at a unit's position - `WorldMarker(index, x, y, z)` -- place marker at world coordinates -- `ClearWorldMarker(index)` / `ClearWorldMarker()` -- remove one or all markers +- `ClearWorldMarker(index)` / `ClearWorldMarker()` -- remove one or all markers (returns 1 on success, nil if no permission) +- `CanSetWorldMarkers()` -- returns 1 if the local player is party/raid leader or raid assist, nil otherwise -**DLL:** `markers.dll` +**DLL:** `worldmarkers.dll` --- @@ -83,7 +86,7 @@ Prevents a class of crashes caused by stale UI frame anchor pointers. No configu ### Transmog Fix -Eliminates FPS lag spikes caused by rapid equipment visual updates during transmog changes. No configuration needed, install and forget. +Eliminates FPS drops caused by rapid equipment visual updates when transmogged items lose durability. No configuration needed, install and forget. **DLL:** `transmogfix.dll` @@ -91,9 +94,15 @@ Eliminates FPS lag spikes caused by rapid equipment visual updates during transm ### Custom Assets -Enables loading loose asset files (textures, models) from disk without repacking MPQ archives. Also supports multi-character patch archive names. No configuration needed, install and forget. +Enables loading loose game asset files (models, textures, etc.) from the `Data/` directory without repacking MPQ archives. Place files in `Data/` mirroring the game's internal paths (e.g. `Data/Character/Troll/Female/TrollFemale.m2`) and they will be used instead of the MPQ version. -**DLL:** `assetfix.dll` +At startup, recursively scans `Data/` and indexes all non-MPQ files into an O(1) hash set, so loose file lookups are as fast as MPQ lookups. + +Also allows multi-character patch archive names (e.g. `patch-12.mpq`, `patch-jimbo.mpq`). Patch archives are sorted case-insensitively by filename — last in the sort gets highest priority, and all patches override the base archives. + +No configuration needed, install and forget. + +**DLL:** `looseassets.dll` --- diff --git a/README.md b/README.md index 7a84761..cac4263 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,14 @@ interaction helpers, and an embedded addon with Lua API + keybindings. | **Outline** | JFA-based screen-space outlines for targets, raid marks, dead players. See [src/outline/README.md](src/outline/README.md). | | **Screenshot** | Hooks CTgaFile::Write for screenshot capture. | | **Interact** | Nearest NPC/object interaction, bulk looting with queue processing. | -| **Embedded Addon** | Virtual addon loaded from DLL memory — .toc, .lua, .xml served via file I/O hook. Registers Lua commands and keybindings without any on-disk addon folder. | +| **Markers** | World-space raid markers (5 colors) using M2 model entities. Proximity respawn, group sync, animated spawn/despawn. Lua API + slash commands (`/wm`, `/cwm`). | +| **Framecrash** | Anchor vtable guards — prevents crashes from dangling relativeTo pointers and NULL frame refs. | +| **Combatlog** | Combat log fixes. | +| **Minimap Icons** | Minimap icon fixes. | +| **Transmogfix** | Coalesces transmog durability update packets to prevent death frame drops. | +| **Loose Assets** | Loose file loading, permissive MPQ glob patterns, pre-indexed file hash set. | +| **Healtextfix** | Heal text display fix. | +| **Embedded Addon** | Virtual addons loaded from DLL memory — .toc, .lua, .xml, .m2, .blp served via file I/O hooks (LoadFile + Storm layer). No on-disk addon folder needed. | | **Lua Protection Bypass** | Stubs the Lua callback address validator to allow C function registration. | ## Consolidation Plan @@ -34,7 +41,7 @@ Users can pick the full package or grab only the features they want. ```zig // build.zig options (planned) -const enable_assetfix = b.option(bool, "assetfix", "Enable asset/MPQ fixes") orelse true; +const enable_looseassets = b.option(bool, "looseassets", "Enable loose file loading & permissive patch glob") orelse true; const enable_transmogfix = b.option(bool, "transmogfix", "Enable transmog coalesce fix") orelse true; const enable_interact = b.option(bool, "interact", "Enable interact helpers") orelse true; const enable_outline = b.option(bool, "outline", "Enable outline rendering") orelse true; @@ -45,14 +52,14 @@ const enable_outline = b.option(bool, "outline", "Enable outline rendering") ore zig build # Single-feature builds — one DLL per feature for individual distribution -zig build -Dassetfix=true -Dtransmogfix=false -Dinteract=false -Doutline=false -zig build -Dassetfix=false -Dtransmogfix=true -Dinteract=false -Doutline=false +zig build -Dlooseassets=true -Dtransmogfix=false -Dinteract=false -Doutline=false +zig build -Dlooseassets=false -Dtransmogfix=true -Dinteract=false -Doutline=false # etc. ``` Release artifacts: - `weirdutils.dll` — everything -- `assetfix.dll` — just asset/MPQ fixes +- `looseassets.dll` — just asset/MPQ fixes - `transmogfix.dll` — just transmog coalesce - `interact.dll` — just interact/loot helpers - `outline.dll` — just outline rendering @@ -62,14 +69,14 @@ All built from this repo, all sharing the same hook library and codebase. ### Per-Feature Named Mutex A user might load the full DLL alongside one of the smaller single-feature DLLs -(e.g. they use `weirdutils.dll` for everything but also have `assetfix.dll` from +(e.g. they use `weirdutils.dll` for everything but also have `looseassets.dll` from before they switched). Each feature module claims a **named mutex** on load — if it's already held, that module skips hook installation. This way any combination of DLLs coexists safely with no duplicate hooks. ```zig // Each module creates a process-specific named mutex on init -const mutex = CreateMutexA(null, 1, "Local\\WeirdUtils_AssetFix_{pid}"); +const mutex = CreateMutexA(null, 1, "Local\\WeirdUtils_LooseAssets_{pid}"); if (GetLastError() == ERROR_ALREADY_EXISTS) { // Another DLL already owns this feature's hooks — skip CloseHandle(mutex); @@ -120,7 +127,7 @@ gh release create v1.0 --repo YourName/WeirdUtils \ --title "v1.0" --notes "Release notes" \ ./zig-out/lib/weirdutils.dll \ ./builds/outline.dll \ - ./builds/assetfix.dll + ./builds/looseassets.dll ``` ## Project Structure diff --git a/build.zig b/build.zig index 94d1584..8b9e3f6 100644 --- a/build.zig +++ b/build.zig @@ -12,12 +12,12 @@ pub fn build(b: *std.Build) void { const enable_screenshot = b.option(bool, "screenshot", "Enable screenshot module") orelse true; 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_worldmarkers = b.option(bool, "worldmarkers", "Enable world markers module") 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_looseassets = b.option(bool, "looseassets", "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 @@ -25,12 +25,12 @@ pub fn build(b: *std.Build) void { build_options.addOption(bool, "enable_screenshot", enable_screenshot); build_options.addOption(bool, "enable_interact", enable_interact); build_options.addOption(bool, "enable_outline", enable_outline); - build_options.addOption(bool, "enable_markers", enable_markers); + build_options.addOption(bool, "enable_worldmarkers", enable_worldmarkers); build_options.addOption(bool, "enable_framecrash", enable_framecrash); build_options.addOption(bool, "enable_combatlog", enable_combatlog); 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_looseassets", enable_looseassets); build_options.addOption(bool, "enable_healtextfix", enable_healtextfix); const build_options_module = build_options.createModule(); @@ -60,30 +60,30 @@ 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, healtextfix: bool }; + const Variant = struct { name: []const u8, screenshot: bool, interact: bool, outline: bool, worldmarkers: bool, framecrash: bool, combatlog: bool, minimapicons: bool, transmogfix: bool, looseassets: 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, .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 }, + .{ .name = "full", .screenshot = true, .interact = true, .outline = true, .worldmarkers = true, .framecrash = true, .combatlog = true, .minimapicons = true, .transmogfix = true, .looseassets = true, .healtextfix = true }, + .{ .name = "screenshot", .screenshot = true, .interact = false, .outline = false, .worldmarkers = false, .framecrash = true, .combatlog = true, .minimapicons = false, .transmogfix = false, .looseassets = false, .healtextfix = false }, + .{ .name = "interact", .screenshot = false, .interact = true, .outline = false, .worldmarkers = false, .framecrash = true, .combatlog = true, .minimapicons = false, .transmogfix = false, .looseassets = false, .healtextfix = false }, + .{ .name = "outline", .screenshot = false, .interact = false, .outline = true, .worldmarkers = false, .framecrash = true, .combatlog = true, .minimapicons = false, .transmogfix = false, .looseassets = false, .healtextfix = false }, + .{ .name = "worldmarkers", .screenshot = false, .interact = false, .outline = false, .worldmarkers = true, .framecrash = true, .combatlog = true, .minimapicons = false, .transmogfix = false, .looseassets = false, .healtextfix = false }, + .{ .name = "framecrash", .screenshot = false, .interact = false, .outline = false, .worldmarkers = false, .framecrash = true, .combatlog = false, .minimapicons = false, .transmogfix = false, .looseassets = false, .healtextfix = false }, + .{ .name = "combatlog", .screenshot = false, .interact = false, .outline = false, .worldmarkers = false, .framecrash = false, .combatlog = true, .minimapicons = false, .transmogfix = false, .looseassets = false, .healtextfix = false }, + .{ .name = "minimapicons", .screenshot = false, .interact = false, .outline = false, .worldmarkers = false, .framecrash = true, .combatlog = false, .minimapicons = true, .transmogfix = false, .looseassets = false, .healtextfix = false }, + .{ .name = "transmogfix", .screenshot = false, .interact = false, .outline = false, .worldmarkers = false, .framecrash = false, .combatlog = false, .minimapicons = false, .transmogfix = true, .looseassets = false, .healtextfix = false }, + .{ .name = "looseassets", .screenshot = false, .interact = false, .outline = false, .worldmarkers = false, .framecrash = false, .combatlog = false, .minimapicons = false, .transmogfix = false, .looseassets = true, .healtextfix = false }, + .{ .name = "healtextfix", .screenshot = false, .interact = false, .outline = false, .worldmarkers = false, .framecrash = false, .combatlog = false, .minimapicons = false, .transmogfix = false, .looseassets = false, .healtextfix = true }, }) |variant| { const opts = b.addOptions(); opts.addOption(bool, "enable_screenshot", variant.screenshot); opts.addOption(bool, "enable_interact", variant.interact); opts.addOption(bool, "enable_outline", variant.outline); - opts.addOption(bool, "enable_markers", variant.markers); + opts.addOption(bool, "enable_worldmarkers", variant.worldmarkers); opts.addOption(bool, "enable_framecrash", variant.framecrash); opts.addOption(bool, "enable_combatlog", variant.combatlog); 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_looseassets", variant.looseassets); opts.addOption(bool, "enable_healtextfix", variant.healtextfix); const variant_lib = b.addLibrary(.{ diff --git a/src/interact/interact.zig b/src/interact/interact.zig index 2e301a2..1b371fe 100644 --- a/src/interact/interact.zig +++ b/src/interact/interact.zig @@ -129,7 +129,7 @@ fn rightClickInteract(pointer: u32, autoloot: i32, fun_ptr: usize) void { : [_] "{ecx}" (pointer), [autoloot] "r" (autoloot), [func] "r" (fun_ptr), - : .{ .eax = true, .edx = true, .memory = true, .cc = true }); + : .{ .eax = true, .ecx = true, .edx = true, .memory = true, .cc = true }); } // ============================================================================= diff --git a/src/assetfix/assetfix.zig b/src/looseassets/looseassets.zig similarity index 97% rename from src/assetfix/assetfix.zig rename to src/looseassets/looseassets.zig index bef9541..bb7431e 100644 --- a/src/assetfix/assetfix.zig +++ b/src/looseassets/looseassets.zig @@ -1,5 +1,5 @@ // ============================================================================= -// assetfix - Loose file loading & permissive patch glob +// looseassets - Loose file loading & permissive patch glob // ============================================================================= // // 1. Patches patch-?.MPQ → patch-*.MPQ so multi-char patch names work @@ -192,7 +192,7 @@ fn checkFileExistenceDetour(filename_ptr: u32, flags: u32, output_buffer_ptr: u3 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)]}); + con.fmt("[looseassets] loose hit: \"{s}\"\n", .{raw[0..cStrLen(raw)]}); if (output_buffer_ptr != 0) { const disk_len = cStrLen(disk_path); @@ -281,11 +281,11 @@ var g_mutex: ?*anyopaque = null; var g_is_hook_owner: bool = false; pub fn installHooks() void { - con.print("[assetfix] Module loaded\n"); + con.print("[looseassets] 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; + const mutex_name = std.fmt.bufPrint(&mutex_name_buf, "Local\\LooseassetsHook_{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])); @@ -295,7 +295,7 @@ pub fn installHooks() void { _ = CloseHandle(g_mutex.?); g_mutex = null; g_is_hook_owner = false; - con.print("[assetfix] Another DLL owns hooks (mutex taken), skipping\n"); + con.print("[looseassets] Another DLL owns hooks (mutex taken), skipping\n"); return; } g_is_hook_owner = true; diff --git a/src/lua.zig b/src/lua.zig index 4b7aee8..bb7c340 100644 --- a/src/lua.zig +++ b/src/lua.zig @@ -66,8 +66,9 @@ pub fn pushnil(L: State) void { pub fn pushnumber(L: State, n: f64) void { // __thiscall: ECX=L, f64 on stack [EBP+8]/[EBP+0xc], ret 8. + // .never_tail: callee-cleanup pops stack args — tail-call would corrupt the stack. const f: *const fn (State, f64) callconv(.{ .x86_thiscall = .{} }) void = @ptrFromInt(0x6F3810); - f(L, n); + @call(.never_tail, f, .{ L, n }); } pub fn pushstring(L: State, s: [*:0]const u8) void { @@ -82,7 +83,7 @@ pub fn pushboolean(L: State, b: i32) void { pub fn pushcclosure(L: State, func: usize, n: i32) void { const f: *const fn (State, usize, i32) callconv(fc) void = @ptrFromInt(0x6F3920); - f(L, func, n); + @call(.never_tail, f, .{ L, func, n }); } pub fn tonumber(L: State, index: i32) f64 { @@ -122,7 +123,7 @@ pub fn next(L: State, index: i32) i32 { pub fn pcall(L: State, nargs: i32, nresults: i32, errfunc: i32) i32 { const f: *const fn (State, i32, i32, i32) callconv(fc) i32 = @ptrFromInt(0x6F41A0); - return f(L, nargs, nresults, errfunc); + return @call(.never_tail, f, .{ L, nargs, nresults, errfunc }); } pub fn luaError(L: State, msg: [*:0]const u8) void { @@ -146,7 +147,7 @@ pub const LuaReg = extern struct { pub fn openlib(L: State, libname: ?[*:0]const u8, funcs: [*]const LuaReg, nup: i32) void { const f: *const fn (State, ?[*:0]const u8, [*]const LuaReg, i32) callconv(fc) void = @ptrFromInt(0x6F4DC0); - f(L, libname, funcs, nup); + @call(.never_tail, f, .{ L, libname, funcs, nup }); } pub fn checknumber(L: State, index: i32) f64 { diff --git a/src/main.zig b/src/main.zig index 51b98e9..32138ad 100644 --- a/src/main.zig +++ b/src/main.zig @@ -7,12 +7,12 @@ const build_opts = struct { const screenshot = @import("build_options").enable_screenshot; const interact = @import("build_options").enable_interact; const outline = @import("build_options").enable_outline; - const markers = @import("build_options").enable_markers; + const worldmarkers = @import("build_options").enable_worldmarkers; const framecrash = @import("build_options").enable_framecrash; const combatlog = @import("build_options").enable_combatlog; const minimapicons = @import("build_options").enable_minimapicons; const transmogfix = @import("build_options").enable_transmogfix; - const assetfix = @import("build_options").enable_assetfix; + const looseassets = @import("build_options").enable_looseassets; const healtextfix = @import("build_options").enable_healtextfix; }; @@ -20,12 +20,12 @@ const build_opts = struct { 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/api.zig") else struct {}; -const markers = if (build_opts.markers) @import("markers/markers.zig") else struct {}; +const markers = if (build_opts.worldmarkers) @import("markers/markers.zig") else struct {}; const framecrash = if (build_opts.framecrash) @import("framecrash/framecrash.zig") else struct {}; const combatlog = if (build_opts.combatlog) @import("combatlog/combatlog.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 assetfix = if (build_opts.assetfix) @import("assetfix/assetfix.zig") else struct {}; +const looseassets = if (build_opts.looseassets) @import("looseassets/looseassets.zig") else struct {}; const healtextfix = if (build_opts.healtextfix) @import("healtextfix/healtextfix.zig") else struct {}; const WINAPI = std.builtin.CallingConvention.winapi; @@ -87,7 +87,7 @@ fn weirdUtilsVersion(L: lua.State) callconv(.c) u32 { : : [_] "{ecx}" (@intFromPtr(L)), [func] "r" (@as(u32, 0x6F3810)), - : .{ .eax = true, .edx = true, .memory = true, .cc = true } + : .{ .eax = true, .ecx = true, .edx = true, .memory = true, .cc = true } ); return 1; } @@ -108,15 +108,22 @@ fn registerLuaFunctions() void { if (build_opts.outline) { registerFunction("OutlineCommand", @intFromPtr(&outline.outlineCommand)); } - if (build_opts.markers and markers.isActive()) { + if (build_opts.worldmarkers and markers.isActive()) { + // User-facing functions stay global registerFunction("WorldMarker", @intFromPtr(&markers.luaWorldMarker)); registerFunction("ClearWorldMarker", @intFromPtr(&markers.luaClearWorldMarker)); - registerFunction("SetMarkerDef", @intFromPtr(&markers.luaSetMarkerDef)); - registerFunction("SetMarkerDefSync", @intFromPtr(&markers.luaSetMarkerDefSync)); - registerFunction("ClearMarkerDef", @intFromPtr(&markers.luaClearMarkerDef)); - registerFunction("GetMarkerDef", @intFromPtr(&markers.luaGetMarkerDef)); - registerFunction("GetCurrentAreaId", @intFromPtr(&markers.luaGetCurrentAreaId)); - registerFunction("CanSetMarkers", @intFromPtr(&markers.luaCanSetMarkers)); + registerFunction("CanSetWorldMarkers", @intFromPtr(&markers.luaCanSetMarkers)); + + // Internal functions in WorldMarkers table (via luaL_openlib) + const lib = [_]lua.LuaReg{ + .{ .name = "SetMarkerDef", .func = @intFromPtr(&markers.luaSetMarkerDef) }, + .{ .name = "SetMarkerDefSync", .func = @intFromPtr(&markers.luaSetMarkerDefSync) }, + .{ .name = "ClearMarkerDef", .func = @intFromPtr(&markers.luaClearMarkerDef) }, + .{ .name = "GetMarkerDef", .func = @intFromPtr(&markers.luaGetMarkerDef) }, + + .{ .name = null, .func = 0 }, // sentinel + }; + lua.openlib(lua.getContext(), "WorldMarkers", &lib, 0); } } @@ -160,14 +167,14 @@ const outline_files = if (build_opts.outline) [_]FileEntry{ .{ .name = "Bindings.xml", .data = @embedFile("outline/addon/Bindings.xml") }, } else [_]FileEntry{}; -const markers_files = if (build_opts.markers) [_]FileEntry{ +const markers_files = if (build_opts.worldmarkers) [_]FileEntry{ .{ .name = "Markers.toc", .data = @embedFile("markers/addon/Markers.toc") }, .{ .name = "Markers.lua", .data = @embedFile("markers/addon/Markers.lua") }, .{ .name = "Bindings.xml", .data = @embedFile("markers/addon/Bindings.xml") }, } else [_]FileEntry{}; // Marker model + skin + textures served under Spells\ prefix -const markers_spells_assets = if (build_opts.markers) [_]FileEntry{ +const markers_spells_assets = if (build_opts.worldmarkers) [_]FileEntry{ // Models (5 colors) .{ .name = "Raid_UI_FX_Yellow.m2", .data = @embedFile("markers/assets/Spells/Raid_UI_FX_Yellow.m2") }, .{ .name = "Raid_UI_FX_Cyan.m2", .data = @embedFile("markers/assets/Spells/Raid_UI_FX_Cyan.m2") }, @@ -188,18 +195,18 @@ const markers_spells_assets = if (build_opts.markers) [_]FileEntry{ } else [_]FileEntry{}; // Shared effect textures served under World\Expansion01\Doodads\Zulaman\Doors\ prefix -const markers_world_assets = if (build_opts.markers) [_]FileEntry{ +const markers_world_assets = if (build_opts.worldmarkers) [_]FileEntry{ .{ .name = "T_VFX_FIRE03_A.BLP", .data = @embedFile("markers/assets/World/Expansion01/Doodads/Zulaman/Doors/T_VFX_FIRE03_A.BLP") }, .{ .name = "T_VFX_BORDER6.BLP", .data = @embedFile("markers/assets/World/Expansion01/Doodads/Zulaman/Doors/T_VFX_BORDER6.BLP") }, } else [_]FileEntry{}; // XYZ debug model (renamed to avoid collision with game's built-in xyz.m2) -const markers_xyz_model = if (build_opts.markers) [_]FileEntry{ +const markers_xyz_model = if (build_opts.worldmarkers) [_]FileEntry{ .{ .name = "WU_XYZ.m2", .data = @embedFile("markers/assets/Spells/WU_XYZ.m2") }, } else [_]FileEntry{}; // XYZ texture served under World\ArtTest\Boxtest\ (matches M2 internal reference) -const markers_xyz_texture = if (build_opts.markers) [_]FileEntry{ +const markers_xyz_texture = if (build_opts.worldmarkers) [_]FileEntry{ .{ .name = "xyz.blp", .data = @embedFile("markers/assets/Spells/xyz.blp") }, } else [_]FileEntry{}; @@ -340,7 +347,7 @@ fn callInitFileContext(ctx: [*]u8, file_type: u32) void { : [_] "{ecx}" (@intFromPtr(ctx)), [ftype] "r" (file_type), [func] "r" (@as(u32, 0x647290)), - : .{ .eax = true, .edx = true, .memory = true, .cc = true } + : .{ .eax = true, .ecx = true, .edx = true, .memory = true, .cc = true } ); } @@ -588,7 +595,7 @@ fn loadModelAsyncDetour(model: u32, file_handle: u32, should_use_callback: u32) // Call original CleanupFileHandleResources through the trampoline (bypasses our // detour). Must clean up file context before processLoadedModelData runs. con.fmt("[file] cleanup via trampoline fh=0x{x}\n", .{file_handle}); - cleanup_file_handle_hook.original()(file_handle); + cleanup_file_handle_hook.callOriginal(.{file_handle}); con.print("[file] cleanup done\n"); // Dump model fields before processLoadedModelData @@ -709,7 +716,7 @@ fn loadAddonsDetour(error_handler: u32) callconv(fc) void { error_handler, ); } - if (build_opts.markers and markers.isActive()) { + if (build_opts.worldmarkers and markers.isActive()) { callLoadFileListWithIncludes( "Interface\\AddOns\\Markers\\Markers.toc", &md5ctx, @@ -732,7 +739,7 @@ fn callLoadFileListWithIncludes(toc_path: [*:0]const u8, md5ctx: *[88]u8, error_ [_] "{edx}" (@intFromPtr(md5ctx)), [eh] "r" (error_handler), [func] "r" (@as(u32, 0x6EDB90)), - : .{ .eax = true, .memory = true, .cc = true } + : .{ .eax = true, .ecx = true, .edx = true, .memory = true, .cc = true } ); } @@ -749,7 +756,7 @@ fn callLoadUIBindingsFromFile(path: [*:0]const u8, md5ctx: *[88]u8, callback: u3 [path] "r" (@intFromPtr(path)), [md5] "r" (@intFromPtr(md5ctx)), [cb] "r" (callback), - : .{ .eax = true, .memory = true, .cc = true } + : .{ .eax = true, .ecx = true, .edx = true, .memory = true, .cc = true } ); } @@ -796,13 +803,13 @@ const ModuleHooks = struct { /// 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.looseassets) .{ .install = looseassets.installHooks, .remove = looseassets.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 } else .{}, + if (build_opts.worldmarkers) .{ .install = markers.installHooks, .remove = markers.removeHooks } 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 .{}, @@ -810,7 +817,7 @@ const modules = [_]ModuleHooks{ fn shutdownDetour() callconv(sc) void { // Clear marker definitions on logout/exit (not on map change). - if (build_opts.markers) markers.onShutdown(); + if (build_opts.worldmarkers) markers.onShutdown(); // Clean up world objects BEFORE game shutdown — atexit handlers run before // DllMain so modules with remove_on_shutdown must destroy here. diff --git a/src/markers/MPQ_FILESYSTEM_RESEARCH.md b/src/markers/MPQ_FILESYSTEM_RESEARCH.md index 9f52b3b..9cfbef5 100644 --- a/src/markers/MPQ_FILESYSTEM_RESEARCH.md +++ b/src/markers/MPQ_FILESYSTEM_RESEARCH.md @@ -408,9 +408,9 @@ A minimal V1 MPQ for ~20 uncompressed files requires: The hash table uses a specific encryption algorithm (documented in wowdev.wiki and StormLib source). Could implement in Zig comptime or use a build-time tool. -### Assetfix Synergy +### Loose Assets Synergy -The [assetfix project](/media/storage/projects/zig/assetfix/) already implements the glob pattern patch (`0x82edc2: '?' → '*'`) for multi-character patch names. If combined with assetfix, the temp file could use any name like `patch-weirdutils.MPQ`. +The [looseassets project](/media/storage/projects/zig/looseassets/) already implements the glob pattern patch (`0x82edc2: '?' → '*'`) for multi-character patch names. If combined with looseassets, the temp file could use any name like `patch-weirdutils.MPQ`. --- @@ -532,11 +532,11 @@ If we could insert entries into this hash table mapping our asset paths to disk --- -## Approach D: CheckFileExistence Hook (assetfix Pattern) +## Approach D: CheckFileExistence Hook (looseassets Pattern) ### Concept -The [assetfix project](/media/storage/projects/zig/assetfix/) takes a different approach entirely: +The [looseassets project](/media/storage/projects/zig/looseassets/) takes a different approach entirely: 1. **NOP two gates** in `File_FindInArchive` (0x654b5c, 0x654b6a) that restrict `CheckFileExistence` to only "Interface/AddOns" paths 2. **Hook `CheckFileExistence`** (0x654DD0) to check a hash map of loose disk files @@ -544,7 +544,7 @@ The [assetfix project](/media/storage/projects/zig/assetfix/) takes a different This works for **disk-based** loose files but not for in-memory embedded data. However, combined with writing temp files to disk, it provides a clean single-hook solution. -### Key Addresses (from assetfix) +### Key Addresses (from looseassets) | Target | Address | Patch | |--------|---------|-------| @@ -564,7 +564,7 @@ This works for **disk-based** loose files but not for in-memory embedded data. H | Fake contexts | ~20 | **0** | 0 | 1 | 0 | 0 | | Async handling | Manual | **Native** | Native | Uncertain | Native | Native | | Build complexity | Low | **Low** | Medium (MPQ builder) | Medium (MPQ builder) | High (struct RE) | Low | -| RE work needed | Done | **Minimal** (verify path redirect) | Archive_OpenUnified conv | Same as A + async verify | Full struct layout | Already done (assetfix) | +| RE work needed | Done | **Minimal** (verify path redirect) | Archive_OpenUnified conv | Same as A + async verify | Full struct layout | Already done (looseassets) | | Risk | Proven but fragile | **Low** (real OS handles) | Low (native I/O) | Medium (async path?) | High (struct mismatch) | Low | | Cleanup needed | None | **Auto** (DELETE_ON_CLOSE) | Delete temp file | None | Remove from array | Delete temp files | | Purely in-memory | Yes | Mostly (cache-backed) | No | Yes | Yes | No | @@ -595,9 +595,9 @@ This works for **disk-based** loose files but not for in-memory embedded data. H If a single-file solution is preferred over ~20 temp files, building a real MPQ and registering it via `Archive_OpenUnified` eliminates all hooks entirely. The cost is implementing an MPQ V1 builder (hash table encryption, block table, header). Could be combined with Approach E: use temp files now, migrate to MPQ later. -### Worth Combining With: Assetfix Integration +### Worth Combining With: Loose Assets Integration -The assetfix project's `CheckFileExistence` hook and glob pattern patch could complement either approach, especially for supporting user-provided loose asset files alongside our embedded ones. +The looseassets project's `CheckFileExistence` hook and glob pattern patch could complement either approach, especially for supporting user-provided loose asset files alongside our embedded ones. --- @@ -687,6 +687,6 @@ The assetfix project's `CheckFileExistence` hook and glob pattern patch could co | Archive search critsec | 0xc54008 | Critical section for archive ops | | Archive search state | 0xc53ff0 | Used by File_FindInArchive | | Patch glob "patch-?.MPQ" | 0x82edbc | Glob pattern for patch discovery | -| Patch glob char | 0x82edc2 | The '?' byte (assetfix patches to '*') | +| Patch glob char | 0x82edc2 | The '?' byte (looseassets patches to '*') | | Data path format | 0x82edc8 | `"Data\%s"` format string | | SArchive RTTI | 0x82e248 | `".PAVSArchive@@"` | diff --git a/src/markers/addon/Markers.lua b/src/markers/addon/Markers.lua index 42e3aa0..0a835ec 100644 --- a/src/markers/addon/Markers.lua +++ b/src/markers/addon/Markers.lua @@ -20,7 +20,7 @@ end -- Permission model: ALL permission checks are enforced DLL-side. -- WorldMarker/ClearWorldMarker: DLL checks local player is leader/assist. -- SetMarkerDef/ClearMarkerDef: DLL checks sender name against roster. --- CanSetMarkers(): DLL returns 1 if local player has permission. +-- CanSetWorldMarkers(): DLL returns 1 if local player has permission. -- ============================================================================= local MSG_PREFIX = "WMark" @@ -63,7 +63,7 @@ local function broadcastAllDefs() if not ch then return end local count = 0 for i = 1, NUM_MARKERS do - local x, y, z, areaId = GetMarkerDef(i) + local x, y, z, areaId = WorldMarkers.GetMarkerDef(i) if x then local msg = "SF:" .. i .. ":" .. x .. ":" .. y .. ":" .. z .. ":" .. areaId log("SEND [" .. ch .. "] " .. msg) @@ -110,7 +110,7 @@ function WorldMarker(index, ...) denyCount = 0 local ch = getChannel() if ch then - local x, y, z, areaId = GetMarkerDef(index) + local x, y, z, areaId = WorldMarkers.GetMarkerDef(index) if x then broadcastPlace(index, x, y, z, areaId) end @@ -185,7 +185,7 @@ local function onAddonMessage(prefix, message, channel, sender) if cmd == "SR" then -- Normal sync request: only leader/assist responds log(" sync request from " .. tostring(sender)) - if CanSetMarkers() then + if CanSetWorldMarkers() then broadcastAllDefs() end return @@ -205,7 +205,7 @@ local function onAddonMessage(prefix, message, channel, sender) end local idx, x, y, z, areaId = parseMarkerFields(parts) if idx then - SetMarkerDefSync(idx, x, y, z, areaId, sender) + WorldMarkers.SetMarkerDefSync(idx, x, y, z, areaId, sender) else log(" PARSE FAIL") end @@ -216,17 +216,17 @@ local function onAddonMessage(prefix, message, channel, sender) if cmd == "P" then local idx, x, y, z, areaId = parseMarkerFields(parts) if idx then - SetMarkerDef(idx, x, y, z, areaId, sender) + WorldMarkers.SetMarkerDef(idx, x, y, z, areaId, sender) else log(" PARSE FAIL") end elseif cmd == "C" then local idx = tonumber(parts[2]) if idx then - ClearMarkerDef(idx, sender) + WorldMarkers.ClearMarkerDef(idx, sender) end elseif cmd == "CA" then - ClearMarkerDef(sender) + WorldMarkers.ClearMarkerDef(sender) else log(" unknown: " .. tostring(cmd)) end @@ -244,7 +244,7 @@ local function broadcastSyncRequest() end -- Reset first-responder lock before requesting syncSender = nil - local cmd = CanSetMarkers() and "LSR" or "SR" + local cmd = CanSetWorldMarkers() and "LSR" or "SR" log("SEND [" .. ch .. "] " .. cmd) SendAddonMessage(MSG_PREFIX, cmd, ch) end @@ -302,7 +302,7 @@ rosterTimer:SetScript("OnUpdate", function() rosterTimer.pending = false rosterTimer.extensions = 0 log("roster timer fired, broadcasting") - if CanSetMarkers() then + if CanSetWorldMarkers() then broadcastAllDefs() end end diff --git a/src/markers/markers.zig b/src/markers/markers.zig index 4d8e852..97ae50a 100644 --- a/src/markers/markers.zig +++ b/src/markers/markers.zig @@ -5,15 +5,19 @@ //! Markers persist across zone transitions via MarkerDef definitions. //! Entities are respawned automatically when the player approaches within 200y. //! -//! Lua API: +//! Lua API (globals): //! WorldMarker(index, x, y, z) — place marker at coordinates //! WorldMarker(index, "unit") — place marker at unit's position //! WorldMarker(index) — place marker at cursor terrain position //! ClearWorldMarker(index) — remove specific marker (1-5) //! ClearWorldMarker() — remove all markers -//! SetMarkerDef(i, x, y, z, area) — store definition (no immediate spawn) -//! ClearMarkerDef([index]) — clear definition (and entity) -//! GetMarkerDef(index) — returns x, y, z, areaId or nil +//! CanSetWorldMarkers() — returns 1 if leader/assist, nil otherwise +//! +//! Lua API (WorldMarkers table — internal, used by addon): +//! WorldMarkers.SetMarkerDef(i, x, y, z, area, sender) +//! WorldMarkers.SetMarkerDefSync(i, x, y, z, area, sender) +//! WorldMarkers.ClearMarkerDef([index,] sender) +//! WorldMarkers.GetMarkerDef(index) — returns x, y, z, areaId or nil const std = @import("std"); const hook = @import("zhook"); @@ -151,7 +155,7 @@ fn getNameFromGUID(guid_lo: u32, guid_hi: u32) ?[*:0]const u8 { : [_] "{ecx}" (@as(u32, o.NAME_CACHE_OBJ)), [a] "r" (&stack_args), [func] "r" (@as(u32, o.FN_NAME_CACHE_LOOKUP)), - : .{ .edx = true, .memory = true, .cc = true }); + : .{ .ecx = true, .edx = true, .memory = true, .cc = true }); return if (result != 0) @ptrFromInt(result) else null; } @@ -340,7 +344,7 @@ fn createEntityInstance(path: [*:0]const u8, pos: *[3]f32, facing: f32, flags: u [_] "{edx}" (@intFromPtr(pos)), [a] "r" (&stack_args), [func] "r" (o.FN_CREATE_ENTITY_INSTANCE), - : .{ .memory = true, .cc = true }); + : .{ .ecx = true, .edx = true, .memory = true, .cc = true }); return if (result != 0) @ptrFromInt(result) else null; } @@ -351,7 +355,7 @@ fn cleanupEntity(obj: *anyopaque) void { : : [_] "{ecx}" (@intFromPtr(obj)), [func] "r" (o.FN_CLEANUP_ENTITY), - : .{ .eax = true, .edx = true, .memory = true, .cc = true }); + : .{ .eax = true, .ecx = true, .edx = true, .memory = true, .cc = true }); } // ============================================================================= @@ -389,7 +393,7 @@ fn playAnimation(entity: *anyopaque, anim_id: u32, queue: bool) void { : [_] "{ecx}" (model), [a] "r" (&stack_args), [func] "r" (o.FN_PLAY_BONE_ANIMATION), - : .{ .eax = true, .edx = true, .memory = true, .cc = true }); + : .{ .eax = true, .ecx = true, .edx = true, .memory = true, .cc = true }); } /// Clean up despawning entities whose Decay animation has finished. @@ -800,7 +804,7 @@ pub fn luaGetMarkerDef(L: lua.State) callconv(.c) u32 { return 4; } -/// Lua: local ok = CanSetMarkers() +/// Lua: local ok = CanSetWorldMarkers() /// Returns 1 if the local player has permission (leader/assist), nil otherwise. /// Used by the addon for broadcast/sync decisions. pub fn luaCanSetMarkers(L: lua.State) callconv(.c) u32 { @@ -811,13 +815,7 @@ pub fn luaCanSetMarkers(L: lua.State) callconv(.c) u32 { return 0; } -/// Lua: local areaId = GetCurrentAreaId() -/// Returns the current zone area ID from the game global. -pub fn luaGetCurrentAreaId(L: lua.State) callconv(.c) u32 { - const area_id = hook.readMem(u32, o.ZONE_AREA_ID); - lua.pushnumber(L, @floatCast(@as(f64, @floatFromInt(area_id)))); - return 1; -} + // ============================================================================= // World teardown hook diff --git a/src/screenshot/screenshot.zig b/src/screenshot/screenshot.zig index ae0f207..a9dfb51 100644 --- a/src/screenshot/screenshot.zig +++ b/src/screenshot/screenshot.zig @@ -288,7 +288,7 @@ fn luaPushNumber(L_ptr: usize, n: f64) void { [lo] "r" (raw[0]), [hi] "r" (raw[1]), [func] "r" (@as(u32, 0x6F3810)), - : .{ .eax = true, .edx = true, .memory = true, .cc = true } + : .{ .eax = true, .ecx = true, .edx = true, .memory = true, .cc = true } ); }