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.
This commit is contained in:
+3
-2
@@ -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`
|
||||
|
||||
|
||||
@@ -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" },
|
||||
|
||||
+22
-10
@@ -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",
|
||||
|
||||
@@ -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_<name>` 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/<acct>/<realm>/<char>/SavedVariables/MinimapIcons.lua` exists on disk
|
||||
4. Check `WTF/Account/<acct>/<realm>/<char>/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.
|
||||
|
||||
@@ -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
|
||||
@@ -1,7 +0,0 @@
|
||||
## Interface: 11200
|
||||
## Title: WeirdUtils_Screenshot
|
||||
## Notes: PNG screenshot capture with configurable compression
|
||||
## Version: 1.0
|
||||
|
||||
|
||||
Screenshot.lua
|
||||
@@ -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
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user