From 37d42d09c209ae94cb1fb69feb0433ac6fb1160a Mon Sep 17 00:00:00 2001 From: MarcelineVQ Date: Thu, 26 Feb 2026 15:30:21 -0800 Subject: [PATCH] Add markers module, framecrash stub, debug console, build-flag wiring Markers: client-side world object system using CreateGameObject. Places M2 models at arbitrary world positions via Lua commands (/mark test, /mark pos). Includes embedded addon, xyz.m2/blp assets served from DLL memory, position helpers from unit movement struct, and object lifecycle management (create, cleanup, reposition, alpha, animation). Framecrash: stub module with reference to crash at 0x007A2452. Console: debug output via AllocConsole/WriteConsoleA, compiles out entirely in non-Debug builds. Used by markers and file serve logging. Build: markers added to feature flag matrix and all-variants step. Main: conditional markers import, Lua function registration, embedded addon + asset file serving, console init/deinit lifecycle. Outline README: added misc planned features and debug mode notes. --- build.zig | 14 +- src/console.zig | 65 +++++ src/framecrash/framecrash.zig | 5 + src/main.zig | 38 +++ src/markers/addon/Bindings.xml | 5 + src/markers/addon/Markers.lua | 46 +++ src/markers/addon/Markers.toc | 8 + .../assets/World/ArtTest/Boxtest/xyz.blp | Bin 0 -> 44876 bytes .../assets/World/ArtTest/Boxtest/xyz.m2 | Bin 0 -> 8128 bytes src/markers/markers.zig | 271 ++++++++++++++++++ src/markers/offsets.zig | 96 +++++++ src/outline/README.md | 5 + 12 files changed, 548 insertions(+), 5 deletions(-) create mode 100644 src/console.zig create mode 100644 src/framecrash/framecrash.zig create mode 100644 src/markers/addon/Bindings.xml create mode 100644 src/markers/addon/Markers.lua create mode 100644 src/markers/addon/Markers.toc create mode 100644 src/markers/assets/World/ArtTest/Boxtest/xyz.blp create mode 100644 src/markers/assets/World/ArtTest/Boxtest/xyz.m2 create mode 100644 src/markers/markers.zig create mode 100644 src/markers/offsets.zig diff --git a/build.zig b/build.zig index b62e447..fcde2f5 100644 --- a/build.zig +++ b/build.zig @@ -12,12 +12,14 @@ pub fn build(b: *std.Build) void { const enable_screenshot = b.option(bool, "screenshot", "Enable screenshot module") orelse true; const enable_interact = b.option(bool, "interact", "Enable interact module") orelse true; const enable_outline = b.option(bool, "outline", "Enable outline module") orelse true; + const enable_markers = b.option(bool, "markers", "Enable markers module") orelse true; // Create build options module const build_options = b.addOptions(); build_options.addOption(bool, "enable_screenshot", enable_screenshot); build_options.addOption(bool, "enable_interact", enable_interact); build_options.addOption(bool, "enable_outline", enable_outline); + build_options.addOption(bool, "enable_markers", enable_markers); const build_options_module = build_options.createModule(); const hook_mod = b.dependency("hook", .{ @@ -45,16 +47,18 @@ pub fn build(b: *std.Build) void { const build_all_step = b.step("all-variants", "Build all DLL variants"); // Helper to create a single-module build - inline for (&[_]struct { name: []const u8, screenshot: bool, interact: bool, outline: bool }{ - .{ .name = "full", .screenshot = true, .interact = true, .outline = true }, - .{ .name = "screenshot", .screenshot = true, .interact = false, .outline = false }, - .{ .name = "interact", .screenshot = false, .interact = true, .outline = false }, - .{ .name = "outline", .screenshot = false, .interact = false, .outline = true }, + inline for (&[_]struct { name: []const u8, screenshot: bool, interact: bool, outline: bool, markers: bool }{ + .{ .name = "full", .screenshot = true, .interact = true, .outline = true, .markers = true }, + .{ .name = "screenshot", .screenshot = true, .interact = false, .outline = false, .markers = false }, + .{ .name = "interact", .screenshot = false, .interact = true, .outline = false, .markers = false }, + .{ .name = "outline", .screenshot = false, .interact = false, .outline = true, .markers = false }, + .{ .name = "markers", .screenshot = false, .interact = false, .outline = false, .markers = true }, }) |variant| { const opts = b.addOptions(); opts.addOption(bool, "enable_screenshot", variant.screenshot); opts.addOption(bool, "enable_interact", variant.interact); opts.addOption(bool, "enable_outline", variant.outline); + opts.addOption(bool, "enable_markers", variant.markers); const variant_lib = b.addLibrary(.{ .name = variant.name, diff --git a/src/console.zig b/src/console.zig new file mode 100644 index 0000000..9f7902c --- /dev/null +++ b/src/console.zig @@ -0,0 +1,65 @@ +//! Debug console — compiles out entirely in non-Debug builds. +//! +//! Usage from any module: +//! const con = @import("../console.zig"); // or appropriate relative path +//! con.print("[markers] loaded\n"); +//! con.fmt("[markers] pos = {d}, {d}, {d}\n", .{ x, y, z }); + +const std = @import("std"); +const builtin = @import("builtin"); + +const debug = builtin.mode == .Debug; + +const WINAPI = std.builtin.CallingConvention.winapi; + +const win = if (debug) struct { + extern "kernel32" fn AllocConsole() callconv(WINAPI) i32; + extern "kernel32" fn FreeConsole() callconv(WINAPI) i32; + extern "kernel32" fn SetConsoleTitleA(title: [*:0]const u8) callconv(WINAPI) i32; + extern "kernel32" fn GetStdHandle(nStdHandle: u32) callconv(WINAPI) ?*anyopaque; + extern "kernel32" fn WriteConsoleA( + hOut: *anyopaque, + buf: [*]const u8, + len: u32, + written: ?*u32, + reserved: ?*anyopaque, + ) callconv(WINAPI) i32; + + const STD_OUTPUT_HANDLE: u32 = 0xFFFFFFF5; + var handle: ?*anyopaque = null; +} else void; + +pub fn init() void { + if (debug) { + _ = win.AllocConsole(); + _ = win.SetConsoleTitleA("weirdutils"); + win.handle = win.GetStdHandle(win.STD_OUTPUT_HANDLE); + print("[weirdutils] Console attached\n"); + } +} + +pub fn deinit() void { + if (debug) { + if (win.handle != null) { + print("[weirdutils] Detaching\n"); + _ = win.FreeConsole(); + win.handle = null; + } + } +} + +pub fn print(msg: []const u8) void { + if (debug) { + if (win.handle) |h| { + _ = win.WriteConsoleA(h, msg.ptr, @intCast(msg.len), null, null); + } + } +} + +pub fn fmt(comptime f: []const u8, args: anytype) void { + if (debug) { + var buf: [512]u8 = undefined; + const msg = std.fmt.bufPrint(&buf, f, args) catch return; + print(msg); + } +} diff --git a/src/framecrash/framecrash.zig b/src/framecrash/framecrash.zig new file mode 100644 index 0000000..dfa389b --- /dev/null +++ b/src/framecrash/framecrash.zig @@ -0,0 +1,5 @@ +//! Framecrash fix module. +//! +//! Reference crash log: /media/bigfaststore/games/twmoa_1172/Errors/2026-02-25 19.38.01 Crash.txt + +//We are finally going to solve: The instruction at "0x007A2452" referenced memory at "0x16C0FFE8". diff --git a/src/main.zig b/src/main.zig index f61bd39..840443c 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,17 +1,20 @@ const std = @import("std"); const hook = @import("hook"); +pub const con = @import("console.zig"); // Build options for conditional module compilation const build_opts = struct { const screenshot = @import("build_options").enable_screenshot; const interact = @import("build_options").enable_interact; const outline = @import("build_options").enable_outline; + const markers = @import("build_options").enable_markers; }; // Conditional module imports const screenshot = if (build_opts.screenshot) @import("screenshot/screenshot.zig") else struct {}; const interact = if (build_opts.interact) @import("interact/interact.zig") else struct {}; const outline = if (build_opts.outline) @import("outline/api.zig") else struct {}; +const markers = if (build_opts.markers) @import("markers/markers.zig") else struct {}; const WINAPI = std.builtin.CallingConvention.winapi; const fc: std.builtin.CallingConvention = .{ .x86_fastcall = .{} }; @@ -243,6 +246,12 @@ fn registerLuaFunctions() void { if (build_opts.outline) { 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("GetPlayerPosition", @intFromPtr(&markers.luaGetPlayerPosition)); + } } // ============================================================================= @@ -285,12 +294,25 @@ const outline_files = if (build_opts.outline) [_]FileEntry{ .{ .name = "Bindings.xml", .data = @embedFile("outline/addon/Bindings.xml") }, } else [_]FileEntry{}; +const markers_files = if (build_opts.markers) [_]FileEntry{ + .{ .name = "Markers.toc", .data = @embedFile("markers/addon/Markers.toc") }, + .{ .name = "Markers.lua", .data = @embedFile("markers/addon/Markers.lua") }, + .{ .name = "Bindings.xml", .data = @embedFile("markers/addon/Bindings.xml") }, +} else [_]FileEntry{}; + +const markers_assets = if (build_opts.markers) [_]FileEntry{ + .{ .name = "xyz.m2", .data = @embedFile("markers/assets/World/ArtTest/Boxtest/xyz.m2") }, + .{ .name = "xyz.blp", .data = @embedFile("markers/assets/World/ArtTest/Boxtest/xyz.blp") }, +} else [_]FileEntry{}; + // All addon prefixes to check const addon_prefixes = [_]AddonPrefix{ .{ .prefix = core_prefix, .files = &core_files }, .{ .prefix = "Interface\\AddOns\\Screenshot\\", .files = &screenshot_files }, .{ .prefix = "Interface\\AddOns\\Interact\\", .files = &interact_files }, .{ .prefix = "Interface\\AddOns\\Outline\\", .files = &outline_files }, + .{ .prefix = "Interface\\AddOns\\Markers\\", .files = &markers_files }, + .{ .prefix = "World\\ArtTest\\Boxtest\\", .files = &markers_assets }, }; fn findEmbeddedFile(path: [*:0]const u8) ?*const FileEntry { @@ -357,6 +379,7 @@ fn loadFileDetour( buf_out.* = buf; if (size_out) |s| s.* = data_len; + con.fmt("[file] served embedded: {s} ({d} bytes)\n", .{ std.mem.span(path), data_len }); return 1; } @@ -435,6 +458,18 @@ fn loadAddonsDetour(error_handler: u32, _edx: u32) callconv(.c) void { error_handler, ); } + if (build_opts.markers) { + callLoadFileListWithIncludes( + "Interface\\AddOns\\Markers\\Markers.toc", + &md5ctx, + error_handler, + ); + callLoadUIBindingsFromFile( + "Interface\\AddOns\\Markers\\Bindings.xml", + &md5ctx, + error_handler, + ); + } } fn callOrigLoadAddons(error_handler: u32) void { @@ -511,6 +546,8 @@ fn shutdownDetour() callconv(sc) void { // ============================================================================= fn install() void { + con.init(); + con.print("[weirdutils] Installing hooks\n"); _ = protection_hook.install(0x42a320, 6, @intFromPtr(&luaProtectionDetour), &.{}); _ = file_hook.install(0x648620, 6, @intFromPtr(&loadFileDetour), &.{}); _ = lsf_hook.install(0x490250, 6, @intFromPtr(&loadScriptFunctionsDetour), &.{1}); @@ -547,6 +584,7 @@ fn uninstall() void { lsf_hook.remove(); file_hook.remove(); protection_hook.remove(); + con.deinit(); } // ============================================================================= diff --git a/src/markers/addon/Bindings.xml b/src/markers/addon/Bindings.xml new file mode 100644 index 0000000..7077d2c --- /dev/null +++ b/src/markers/addon/Bindings.xml @@ -0,0 +1,5 @@ + + + TestMarkerToggle() + + diff --git a/src/markers/addon/Markers.lua b/src/markers/addon/Markers.lua new file mode 100644 index 0000000..c3ba0e7 --- /dev/null +++ b/src/markers/addon/Markers.lua @@ -0,0 +1,46 @@ +-- Markers addon (embedded in DLL, loaded from memory) +-- Part of WeirdUtils - only loaded when markers module is compiled + +MARKERS_VERSION = 1 + +BINDING_HEADER_MARKERS = "Markers" + +local frame = CreateFrame("Frame") +frame:RegisterEvent("ADDON_LOADED") +frame:RegisterEvent("PLAYER_LOGIN") + +frame:SetScript("OnEvent", function() + if event == "PLAYER_LOGIN" then + DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00Markers|r v" .. MARKERS_VERSION .. " loaded") + end +end) + +SLASH_MARKERS1 = "/markers" +SLASH_MARKERS2 = "/mark" +SlashCmdList["MARKERS"] = function(msg) + msg = string.lower(msg or "") + + 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 pos - Show player position") + DEFAULT_CHAT_FRAME:AddMessage(" /mark on - Create test marker") + DEFAULT_CHAT_FRAME:AddMessage(" /mark off - Destroy test marker") + + 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 == "pos" or msg == "position" then + local x, y, z = GetPlayerPosition() + DEFAULT_CHAT_FRAME:AddMessage(string.format("Position: %.2f, %.2f, %.2f", x, y, z)) + + else + DEFAULT_CHAT_FRAME:AddMessage("|cffff0000Unknown command:|r " .. msg) + end +end diff --git a/src/markers/addon/Markers.toc b/src/markers/addon/Markers.toc new file mode 100644 index 0000000..ada3133 --- /dev/null +++ b/src/markers/addon/Markers.toc @@ -0,0 +1,8 @@ +## Interface: 11200 +## Title: Markers +## Notes: Client-side raid markers and indicators +## Version: 1.0 +## Dependencies: WeirdUtils + +Markers.lua +Bindings.xml diff --git a/src/markers/assets/World/ArtTest/Boxtest/xyz.blp b/src/markers/assets/World/ArtTest/Boxtest/xyz.blp new file mode 100644 index 0000000000000000000000000000000000000000..3b74bad70d649b96e96f99f35ee4b584a83be204 GIT binary patch literal 44876 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 LKlsk3quBlriJp-P literal 0 HcmV?d00001 diff --git a/src/markers/assets/World/ArtTest/Boxtest/xyz.m2 b/src/markers/assets/World/ArtTest/Boxtest/xyz.m2 new file mode 100644 index 0000000000000000000000000000000000000000..8e0d3f3a84c682c1a5b034925e08515288585d9d GIT binary patch literal 8128 zcmeHMO=ule6h3JZ8%=9oli233Nwn3bCZ?}VwP~BYm!@eeNFfWsO~FzvB198vlvD~G zx^XKAu3ThiL6GN0DA<=S1Q#MyaN)ALsmnkqxQpkz=iGN^?z{KB1d7e#4dk7fJKsI` zJ7>Ocn3-IhJC>&=q81{W=a}Abc$t3?OZ@XI$9sCb&J!?xxs~Wmj!PVoW4)7qbi?@v zgM!Nq*-;m9exAoo9*&s5q{oTIFEy@eEOA_%y}Q=Fr=^Tc-w!^T?wz<(e0=h^;+31F zBF3KPwsQUR;PS1z-SEauxBs=<4eo7h1cx^<*jxjHjz7)q|60FR51wTGSjX0s-rwpP z_2$Ovv1?;C9oXBpnHurF}k{e?mwf-^v>5aed;{F-H7tcTZ zu0R(Dr({0S0^{HPv#SumC*XaK@yS0oDr-!w^pzucPMH)tKR#T%mBOE5d@Vk}Z;p@j zFvR~V^Z#M|uc>Ukf)DWr{&ocK^N-gn`9u7}e9-Y^&F7En^&wxcQWYP;`~1;Q;J=OF zL;S<_N`9YzoDZ~26yhJgzJd?&uZ8#d$Lp2+K7Z)By8q69KL4=(9p2|3_Lsx^{KNgs z;?w$v-m0&c)QbN5e8T;4c%M($k6FCWr`qoh@AnhWa!ahw>(jRi&W{eia`biqb=VE~ z_t=jCd}1-_Xz>^e8kL;S1jb$Fk@&7b9y=A7P_?-h8SkKsf7tDpZ}PM^Q!ldad`eg0uRpk8d4 zm!2Ede`4T_jq%Uc>+;9=$M8OUnD3TvSGgDD?LEI&;yg#Z z&YSOAybmnxlj8>*&D(ab#9@wa@o8XZcpupN9C2Lg6U_a7iR&C+S8Q%Tu&W$_P4)|B zRlk?r~dYN9KLo`B%=~Wt~F?x;0=?LZNC>^5-I?l}Apc6DnC+QTOrYV}H z0u^b7&d@BKr8zoB=jj4-xlmz+k+XUZsG{OA^R(tbwiB8cD@^EGSoMgm4<@6^3G5Cj zPcS~9+)>4l)`QCWwMJCCPwPepy0jPQN{99bood&fp?f>EkLYN#_SO{D_}OF!px6cD z19;)*-#;stmlq1(eDz}?ry1nChjB_Kr|752s0peP-OL!ZL3N^+Nux%nQgo6xYK3Y= zA0tN1P_^h{*r*+<7d`YCH^3F5gPd^-TqD=N&A17!5`V#A`2Gwl z&0E&9&CHL}w9Y^8EM8l9|I(Rj%Wq%(WckwU;>t22|JQc>%EFQXyZ_kW=_NlT&*d{5 SLtFZPxuNqMxxn^+70 literal 0 HcmV?d00001 diff --git a/src/markers/markers.zig b/src/markers/markers.zig new file mode 100644 index 0000000..83b2b8f --- /dev/null +++ b/src/markers/markers.zig @@ -0,0 +1,271 @@ +//! Client-side marker system using CreateGameObject_WithProperties +//! +//! Creates world objects at arbitrary positions for raid markers, +//! player indicators, etc. + +const hook = @import("hook"); +const o = @import("offsets.zig"); +const wow = @import("../outline/wow.zig"); +const con = @import("../console.zig"); + +// ============================================================================= +// Position helpers +// ============================================================================= + +pub const Vec3 = struct { + x: f32, + y: f32, + z: f32, +}; + +/// Get unit position from movement struct +pub fn getUnitPosition(unit: u32) Vec3 { + if (unit == 0) return .{ .x = 0, .y = 0, .z = 0 }; + + const movement = hook.readMem(u32, unit + o.UNIT_MOVEMENT_OFFSET); + if (movement == 0 or movement < 0x10000) return .{ .x = 0, .y = 0, .z = 0 }; + + return .{ + .x = hook.readMem(f32, movement + o.MOVEMENT_POS_X), + .y = hook.readMem(f32, movement + o.MOVEMENT_POS_Y), + .z = hook.readMem(f32, movement + o.MOVEMENT_POS_Z), + }; +} + +// ============================================================================= +// GameObject creation - function pointer wrappers +// ============================================================================= + +/// CreateGameObject_WithProperties — __fastcall: +/// ECX = model, EDX = callback1 +/// Stack (callee-clean, RET 0x14): callback2, x, y, z, flags +pub fn createGameObject( + model: ?*anyopaque, + callback1: ?*anyopaque, + callback2: ?*anyopaque, + x: f32, + y: f32, + z: f32, + flags: u32, +) ?*anyopaque { + // 5 stack params (callee cleans via RET 0x14) + const stack_args = [5]u32{ + if (callback2) |c| @intCast(@intFromPtr(c)) else 0, + @as(u32, @bitCast(x)), + @as(u32, @bitCast(y)), + @as(u32, @bitCast(z)), + flags, + }; + + const result: u32 = asm volatile ( + \\ push 16(%[a]) + \\ push 12(%[a]) + \\ push 8(%[a]) + \\ push 4(%[a]) + \\ push (%[a]) + \\ call *%[func] + : [ret] "={eax}" (-> u32), + : [_] "{ecx}" (if (model) |m| @intFromPtr(m) else @as(usize, 0)), + [_] "{edx}" (if (callback1) |c| @intFromPtr(c) else @as(usize, 0)), + [a] "r" (&stack_args), + [func] "r" (o.FN_CREATE_GAMEOBJECT), + : .{ .memory = true, .cc = true } + ); + + return if (result != 0) @ptrFromInt(result) else null; +} + +/// CleanupWorldObject(obj) - __thiscall: ECX = obj, 0 stack params +pub fn cleanupWorldObject(obj: *anyopaque) void { + asm volatile ("call *%[func]" + : + : [_] "{ecx}" (@intFromPtr(obj)), + [func] "r" (o.FN_CLEANUP_WORLD_OBJECT), + : .{ .eax = true, .edx = true, .memory = true, .cc = true } + ); +} + +/// Update object position directly +pub fn setObjectPosition(obj: *anyopaque, pos: Vec3) void { + const ptr: u32 = @intCast(@intFromPtr(obj)); + @as(*f32, @ptrFromInt(ptr + o.OBJ_POS_X)).* = pos.x; + @as(*f32, @ptrFromInt(ptr + o.OBJ_POS_Y)).* = pos.y; + @as(*f32, @ptrFromInt(ptr + o.OBJ_POS_Z)).* = pos.z; +} + +/// Set object alpha (0-255) +pub fn setObjectAlpha(obj: *anyopaque, alpha: u8) void { + const ptr: u32 = @intCast(@intFromPtr(obj)); + // ARGB format - alpha in high byte + const color: u32 = (@as(u32, alpha) << 24) | 0x00189680; + @as(*u32, @ptrFromInt(ptr + o.OBJ_COLOR)).* = color; +} + +// ============================================================================= +// Animation +// ============================================================================= + +/// PlayAnimation(obj, animId) - __thiscall: ECX = obj, 1 stack param (callee-clean, RET 4) +pub fn playAnimation(obj: *anyopaque, anim_id: u32) void { + asm volatile ( + \\push %[anim] + \\call *%[func] + : + : [_] "{ecx}" (@intFromPtr(obj)), + [anim] "r" (anim_id), + [func] "r" (o.FN_PLAY_ANIMATION), + : .{ .eax = true, .edx = true, .memory = true, .cc = true } + ); +} + +// ============================================================================= +// Marker management +// ============================================================================= + +var test_marker: ?*anyopaque = null; + +const MODEL_PATH: [*:0]const u8 = "World\\ArtTest\\Boxtest\\xyz.m2"; + +/// loadModelByName — __fastcall(ECX=path), returns model cache entry or null. +fn loadModel(path: [*:0]const u8) ?*anyopaque { + con.fmt("[markers] loadModelByName(\"{s}\")\n", .{@as([*:0]const u8, path)}); + const result: u32 = asm volatile ("call *%[func]" + : [ret] "={eax}" (-> u32), + : [_] "{ecx}" (@intFromPtr(path)), + [func] "r" (o.FN_LOAD_MODEL_BY_NAME), + : .{ .edx = true, .memory = true, .cc = true } + ); + if (result != 0) { + con.fmt("[markers] -> model at 0x{X:0>8}\n", .{result}); + } else { + con.print("[markers] -> FAILED (returned null)\n"); + } + return if (result != 0) @ptrFromInt(result) else null; +} + +/// Create a test marker at player position +pub fn createTestMarker() ?*anyopaque { + const player = wow.getLocalPlayer(); + if (player == 0) { + con.print("[markers] createTestMarker: 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; + + const model = loadModel(MODEL_PATH); + con.fmt("[markers] createGameObject(model={?}, pos=({d:.1},{d:.1},{d:.1}))\n", .{ + @as(?usize, if (model) |m| @intFromPtr(m) else null), + pos.x, pos.y, pos.z + 1.0, + }); + const marker = createGameObject(model, null, null, pos.x, pos.y, pos.z + 1.0, 0); + + if (marker) |m| { + con.fmt("[markers] -> object at 0x{X:0>8}\n", .{@intFromPtr(m)}); + test_marker = m; + } else { + con.print("[markers] -> createGameObject FAILED\n"); + } + + return marker; +} + +/// Destroy test marker +pub fn destroyTestMarker() void { + if (test_marker) |m| { + cleanupWorldObject(m); + test_marker = null; + } +} + +/// Update test marker position to follow player +pub fn updateTestMarker() void { + if (test_marker == null) return; + + const player = wow.getLocalPlayer(); + if (player == 0) return; + + const pos = getUnitPosition(player); + if (pos.x == 0 and pos.y == 0 and pos.z == 0) return; + + setObjectPosition(test_marker.?, .{ .x = pos.x, .y = pos.y, .z = pos.z + 1.0 }); +} + +// ============================================================================= +// 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 } + ); +} + +// ============================================================================= +// 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"))); + } + return 1; +} + +/// 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: 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"))); + } + return 1; +} + +/// Lua: local x, y, z = GetPlayerPosition() +pub fn luaGetPlayerPosition(L: u32) callconv(.c) u32 { + const player = wow.getLocalPlayer(); + if (player == 0) return 0; + + const pos = getUnitPosition(player); + luaPushNumber(L, @floatCast(pos.x)); + luaPushNumber(L, @floatCast(pos.y)); + luaPushNumber(L, @floatCast(pos.z)); + return 3; +} + +// ============================================================================= +// Install hooks +// ============================================================================= + +pub fn installHooks() void { + // Nothing to hook yet - markers are created via Lua commands +} + +pub fn removeHooks() void { + destroyTestMarker(); +} diff --git a/src/markers/offsets.zig b/src/markers/offsets.zig new file mode 100644 index 0000000..604f2e2 --- /dev/null +++ b/src/markers/offsets.zig @@ -0,0 +1,96 @@ +//! Offsets for marker/game object system + +// ============================================================================= +// Unit position +// ============================================================================= + +/// Unit + this → movement struct pointer +pub const UNIT_MOVEMENT_OFFSET: usize = 0x118; + +/// Movement struct + this → X coordinate (float) +pub const MOVEMENT_POS_X: usize = 0x10; + +/// Movement struct + this → Y coordinate (float) +pub const MOVEMENT_POS_Y: usize = 0x14; + +/// Movement struct + this → Z coordinate (float) +pub const MOVEMENT_POS_Z: usize = 0x18; + +// ============================================================================= +// GameObject creation functions +// ============================================================================= + +/// CreateGameObject_WithProperties(model_ECX, callback1_EDX, callback2, x, y, z, flags) +/// __fastcall — ECX=model, EDX=callback1, 5 stack params. Callee cleans (RET 0x14). +/// Ghidra: 55 8B EC 83 EC 14 ... 89 4D FC (saves ECX) ... 89 55 F8 (saves EDX) ... C2 14 00. +pub const FN_CREATE_GAMEOBJECT: usize = 0x00670db0; + +/// AllocateAndInitializeWorldObject(initializeFlag) +/// __fastcall returns void** +pub const FN_ALLOCATE_WORLD_OBJECT: usize = 0x006a0930; + +/// CleanupWorldObject(object) +/// __thiscall +pub const FN_CLEANUP_WORLD_OBJECT: usize = 0x0069d730; + +/// DestroyWorldObjectAndRelease(object) +/// __fastcall +pub const FN_DESTROY_WORLD_OBJECT: usize = 0x006a0a70; + +// ============================================================================= +// Model loading +// ============================================================================= + +/// loadModelByName(path) +/// __fastcall returns model cache entry +pub const FN_LOAD_MODEL_BY_NAME: usize = 0x006d4640; + +/// createModelAttachment(resourceManager, path, flags) +/// __thiscall returns render context +pub const FN_CREATE_MODEL_ATTACHMENT: usize = 0x00707350; + +// ============================================================================= +// Animation control +// ============================================================================= + +/// PlayAnimation(object, animId) +/// __thiscall +pub const FN_PLAY_ANIMATION: usize = 0x0076cf50; + +/// CM2Model__PlayBoneAnimation(model, boneIndex, animId, seqIndex, animData, speed, blend, queue) +/// __thiscall +pub const FN_PLAY_BONE_ANIMATION: usize = 0x007121a0; + +/// HasAnimation(model, animId) +/// __thiscall returns bool +pub const FN_HAS_ANIMATION: usize = 0x00711960; + +// ============================================================================= +// Animation IDs (from string table) +// ============================================================================= + +pub const ANIM_SPAWN: u32 = 0; // TODO: find actual ID +pub const ANIM_DESPAWN: u32 = 0; // TODO: find actual ID +pub const ANIM_BIRTH: u32 = 0; // TODO: find actual ID + +// ============================================================================= +// Object structure offsets (from CreateGameObject_WithProperties analysis) +// ============================================================================= + +/// WorldObject + this → model pointer +pub const OBJ_MODEL: usize = 0x88; + +/// WorldObject + this → position X (float*) +pub const OBJ_POS_X: usize = 0xC0; + +/// WorldObject + this → position Y (float*) +pub const OBJ_POS_Y: usize = 0xC4; + +/// WorldObject + this → position Z (float*) +pub const OBJ_POS_Z: usize = 0xC8; + +/// WorldObject + this → color ARGB (alpha in high byte) +pub const OBJ_COLOR: usize = 0x24; + +/// WorldObject + this → flags +pub const OBJ_FLAGS: usize = 0x90; diff --git a/src/outline/README.md b/src/outline/README.md index 9b11cc6..15d8bd0 100644 --- a/src/outline/README.md +++ b/src/outline/README.md @@ -219,6 +219,11 @@ OutlineCommand("off") -- disable outlines (mount is parent, rider is attached, gear is attached to rider?). All child models (rider body, gear) should be included in the silhouette. +- ** MISC **: target outlines on players should be diff color than on mobs. Also when a mob and you are in melee range your outline should merge with his probably, so show you who you'te attacking. + Also we should shade targeted units not just outline them. And each outline and shade be a setting toggle + +- **debug**: we should add to our debug mode coloring that colors each object type a different solid color, so that they issues arise it's easy to determine what object type is causing issues + ## Future Architecture Notes - **Hook "should render" function for model tracking**: the perfboost system uses a