Fix release-mode crashes: inline asm register clobbers and fastcall ABI

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()
This commit is contained in:
MarcelineVQ
2026-03-02 23:38:04 -08:00
parent b69c3a55a1
commit c04680788d
11 changed files with 126 additions and 104 deletions
+16 -7
View File
@@ -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`
---
+15 -8
View File
@@ -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
+18 -18
View File
@@ -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(.{
+1 -1
View File
@@ -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 });
}
// =============================================================================
@@ -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;
+5 -4
View File
@@ -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 {
+32 -25
View File
@@ -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.
+9 -9
View File
@@ -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@@"` |
+10 -10
View File
@@ -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
+14 -16
View File
@@ -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
+1 -1
View File
@@ -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 }
);
}