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) {