From d2f91fe8c25d9a7e370af2dc3e35a5fd4607f1a0 Mon Sep 17 00:00:00 2001 From: MarcelineVQ Date: Sun, 8 Mar 2026 12:24:19 -0700 Subject: [PATCH] Replace screenshot addon with screenshotQuality CVar Remove the embedded Screenshot addon (TOC + Lua) and WeirdUtilsScreenshot() Lua function. Compression quality is now controlled entirely via a CVar (saved to config.wtf), read fresh on each screenshot. CVar 0 disables PNG and falls through to original TGA. --- DLL_README.md | 5 +- build.zig | 2 +- .../{Interact.toc => WeirdUtils_Interact.toc} | 0 ...essions.toc => WeirdUtils_LogSessions.toc} | 0 src/main.zig | 32 ++++--- ...arkers.toc => WeirdUtils_WorldMarkers.toc} | 0 src/minimapicons/RESEARCH.md | 87 +++++++++++++++++- ...pIcons.toc => WeirdUtils_MinimapIcons.toc} | 0 .../{Outline.toc => WeirdUtils_Outline.toc} | 0 src/screenshot/addon/Screenshot.lua | 32 ------- src/screenshot/addon/Screenshot.toc | 7 -- src/screenshot/screenshot.zig | 90 +++++-------------- 12 files changed, 132 insertions(+), 123 deletions(-) rename src/interact/addon/{Interact.toc => WeirdUtils_Interact.toc} (100%) rename src/logsessions/addon/{LogSessions.toc => WeirdUtils_LogSessions.toc} (100%) rename src/markers/addon/{WorldMarkers.toc => WeirdUtils_WorldMarkers.toc} (100%) rename src/minimapicons/addon/{MinimapIcons.toc => WeirdUtils_MinimapIcons.toc} (100%) rename src/outline/addon/{Outline.toc => WeirdUtils_Outline.toc} (100%) delete mode 100644 src/screenshot/addon/Screenshot.lua delete mode 100644 src/screenshot/addon/Screenshot.toc diff --git a/DLL_README.md b/DLL_README.md index 7c18bb1..ce437d9 100644 --- a/DLL_README.md +++ b/DLL_README.md @@ -68,9 +68,10 @@ Best used via keybindings (available in the Key Bindings menu) or macros: Saves screenshots as compressed PNG files instead of the default uncompressed TGA format. Runs on a background thread with no frame drops. -- `/screenshot 0` through `/screenshot 9` -- set compression level (0 = fast, 9 = smallest, default 6) +Controlled via the `screenshotQuality` CVar (saved to config.wtf): -Compression level 6 provides the best balance of quality and file size. Enabled automatically on install. +- `/script SetCVar("screenshotQuality", "6")` -- set compression level (1 = fast, 9 = smallest, default 6) +- `/script SetCVar("screenshotQuality", "0")` -- disable PNG, use original TGA format **DLL:** `screenshot.dll` diff --git a/build.zig b/build.zig index 8e1227e..5412be1 100644 --- a/build.zig +++ b/build.zig @@ -13,7 +13,7 @@ const ModuleDesc = struct { /// Single source of truth for all modules. Adding a module here is enough /// to wire up the build option, build_options passthrough, and DLL variant. const module_list = [_]ModuleDesc{ - .{ .name = "screenshot", .desc = "Enable screenshot module", .addon_name = "Screenshot" }, + .{ .name = "screenshot", .desc = "Enable screenshot module" }, .{ .name = "interact", .desc = "Enable interact module", .addon_name = "Interact" }, .{ .name = "outline", .desc = "Enable outline module", .default = false, .addon_name = "Outline" }, .{ .name = "worldmarkers", .desc = "Enable world markers module", .src_dir = "markers", .addon_name = "WorldMarkers" }, diff --git a/src/interact/addon/Interact.toc b/src/interact/addon/WeirdUtils_Interact.toc similarity index 100% rename from src/interact/addon/Interact.toc rename to src/interact/addon/WeirdUtils_Interact.toc diff --git a/src/logsessions/addon/LogSessions.toc b/src/logsessions/addon/WeirdUtils_LogSessions.toc similarity index 100% rename from src/logsessions/addon/LogSessions.toc rename to src/logsessions/addon/WeirdUtils_LogSessions.toc diff --git a/src/main.zig b/src/main.zig index 6dce5df..e479520 100644 --- a/src/main.zig +++ b/src/main.zig @@ -78,9 +78,6 @@ fn allocateGameBuffer(size: u32) ?[*]u8 { fn registerLuaFunctions() void { // 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)); @@ -141,13 +138,15 @@ const EmbedModule = struct { addon_name: ?[]const u8 = null, addon_files_opt: ?[]const u8 = null, asset_files_opt: ?[]const u8 = null, + /// If true, addon is marked as SECURE (hidden from addon list, always loaded). + hidden: bool = false, }; const embed_modules = [_]EmbedModule{ - .{ .option = "screenshot", .addon_name = "WeirdUtils_Screenshot", .addon_files_opt = "screenshot_addon_files" }, + .{ .option = "screenshot" }, .{ .option = "interact", .addon_name = "WeirdUtils_Interact", .addon_files_opt = "interact_addon_files" }, .{ .option = "outline", .addon_name = "WeirdUtils_Outline", .addon_files_opt = "outline_addon_files" }, - .{ .option = "worldmarkers", .addon_name = "WeirdUtils_WorldMarkers", .addon_files_opt = "worldmarkers_addon_files", .asset_files_opt = "worldmarkers_asset_files" }, + .{ .option = "worldmarkers", .addon_name = "WeirdUtils_WorldMarkers", .addon_files_opt = "worldmarkers_addon_files", .asset_files_opt = "worldmarkers_asset_files", .hidden = true }, .{ .option = "logsessions", .addon_name = "WeirdUtils_LogSessions", .addon_files_opt = "logsessions_addon_files" }, .{ .option = "minimapicons", .addon_name = "WeirdUtils_MinimapIcons", .addon_files_opt = "minimapicons_addon_files", .asset_files_opt = "minimapicons_asset_files" }, }; @@ -809,7 +808,7 @@ fn setupAddonsDetour(mgr_ptr: u32) callconv(fc) void { else true; - if (load) { + if (load and !mod.hidden) { const name: [*:0]const u8 = comptime (mod.addon_name.? ++ "\x00").ptr; con.fmt("[addons] registering embedded addon: {s}\n", .{name}); callLoadAddonTOC(name); @@ -840,10 +839,6 @@ var load_addons_hook: hook.Detour(fn (u32) callconv(fc) void) = .{}; fn loadAddonsDetour(error_handler: u32) callconv(fc) void { load_addons_hook.callOriginal(.{error_handler}); - // The game's LoadAddonRecursive now handles .lua/.toc loading and saved - // variables for registered addons, but Bindings.xml loading uses - // preloadFileWithFlags which may not go through our file hook. - // Explicitly load bindings for embedded addons that include them. var md5ctx = std.mem.zeroes([88]u8); inline for (embed_modules) |mod| { @@ -858,6 +853,23 @@ fn loadAddonsDetour(error_handler: u32) callconv(fc) void { if (load) { const addon_name = comptime mod.addon_name.?; const paths = comptime @field(build_options, mod.addon_files_opt.?); + + if (mod.hidden) { + // Hidden addons are not registered via LoadAddonTOC, so the game + // doesn't know about them. Load their files directly to keep them + // invisible in the addon list. + const toc_name = comptime findTocName(paths); + if (toc_name) |tn| { + callLoadFileListWithIncludes( + "Interface\\AddOns\\" ++ addon_name ++ "\\" ++ tn, + &md5ctx, + error_handler, + ); + } + } + + // Bindings.xml must be loaded explicitly for all addons -- the game's + // preloadFileWithFlags doesn't go through our file hook. if (comptime hasFile(paths, "Bindings.xml")) { callLoadUIBindingsFromFile( "Interface\\AddOns\\" ++ addon_name ++ "\\Bindings.xml", diff --git a/src/markers/addon/WorldMarkers.toc b/src/markers/addon/WeirdUtils_WorldMarkers.toc similarity index 100% rename from src/markers/addon/WorldMarkers.toc rename to src/markers/addon/WeirdUtils_WorldMarkers.toc diff --git a/src/minimapicons/RESEARCH.md b/src/minimapicons/RESEARCH.md index 62c6798..169bad0 100644 --- a/src/minimapicons/RESEARCH.md +++ b/src/minimapicons/RESEARCH.md @@ -96,12 +96,93 @@ Hook `SetupAddonProcessing` (0x0051c740). After calling the original (which runs - `LoadAddonRecursive` will find our addon, load its files (via our hook), load saved vars from WTF/, and fire ADDON_LOADED - `SaveAddonVariables` will find our addon and write its variables to WTF/ on logout -This also means **Bindings.xml** will be loaded automatically if included in the TOC -- no need to handle it separately. +**Note:** Bindings.xml loading via `preloadFileWithFlags` does NOT go through our file hook, so we still explicitly call `callLoadUIBindingsFromFile` in `loadAddonsDetour` for addons with Bindings.xml. + +### Current Status (WORKING) + +- SavedVariablesPerCharacter persists to `WTF/Account/.../SavedVariables/WeirdUtils_MinimapIcons.lua` +- Addon names use `WeirdUtils_` namespace (6 addons) +- Game's `LoadAddonRecursive` handles .lua/.toc loading and saved variables +- Explicit bindings loading still needed (kept in `loadAddonsDetour`) ### Verification Plan -1. Hook `SetupAddonProcessing`, call `LoadAddonTOC("MinimapIcons")` after original +1. Hook `SetupAddonProcessing`, call `LoadAddonTOC("WeirdUtils_MinimapIcons")` after original 2. Check console for `[file] served embedded` messages for the .toc read during `LoadAddonTOC` 3. Toggle some NPC categories, log out -4. Check `WTF/Account////SavedVariables/MinimapIcons.lua` exists on disk +4. Check `WTF/Account////SavedVariables/WeirdUtils_MinimapIcons.lua` exists on disk 5. Re-login, verify toggles persisted + +## Login Screen Addon List (WIP) + +### Goal +Make DLL-embedded addons appear in the character select screen's "AddOns" button list. +This allows users to enable/disable them before entering world, and the DLL could +check the enabled state to skip hooking for disabled addons. + +### Architecture + +Two separate Lua C function tables exist for addon management: +- **Glue (login/charselect) table** at 0x008374a0 -- functions in 0x0046dxxx range +- **In-game table** at 0x0083e488 -- functions in 0x0048exxx range + +Both read from the **same** underlying addon data: +- `GetAddonCount` (0x0051def0): returns `PTR_00be1b90` (simple count) +- `GetAddonByIndex` (0x0051df00): returns `PTR_00be1b94[index]` (flat array of addon ptrs) + +These globals (`PTR_00be1b90` count, `PTR_00be1b94` array) are separate from the +hash table (PTR_00be1b7c) and linked list (PTR_00be1b6c) -- they're a flat indexed +view, likely populated after `ProcessAddonDirectory` finishes. + +#### Glue Lua Functions (charselect screen) + +| Address | Function | +|------------|----------| +| 0x0046d420 | GetNumAddOns | +| 0x0046d460 | GetAddOnInfo -- returns: name, title, notes, url, loadable, reason, security, isNew | +| 0x0046d5e0 | LaunchAddOnURL | +| 0x0046d650 | GetAddOnDependencies | +| 0x0046d6f0 | GetAddOnEnableState | +| 0x0046d7b0 | EnableAddOn | +| 0x0046d850 | EnableAllAddOns | +| 0x0046d8a0 | DisableAddOn | +| 0x0046d940 | DisableAllAddOns | +| 0x0046d990 | SaveAddOns | +| 0x0046d9a0 | ResetAddOns | + +### Call Flow + +``` +HandleLogin (0x0046afb0) -- triggered by login credentials submit + -> SetupAddonProcessing (0x0051c740) -- OUR HOOK IS HERE + -> ShutdownAddonSystem (0x0051fa40) -- clears old state + -> ProcessAddonDirectory (0x0051c760) -- filesystem scan + -> [our hook: callLoadAddonTOC per embedded addon] +``` + +After `HandleLogin` returns, the client transitions to the character select screen +where the "AddOns" button is available. The Lua `GetNumAddOns`/`GetAddOnInfo` calls +read from the flat array at PTR_00be1b90/PTR_00be1b94. + +### Open Questions + +1. **When is PTR_00be1b90/PTR_00be1b94 populated?** These are a flat indexed array, + not the hash table. Possibly built from the linked list after `ProcessAddonDirectory` + finishes, or lazily on first `GetAddonCount` call. Need to find xrefs to PTR_00be1b90 + to determine who writes to it and when. + +2. **Does our `LoadAddonTOC` call also update the flat array?** If it only adds to the + hash table + linked list but not the flat array, our addons won't appear in the UI. + Need to trace `LoadAddonTOC` -> `Container_MoveOrInsertElement` to see if the flat + array is updated as a side effect. + +3. **Timing**: Our hook fires after `SetupAddonProcessing` returns (which is after + `ProcessAddonDirectory`). If the flat array is built at the end of + `ProcessAddonDirectory` and never updated, we'd need to either: + - Hook ProcessAddonDirectory and inject our addons before the array is built + - Manually append to PTR_00be1b90/PTR_00be1b94 after calling LoadAddonTOC + - Find and call whatever rebuilds the flat array + +4. **Enable/Disable state**: `EnableAddOn`/`DisableAddOn` write to `AddOns.txt` via + `SaveAddOns`. The DLL could check this file or the addon struct's enabled field + at startup to decide whether to install hooks for a given module. diff --git a/src/minimapicons/addon/MinimapIcons.toc b/src/minimapicons/addon/WeirdUtils_MinimapIcons.toc similarity index 100% rename from src/minimapicons/addon/MinimapIcons.toc rename to src/minimapicons/addon/WeirdUtils_MinimapIcons.toc diff --git a/src/outline/addon/Outline.toc b/src/outline/addon/WeirdUtils_Outline.toc similarity index 100% rename from src/outline/addon/Outline.toc rename to src/outline/addon/WeirdUtils_Outline.toc diff --git a/src/screenshot/addon/Screenshot.lua b/src/screenshot/addon/Screenshot.lua deleted file mode 100644 index d6dcd8d..0000000 --- a/src/screenshot/addon/Screenshot.lua +++ /dev/null @@ -1,32 +0,0 @@ --- Screenshot addon (embedded in DLL, loaded from memory) - -BINDING_HEADER_SCREENSHOT = "Screenshot" - -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 deleted file mode 100644 index 3210498..0000000 --- a/src/screenshot/addon/Screenshot.toc +++ /dev/null @@ -1,7 +0,0 @@ -## Interface: 11200 -## Title: WeirdUtils_Screenshot -## Notes: PNG screenshot capture with configurable compression -## Version: 1.0 - - -Screenshot.lua diff --git a/src/screenshot/screenshot.zig b/src/screenshot/screenshot.zig index 169a9b2..c2d9f96 100644 --- a/src/screenshot/screenshot.zig +++ b/src/screenshot/screenshot.zig @@ -53,9 +53,22 @@ const ERROR_ALREADY_EXISTS: u32 = 183; // State // ============================================================================= -var enabled: bool = true; -var compression_level: i32 = 6; // user-facing 0–9, kept for Lua interface const tc: std.builtin.CallingConvention = .{ .x86_thiscall = .{} }; +const fc: std.builtin.CallingConvention = .{ .x86_fastcall = .{} }; + +// CVar for compression level persistence (0–9, default 6) +const CVAR_NAME = "screenshotQuality"; +const CVAR_LOOKUP: usize = 0x0063DEC0; +const RegisterCVarFn = *const fn ([*:0]const u8, u32, u32, [*:0]const u8, u32, u32, u32, u32) callconv(fc) u32; +const registerCVar: RegisterCVarFn = @ptrFromInt(0x0063DB90); + +fn readCVarQuality() i32 { + const cvar_ptr = hook.fastcall(u32, CVAR_LOOKUP, @intFromPtr(@as([*:0]const u8, CVAR_NAME)), @as(u32, 0)); + if (cvar_ptr == 0) return 6; + const val = hook.readMem(i32, cvar_ptr + 40); + return std.math.clamp(val, 0, 9); +} + const TgaWriteFn = fn (u32, u32) callconv(tc) i32; var tga_hook: hook.Detour(TgaWriteFn) = .{}; var screenshot_dir: [260]u8 = undefined; @@ -141,7 +154,9 @@ fn callOriginal(self: u32, filename: u32) i32 { // ============================================================================= fn tgaWriteDetour(self: u32, filename: u32) callconv(tc) i32 { - if (!enabled) return callOriginal(self, filename); + // CVar 0 = disabled, fall through to original TGA write + const quality = readCVarQuality(); + if (quality == 0) return callOriginal(self, filename); // Validate TGA header fields const pixel_data = hook.readMem(u32, self + 0x04); @@ -170,7 +185,7 @@ fn tgaWriteDetour(self: u32, filename: u32) callconv(tc) i32 { while (!mutex.tryLock()) {} defer mutex.unlock(); - if (!enqueue(.{ .buffer = buffer.ptr, .width = width, .height = height, .size = size, .level = png.mapLevel(compression_level) })) { + if (!enqueue(.{ .buffer = buffer.ptr, .width = width, .height = height, .size = size, .level = png.mapLevel(quality) })) { std.heap.page_allocator.free(buffer); return callOriginal(self, filename); } @@ -278,70 +293,6 @@ fn writePng(path: [*:0]const u8, pixels: [*]const u8, width: u16, height: u16, l png.encode(handle, writeToFile, pixels, width, height, level); } -// ============================================================================= -// Lua helper: push f64 onto Lua stack via __fastcall(L_ECX, f64_on_stack) -// ============================================================================= - -fn luaPushNumber(L_ptr: usize, n: f64) void { - // lua_pushnumber at 0x6F3810 is __fastcall(L, double) - // double skips EDX, goes on stack (8 bytes). Callee cleans with ret 8. - const raw: [2]u32 = @bitCast(n); - asm volatile ( - \\push %[hi] - \\push %[lo] - \\call *%[func] - : - : [_] "{ecx}" (L_ptr), - [lo] "r" (raw[0]), - [hi] "r" (raw[1]), - [func] "r" (@as(u32, 0x6F3810)), - : .{ .eax = true, .ecx = true, .edx = true, .memory = true, .cc = true }); -} - -// ============================================================================= -// Lua C function: WeirdUtilsScreenshot(...) -// No args → returns enabled (bool), compression_level (number) -// ("enable") → enable PNG screenshots -// ("disable") → disable (fall through to original TGA) -// ("quality", N) → set compression level 0–9 (kept for addon compat) -// ============================================================================= - -pub fn screenshotCommand(L: *anyopaque) callconv(.c) u32 { - const L_ptr = @intFromPtr(L); - - // lua_gettop(L) - __fastcall(L_ECX), EDX unused - const nargs = hook.fastcall(i32, 0x6F3070, L_ptr, @as(u32, 0)); - - if (nargs == 0) { - // lua_pushboolean(L, enabled) - hook.fastcall(void, 0x6F39F0, L_ptr, @as(i32, if (enabled) 1 else 0)); - // lua_pushnumber(L, compression_level) - luaPushNumber(L_ptr, @floatFromInt(compression_level)); - return 2; - } - - // lua_tostring(L, 1) - __fastcall(L_ECX, index_EDX) - const raw_str = hook.fastcall(usize, 0x6F3690, L_ptr, @as(i32, 1)); - if (raw_str != 0) { - const str: [*:0]const u8 = @ptrFromInt(raw_str); - const arg = std.mem.span(str); - - if (std.mem.eql(u8, arg, "enable")) { - enabled = true; - } else if (std.mem.eql(u8, arg, "disable")) { - enabled = false; - } else if (std.mem.eql(u8, arg, "quality")) { - if (nargs >= 2) { - // lua_tonumber(L, 2) - __fastcall(L_ECX, index_EDX), returns f64 in ST(0) - const level = hook.fastcall(f64, 0x6F3620, L_ptr, @as(i32, 2)); - compression_level = std.math.clamp(@as(i32, @intFromFloat(level)), 0, 9); - } - } - } - - return 0; -} - // ============================================================================= // Install / Remove // ============================================================================= @@ -354,6 +305,9 @@ pub fn installHook() void { g_is_hook_owner = result.is_owner; if (!g_is_hook_owner) return; + // Register CVar for compression quality persistence (saved to config.wtf) + _ = registerCVar(CVAR_NAME, 0, 0, "6", 0, 1, 0, 0); + // CTgaFile::Write at 0x5a4810 // __thiscall(self, filename) ret 4 //