Register DLL-embedded addons via LoadAddonTOC for SavedVariables support
Hook SetupAddonProcessing (0x51C740) to call LoadAddonTOC for each embedded addon after the game's directory scan. This registers them in the internal addon hash table so the game handles SavedVariables loading/saving, file loading, and ADDON_LOADED events natively. - Add SavedVariablesPerCharacter: UI_MinimapIcons to .toc - Add VARIABLES_LOADED handler to load/initialize toggle state - Add default=1 for Innkeeper, Repair, Brainwasher categories - Write UI_MinimapIcons on toggle for save-on-logout persistence - Remove manual callLoadFileListWithIncludes (game handles it now) - Keep explicit Bindings.xml loading (preloadFileWithFlags bypass)
This commit is contained in:
+55
-9
@@ -783,8 +783,56 @@ fn loadScriptFunctionsDetour() callconv(sc) void {
|
||||
registerLuaFunctions();
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Hook: SetupAddonProcessing (0x51C740)
|
||||
// Called during HandleLogin. After the original runs (which scans Interface\AddOns\
|
||||
// on the filesystem), we call LoadAddonTOC for each DLL-embedded addon to register
|
||||
// them in the game's internal addon hash table. This makes the game aware of our
|
||||
// addons for SavedVariables, Bindings.xml, and proper load ordering.
|
||||
// =============================================================================
|
||||
|
||||
var setup_addons_hook: hook.Detour(fn (u32) callconv(fc) void) = .{};
|
||||
|
||||
fn setupAddonsDetour(mgr_ptr: u32) callconv(fc) void {
|
||||
setup_addons_hook.callOriginal(.{mgr_ptr});
|
||||
|
||||
// Register each embedded addon via LoadAddonTOC. The game reads the .toc
|
||||
// file through LoadFileWithTextureResourceFallback, which our loadFileDetour
|
||||
// intercepts to serve the embedded content. This populates the addon's
|
||||
// SavedVariablesPerCharacter list so the game saves/loads them from WTF/.
|
||||
inline for (embed_modules) |mod| {
|
||||
if (comptime mod.addon_name == null) continue;
|
||||
if (!@field(build_options, "enable_" ++ mod.option)) continue;
|
||||
|
||||
const load = if (comptime std.mem.eql(u8, mod.option, "worldmarkers"))
|
||||
markers.isActive()
|
||||
else
|
||||
true;
|
||||
|
||||
if (load) {
|
||||
const name: [*:0]const u8 = comptime (mod.addon_name.? ++ "\x00").ptr;
|
||||
con.fmt("[addons] registering embedded addon: {s}\n", .{name});
|
||||
callLoadAddonTOC(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Call LoadAddonTOC (0x0051c9b0) -- __fastcall(ECX=addonName), RET
|
||||
fn callLoadAddonTOC(addon_name: [*:0]const u8) void {
|
||||
asm volatile (
|
||||
\\call *%[func]
|
||||
:
|
||||
: [_] "{ecx}" (@intFromPtr(addon_name)),
|
||||
[func] "{eax}" (@as(u32, 0x0051c9b0)),
|
||||
: .{ .eax = true, .ecx = true, .edx = true, .memory = true, .cc = true });
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Hook: LoadAddonsRecursively (0x51F600)
|
||||
// With SetupAddonProcessing registering our addons, the game's own
|
||||
// LoadAddonRecursive now handles loading files, Bindings.xml, and saved
|
||||
// variables for embedded addons. This hook is kept for any future
|
||||
// post-load work but no longer manually loads addon files.
|
||||
// =============================================================================
|
||||
|
||||
var load_addons_hook: hook.Detour(fn (u32) callconv(fc) void) = .{};
|
||||
@@ -792,13 +840,16 @@ 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| {
|
||||
if (comptime mod.addon_name == null) continue;
|
||||
if (!@field(build_options, "enable_" ++ mod.option)) continue;
|
||||
|
||||
// Module-specific runtime checks
|
||||
const load = if (comptime std.mem.eql(u8, mod.option, "worldmarkers"))
|
||||
markers.isActive()
|
||||
else
|
||||
@@ -807,14 +858,6 @@ 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.?);
|
||||
const toc_name = comptime findTocName(paths);
|
||||
if (toc_name) |tn| {
|
||||
callLoadFileListWithIncludes(
|
||||
"Interface\\AddOns\\" ++ addon_name ++ "\\" ++ tn,
|
||||
&md5ctx,
|
||||
error_handler,
|
||||
);
|
||||
}
|
||||
if (comptime hasFile(paths, "Bindings.xml")) {
|
||||
callLoadUIBindingsFromFile(
|
||||
"Interface\\AddOns\\" ++ addon_name ++ "\\Bindings.xml",
|
||||
@@ -985,6 +1028,7 @@ fn install() void {
|
||||
if (m.install) |inst| inst();
|
||||
}
|
||||
|
||||
_ = setup_addons_hook.attach(0x51C740, &setupAddonsDetour);
|
||||
_ = load_addons_hook.attach(0x51F600, &loadAddonsDetour);
|
||||
_ = engine_init_hook.attach(0x46a400, &engineInitDetour);
|
||||
_ = logout_hook.attach(0x491180, &logoutDetour);
|
||||
@@ -1004,6 +1048,7 @@ fn uninstall() void {
|
||||
}
|
||||
|
||||
load_addons_hook.detach();
|
||||
setup_addons_hook.detach();
|
||||
lsf_hook.detach();
|
||||
file_hook.detach();
|
||||
removeFileHooks();
|
||||
@@ -1080,6 +1125,7 @@ fn disableAll() callconv(.c) i32 {
|
||||
logout_hook.detach();
|
||||
engine_init_hook.detach();
|
||||
load_addons_hook.detach();
|
||||
setup_addons_hook.detach();
|
||||
lsf_hook.detach();
|
||||
file_hook.detach();
|
||||
removeFileHooks();
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
# MinimapIcons Research
|
||||
|
||||
## SavedVariables -- Addon Registration System
|
||||
|
||||
### Problem
|
||||
DLL-embedded addons are served via `loadFileDetour` (hook on `LoadFileWithTextureResourceFallback` at 0x648620). The game reads our embedded .toc and .lua files successfully, but **SavedVariablesPerCharacter never persists to disk**. The addon's toggle state resets every session.
|
||||
|
||||
### Root Cause
|
||||
The game discovers addons by scanning `Interface\AddOns\` on the **filesystem** via `EnumerateDirectoryWithCallback`. Since our addon has no real directory on disk, `ProcessAddonFilePath` is never called, `LoadAddonTOC` never runs, and the addon is never registered in the internal addon hash table. Without registration, the save system has no record of which variables to persist.
|
||||
|
||||
The addon's .lua code still runs (served via `loadFileDetour`), but it's loaded through a different path -- `loadFileListWithIncludes` reads the TOC file list and loads each .lua/.xml, all of which go through our hook. The disconnect is that `LoadAddonTOC` (which parses `## SavedVariablesPerCharacter` and registers the variable names) only runs during the directory-scan phase.
|
||||
|
||||
### Addon Loading Sequence
|
||||
|
||||
```
|
||||
HandleLogin (0x0046afb0)
|
||||
-> SetupAddonProcessing (0x0051c740)
|
||||
-> ProcessAddonDirectory (0x0051c760)
|
||||
-> EnumerateDirectoryWithCallback("Interface\\AddOns\\", ...) -- filesystem scan
|
||||
-> ProcessAddonFilePath (0x0051c910) -- per .toc file found
|
||||
-> LoadAddonTOC (0x0051c9b0) -- parses TOC, registers addon in hash table
|
||||
-- reads TOC via LoadFileWithTextureResourceFallback (hookable!)
|
||||
|
||||
InitializeGameInterface (0x0048fbf0)
|
||||
-> LoadAddonSavedVariables (0x0051ebe0) -- loads WTF/.../AddOns.txt (enabled/disabled state per addon)
|
||||
-> LoadAddonsRecursively (0x0051f600) -- iterates registered addon list
|
||||
-> LoadAddonRecursive (0x0051f242) -- per addon:
|
||||
1. loadFileListWithIncludes() -- loads TOC file list (.lua/.xml)
|
||||
2. Loads Bindings.xml if exists
|
||||
3. Loads WTF/Account/<acct>/SavedVariables/<name>.lua (account-wide)
|
||||
4. Loads WTF/Account/<acct>/<realm>/<char>/SavedVariables/<name>.lua (per-character)
|
||||
5. FireLuaEvent(0x1ad) -- ADDON_LOADED event
|
||||
-> Config_LoadSavedVariables (0x0051f650) -- called from World_HandlePlayerLogin (0x00490bd0) on logout/exit
|
||||
```
|
||||
|
||||
### Key Functions
|
||||
|
||||
| Address | Name | Convention | Notes |
|
||||
|------------|----------------------------|------------|-------|
|
||||
| 0x0051c740 | SetupAddonProcessing | ? | Calls ProcessAddonDirectory |
|
||||
| 0x0051c760 | ProcessAddonDirectory | cdecl/void | Scans filesystem, builds addon list |
|
||||
| 0x0051c910 | ProcessAddonFilePath | __fastcall(ECX=path) | Per-.toc callback, strips prefix, calls LoadAddonTOC |
|
||||
| 0x0051c9b0 | LoadAddonTOC | __fastcall(ECX=addonName) | Parses TOC, registers addon. RET (no stack cleanup) |
|
||||
| 0x0051ebe0 | LoadAddonSavedVariables | __fastcall(ECX=addonName or NULL for all) | Loads AddOns.txt state |
|
||||
| 0x0051f242 | LoadAddonRecursive | __fastcall(ECX=addonName, EDX=loadFlag, stack=callback) | Loads one addon + saved vars |
|
||||
| 0x0051f600 | LoadAddonsRecursively | __fastcall(ECX=callback) | Iterates all registered addons |
|
||||
| 0x0051f650 | SaveAddonVariables | cdecl/void | Writes all dirty addons' saved vars to WTF/ |
|
||||
| 0x0051fa40 | ShutdownAddonSystem | ? | Cleanup |
|
||||
|
||||
### Addon Data Structure (hash table entry)
|
||||
|
||||
```
|
||||
+0x00: u32 hash of addon name
|
||||
+0x04: ... (hash table linkage)
|
||||
+0x14: *u8 addon name string (entry[5])
|
||||
+0x18: u8 dirty flag (needs variable save)
|
||||
+0x19: u8 loaded flag (entry[6] byte 1)
|
||||
+0x1c: u32 interface version (## Interface)
|
||||
+0x20: u32 revision (## Revision)
|
||||
+0x26-0x2f: hash table for Title/Notes/Author/Version metadata
|
||||
+0x28: u8 secure flag (## Secure)
|
||||
+0x2b: u8 default state enabled/disabled (## DefaultState)
|
||||
+0x2c: u8 load on demand flag (## LoadOnDemand)
|
||||
+0x34: u32 line count in TOC
|
||||
|
||||
Dynamic arrays (each: capacity/count/data_ptr/growth at 3 dword intervals):
|
||||
+0x38/3c/40/44: OptionalDeps (## OptionalDep)
|
||||
+0x48/4c/50/54: RequiredDeps (## RequiredDep / ## Dep)
|
||||
+0x58/5c/60/64: LoadWith/Dependencies (## LoadWith)
|
||||
+0x68/6c/70/74: SavedVariables (## SavedVariables) -- account-wide
|
||||
+0x78/7c/80/84: SavedVariablesPerCharacter (## SavedVariablesPerCharacter)
|
||||
+0x8c-0x98: dependents array
|
||||
```
|
||||
|
||||
### Key Globals
|
||||
|
||||
| Address | Name | Purpose |
|
||||
|------------|------|---------|
|
||||
| 0x00be1b6c | PTR_00be1b6c | Addon linked list head |
|
||||
| 0x00be1b64 | PTR_00be1b64 | Linked list base for traversal (node+4 offset) |
|
||||
| 0x00be1b7c | PTR_00be1b7c | Hash table buckets array |
|
||||
| 0x00be1b84 | PTR_00be1b84 | Hash table mask (0xffffffff = uninitialized) |
|
||||
| 0x00be1bd8 | PTR_00be1bd8 | Saved variables state list head |
|
||||
| 0x00be1b60 | PTR_00be1b60 | Hash table control structure |
|
||||
|
||||
### Fix: Hook SetupAddonProcessing
|
||||
|
||||
`LoadAddonTOC` reads the TOC file via `LoadFileWithTextureResourceFallback`, which our `loadFileDetour` already intercepts. So calling `LoadAddonTOC("MinimapIcons")` will:
|
||||
1. Hit our file hook, serve the embedded TOC content
|
||||
2. Parse `## SavedVariablesPerCharacter: UI_MinimapIcons`
|
||||
3. Register the addon in the hash table with the variable name at +0x78/7c/80
|
||||
|
||||
Hook `SetupAddonProcessing` (0x0051c740). After calling the original (which runs `ProcessAddonDirectory` and initializes the addon system), call `LoadAddonTOC` for each DLL-embedded addon. This ensures:
|
||||
- The addon memory pool is initialized
|
||||
- Our addons appear in the list before `LoadAddonsRecursively` 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.
|
||||
|
||||
### Verification Plan
|
||||
|
||||
1. Hook `SetupAddonProcessing`, call `LoadAddonTOC("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
|
||||
5. Re-login, verify toggles persisted
|
||||
@@ -87,11 +87,11 @@ local NPC_CATEGORIES = {
|
||||
{ name = "Auctioneer", trackingType = "auctioneer", icon = "Interface\\Minimap\\Tracking\\Auctioneer" },
|
||||
{ name = "Banker", trackingType = "banker", icon = "Interface\\Minimap\\Tracking\\Banker" },
|
||||
{ name = "Battle Master", trackingType = "battlemaster", icon = "Interface\\Minimap\\Tracking\\BattleMaster" },
|
||||
{ name = "Brainwasher", trackingType = "brainwasher", icon = "Interface\\Minimap\\Tracking\\Brainwasher", scale = 1.7 },
|
||||
{ name = "Brainwasher", trackingType = "brainwasher", icon = "Interface\\Minimap\\Tracking\\Brainwasher", scale = 1.7, default = 1 },
|
||||
{ name = "Class Trainer", trackingType = "trainer", icon = "Interface\\Minimap\\Tracking\\Class",
|
||||
getFilter = function() return (UnitClass("player")) end },
|
||||
{ name = "Flight Master", trackingType = "flightmaster", icon = "Interface\\Minimap\\Tracking\\FlightMaster" },
|
||||
{ name = "Innkeeper", trackingType = "innkeeper", icon = "Interface\\Minimap\\Tracking\\Innkeeper" },
|
||||
{ name = "Innkeeper", trackingType = "innkeeper", icon = "Interface\\Minimap\\Tracking\\Innkeeper", default = 1 },
|
||||
{ name = "Mailbox", trackingType = "mailbox", icon = "Interface\\Minimap\\Tracking\\Mailbox" },
|
||||
{ name = "Poison Vendor", trackingType = "vendor", icon = "Interface\\Minimap\\Tracking\\Poison",
|
||||
getFilter = function() return getLocaleFilter(POISON_FILTERS) end },
|
||||
@@ -99,7 +99,7 @@ local NPC_CATEGORIES = {
|
||||
getExclude = function() return UnitClass("player") end },
|
||||
{ name = "Reagent Vendor", trackingType = "vendor", icon = "Interface\\Minimap\\Tracking\\Reagents",
|
||||
getFilter = function() return getLocaleFilter(REAGENT_FILTERS) end },
|
||||
{ name = "Repair", trackingType = "repair", icon = "Interface\\Minimap\\Tracking\\Repair" },
|
||||
{ name = "Repair", trackingType = "repair", icon = "Interface\\Minimap\\Tracking\\Repair", default = 1 },
|
||||
{ name = "Stable Master", trackingType = "stablemaster", icon = "Interface\\Minimap\\Tracking\\StableMaster" },
|
||||
{ name = "Trade Goods", trackingType = "vendor", icon = "Interface\\Minimap\\Tracking\\Profession",
|
||||
getFilter = function() return getLocaleFilter(TRADE_FILTERS) end },
|
||||
@@ -109,10 +109,10 @@ local NPC_CATEGORIES = {
|
||||
}
|
||||
|
||||
-- =============================================================================
|
||||
-- NPC tracking state (persists per session, no SavedVariables)
|
||||
-- NPC tracking state (persisted per-character via SavedVariablesPerCharacter)
|
||||
-- =============================================================================
|
||||
|
||||
local activeNpcCategories = {} -- name -> 1/nil
|
||||
local activeNpcCategories = {} -- name -> 1/nil, loaded from UI_MinimapIcons
|
||||
|
||||
-- Sync DLL tracking state with addon toggle state.
|
||||
-- Each category gets its own DLL call. Categories with a subnameFilter
|
||||
@@ -232,6 +232,7 @@ UIDropDownMenu_Initialize(dropdown, function()
|
||||
else
|
||||
activeNpcCategories[catName] = 1
|
||||
end
|
||||
UI_MinimapIcons = activeNpcCategories
|
||||
updateDllTracking()
|
||||
end
|
||||
UIDropDownMenu_AddButton(info)
|
||||
@@ -309,12 +310,25 @@ end)
|
||||
-- =============================================================================
|
||||
|
||||
local events = CreateFrame("Frame")
|
||||
events:RegisterEvent("VARIABLES_LOADED")
|
||||
events:RegisterEvent("PLAYER_LOGIN")
|
||||
events:RegisterEvent("SPELLS_CHANGED")
|
||||
events:RegisterEvent("PLAYER_AURAS_CHANGED")
|
||||
|
||||
events:SetScript("OnEvent", function()
|
||||
if event == "PLAYER_LOGIN" or event == "SPELLS_CHANGED" then
|
||||
if event == "VARIABLES_LOADED" then
|
||||
if UI_MinimapIcons then
|
||||
activeNpcCategories = UI_MinimapIcons
|
||||
else
|
||||
for _, cat in ipairs(NPC_CATEGORIES) do
|
||||
if cat.default then
|
||||
activeNpcCategories[cat.name] = 1
|
||||
end
|
||||
end
|
||||
UI_MinimapIcons = activeNpcCategories
|
||||
end
|
||||
updateDllTracking()
|
||||
elseif event == "PLAYER_LOGIN" or event == "SPELLS_CHANGED" then
|
||||
scanSpellbook()
|
||||
end
|
||||
MiniMapTrackingFrame:Show()
|
||||
|
||||
@@ -2,5 +2,6 @@
|
||||
## Title: MinimapIcons
|
||||
## Notes: Minimap tracking spell dropdown
|
||||
## Version: 1.0
|
||||
## SavedVariablesPerCharacter: UI_MinimapIcons
|
||||
|
||||
MinimapIcons.lua
|
||||
Reference in New Issue
Block a user