Replace test marker with WorldMarker/ClearWorldMarker Lua API

5 marker slots (Yellow, Cyan, Green, Purple, Red) with placement
by coords, unit ID, or cursor position (stubbed pending terrain
offset research). Key bindings for all 5 markers + clear all.
This commit is contained in:
MarcelineVQ
2026-03-01 16:23:51 -08:00
parent 608e1c43d5
commit d59cf42514
4 changed files with 355 additions and 254 deletions
+70 -118
View File
@@ -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();
}
+17 -2
View File
@@ -1,5 +1,20 @@
<Bindings>
<Binding name="Toggle Test Marker" header="MARKERS">
TestMarkerToggle()
<Binding name="Place Marker 1" header="MARKERS">
WorldMarker(1)
</Binding>
<Binding name="Place Marker 2" header="MARKERS">
WorldMarker(2)
</Binding>
<Binding name="Place Marker 3" header="MARKERS">
WorldMarker(3)
</Binding>
<Binding name="Place Marker 4" header="MARKERS">
WorldMarker(4)
</Binding>
<Binding name="Place Marker 5" header="MARKERS">
WorldMarker(5)
</Binding>
<Binding name="Clear All Markers" header="MARKERS">
ClearWorldMarker()
</Binding>
</Bindings>
+54 -15
View File
@@ -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> <unit> - Place marker at unit")
DEFAULT_CHAT_FRAME:AddMessage(" /mark <1-5> <x> <y> <z> - 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 <index>
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> <x> <y> <z>")
end
elseif parts[2] then
-- /mark 1 target
WorldMarker(index, parts[2])
else
-- /mark 1 (cursor position)
WorldMarker(index)
end
end
end
+214 -119
View File
@@ -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) {