From cd7e585412174b8fa60ce5e95363da93e2171b0e Mon Sep 17 00:00:00 2001 From: MarcelineVQ Date: Wed, 25 Feb 2026 19:15:08 -0800 Subject: [PATCH] Restructure modules with per-module addon subdirs - Each module (outline, interact, screenshot) now has its own addon/ subdir with .toc, .lua, and Bindings.xml - Core WeirdUtils addon lives in src/core/addon/ - build.zig supports compile-time feature gating via -D options - main.zig uses build_options for conditional imports and addon embedding - Module addons only load when their module is compiled in - 'zig build all-variants' produces full.dll + 3 single-module DLLs --- build.zig | 48 +++++ src/addon/WeirdUtils.lua | 65 ------- src/core/addon/WeirdUtils.lua | 36 ++++ src/{ => core}/addon/WeirdUtils.toc | 3 +- src/{ => interact}/addon/Bindings.xml | 2 +- src/interact/addon/Interact.lua | 31 +++ src/interact/addon/Interact.toc | 8 + src/{ => interact}/interact.zig | 0 src/main.zig | 260 +++++++++++++++----------- src/outline/addon/Bindings.xml | 5 + src/outline/addon/Outline.lua | 37 ++++ src/outline/addon/Outline.toc | 8 + src/screenshot/addon/Bindings.xml | 5 + src/screenshot/addon/Screenshot.lua | 46 +++++ src/screenshot/addon/Screenshot.toc | 8 + src/{ => screenshot}/png.zig | 0 src/{ => screenshot}/screenshot.zig | 0 17 files changed, 381 insertions(+), 181 deletions(-) delete mode 100644 src/addon/WeirdUtils.lua create mode 100644 src/core/addon/WeirdUtils.lua rename src/{ => core}/addon/WeirdUtils.toc (51%) rename src/{ => interact}/addon/Bindings.xml (81%) create mode 100644 src/interact/addon/Interact.lua create mode 100644 src/interact/addon/Interact.toc rename src/{ => interact}/interact.zig (100%) create mode 100644 src/outline/addon/Bindings.xml create mode 100644 src/outline/addon/Outline.lua create mode 100644 src/outline/addon/Outline.toc create mode 100644 src/screenshot/addon/Bindings.xml create mode 100644 src/screenshot/addon/Screenshot.lua create mode 100644 src/screenshot/addon/Screenshot.toc rename src/{ => screenshot}/png.zig (100%) rename src/{ => screenshot}/screenshot.zig (100%) diff --git a/build.zig b/build.zig index bd5d3fd..b62e447 100644 --- a/build.zig +++ b/build.zig @@ -8,6 +8,18 @@ pub fn build(b: *std.Build) void { }); const optimize = b.standardOptimizeOption(.{}); + // Build options for conditional module compilation + 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; + + // 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); + const build_options_module = build_options.createModule(); + const hook_mod = b.dependency("hook", .{ .target = target, .optimize = optimize, @@ -22,9 +34,45 @@ pub fn build(b: *std.Build) void { .optimize = optimize, .imports = &.{ .{ .name = "hook", .module = hook_mod }, + .{ .name = "build_options", .module = build_options_module }, }, }), }); b.installArtifact(lib); + + // Convenience step to build all single-module variants + 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 }, + }) |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); + + const variant_lib = b.addLibrary(.{ + .name = variant.name, + .linkage = .dynamic, + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + .imports = &.{ + .{ .name = "hook", .module = hook_mod }, + .{ .name = "build_options", .module = opts.createModule() }, + }, + }), + }); + + const install_variant = b.addInstallArtifact(variant_lib, .{ + .dest_dir = .{ .override = .{ .custom = "variants" } }, + }); + build_all_step.dependOn(&install_variant.step); + } } diff --git a/src/addon/WeirdUtils.lua b/src/addon/WeirdUtils.lua deleted file mode 100644 index e0f71c9..0000000 --- a/src/addon/WeirdUtils.lua +++ /dev/null @@ -1,65 +0,0 @@ --- WeirdUtils addon (embedded in DLL, loaded from memory) - -WEIRDUTILS_VERSION = 1 - --- Binding header display name -BINDING_HEADER_WEIRDUTILS = "Weird Utils" - -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("|cff00ff00WeirdUtils|r v" .. WEIRDUTILS_VERSION .. " loaded") - end -end) - -SLASH_WEIRDUTILS1 = "/weirdutils" -SLASH_WEIRDUTILS2 = "/wu" -SlashCmdList["WEIRDUTILS"] = function(msg) - if msg == "version" then - DEFAULT_CHAT_FRAME:AddMessage("WeirdUtils v" .. WEIRDUTILS_VERSION) - elseif msg == "test" then - local result = WeirdUtilsTest() - DEFAULT_CHAT_FRAME:AddMessage("C function returned: " .. tostring(result)) - elseif msg == "ss" or msg == "screenshot" then - local on, level = WeirdUtilsScreenshot() - DEFAULT_CHAT_FRAME:AddMessage("Screenshots: " .. (on and "ON" or "OFF") .. " (quality " .. level .. ")") - elseif string.find(msg, "^ss ") or string.find(msg, "^screenshot ") then - local sub = string.match(msg, "^%S+ (.+)") - if sub == "on" or sub == "enable" then - WeirdUtilsScreenshot("enable") - DEFAULT_CHAT_FRAME:AddMessage("Screenshots enabled") - elseif sub == "off" or sub == "disable" then - WeirdUtilsScreenshot("disable") - DEFAULT_CHAT_FRAME:AddMessage("Screenshots disabled") - elseif tonumber(sub) then - 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") - elseif msg == "outline" or msg == "outline status" then - local on = OutlineCommand() - DEFAULT_CHAT_FRAME:AddMessage("Outlines: " .. (on and "|cff00ff00ON|r" or "|cffff0000OFF|r")) - elseif msg == "outline on" then - OutlineCommand("on") - DEFAULT_CHAT_FRAME:AddMessage("Outlines |cff00ff00enabled|r") - elseif msg == "outline off" then - OutlineCommand("off") - DEFAULT_CHAT_FRAME:AddMessage("Outlines |cffff0000disabled|r") - else - DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00WeirdUtils|r commands:") - DEFAULT_CHAT_FRAME:AddMessage(" /wu version - Show version") - DEFAULT_CHAT_FRAME:AddMessage(" /wu test - Test C function call") - 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") - DEFAULT_CHAT_FRAME:AddMessage(" /wu outline - Outline status") - DEFAULT_CHAT_FRAME:AddMessage(" /wu outline on|off - Enable/disable outlines") - end -end diff --git a/src/core/addon/WeirdUtils.lua b/src/core/addon/WeirdUtils.lua new file mode 100644 index 0000000..ae6f403 --- /dev/null +++ b/src/core/addon/WeirdUtils.lua @@ -0,0 +1,36 @@ +-- WeirdUtils core addon (embedded in DLL, loaded from memory) +-- This is always loaded - module addons (Outline, Interact, Screenshot) are loaded separately + +WEIRDUTILS_VERSION = 1 + +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("|cff00ff00WeirdUtils|r v" .. WEIRDUTILS_VERSION .. " core loaded") + end +end) + +SLASH_WEIRDUTILS1 = "/weirdutils" +SLASH_WEIRDUTILS2 = "/wu" +SlashCmdList["WEIRDUTILS"] = function(msg) + msg = string.lower(msg or "") + if msg == "" or msg == "help" then + DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00WeirdUtils|r v" .. WEIRDUTILS_VERSION .. " commands:") + DEFAULT_CHAT_FRAME:AddMessage(" /wu version - Show version") + DEFAULT_CHAT_FRAME:AddMessage(" /wu test - Test C function call") + DEFAULT_CHAT_FRAME:AddMessage("Module commands (if compiled):") + DEFAULT_CHAT_FRAME:AddMessage(" /outline, /ol - Outline controls") + DEFAULT_CHAT_FRAME:AddMessage(" /interact - Interact/loot info") + DEFAULT_CHAT_FRAME:AddMessage(" /ss, /screenshot - Screenshot controls") + elseif msg == "version" then + DEFAULT_CHAT_FRAME:AddMessage("WeirdUtils v" .. WEIRDUTILS_VERSION) + elseif msg == "test" then + local result = WeirdUtilsTest() + DEFAULT_CHAT_FRAME:AddMessage("C function returned: " .. tostring(result)) + else + DEFAULT_CHAT_FRAME:AddMessage("|cffff0000Unknown command:|r " .. msg) + end +end diff --git a/src/addon/WeirdUtils.toc b/src/core/addon/WeirdUtils.toc similarity index 51% rename from src/addon/WeirdUtils.toc rename to src/core/addon/WeirdUtils.toc index d68e10e..c3a5423 100644 --- a/src/addon/WeirdUtils.toc +++ b/src/core/addon/WeirdUtils.toc @@ -1,7 +1,6 @@ ## Interface: 11200 ## Title: WeirdUtils -## Notes: Utility functions provided by weirdutils DLL +## Notes: Core utilities DLL framework ## Version: 1.0 WeirdUtils.lua -Bindings.xml diff --git a/src/addon/Bindings.xml b/src/interact/addon/Bindings.xml similarity index 81% rename from src/addon/Bindings.xml rename to src/interact/addon/Bindings.xml index 9187853..58387d4 100644 --- a/src/addon/Bindings.xml +++ b/src/interact/addon/Bindings.xml @@ -1,5 +1,5 @@ - + InteractNearest(0) diff --git a/src/interact/addon/Interact.lua b/src/interact/addon/Interact.lua new file mode 100644 index 0000000..44f9cdf --- /dev/null +++ b/src/interact/addon/Interact.lua @@ -0,0 +1,31 @@ +-- Interact addon (embedded in DLL, loaded from memory) +-- Part of WeirdUtils - only loaded when interact module is compiled + +INTERACT_VERSION = 1 + +BINDING_HEADER_INTERACT = "Interact" + +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("|cff00ff00Interact|r v" .. INTERACT_VERSION .. " loaded") + end +end) + +SLASH_INTERACT1 = "/interact" +SlashCmdList["INTERACT"] = function(msg) + msg = string.lower(msg or "") + if msg == "" or msg == "help" then + DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00Interact|r commands:") + DEFAULT_CHAT_FRAME:AddMessage(" /interact - Show this help") + DEFAULT_CHAT_FRAME:AddMessage(" Key bind: Interact - Interact with nearest NPC/object") + DEFAULT_CHAT_FRAME:AddMessage(" Key bind: Interact (auto-loot) - Interact and auto-loot") + DEFAULT_CHAT_FRAME:AddMessage(" Key bind: Loot All Corpses - Loot all nearby corpses") + DEFAULT_CHAT_FRAME:AddMessage(" /run InteractNearest(0) - Interact without loot") + DEFAULT_CHAT_FRAME:AddMessage(" /run InteractNearest(1) - Interact with auto-loot") + DEFAULT_CHAT_FRAME:AddMessage(" /run LootAllCorpses() - Loot all nearby corpses") + end +end diff --git a/src/interact/addon/Interact.toc b/src/interact/addon/Interact.toc new file mode 100644 index 0000000..9d59306 --- /dev/null +++ b/src/interact/addon/Interact.toc @@ -0,0 +1,8 @@ +## Interface: 11200 +## Title: Interact +## Notes: Nearest NPC/object interaction and bulk looting +## Version: 1.0 +## Dependencies: WeirdUtils + +Interact.lua +Bindings.xml diff --git a/src/interact.zig b/src/interact/interact.zig similarity index 100% rename from src/interact.zig rename to src/interact/interact.zig diff --git a/src/main.zig b/src/main.zig index e66172b..f61bd39 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,8 +1,17 @@ const std = @import("std"); const hook = @import("hook"); -const screenshot = @import("screenshot.zig"); -const interact = @import("interact.zig"); -const outline = @import("outline/api.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; +}; + +// 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 WINAPI = std.builtin.CallingConvention.winapi; const fc: std.builtin.CallingConvention = .{ .x86_fastcall = .{} }; @@ -14,8 +23,6 @@ const sc: std.builtin.CallingConvention = .{ .x86_stdcall = .{} }; var protection_hook: hook.Hook = .{}; -/// Empty detour — replaces the Lua callback address validator at 0x42a320. -/// Prologue: 55 8B EC 83 EC 40 = 6 bytes, no fixups fn luaProtectionDetour() callconv(.c) void {} // ============================================================================= @@ -145,7 +152,6 @@ pub const lua = struct { } pub fn luaError(L: State, msg: [*:0]const u8) void { - // lua_error is __cdecl(L, msg) at 0x6F4940 asm volatile ( \\push %[msg] \\push %[L] @@ -179,14 +185,10 @@ pub const lua = struct { // Game function wrappers // ============================================================================= -/// FrameScript::Register(name_ECX, func_EDX) at 0x704120 fn registerFunction(name: [*:0]const u8, func_addr: usize) void { hook.fastcall(void, 0x704120, @intFromPtr(name), func_addr); } -/// M2_AllocateModelBuffer(size, source_file, line, flags) at 0x6462E0 -/// __stdcall, 4 params, ret 0x10 — allocates from WoW's internal memory pool. -/// Returned buffer can be freed by game code via FreeFileResourceMemory. fn allocateGameBuffer(size: u32) ?[*]u8 { return asm volatile ( \\push $0 @@ -205,13 +207,6 @@ fn allocateGameBuffer(size: u32) ?[*]u8 { // ============================================================================= // Custom C functions (callable from Lua) // ============================================================================= -// -// FIX: Cannot use the lua.* wrappers (e.g. lua.pushstring) from inside Lua -// C callbacks. The wrappers call through typed callconv(fc) function pointers, -// but Zig 0.15's x86 codegen is broken for callconv(fc) — it generates -// `ret 0x4` instead of plain `ret` for fastcall calls with ≤2 register params, -// corrupting the stack. Work around by using hook.fastcall / raw inline asm -// which bypasses Zig's calling-convention codegen entirely. fn weirdUtilsTest(L: lua.State) callconv(.c) u32 { hook.fastcall(void, 0x6F3890, @intFromPtr(L), @intFromPtr(@as([*:0]const u8, "WeirdUtils is working!"))); @@ -219,8 +214,6 @@ fn weirdUtilsTest(L: lua.State) callconv(.c) u32 { } fn weirdUtilsVersion(L: lua.State) callconv(.c) u32 { - // lua_pushnumber is __fastcall(L_ECX, f64_stack) — f64 skips EDX and goes - // on the stack. Callee cleans with ret 8. asm volatile ( \\sub $8, %%esp \\fld1 @@ -235,66 +228,109 @@ fn weirdUtilsVersion(L: lua.State) callconv(.c) u32 { } fn registerLuaFunctions() void { + // Core functions (always registered) registerFunction("WeirdUtilsTest", @intFromPtr(&weirdUtilsTest)); registerFunction("WeirdUtilsVersion", @intFromPtr(&weirdUtilsVersion)); - registerFunction("WeirdUtilsScreenshot", @intFromPtr(&screenshot.screenshotCommand)); - registerFunction("InteractNearest", @intFromPtr(&interact.interactNearest)); - registerFunction("LootAllCorpses", @intFromPtr(&interact.lootAllCorpses)); - registerFunction("OutlineCommand", @intFromPtr(&outline.outlineCommand)); + + // Conditional module functions + if (build_opts.screenshot) { + registerFunction("WeirdUtilsScreenshot", @intFromPtr(&screenshot.screenshotCommand)); + } + if (build_opts.interact) { + registerFunction("InteractNearest", @intFromPtr(&interact.interactNearest)); + registerFunction("LootAllCorpses", @intFromPtr(&interact.lootAllCorpses)); + } + if (build_opts.outline) { + registerFunction("OutlineCommand", @intFromPtr(&outline.outlineCommand)); + } } // ============================================================================= // Embedded addon files // ============================================================================= -const addon_prefix = "Interface\\AddOns\\WeirdUtils\\"; +const AddonPrefix = struct { + prefix: []const u8, + files: []const FileEntry, +}; const FileEntry = struct { name: []const u8, data: []const u8, }; -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") }, +// Core addon (always included) +const core_prefix = "Interface\\AddOns\\WeirdUtils\\"; +const core_files = [_]FileEntry{ + .{ .name = "WeirdUtils.toc", .data = @embedFile("core/addon/WeirdUtils.toc") }, + .{ .name = "WeirdUtils.lua", .data = @embedFile("core/addon/WeirdUtils.lua") }, +}; + +// Conditional module addons +const screenshot_files = if (build_opts.screenshot) [_]FileEntry{ + .{ .name = "Screenshot.toc", .data = @embedFile("screenshot/addon/Screenshot.toc") }, + .{ .name = "Screenshot.lua", .data = @embedFile("screenshot/addon/Screenshot.lua") }, + .{ .name = "Bindings.xml", .data = @embedFile("screenshot/addon/Bindings.xml") }, +} else [_]FileEntry{}; + +const interact_files = if (build_opts.interact) [_]FileEntry{ + .{ .name = "Interact.toc", .data = @embedFile("interact/addon/Interact.toc") }, + .{ .name = "Interact.lua", .data = @embedFile("interact/addon/Interact.lua") }, + .{ .name = "Bindings.xml", .data = @embedFile("interact/addon/Bindings.xml") }, +} else [_]FileEntry{}; + +const outline_files = if (build_opts.outline) [_]FileEntry{ + .{ .name = "Outline.toc", .data = @embedFile("outline/addon/Outline.toc") }, + .{ .name = "Outline.lua", .data = @embedFile("outline/addon/Outline.lua") }, + .{ .name = "Bindings.xml", .data = @embedFile("outline/addon/Bindings.xml") }, +} 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 }, }; fn findEmbeddedFile(path: [*:0]const u8) ?*const FileEntry { const path_span = std.mem.span(path); - if (path_span.len <= addon_prefix.len) return null; - // Case-insensitive prefix check — WoW paths use mixed case - for (path_span[0..addon_prefix.len], addon_prefix) |a, b| { - const la = if (a >= 'A' and a <= 'Z') a + 32 else a; - const lb = if (b >= 'A' and b <= 'Z') b + 32 else b; - if (la != lb) return null; - } - const relative = path_span[addon_prefix.len..]; - for (&embedded_files) |*entry| { - if (relative.len != entry.name.len) continue; - var match = true; - for (relative, entry.name) |a, b| { + + for (&addon_prefixes) |*addon| { + if (path_span.len <= addon.prefix.len) continue; + + // Case-insensitive prefix check + var prefix_match = true; + for (path_span[0..addon.prefix.len], addon.prefix) |a, b| { const la = if (a >= 'A' and a <= 'Z') a + 32 else a; const lb = if (b >= 'A' and b <= 'Z') b + 32 else b; if (la != lb) { - match = false; + prefix_match = false; break; } } - if (match) return entry; + if (!prefix_match) continue; + + const relative = path_span[addon.prefix.len..]; + for (addon.files) |*entry| { + if (relative.len != entry.name.len) continue; + var match = true; + for (relative, entry.name) |a, b| { + const la = if (a >= 'A' and a <= 'Z') a + 32 else a; + const lb = if (b >= 'A' and b <= 'Z') b + 32 else b; + if (la != lb) { + match = false; + break; + } + } + if (match) return entry; + } } return null; } // ============================================================================= // Hook: LoadFileWithTextureResourceFallback (0x648620) -// __stdcall(unk, path, buf_out, size_out, extra_alloc, flags, async) → ret 0x1C -// Prologue: 55 8B EC 8B 4D 1C = 6 bytes, no fixups -// -// The central file I/O function — all .toc, .lua, .xml, and texture file reads -// go through here. We intercept reads for our addon prefix and serve from -// DLL-embedded memory, allocated with the game's own allocator so the game -// can free the buffer normally. // ============================================================================= var file_hook: hook.Hook = .{}; @@ -315,7 +351,6 @@ fn loadFileDetour( @memcpy(buf[0..entry.data.len], entry.data); - // Zero extra bytes (null terminator for text files when extra_alloc=1) if (extra_alloc > 0) { @memset(buf[entry.data.len..][0..extra_alloc], 0); } @@ -325,7 +360,6 @@ fn loadFileDetour( return 1; } - // Not our file — forward to original const orig = file_hook.getTrampoline( *const fn (u32, [*:0]const u8, *?[*]u8, ?*u32, u32, u32, u32) callconv(sc) u32, ); @@ -334,7 +368,6 @@ fn loadFileDetour( // ============================================================================= // Hook: LoadScriptFunctions (0x490250) -// Prologue: 56 E8 FA 60 27 00 = 6 bytes, fixup at offset 1 // ============================================================================= var lsf_hook: hook.Hook = .{}; @@ -347,10 +380,6 @@ fn loadScriptFunctionsDetour() callconv(sc) void { // ============================================================================= // Hook: LoadAddonsRecursively (0x51F600) -// __fastcall(error_handler_ECX) — prologue: 53 8B 1D ... = 7 bytes, no fixups -// After all real addons load, we call loadFileListWithIncludes with our .toc -// path, triggering the full addon loading pipeline (toc parse → lua exec → -// xml parse) with file reads served from embedded memory via the file hook. // ============================================================================= var load_addons_hook: hook.Hook = .{}; @@ -358,32 +387,54 @@ var load_addons_hook: hook.Hook = .{}; fn loadAddonsDetour(error_handler: u32, _edx: u32) callconv(.c) void { _ = _edx; - // Call original — loads all player addons callOrigLoadAddons(error_handler); - // Trigger our addon through the real loading pipeline. - // loadFileListWithIncludes will read our .toc via LoadFileWithTextureResourceFallback - // (intercepted by file_hook), parse it, and call processIncludeFile for each entry, - // which loads .lua files through the same hooked path. - // - // FIX: loadFileListWithIncludes unconditionally calls MD5_Update on the md5ctx - // param (EDX). Passing NULL here caused the original crash-on-load — an access - // violation inside MD5_Update. Allocate a zeroed 88-byte context on the stack. var md5ctx = std.mem.zeroes([88]u8); + + // Always load core addon callLoadFileListWithIncludes( "Interface\\AddOns\\WeirdUtils\\WeirdUtils.toc", &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, - ); + // Conditionally load module addons + if (build_opts.screenshot) { + callLoadFileListWithIncludes( + "Interface\\AddOns\\Screenshot\\Screenshot.toc", + &md5ctx, + error_handler, + ); + callLoadUIBindingsFromFile( + "Interface\\AddOns\\Screenshot\\Bindings.xml", + &md5ctx, + error_handler, + ); + } + if (build_opts.interact) { + callLoadFileListWithIncludes( + "Interface\\AddOns\\Interact\\Interact.toc", + &md5ctx, + error_handler, + ); + callLoadUIBindingsFromFile( + "Interface\\AddOns\\Interact\\Bindings.xml", + &md5ctx, + error_handler, + ); + } + if (build_opts.outline) { + callLoadFileListWithIncludes( + "Interface\\AddOns\\Outline\\Outline.toc", + &md5ctx, + error_handler, + ); + callLoadUIBindingsFromFile( + "Interface\\AddOns\\Outline\\Bindings.xml", + &md5ctx, + error_handler, + ); + } } fn callOrigLoadAddons(error_handler: u32) void { @@ -396,11 +447,7 @@ fn callOrigLoadAddons(error_handler: u32) void { ); } -/// loadFileListWithIncludes(path_ECX, md5ctx_EDX, error_handler_stack) -/// __fastcall at 0x6EDB90, ret 4 (1 stack param) fn callLoadFileListWithIncludes(toc_path: [*:0]const u8, md5ctx: *[88]u8, error_handler: u32) void { - // __fastcall: ECX=path, EDX=md5ctx, stack: error_handler - // Callee cleans the 1 stack arg (ret 4) asm volatile ( \\push %[eh] \\call *%[func] @@ -413,12 +460,8 @@ 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] @@ -436,16 +479,6 @@ fn callLoadUIBindingsFromFile(path: [*:0]const u8, md5ctx: *[88]u8, callback: u3 // ============================================================================= // Hook: GameEngine_MainInitialize (0x46a400) -// __stdcall(void) — prologue: 55 8B EC 83 EC 28 = 6 bytes, no fixups -// -// Called once from InitializeAllSubsystems during the init callback inside -// WinMain. Fires after Lua env, UI frames, event tables, and DB tables are -// set up, but before the event loop starts frame callbacks (i.e. before the -// login screen renders and screenshots become possible). -// -// We install the screenshot hook here to guarantee it runs AFTER all -// DLL_PROCESS_ATTACH hooks (including UnitXP's CTgaFile::Write hook), -// making us the outermost detour in the hook chain. // ============================================================================= var engine_init_hook: hook.Hook = .{}; @@ -453,13 +486,17 @@ var engine_init_hook: hook.Hook = .{}; fn engineInitDetour() callconv(sc) void { const orig = engine_init_hook.getTrampoline(*const fn () callconv(sc) void); orig(); - screenshot.installHook(); - _ = outline.init(); + + if (build_opts.screenshot) { + screenshot.installHook(); + } + if (build_opts.outline) { + _ = outline.init(); + } } // ============================================================================= // Hook: CGGameUI_Shutdown (0x490BD0) -// Prologue: 56 E8 7A 83 FC FF = 6 bytes, fixup at offset 1 // ============================================================================= var shutdown_hook: hook.Hook = .{}; @@ -474,41 +511,38 @@ fn shutdownDetour() callconv(sc) void { // ============================================================================= fn install() void { - // 1. Lua protection bypass — empty stub replaces address validator _ = protection_hook.install(0x42a320, 6, @intFromPtr(&luaProtectionDetour), &.{}); - - // 2. File I/O hook — serve embedded addon files from memory - // Must be installed before LoadAddonsRecursively hook fires _ = file_hook.install(0x648620, 6, @intFromPtr(&loadFileDetour), &.{}); - - // 3. LoadScriptFunctions — register C functions after built-in commands _ = lsf_hook.install(0x490250, 6, @intFromPtr(&loadScriptFunctionsDetour), &.{1}); - // 4. LoadAddonsRecursively — trigger our addon load after real addons - // Uses thunk: __fastcall(ECX) → cdecl(ecx_val, edx_val) 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)); } - // 5. Interact hooks — SceneEnd for per-frame loot queue processing - interact.installHooks(); + if (build_opts.interact) { + 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), &.{}); - - // 7. CGGameUI_Shutdown — cleanup _ = shutdown_hook.install(0x490BD0, 6, @intFromPtr(&shutdownDetour), &.{1}); } fn uninstall() void { shutdown_hook.remove(); engine_init_hook.remove(); - outline.cleanup(); - screenshot.removeHook(); - interact.removeHooks(); + + if (build_opts.outline) { + outline.cleanup(); + } + if (build_opts.screenshot) { + screenshot.removeHook(); + } + if (build_opts.interact) { + interact.removeHooks(); + } + load_addons_hook.remove(); lsf_hook.remove(); file_hook.remove(); @@ -525,8 +559,8 @@ pub export fn DllMain( _: ?*anyopaque, ) callconv(WINAPI) i32 { switch (reason) { - 1 => install(), // DLL_PROCESS_ATTACH - 0 => uninstall(), // DLL_PROCESS_DETACH + 1 => install(), + 0 => uninstall(), else => {}, } return 1; diff --git a/src/outline/addon/Bindings.xml b/src/outline/addon/Bindings.xml new file mode 100644 index 0000000..5d9dc79 --- /dev/null +++ b/src/outline/addon/Bindings.xml @@ -0,0 +1,5 @@ + + + local on = OutlineCommand(); OutlineCommand(on and "off" or "on") + + diff --git a/src/outline/addon/Outline.lua b/src/outline/addon/Outline.lua new file mode 100644 index 0000000..6a70d46 --- /dev/null +++ b/src/outline/addon/Outline.lua @@ -0,0 +1,37 @@ +-- Outline addon (embedded in DLL, loaded from memory) +-- Part of WeirdUtils - only loaded when outline module is compiled + +OUTLINE_VERSION = 1 + +BINDING_HEADER_OUTLINE = "Outline" + +local frame = CreateFrame("Frame") +frame:RegisterEvent("ADDON_LOADED") +frame:RegisterEvent("PLAYER_LOGIN") + +frame:SetScript("OnEvent", function() + if event == "PLAYER_LOGIN" then + local on = OutlineCommand() + DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00Outline|r v" .. OUTLINE_VERSION .. " loaded (" .. (on and "enabled" or "disabled") .. ")") + end +end) + +SLASH_OUTLINE1 = "/outline" +SLASH_OUTLINE2 = "/ol" +SlashCmdList["OUTLINE"] = function(msg) + msg = string.lower(msg or "") + if msg == "" or msg == "status" then + local on = OutlineCommand() + DEFAULT_CHAT_FRAME:AddMessage("Outlines: " .. (on and "|cff00ff00ON|r" or "|cffff0000OFF|r")) + elseif msg == "on" or msg == "enable" then + OutlineCommand("on") + DEFAULT_CHAT_FRAME:AddMessage("Outlines |cff00ff00enabled|r") + elseif msg == "off" or msg == "disable" then + OutlineCommand("off") + DEFAULT_CHAT_FRAME:AddMessage("Outlines |cffff0000disabled|r") + else + DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00Outline|r commands:") + DEFAULT_CHAT_FRAME:AddMessage(" /ol - Show status") + DEFAULT_CHAT_FRAME:AddMessage(" /ol on|off - Toggle outlines") + end +end diff --git a/src/outline/addon/Outline.toc b/src/outline/addon/Outline.toc new file mode 100644 index 0000000..bccac70 --- /dev/null +++ b/src/outline/addon/Outline.toc @@ -0,0 +1,8 @@ +## Interface: 11200 +## Title: Outline +## Notes: Screen-space outline rendering for targets, raid marks, and corpses +## Version: 1.0 +## Dependencies: WeirdUtils + +Outline.lua +Bindings.xml diff --git a/src/screenshot/addon/Bindings.xml b/src/screenshot/addon/Bindings.xml new file mode 100644 index 0000000..2e8583d --- /dev/null +++ b/src/screenshot/addon/Bindings.xml @@ -0,0 +1,5 @@ + + + local on = WeirdUtilsScreenshot(); WeirdUtilsScreenshot(on and "disable" or "enable") + + diff --git a/src/screenshot/addon/Screenshot.lua b/src/screenshot/addon/Screenshot.lua new file mode 100644 index 0000000..bdfc9dc --- /dev/null +++ b/src/screenshot/addon/Screenshot.lua @@ -0,0 +1,46 @@ +-- Screenshot addon (embedded in DLL, loaded from memory) +-- Part of WeirdUtils - only loaded when screenshot module is compiled + +SCREENSHOT_VERSION = 1 + +BINDING_HEADER_SCREENSHOT = "Screenshot" + +local frame = CreateFrame("Frame") +frame:RegisterEvent("ADDON_LOADED") +frame:RegisterEvent("PLAYER_LOGIN") + +frame:SetScript("OnEvent", function() + if event == "PLAYER_LOGIN" then + local on, level = WeirdUtilsScreenshot() + DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00Screenshot|r v" .. SCREENSHOT_VERSION .. " loaded (" .. (on and "enabled" or "disabled") .. ", quality " .. level .. ")") + end +end) + +SLASH_SCREENSHOT1 = "/ss" +SLASH_SCREENSHOT2 = "/screenshot" +SlashCmdList["SCREENSHOT"] = function(msg) + msg = string.lower(msg or "") + if msg == "" or msg == "status" then + local on, level = WeirdUtilsScreenshot() + DEFAULT_CHAT_FRAME:AddMessage("Screenshots: " .. (on and "|cff00ff00ON|r" or "|cffff0000OFF|r") .. " (quality " .. level .. ")") + elseif msg == "on" or msg == "enable" then + WeirdUtilsScreenshot("enable") + DEFAULT_CHAT_FRAME:AddMessage("Screenshots |cff00ff00enabled|r") + elseif msg == "off" or msg == "disable" then + WeirdUtilsScreenshot("disable") + DEFAULT_CHAT_FRAME:AddMessage("Screenshots |cffff0000disabled|r") + elseif tonumber(msg) then + local q = tonumber(msg) + if q >= 0 and q <= 9 then + WeirdUtilsScreenshot("quality", q) + DEFAULT_CHAT_FRAME:AddMessage("Screenshot quality: " .. q) + else + DEFAULT_CHAT_FRAME:AddMessage("|cffff0000Quality must be 0-9|r") + end + else + DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00Screenshot|r commands:") + DEFAULT_CHAT_FRAME:AddMessage(" /ss - Show status") + DEFAULT_CHAT_FRAME:AddMessage(" /ss on|off - Enable/disable PNG screenshots") + DEFAULT_CHAT_FRAME:AddMessage(" /ss 0-9 - Set compression level (0=fast, 9=small)") + end +end diff --git a/src/screenshot/addon/Screenshot.toc b/src/screenshot/addon/Screenshot.toc new file mode 100644 index 0000000..272384f --- /dev/null +++ b/src/screenshot/addon/Screenshot.toc @@ -0,0 +1,8 @@ +## Interface: 11200 +## Title: Screenshot +## Notes: PNG screenshot capture with configurable compression +## Version: 1.0 +## Dependencies: WeirdUtils + +Screenshot.lua +Bindings.xml diff --git a/src/png.zig b/src/screenshot/png.zig similarity index 100% rename from src/png.zig rename to src/screenshot/png.zig diff --git a/src/screenshot.zig b/src/screenshot/screenshot.zig similarity index 100% rename from src/screenshot.zig rename to src/screenshot/screenshot.zig