From c0d9a209437c94ff2c9d2466ebbd7d0af0b0f8c4 Mon Sep 17 00:00:00 2001 From: MarcelineVQ Date: Mon, 23 Feb 2026 20:43:20 -0800 Subject: [PATCH] Port interact/loot features from standalone interact DLL Add InteractNearest() and LootAllCorpses() as registered Lua C functions, ported from the standalone interact DLL. Uses the existing Lua protection bypass instead of NOP-padding trampolines. - interact.zig: game API wrappers, object scanning, loot queue with SceneEnd hook for per-frame processing - Bindings.xml: keybinding definitions loaded via LoadUIBindingsFromFile (called explicitly since our virtual addon bypasses LoadAddonRecursive) - Lua addon: BINDING_HEADER_WEIRDUTILS, Interact() wrapper, /wu interact --- src/addon/Bindings.xml | 11 ++ src/addon/WeirdUtils.lua | 8 + src/addon/WeirdUtils.toc | 2 + src/interact.zig | 414 +++++++++++++++++++++++++++++++++++++++ src/main.zig | 42 +++- 5 files changed, 475 insertions(+), 2 deletions(-) create mode 100644 src/addon/Bindings.xml create mode 100644 src/interact.zig diff --git a/src/addon/Bindings.xml b/src/addon/Bindings.xml new file mode 100644 index 0000000..9187853 --- /dev/null +++ b/src/addon/Bindings.xml @@ -0,0 +1,11 @@ + + + InteractNearest(0) + + + InteractNearest(1) + + + LootAllCorpses() + + diff --git a/src/addon/WeirdUtils.lua b/src/addon/WeirdUtils.lua index 9ffdf51..4de6d2f 100644 --- a/src/addon/WeirdUtils.lua +++ b/src/addon/WeirdUtils.lua @@ -2,6 +2,9 @@ WEIRDUTILS_VERSION = 1 +-- Binding header display name +BINDING_HEADER_WEIRDUTILS = "Weird Utils" + local frame = CreateFrame("Frame") frame:RegisterEvent("ADDON_LOADED") frame:RegisterEvent("PLAYER_LOGIN") @@ -35,6 +38,10 @@ SlashCmdList["WEIRDUTILS"] = function(msg) WeirdUtilsScreenshot("quality", tonumber(sub)) DEFAULT_CHAT_FRAME:AddMessage("Screenshot quality: " .. sub) end + elseif msg == "interact" then + DEFAULT_CHAT_FRAME:AddMessage("Interact: InteractNearest() and LootAllCorpses() available") + DEFAULT_CHAT_FRAME:AddMessage(" Bind in Key Bindings > Interact, or use /run InteractNearest(0)") + DEFAULT_CHAT_FRAME:AddMessage(" /run LootAllCorpses() - Loot all nearby corpses") else DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00WeirdUtils|r commands:") DEFAULT_CHAT_FRAME:AddMessage(" /wu version - Show version") @@ -42,5 +49,6 @@ SlashCmdList["WEIRDUTILS"] = function(msg) DEFAULT_CHAT_FRAME:AddMessage(" /wu ss - Screenshot status") DEFAULT_CHAT_FRAME:AddMessage(" /wu ss on|off - Enable/disable PNG screenshots") DEFAULT_CHAT_FRAME:AddMessage(" /wu ss 0-9 - Set compression level") + DEFAULT_CHAT_FRAME:AddMessage(" /wu interact - Interact/loot info") end end diff --git a/src/addon/WeirdUtils.toc b/src/addon/WeirdUtils.toc index c5aa788..d68e10e 100644 --- a/src/addon/WeirdUtils.toc +++ b/src/addon/WeirdUtils.toc @@ -2,4 +2,6 @@ ## Title: WeirdUtils ## Notes: Utility functions provided by weirdutils DLL ## Version: 1.0 + WeirdUtils.lua +Bindings.xml diff --git a/src/interact.zig b/src/interact.zig new file mode 100644 index 0000000..f3c7be8 --- /dev/null +++ b/src/interact.zig @@ -0,0 +1,414 @@ +const hook = @import("hook"); + +const WINAPI = @import("std").builtin.CallingConvention.winapi; +extern "kernel32" fn GetTickCount() callconv(WINAPI) u32; + +// ============================================================================= +// Offsets +// ============================================================================= + +const Offsets = struct { + const ADDR_SceneEnd: usize = 0x5A17A0; + const FUN_GET_OBJECT_POINTER: usize = 0x464870; + const FUN_IS_IN_WORLD: usize = 0xB4B424; + const FUN_RIGHT_CLICK_UNIT: usize = 0x60BEA0; + const FUN_RIGHT_CLICK_OBJECT: usize = 0x5F8660; + const FUN_SET_TARGET: usize = 0x493540; + const LOOT_GUID_LO: usize = 0x00B71B48; + const LOOT_GUID_HI: usize = 0x00B71B4C; + const LUA_ERROR: usize = 0x6F4940; + const LUA_ISNUMBER: usize = 0x6F34D0; + const LUA_TONUMBER: usize = 0x6F3620; + const VISIBLE_OBJECTS: usize = 0xB41414; +}; + +// ============================================================================= +// Types +// ============================================================================= + +const ObjectType = enum(u32) { + none = 0, + item = 1, + container = 2, + unit = 3, + player = 4, + game_object = 5, + dynamic_object = 6, + corpse = 7, +}; + +const C3Vector = struct { + y: f32, + x: f32, + z: f32, +}; + +// ============================================================================= +// Game API +// ============================================================================= + +fn getObjectPointer(guid: u64) u32 { + // __stdcall(u32 guidLow, u32 guidHigh): both on stack, callee cleans (ret 8) + const lo: u32 = @truncate(guid); + const hi: u32 = @truncate(guid >> 32); + return asm volatile ( + \\push %[hi] + \\push %[lo] + \\call *%[func] + : [ret] "={eax}" (-> u32), + : [lo] "r" (lo), + [hi] "r" (hi), + [func] "r" (@as(u32, Offsets.FUN_GET_OBJECT_POINTER)), + : .{ .ecx = true, .edx = true, .memory = true, .cc = true }); +} + +fn isInWorld() bool { + return hook.readMem(u8, Offsets.FUN_IS_IN_WORLD) != 0; +} + +fn getUnitPosition(unit: u32) C3Vector { + return .{ + .y = hook.readMem(f32, unit + 0x09B8), + .x = hook.readMem(f32, unit + 0x09BC), + .z = hook.readMem(f32, unit + 0x09C0), + }; +} + +fn getObjectPosition(pointer: u32) C3Vector { + const p = hook.readMem(u32, pointer + 0x110); + return .{ + .y = hook.readMem(f32, p + 0x24), + .x = hook.readMem(f32, p + 0x28), + .z = hook.readMem(f32, p + 0x2C), + }; +} + +fn getUnitHealth(unit: u32) i32 { + return hook.readMem(i32, hook.readMem(u32, unit + 0x8) + 0x58); +} + +fn isUnitLootable(unit: u32) bool { + const flags = hook.readMem(i32, hook.readMem(u32, unit + 0x8) + 0x23C); + return (flags & 1) != 0; +} + +fn isUnitSkinnable(unit: u32) bool { + const flags = hook.readMem(i32, hook.readMem(u32, unit + 0x8) + 0xB8); + return (flags & 0x4000000) == 0x4000000; +} + +fn setTarget(guid: u64) void { + // __stdcall(uint64_t guid): push hi, push lo, callee cleans 8 + const lo: u32 = @truncate(guid); + const hi: u32 = @truncate(guid >> 32); + asm volatile ( + \\push %[hi] + \\push %[lo] + \\call *%[func] + : + : [lo] "r" (lo), + [hi] "r" (hi), + [func] "r" (@as(u32, Offsets.FUN_SET_TARGET)), + : .{ .eax = true, .ecx = true, .edx = true, .memory = true, .cc = true }); +} + +fn rightClickInteract(pointer: u32, autoloot: i32, fun_ptr: usize) void { + // __thiscall: ECX=this (pointer), push autoloot, callee cleans 4 + asm volatile ( + \\push %[autoloot] + \\call *%[func] + : + : [_] "{ecx}" (pointer), + [autoloot] "r" (autoloot), + [func] "r" (fun_ptr), + : .{ .eax = true, .edx = true, .memory = true, .cc = true }); +} + +// ============================================================================= +// Utility +// ============================================================================= + +fn distance3D(v1: C3Vector, v2: C3Vector) f32 { + const dx = v2.x - v1.x; + const dy = v2.y - v1.y; + const dz = v2.z - v1.z; + return @sqrt(dx * dx + dy * dy + dz * dz); +} + +const blacklist = [_]u32{ 179830, 179831, 179785, 179786 }; + +fn isBlacklisted(id: u32) bool { + for (blacklist) |b| { + if (b == id) return true; + } + return false; +} + +// ============================================================================= +// Lua helpers (hook.fastcall to avoid broken callconv(fc) codegen) +// ============================================================================= + +fn luaIsNumber(L: *anyopaque, idx: i32) bool { + return hook.fastcall(u32, Offsets.LUA_ISNUMBER, @intFromPtr(L), idx) != 0; +} + +fn luaToNumber(L: *anyopaque, idx: i32) f64 { + return hook.fastcall(f64, Offsets.LUA_TONUMBER, @intFromPtr(L), idx); +} + +fn luaPrintError(L: *anyopaque, msg: [*:0]const u8) void { + // __cdecl(lua_State*, const char*): both on stack, caller cleans + asm volatile ( + \\push %[msg] + \\push %[L] + \\call *%[func] + \\add $8, %%esp + : + : [L] "r" (@intFromPtr(L)), + [msg] "r" (@intFromPtr(msg)), + [func] "r" (@as(u32, Offsets.LUA_ERROR)), + : .{ .eax = true, .ecx = true, .edx = true, .memory = true, .cc = true }); +} + +// ============================================================================= +// InteractNearest — find closest interactable within 5 yards, right-click it +// ============================================================================= + +pub fn interactNearest(L: *anyopaque) callconv(.c) u32 { + if (!isInWorld()) return 0; + + if (!luaIsNumber(L, 1)) { + luaPrintError(L, "Usage: InteractNearest(autoloot)"); + return 0; + } + + const objects = hook.readMem(u32, Offsets.VISIBLE_OBJECTS); + var current_object = hook.readMem(u32, objects + 0xAC); + + const player_guid = hook.readMem(u64, objects + 0xC0); + const player = getObjectPointer(player_guid); + const p_pos = getUnitPosition(player); + + var best_distance: f32 = 1000.0; + var candidate: u32 = 0xFFFFFFFF; + + while (current_object != 0 and (current_object & 1) == 0) { + const guid = hook.readMem(u64, current_object + 0x30); + const pointer = getObjectPointer(guid); + const obj_type = hook.readMem(u32, pointer + 0x14); + + // Skip objects summoned by players (totems, pets, etc.) + const summoned_by_guid = hook.readMem(u64, hook.readMem(u32, pointer + 0x8) + 0x30); + const summoned_by = getObjectPointer(summoned_by_guid); + + if (summoned_by_guid != 0 and summoned_by != 0) { + const owner_type = hook.readMem(u32, summoned_by + 0x14); + if (owner_type == @intFromEnum(ObjectType.player)) { + current_object = hook.readMem(u32, current_object + 0x3C); + continue; + } + } + + var o_pos: C3Vector = undefined; + if (obj_type == @intFromEnum(ObjectType.unit)) { + o_pos = getUnitPosition(current_object); + } else if (obj_type == @intFromEnum(ObjectType.game_object)) { + o_pos = getObjectPosition(current_object); + } else { + current_object = hook.readMem(u32, current_object + 0x3C); + continue; + } + + const dist = distance3D(o_pos, p_pos); + if (dist <= 5.0 and dist < best_distance) { + if (obj_type == @intFromEnum(ObjectType.unit)) { + const health = getUnitHealth(current_object); + if (health == 0 and (isUnitLootable(current_object) or isUnitSkinnable(current_object))) { + best_distance = dist; + candidate = current_object; + } else if (health > 0) { + best_distance = dist; + candidate = current_object; + } + } else if (obj_type == @intFromEnum(ObjectType.game_object)) { + const id = hook.readMem(u32, pointer + 0x294); + if (!isBlacklisted(id)) { + best_distance = dist; + candidate = current_object; + } + } + } + + current_object = hook.readMem(u32, current_object + 0x3C); + } + + if (candidate == 0xFFFFFFFF) return 0; + + const candidate_guid = hook.readMem(u64, candidate + 0x30); + const candidate_pointer = getObjectPointer(candidate_guid); + const candidate_type = hook.readMem(u32, candidate_pointer + 0x14); + + const autoloot: i32 = @intFromFloat(luaToNumber(L, 1)); + + if (candidate_type == @intFromEnum(ObjectType.unit)) { + setTarget(candidate_guid); + rightClickInteract(candidate, autoloot, Offsets.FUN_RIGHT_CLICK_UNIT); + } else { + rightClickInteract(candidate_pointer, autoloot, Offsets.FUN_RIGHT_CLICK_OBJECT); + } + + return 1; +} + +// ============================================================================= +// LootAllCorpses — queue nearby lootable corpses, loot them sequentially +// ============================================================================= + +const MAX_LOOT_QUEUE: usize = 100; + +var loot_queue: [MAX_LOOT_QUEUE]u64 = .{0} ** MAX_LOOT_QUEUE; +var loot_queue_count: usize = 0; +var loot_queue_index: usize = 0; +var loot_active: bool = false; +var loot_last_interact_time: u32 = 0; +var loot_next_delay: u32 = 0; + +fn interactNextCorpse() void { + while (loot_queue_index < loot_queue_count) { + const guid = loot_queue[loot_queue_index]; + loot_queue_index += 1; + + const pointer = getObjectPointer(guid); + if (pointer == 0) continue; + + const obj_type = hook.readMem(u32, pointer + 0x14); + if (obj_type != @intFromEnum(ObjectType.unit)) continue; + + if (getUnitHealth(pointer) != 0) continue; + if (!isUnitLootable(pointer)) continue; + + setTarget(guid); + rightClickInteract(pointer, 1, Offsets.FUN_RIGHT_CLICK_UNIT); + const now = GetTickCount(); + loot_last_interact_time = now; + // 300ms base + 0-100ms jitter + loot_next_delay = 300 + (now *% 2654435761) % 101; + return; + } + + loot_active = false; +} + +fn processLootQueue() void { + const now = GetTickCount(); + const elapsed = now -% loot_last_interact_time; + + const loot_guid_lo = hook.readMem(u32, Offsets.LOOT_GUID_LO); + const loot_guid_hi = hook.readMem(u32, Offsets.LOOT_GUID_HI); + + if (loot_guid_lo != 0 or loot_guid_hi != 0) { + // Loot window is open — wait for auto-loot to finish. + // If items remain after timeout (e.g. unique items already owned), + // skip to next corpse. + if (elapsed > 1500) { + interactNextCorpse(); + } + return; + } + + // Loot GUID is zero — loot closed or server hasn't responded yet + if (elapsed < loot_next_delay) return; + + interactNextCorpse(); +} + +pub fn lootAllCorpses(_: *anyopaque) callconv(.c) u32 { + if (!isInWorld()) return 0; + + // Cancel any in-progress chain + loot_queue_count = 0; + loot_queue_index = 0; + loot_active = false; + + const objects = hook.readMem(u32, Offsets.VISIBLE_OBJECTS); + var current_object = hook.readMem(u32, objects + 0xAC); + + const player_guid = hook.readMem(u64, objects + 0xC0); + const player = getObjectPointer(player_guid); + const p_pos = getUnitPosition(player); + + while (current_object != 0 and (current_object & 1) == 0) { + if (loot_queue_count >= MAX_LOOT_QUEUE) break; + + const guid = hook.readMem(u64, current_object + 0x30); + const pointer = getObjectPointer(guid); + const obj_type = hook.readMem(u32, pointer + 0x14); + + if (obj_type == @intFromEnum(ObjectType.unit)) { + const o_pos = getUnitPosition(current_object); + const dist = distance3D(o_pos, p_pos); + + if (dist <= 5.0) { + const health = getUnitHealth(current_object); + if (health == 0 and isUnitLootable(current_object)) { + loot_queue[loot_queue_count] = guid; + loot_queue_count += 1; + } + } + } + + current_object = hook.readMem(u32, current_object + 0x3C); + } + + if (loot_queue_count == 0) return 0; + + loot_active = true; + interactNextCorpse(); + + return 0; +} + +// ============================================================================= +// Hook: SceneEnd (0x5A17A0) +// __thiscall(CGxDevice*), 9-byte prologue, no fixups. +// Per-frame hook for processing the loot queue. +// ============================================================================= + +var scene_end_hook = hook.Hook{}; + +fn hookSceneEnd(device: u32, _edx: u32) callconv(.c) void { + _ = _edx; + + if (loot_active) { + processLootQueue(); + } + + callOriginalSceneEnd(device); +} + +fn callOriginalSceneEnd(device: u32) void { + asm volatile ( + \\call *%[func] + : + : [_] "{ecx}" (device), + [func] "r" (scene_end_hook.trampoline), + : .{ .eax = true, .edx = true, .memory = true, .cc = true }); +} + +// ============================================================================= +// Install / Remove +// ============================================================================= + +pub fn installHooks() void { + // SceneEnd — per-frame loot queue processing + // Uses thunk: __fastcall(ECX=device, EDX) → cdecl(device, edx) + if (scene_end_hook.prepare(Offsets.ADDR_SceneEnd, 9, &.{})) { + const thunk = scene_end_hook.mem.? + 32; + _ = hook.buildFastcallToCdeclThunk(thunk, @intFromPtr(&hookSceneEnd), 0); + scene_end_hook.activate(@intFromPtr(thunk)); + } +} + +pub fn removeHooks() void { + scene_end_hook.remove(); +} diff --git a/src/main.zig b/src/main.zig index 5ae3c02..e0dd933 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,6 +1,7 @@ const std = @import("std"); const hook = @import("hook"); const screenshot = @import("screenshot.zig"); +const interact = @import("interact.zig"); const WINAPI = std.builtin.CallingConvention.winapi; const fc: std.builtin.CallingConvention = .{ .x86_fastcall = .{} }; @@ -236,6 +237,8 @@ fn registerLuaFunctions() void { registerFunction("WeirdUtilsTest", @intFromPtr(&weirdUtilsTest)); registerFunction("WeirdUtilsVersion", @intFromPtr(&weirdUtilsVersion)); registerFunction("WeirdUtilsScreenshot", @intFromPtr(&screenshot.screenshotCommand)); + registerFunction("InteractNearest", @intFromPtr(&interact.interactNearest)); + registerFunction("LootAllCorpses", @intFromPtr(&interact.lootAllCorpses)); } // ============================================================================= @@ -251,6 +254,7 @@ const FileEntry = struct { const embedded_files = [_]FileEntry{ .{ .name = "WeirdUtils.toc", .data = @embedFile("addon/WeirdUtils.toc") }, + .{ .name = "Bindings.xml", .data = @embedFile("addon/Bindings.xml") }, .{ .name = "WeirdUtils.lua", .data = @embedFile("addon/WeirdUtils.lua") }, }; @@ -369,6 +373,15 @@ fn loadAddonsDetour(error_handler: u32, _edx: u32) callconv(.c) void { &md5ctx, error_handler, ); + + // Load bindings — LoadAddonRecursive normally calls LoadUIBindingsFromFile + // explicitly for each addon's Bindings.xml (separate from .toc processing). + // Since our addon isn't in the game's addon list, we must call it ourselves. + callLoadUIBindingsFromFile( + "Interface\\AddOns\\WeirdUtils\\Bindings.xml", + &md5ctx, + error_handler, + ); } fn callOrigLoadAddons(error_handler: u32) void { @@ -398,6 +411,27 @@ fn callLoadFileListWithIncludes(toc_path: [*:0]const u8, md5ctx: *[88]u8, error_ ); } +/// LoadUIBindingsFromFile(path_stack, md5ctx_stack, callback_stack) +/// __thiscall at 0x4B6F70: ECX=binding_mgr [0xB71290], ret 0x0C (3 stack params) +/// Parses Bindings.xml and registers binding entries in the key binding manager. +fn callLoadUIBindingsFromFile(path: [*:0]const u8, md5ctx: *[88]u8, callback: u32) void { + const binding_mgr = hook.readMem(u32, 0xB71290); + // Pin func to EDX to stay within 3 "r" registers (EBX/ESI/EDI) + asm volatile ( + \\push %[cb] + \\push %[md5] + \\push %[path] + \\call *%[func] + : + : [_] "{ecx}" (binding_mgr), + [func] "{edx}" (@as(u32, 0x4B6F70)), + [path] "r" (@intFromPtr(path)), + [md5] "r" (@intFromPtr(md5ctx)), + [cb] "r" (callback), + : .{ .eax = true, .memory = true, .cc = true } + ); +} + // ============================================================================= // Hook: GameEngine_MainInitialize (0x46a400) // __stdcall(void) — prologue: 55 8B EC 83 EC 28 = 6 bytes, no fixups @@ -455,11 +489,14 @@ fn install() void { load_addons_hook.activate(@intFromPtr(thunk)); } - // 5. GameEngine_MainInitialize — install screenshot hook after all DLLs load + // 5. Interact hooks — SceneEnd for per-frame loot queue processing + interact.installHooks(); + + // 6. GameEngine_MainInitialize — install screenshot hook after all DLLs load // Fires once inside WinMain, before the login screen renders. _ = engine_init_hook.install(0x46a400, 6, @intFromPtr(&engineInitDetour), &.{}); - // 6. CGGameUI_Shutdown — cleanup + // 7. CGGameUI_Shutdown — cleanup _ = shutdown_hook.install(0x490BD0, 6, @intFromPtr(&shutdownDetour), &.{1}); } @@ -467,6 +504,7 @@ fn uninstall() void { shutdown_hook.remove(); engine_init_hook.remove(); screenshot.removeHook(); + interact.removeHooks(); load_addons_hook.remove(); lsf_hook.remove(); file_hook.remove();