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:
+70
-118
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user