diff --git a/src/main.zig b/src/main.zig index 9e15c07..5deb4cc 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,5 +1,5 @@ const std = @import("std"); -const hook = @import("hook"); +const hook = @import("zhook"); pub const con = @import("console.zig"); // Build options for conditional module compilation @@ -34,9 +34,9 @@ const sc: std.builtin.CallingConvention = .{ .x86_stdcall = .{} }; // Lua Protection Bypass // ============================================================================= -var protection_hook: hook.Hook = .{}; +var protection_hook: hook.Detour(fn () callconv(sc) void) = .{}; -fn luaProtectionDetour() callconv(.c) void {} +fn luaProtectionDetour() callconv(sc) void {} // ============================================================================= // Lua C API wrappers (WoW 1.12.1 — all __fastcall, L in ECX) @@ -257,9 +257,8 @@ fn registerLuaFunctions() void { registerFunction("OutlineCommand", @intFromPtr(&outline.outlineCommand)); } if (build_opts.markers) { - registerFunction("TestMarkerCreate", @intFromPtr(&markers.luaTestMarkerCreate)); - registerFunction("TestMarkerDestroy", @intFromPtr(&markers.luaTestMarkerDestroy)); - registerFunction("TestMarkerToggle", @intFromPtr(&markers.luaTestMarkerToggle)); + registerFunction("WorldMarker", @intFromPtr(&markers.luaWorldMarker)); + registerFunction("ClearWorldMarker", @intFromPtr(&markers.luaClearWorldMarker)); registerFunction("GetPlayerPosition", @intFromPtr(&markers.luaGetPlayerPosition)); } } @@ -400,7 +399,8 @@ fn findEmbeddedFile(path: [*:0]const u8) ?*const FileEntry { // Hook: LoadFileWithTextureResourceFallback (0x648620) // ============================================================================= -var file_hook: hook.Hook = .{}; +const LoadFileFn = fn (u32, [*:0]const u8, *?[*]u8, ?*u32, u32, u32, u32) callconv(sc) u32; +var file_hook: hook.Detour(LoadFileFn) = .{}; fn loadFileDetour( unk: u32, @@ -428,10 +428,7 @@ fn loadFileDetour( return 1; } - const orig = file_hook.getTrampoline( - *const fn (u32, [*:0]const u8, *?[*]u8, ?*u32, u32, u32, u32) callconv(sc) u32, - ); - return orig(unk, path, buf_out, size_out, extra_alloc, flags, async_ptr); + return file_hook.callOriginal(.{ unk, path, buf_out, size_out, extra_alloc, flags, async_ptr }); } // ============================================================================= @@ -445,12 +442,24 @@ fn loadFileDetour( // // Fake context detection: type==0, handle(+0x04)==NULL, embedded_ptr(+0x30)!=0 -var open_file_hook: hook.Hook = .{}; -var get_file_size_hook: hook.Hook = .{}; -var read_file_hook: hook.Hook = .{}; -var cleanup_file_handle_hook: hook.Hook = .{}; -var process_async_hook: hook.Hook = .{}; -var model_load_hook: hook.Hook = .{}; +const OpenFileFn = fn (u32, [*:0]const u8, u32, *u32) callconv(sc) u32; +var open_file_hook: hook.Detour(OpenFileFn) = .{}; + +const GetFileSizeFn = fn (u32, ?*u32) callconv(sc) u32; +var get_file_size_hook: hook.Detour(GetFileSizeFn) = .{}; + +const ReadFileFn = fn (u32, [*]u8, u32, ?*u32, u32, u32) callconv(sc) u32; +var read_file_hook: hook.Detour(ReadFileFn) = .{}; + +const CleanupFileFn = fn (u32) callconv(sc) void; +var cleanup_file_handle_hook: hook.Detour(CleanupFileFn) = .{}; + +const ProcessAsyncFn = fn (u32) callconv(fc) void; +var process_async_hook: hook.Detour(ProcessAsyncFn) = .{}; + +const tc: std.builtin.CallingConvention = .{ .x86_thiscall = .{} }; +const LoadModelFn = fn (u32, u32, u32) callconv(tc) u32; +var model_load_hook: hook.Detour(LoadModelFn) = .{}; // Windows API imports for async handling extern "kernel32" fn EnterCriticalSection(lpCriticalSection: *anyopaque) callconv(WINAPI) void; @@ -541,10 +550,7 @@ fn openFileDetour( return 2; // success (non-zero type code) } - const orig = open_file_hook.getTrampoline( - *const fn (u32, [*:0]const u8, u32, *u32) callconv(sc) u32, - ); - return orig(archive_ptr, path, flags, handle_out); + return open_file_hook.callOriginal(.{ archive_ptr, path, flags, handle_out }); } // --- Hook 2: GetFileSizeFromHandle (0x6487f0) --- @@ -560,10 +566,7 @@ fn getFileSizeDetour( return size; } - const orig = get_file_size_hook.getTrampoline( - *const fn (u32, ?*u32) callconv(sc) u32, - ); - return orig(file_ctx, high_size_out); + return get_file_size_hook.callOriginal(.{ file_ctx, high_size_out }); } // --- Hook 3: ReadFileFromMultipleSources (0x648460) --- @@ -599,16 +602,12 @@ fn readFileDetour( return 1; // success } - const orig = read_file_hook.getTrampoline( - *const fn (u32, [*]u8, u32, ?*u32, u32, u32) callconv(sc) u32, - ); - return orig(ctx, buffer, size, bytes_read_out, async_ptr, param6); + return read_file_hook.callOriginal(.{ ctx, buffer, size, bytes_read_out, async_ptr, param6 }); } // --- Hook 4: processAsyncFileOperation (0x647350) --- -fn processAsyncDetour(param1: u32, _edx: u32) callconv(.c) void { - _ = _edx; +fn processAsyncDetour(param1: u32) callconv(fc) void { const ctx_addr = hook.readMem(u32, param1 + 0x08); if (isFakeFileContext(ctx_addr)) { @@ -654,8 +653,8 @@ fn processAsyncDetour(param1: u32, _edx: u32) callconv(.c) void { return; } - // Not our fake — call original via trampoline (__fastcall ECX=param1) - hook.fastcall(void, process_async_hook.trampoline, param1, 0); + // Not our fake — call original + process_async_hook.callOriginal(.{param1}); } // --- Hook 6: CleanupFileHandleResources (0x648730) --- @@ -674,18 +673,14 @@ fn cleanupFileHandleDetour(file_ctx: u32) callconv(sc) void { // Always use original CleanupFileHandleResources — it handles fake contexts correctly // (NULL-safe checks on +0x04/+0x3C/+0x40/+0x08, then cleanupFileContext + FreeMemory). - const orig = cleanup_file_handle_hook.getTrampoline( - *const fn (u32) callconv(sc) void, - ); - orig(file_ctx); + cleanup_file_handle_hook.callOriginal(.{file_ctx}); if (fake) con.fmt("[file] cleanup FAKE @0x{x} done\n", .{file_ctx}); } // --- Hook 5: loadModelFromFileAsync (0x71d4e0) --- -fn loadModelAsyncDetour(model: u32, _edx: u32, file_handle: u32, should_use_callback: u32) callconv(.c) u32 { - _ = _edx; +fn loadModelAsyncDetour(model: u32, file_handle: u32, should_use_callback: u32) callconv(tc) u32 { // file_handle IS the file context address directly (Ghidra shows pointer* but // the assembly pushes it directly to GetFileSizeFromHandle — no dereference) @@ -734,16 +729,9 @@ fn loadModelAsyncDetour(model: u32, _edx: u32, file_handle: u32, should_use_call // 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, avoids the callconv crash from Issue 1). Original is __stdcall(1). + // detour). Must clean up file context before processLoadedModelData runs. con.fmt("[file] cleanup via trampoline fh=0x{x}\n", .{file_handle}); - asm volatile ( - \\push %[fh] - \\call *%[func] - : - : [fh] "r" (file_handle), - [func] "r" (cleanup_file_handle_hook.trampoline), - : .{ .eax = true, .ecx = true, .edx = true, .memory = true, .cc = true } - ); + cleanup_file_handle_hook.original()(file_handle); con.print("[file] cleanup done\n"); // Dump model fields before processLoadedModelData @@ -774,56 +762,38 @@ fn loadModelAsyncDetour(model: u32, _edx: u32, file_handle: u32, should_use_call return 1; } - // Not our fake — call original trampoline (__thiscall: ECX=model, stack: fileHandle, shouldUseCallback) - return asm volatile ( - \\push %[cb] - \\push %[fh] - \\call *%[func] - : [ret] "={eax}" (-> u32), - : [_] "{ecx}" (model), - [fh] "r" (file_handle), - [cb] "r" (should_use_callback), - [func] "r" (model_load_hook.trampoline), - : .{ .edx = true, .memory = true, .cc = true } - ); + // Not our fake — call original + return model_load_hook.callOriginal(.{ model, file_handle, should_use_callback }); } // --- Install/remove in-memory file hooks --- fn installFileHooks() void { - _ = open_file_hook.install(0x6477c0, 9, @intFromPtr(&openFileDetour), &.{}); - _ = get_file_size_hook.install(0x6487f0, 6, @intFromPtr(&getFileSizeDetour), &.{}); - _ = read_file_hook.install(0x648460, 6, @intFromPtr(&readFileDetour), &.{}); - _ = cleanup_file_handle_hook.install(0x648730, 7, @intFromPtr(&cleanupFileHandleDetour), &.{}); - - // loadModelFromFileAsync is __thiscall(ECX=model, fileHandle, shouldUseCallback) — needs thunk bridge - if (model_load_hook.prepare(0x71d4e0, 6, &.{})) { - const thunk = model_load_hook.mem.? + 32; - _ = hook.buildFastcallToCdeclThunk(thunk, @intFromPtr(&loadModelAsyncDetour), 2); - model_load_hook.activate(@intFromPtr(thunk)); - } - + _ = 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); con.print("[file] in-memory file hooks installed\n"); } fn removeFileHooks() void { - model_load_hook.remove(); - process_async_hook.remove(); - cleanup_file_handle_hook.remove(); - read_file_hook.remove(); - get_file_size_hook.remove(); - open_file_hook.remove(); + 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 lsf_hook: hook.Hook = .{}; +var lsf_hook: hook.Detour(fn () callconv(sc) void) = .{}; fn loadScriptFunctionsDetour() callconv(sc) void { - const orig = lsf_hook.getTrampoline(*const fn () callconv(sc) void); - orig(); + lsf_hook.callOriginal(.{}); registerLuaFunctions(); } @@ -831,12 +801,10 @@ fn loadScriptFunctionsDetour() callconv(sc) void { // Hook: LoadAddonsRecursively (0x51F600) // ============================================================================= -var load_addons_hook: hook.Hook = .{}; +var load_addons_hook: hook.Detour(fn (u32) callconv(fc) void) = .{}; -fn loadAddonsDetour(error_handler: u32, _edx: u32) callconv(.c) void { - _ = _edx; - - callOrigLoadAddons(error_handler); +fn loadAddonsDetour(error_handler: u32) callconv(fc) void { + load_addons_hook.callOriginal(.{error_handler}); var md5ctx = std.mem.zeroes([88]u8); @@ -898,16 +866,6 @@ fn loadAddonsDetour(error_handler: u32, _edx: u32) callconv(.c) void { } } -fn callOrigLoadAddons(error_handler: u32) void { - asm volatile ( - \\call *%[func] - : - : [_] "{ecx}" (error_handler), - [func] "r" (load_addons_hook.trampoline), - : .{ .eax = true, .edx = true, .memory = true, .cc = true } - ); -} - fn callLoadFileListWithIncludes(toc_path: [*:0]const u8, md5ctx: *[88]u8, error_handler: u32) void { asm volatile ( \\push %[eh] @@ -942,11 +900,10 @@ fn callLoadUIBindingsFromFile(path: [*:0]const u8, md5ctx: *[88]u8, callback: u3 // Hook: GameEngine_MainInitialize (0x46a400) // ============================================================================= -var engine_init_hook: hook.Hook = .{}; +var engine_init_hook: hook.Detour(fn () callconv(sc) void) = .{}; fn engineInitDetour() callconv(sc) void { - const orig = engine_init_hook.getTrampoline(*const fn () callconv(sc) void); - orig(); + engine_init_hook.callOriginal(.{}); if (build_opts.screenshot) { screenshot.installHook(); @@ -960,7 +917,7 @@ fn engineInitDetour() callconv(sc) void { // Hook: CGGameUI_Shutdown (0x490BD0) // ============================================================================= -var shutdown_hook: hook.Hook = .{}; +var shutdown_hook: hook.Detour(fn () callconv(sc) void) = .{}; fn shutdownDetour() callconv(sc) void { // Clean up world objects BEFORE game shutdown — atexit handlers run before DllMain @@ -969,8 +926,7 @@ fn shutdownDetour() callconv(sc) void { markers.removeHooks(); } - const orig = shutdown_hook.getTrampoline(*const fn () callconv(sc) void); - orig(); + shutdown_hook.callOriginal(.{}); } // ============================================================================= @@ -980,10 +936,10 @@ fn shutdownDetour() callconv(sc) void { fn install() void { con.init(); con.print("[weirdutils] Installing hooks\n"); - _ = protection_hook.install(0x42a320, 6, @intFromPtr(&luaProtectionDetour), &.{}); + _ = protection_hook.attach(0x42a320, &luaProtectionDetour); installFileHooks(); - _ = file_hook.install(0x648620, 6, @intFromPtr(&loadFileDetour), &.{}); - _ = lsf_hook.install(0x490250, 6, @intFromPtr(&loadScriptFunctionsDetour), &.{1}); + _ = file_hook.attach(0x648620, &loadFileDetour); + _ = lsf_hook.attach(0x490250, &loadScriptFunctionsDetour); if (build_opts.assetfix) { _ = assetfix.installHooks(); @@ -1001,23 +957,19 @@ fn install() void { minimapicons.installHooks(); } - if (load_addons_hook.prepare(0x51F600, 7, &.{})) { - const thunk = load_addons_hook.mem.? + 32; - _ = hook.buildFastcallToCdeclThunk(thunk, @intFromPtr(&loadAddonsDetour), 0); - load_addons_hook.activate(@intFromPtr(thunk)); - } + _ = load_addons_hook.attach(0x51F600, &loadAddonsDetour); if (build_opts.interact) { interact.installHooks(); } - _ = engine_init_hook.install(0x46a400, 6, @intFromPtr(&engineInitDetour), &.{}); - _ = shutdown_hook.install(0x490BD0, 6, @intFromPtr(&shutdownDetour), &.{1}); + _ = engine_init_hook.attach(0x46a400, &engineInitDetour); + _ = shutdown_hook.attach(0x490BD0, &shutdownDetour); } fn uninstall() void { - shutdown_hook.remove(); - engine_init_hook.remove(); + shutdown_hook.detach(); + engine_init_hook.detach(); // Markers must be cleaned up first — destroys world objects while game systems are still alive if (build_opts.markers) { @@ -1048,11 +1000,11 @@ fn uninstall() void { assetfix.removeHooks(); } - load_addons_hook.remove(); - lsf_hook.remove(); - file_hook.remove(); + load_addons_hook.detach(); + lsf_hook.detach(); + file_hook.detach(); removeFileHooks(); - protection_hook.remove(); + protection_hook.detach(); con.deinit(); } diff --git a/src/markers/addon/Bindings.xml b/src/markers/addon/Bindings.xml index 7077d2c..3ae9be8 100644 --- a/src/markers/addon/Bindings.xml +++ b/src/markers/addon/Bindings.xml @@ -1,5 +1,20 @@ - - TestMarkerToggle() + + WorldMarker(1) + + + WorldMarker(2) + + + WorldMarker(3) + + + WorldMarker(4) + + + WorldMarker(5) + + + ClearWorldMarker() diff --git a/src/markers/addon/Markers.lua b/src/markers/addon/Markers.lua index 77edb09..3a5649b 100644 --- a/src/markers/addon/Markers.lua +++ b/src/markers/addon/Markers.lua @@ -1,7 +1,7 @@ -- Markers addon (embedded in DLL, loaded from memory) -- Part of WeirdUtils - only loaded when markers module is compiled -MARKERS_VERSION = 1 +MARKERS_VERSION = 2 BINDING_HEADER_MARKERS = "Markers" @@ -21,25 +21,64 @@ SlashCmdList["MARKERS"] = function(msg) if msg == "" or msg == "help" then DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00Markers|r commands:") - DEFAULT_CHAT_FRAME:AddMessage(" /mark test - Toggle test marker at player position") - DEFAULT_CHAT_FRAME:AddMessage(" /mark on - Create test marker") - DEFAULT_CHAT_FRAME:AddMessage(" /mark off - Destroy test marker") - DEFAULT_CHAT_FRAME:AddMessage(" /mark pos - Show player position") + DEFAULT_CHAT_FRAME:AddMessage(" /mark <1-5> - Place marker at cursor") + DEFAULT_CHAT_FRAME:AddMessage(" /mark <1-5> - Place marker at unit") + DEFAULT_CHAT_FRAME:AddMessage(" /mark <1-5> - Place marker at coords") + DEFAULT_CHAT_FRAME:AddMessage(" /mark clear [1-5] - Clear one or all markers") + DEFAULT_CHAT_FRAME:AddMessage(" /mark pos - Show player position") - elseif msg == "test" or msg == "toggle" then - TestMarkerToggle() - - elseif msg == "on" or msg == "create" then - TestMarkerCreate() - - elseif msg == "off" or msg == "destroy" then - TestMarkerDestroy() + elseif msg == "clear" then + ClearWorldMarker() + DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00Markers:|r all cleared") elseif msg == "pos" or msg == "position" then local x, y, z = GetPlayerPosition() - DEFAULT_CHAT_FRAME:AddMessage(string.format("Position: %.2f, %.2f, %.2f", x, y, z)) + if x then + DEFAULT_CHAT_FRAME:AddMessage(string.format("Position: %.2f, %.2f, %.2f", x, y, z)) + else + DEFAULT_CHAT_FRAME:AddMessage("|cffff0000No player position available|r") + end else - DEFAULT_CHAT_FRAME:AddMessage("|cffff0000Unknown command:|r " .. msg) + -- Parse arguments + local parts = {} + for word in string.gfind(msg, "%S+") do + table.insert(parts, word) + end + + -- /mark clear + if parts[1] == "clear" then + local index = tonumber(parts[2]) + if index then + ClearWorldMarker(index) + else + ClearWorldMarker() + end + return + end + + local index = tonumber(parts[1]) + if not index then + DEFAULT_CHAT_FRAME:AddMessage("|cffff0000Usage:|r /mark <1-5> [unit | x y z] or /mark clear") + return + end + + if tonumber(parts[2]) then + -- /mark 1 x y z + local x = tonumber(parts[2]) + local y = tonumber(parts[3]) + local z = tonumber(parts[4]) + if x and y and z then + WorldMarker(index, x, y, z) + else + DEFAULT_CHAT_FRAME:AddMessage("|cffff0000Usage:|r /mark <1-5> ") + end + elseif parts[2] then + -- /mark 1 target + WorldMarker(index, parts[2]) + else + -- /mark 1 (cursor position) + WorldMarker(index) + end end end diff --git a/src/markers/markers.zig b/src/markers/markers.zig index 9a20615..16ddbdd 100644 --- a/src/markers/markers.zig +++ b/src/markers/markers.zig @@ -1,15 +1,17 @@ -//! Client-side marker system +//! Client-side world marker system //! -//! Creates world objects at arbitrary positions for raid markers, -//! player indicators, etc. +//! Manages up to 5 colored markers placed at world positions. +//! Uses CreateEntityInstance_WithAttachment (0x6707c0) for entity lifecycle. //! -//! Uses CreateEntityInstance_WithAttachment (0x6707c0) — the game's -//! native high-level entity creation API. For M2 models, this routes -//! through CreateWorldUnit which handles all spatial registration, -//! render setup, and lifecycle management. +//! Lua API: +//! 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 const std = @import("std"); -const hook = @import("hook"); +const hook = @import("zhook"); const o = @import("offsets.zig"); const wow = @import("../outline/wow.zig"); const con = @import("../console.zig"); @@ -26,7 +28,22 @@ var g_mutex: ?*anyopaque = null; var g_is_hook_owner: bool = false; // ============================================================================= -// Position helpers +// Constants +// ============================================================================= + +const NUM_MARKERS = 5; +const MARKER_Z_OFFSET: f32 = 2.0; + +const MODEL_PATHS = [NUM_MARKERS][*:0]const u8{ + "Spells\\Raid_UI_FX_Yellow.m2", + "Spells\\Raid_UI_FX_Cyan.m2", + "Spells\\Raid_UI_FX_Green.m2", + "Spells\\Raid_UI_FX_Purple.m2", + "Spells\\Raid_UI_FX_Red.m2", +}; + +// ============================================================================= +// Types // ============================================================================= pub const Vec3 = struct { @@ -35,7 +52,59 @@ pub const Vec3 = struct { z: f32, }; -/// Get unit position from movement struct +// ============================================================================= +// State +// ============================================================================= + +var marker_entities: [NUM_MARKERS]?*anyopaque = .{null} ** NUM_MARKERS; + +// ============================================================================= +// Lua C API (WoW 1.12.1 — all __fastcall, L in ECX) +// ============================================================================= + +const fc = std.builtin.CallingConvention{ .x86_fastcall = .{} }; + +const lapi = struct { + fn gettop(L: u32) i32 { + const f: *const fn (u32) callconv(fc) i32 = @ptrFromInt(0x6F3070); + return f(L); + } + + fn isnumber(L: u32, index: i32) bool { + const f: *const fn (u32, i32) callconv(fc) u32 = @ptrFromInt(0x6F34D0); + return f(L, index) != 0; + } + + fn isstring(L: u32, index: i32) bool { + const f: *const fn (u32, i32) callconv(fc) u32 = @ptrFromInt(0x6F3510); + return f(L, index) != 0; + } + + fn tonumber(L: u32, index: i32) f64 { + const f: *const fn (u32, i32) callconv(fc) f64 = @ptrFromInt(0x6F3620); + return f(L, index); + } + + fn tostring(L: u32, index: i32) ?[*:0]const u8 { + const f: *const fn (u32, i32) callconv(fc) ?[*:0]const u8 = @ptrFromInt(0x6F3690); + return f(L, index); + } + + fn pushstring(L: u32, s: [*:0]const u8) void { + const f: *const fn (u32, [*:0]const u8) callconv(fc) void = @ptrFromInt(0x6F3890); + f(L, s); + } + + fn pushnumber(L: u32, n: f64) void { + const f: *const fn (u32, f64) callconv(fc) void = @ptrFromInt(0x6F3810); + f(L, n); + } +}; + +// ============================================================================= +// Position helpers +// ============================================================================= + pub fn getUnitPosition(unit: u32) Vec3 { if (unit == 0) return .{ .x = 0, .y = 0, .z = 0 }; @@ -49,28 +118,38 @@ pub fn getUnitPosition(unit: u32) Vec3 { }; } +/// Resolve a unit ID string ("player", "target", etc.) to a world position. +fn resolveUnitPosition(unit_id: [*:0]const u8) ?Vec3 { + const guid = wow.unitGUID(unit_id); + if (guid == 0) return null; + const obj = wow.getObjectByGUID(guid); + if (obj == 0) return null; + const pos = getUnitPosition(obj); + if (pos.x == 0 and pos.y == 0 and pos.z == 0) return null; + return pos; +} + +/// Get the terrain position under the mouse cursor. +/// TODO: find the actual game global/function for this. +fn getCursorTerrainPosition() ?Vec3 { + // TODO: implement — needs Ghidra research to find the cursor terrain + // intersection global or CGGameUI member that stores it. + return null; +} + // ============================================================================= // Game function wrappers // ============================================================================= /// CreateEntityInstance_WithAttachment — __fastcall, RET 0x14. -/// ECX=modelPath, EDX=positionVec3ptr, stack: facing, flags, updateNow, param6, param7. -/// Routes M2 models through CreateWorldUnit, WMO models through CreateGameObject. -/// Returns a fully-registered game entity pointer. -/// -/// NOTE: When updateNow=1, UpdateWorldPosition (0x698110) modifies the position -/// vector in-place, snapping the entity's bounding sphere to the terrain chunk -/// grid. This means the entity may render a few units away from the requested -/// position. The offset varies depending on proximity to grid boundaries. -/// Pass updateNow=0 to skip this, at the cost of no spatial grid registration. fn createEntityInstance(path: [*:0]const u8, pos: *[3]f32, facing: f32, flags: u32, update_now: u32) ?*anyopaque { const facing_bits: u32 = @bitCast(facing); const stack_args = [5]u32{ - facing_bits, // param_3: facing angle - flags, // param_4: flags/type - update_now, // param_5: update position immediately (1=yes) - 0, // param_6 - 0, // param_7 + facing_bits, + flags, + update_now, + 0, + 0, }; const result: u32 = asm volatile ( @@ -91,8 +170,6 @@ fn createEntityInstance(path: [*:0]const u8, pos: *[3]f32, facing: f32, flags: u } /// CleanupEntity_ProcessAttachments — __fastcall(ECX=entity), no stack params. -/// High-level destructor: frees attachments, decrements refcount, dispatches -/// to type-specific cleanup (render detach + scene graph removal + heap free). fn cleanupEntity(obj: *anyopaque) void { asm volatile ("call *%[func]" : @@ -105,112 +182,133 @@ fn cleanupEntity(obj: *anyopaque) void { // Marker management // ============================================================================= -var test_marker: ?*anyopaque = null; +/// Place a marker at the given world position. Replaces any existing marker in that slot. +/// index is 0-based (0..4). +fn placeMarker(index: usize, pos: Vec3) bool { + if (index >= NUM_MARKERS) return false; -const MODEL_PATH: [*:0]const u8 = "Spells\\Raid_UI_FX_Yellow.m2"; + // Clear existing marker in this slot + clearMarker(index); -/// Create a test marker at player position using the native entity creation API. -pub fn createTestMarker() ?*anyopaque { - if (test_marker != null) { - con.print("[markers] marker already exists, destroy first\n"); - return test_marker; - } + var position = [3]f32{ pos.x, pos.y, pos.z + MARKER_Z_OFFSET }; - const player = wow.getLocalPlayer(); - if (player == 0) { - con.print("[markers] no local player\n"); - return null; - } - - const pos = getUnitPosition(player); - con.fmt("[markers] player pos = {d:.1}, {d:.1}, {d:.1}\n", .{ pos.x, pos.y, pos.z }); - if (pos.x == 0 and pos.y == 0 and pos.z == 0) return null; - - var position = [3]f32{ pos.x, pos.y, pos.z + 2.0 }; - - con.print("[markers] calling CreateEntityInstance_WithAttachment...\n"); - const obj = createEntityInstance(MODEL_PATH, &position, 0.0, 0, 1) orelse { - con.print("[markers] CreateEntityInstance_WithAttachment FAILED\n"); - return null; + const obj = createEntityInstance(MODEL_PATHS[index], &position, 0.0, 0, 1) orelse { + con.fmt("[markers] failed to create marker {d}\n", .{index + 1}); + return false; }; - const obj_ptr: u32 = @intCast(@intFromPtr(obj)); - con.fmt("[markers] entity created at 0x{X:0>8}\n", .{obj_ptr}); - - // Debug dump key fields - const refcount = hook.readMem(u16, obj_ptr + 0xE); - const flags_90 = hook.readMem(u32, obj_ptr + 0x90); - const model_88 = hook.readMem(u32, obj_ptr + 0x88); - const pos_x = hook.readMem(f32, obj_ptr + 0x5C); - const pos_y = hook.readMem(f32, obj_ptr + 0x60); - const pos_z = hook.readMem(f32, obj_ptr + 0x64); - con.fmt("[markers] refcount={d} flags90=0x{X:0>8} model88=0x{X:0>8}\n", .{ refcount, flags_90, model_88 }); - con.fmt("[markers] bsph(+5C)={d:.1},{d:.1},{d:.1}\n", .{ pos_x, pos_y, pos_z }); - - test_marker = obj; - return obj; + marker_entities[index] = obj; + con.fmt("[markers] marker {d} placed at {d:.1}, {d:.1}, {d:.1}\n", .{ index + 1, pos.x, pos.y, pos.z }); + return true; } -/// Destroy the test marker. Safe to call multiple times. -pub fn destroyTestMarker() void { - const marker = test_marker orelse return; - test_marker = null; - - con.print("[markers] destroying marker...\n"); - cleanupEntity(marker); - con.print("[markers] marker destroyed\n"); +/// Remove a specific marker. index is 0-based. +fn clearMarker(index: usize) void { + if (index >= NUM_MARKERS) return; + if (marker_entities[index]) |existing| { + cleanupEntity(existing); + marker_entities[index] = null; + } } -// ============================================================================= -// Lua helpers -// ============================================================================= - -/// lua_pushnumber at 0x6F3810: __fastcall(L_ECX, double_on_stack). Callee cleans (RET 8). -fn luaPushNumber(L: u32, n: f64) void { - const raw: [2]u32 = @bitCast(n); - asm volatile ( - \\push %[hi] - \\push %[lo] - \\call *%[func] - : - : [_] "{ecx}" (L), - [lo] "r" (raw[0]), - [hi] "r" (raw[1]), - [func] "r" (@as(u32, 0x6F3810)), - : .{ .eax = true, .edx = true, .memory = true, .cc = true }); +/// Remove all markers. +fn clearAllMarkers() void { + var any = false; + for (0..NUM_MARKERS) |i| { + if (marker_entities[i]) |existing| { + cleanupEntity(existing); + marker_entities[i] = null; + any = true; + } + } + if (any) con.print("[markers] all markers cleared\n"); } // ============================================================================= // Lua API // ============================================================================= -/// Lua: TestMarkerCreate() - create marker at player position -pub fn luaTestMarkerCreate(L: u32) callconv(.c) u32 { - if (createTestMarker()) |_| { - hook.fastcall(void, 0x6F3890, L, @intFromPtr(@as([*:0]const u8, "Marker created"))); - } else { - hook.fastcall(void, 0x6F3890, L, @intFromPtr(@as([*:0]const u8, "Failed to create marker"))); +/// Lua: WorldMarker(index [, x, y, z | "unitId"]) +/// WorldMarker(1, x, y, z) — place at coordinates +/// WorldMarker(1, "target") — place at unit's current position +/// WorldMarker(1) — place at cursor terrain position +pub fn luaWorldMarker(L: u32) callconv(.c) u32 { + const nargs = lapi.gettop(L); + + if (nargs < 1 or !lapi.isnumber(L, 1)) { + con.print("[markers] WorldMarker: expected index (1-5)\n"); + return 0; } - return 1; + + const raw_index = @as(i32, @intFromFloat(lapi.tonumber(L, 1))); + if (raw_index < 1 or raw_index > NUM_MARKERS) { + con.print("[markers] WorldMarker: index must be 1-5\n"); + return 0; + } + const index: usize = @intCast(raw_index - 1); + + if (nargs >= 4 and lapi.isnumber(L, 2)) { + // WorldMarker(index, x, y, z) + const x: f32 = @floatCast(lapi.tonumber(L, 2)); + const y: f32 = @floatCast(lapi.tonumber(L, 3)); + const z: f32 = @floatCast(lapi.tonumber(L, 4)); + _ = placeMarker(index, .{ .x = x, .y = y, .z = z }); + } else if (nargs >= 2 and lapi.isstring(L, 2)) { + // WorldMarker(index, "unitId") + const unit_id = lapi.tostring(L, 2) orelse { + con.print("[markers] WorldMarker: invalid unit string\n"); + return 0; + }; + const pos = resolveUnitPosition(unit_id) orelse { + con.fmt("[markers] WorldMarker: unit '{s}' not found\n", .{std.mem.span(unit_id)}); + return 0; + }; + _ = placeMarker(index, pos); + } else { + // WorldMarker(index) — cursor terrain position + const pos = getCursorTerrainPosition() orelse { + con.print("[markers] cursor terrain position not yet implemented, using player\n"); + // Fallback to player position + const player = wow.getLocalPlayer(); + if (player == 0) { + con.print("[markers] no local player\n"); + return 0; + } + const ppos = getUnitPosition(player); + if (ppos.x == 0 and ppos.y == 0 and ppos.z == 0) return 0; + _ = placeMarker(index, ppos); + return 0; + }; + _ = placeMarker(index, pos); + } + + return 0; } -/// Lua: TestMarkerDestroy() - destroy test marker -pub fn luaTestMarkerDestroy(L: u32) callconv(.c) u32 { - destroyTestMarker(); - hook.fastcall(void, 0x6F3890, L, @intFromPtr(@as([*:0]const u8, "Marker destroyed"))); - return 1; -} +/// Lua: ClearWorldMarker([index]) +/// ClearWorldMarker(1) — remove marker 1 +/// ClearWorldMarker() — remove all markers +pub fn luaClearWorldMarker(L: u32) callconv(.c) u32 { + const nargs = lapi.gettop(L); -/// Lua: TestMarkerToggle() - toggle marker on/off -pub fn luaTestMarkerToggle(L: u32) callconv(.c) u32 { - if (test_marker != null) { - destroyTestMarker(); - hook.fastcall(void, 0x6F3890, L, @intFromPtr(@as([*:0]const u8, "Marker off"))); - } else { - _ = createTestMarker(); - hook.fastcall(void, 0x6F3890, L, @intFromPtr(@as([*:0]const u8, "Marker on"))); + if (nargs == 0) { + clearAllMarkers(); + return 0; } - return 1; + + if (!lapi.isnumber(L, 1)) { + con.print("[markers] ClearWorldMarker: expected index (1-5) or no args\n"); + return 0; + } + + const raw_index = @as(i32, @intFromFloat(lapi.tonumber(L, 1))); + if (raw_index < 1 or raw_index > NUM_MARKERS) { + con.print("[markers] ClearWorldMarker: index must be 1-5\n"); + return 0; + } + + clearMarker(@intCast(raw_index - 1)); + return 0; } /// Lua: local x, y, z = GetPlayerPosition() @@ -219,9 +317,9 @@ pub fn luaGetPlayerPosition(L: u32) callconv(.c) u32 { if (player == 0) return 0; const pos = getUnitPosition(player); - luaPushNumber(L, @floatCast(pos.x)); - luaPushNumber(L, @floatCast(pos.y)); - luaPushNumber(L, @floatCast(pos.z)); + lapi.pushnumber(L, @floatCast(pos.x)); + lapi.pushnumber(L, @floatCast(pos.y)); + lapi.pushnumber(L, @floatCast(pos.z)); return 3; } @@ -232,7 +330,6 @@ pub fn luaGetPlayerPosition(L: u32) callconv(.c) u32 { pub fn installHooks() void { con.print("[markers] Module loaded\n"); - // Multi-DLL safety: only one instance per process should own markers var mutex_name_buf: [64]u8 = undefined; const mutex_name = std.fmt.bufPrint(&mutex_name_buf, "Local\\MarkersHook_{d}", .{GetCurrentProcessId()}) catch return; mutex_name_buf[mutex_name.len] = 0; @@ -248,13 +345,11 @@ pub fn installHooks() void { return; } g_is_hook_owner = true; - - // Nothing to hook - markers are created via Lua commands } pub fn removeHooks() void { if (g_is_hook_owner) { - destroyTestMarker(); + clearAllMarkers(); } if (g_is_hook_owner) {