diff --git a/src/main.zig b/src/main.zig index 99c6858..4f6b4c8 100644 --- a/src/main.zig +++ b/src/main.zig @@ -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(); diff --git a/src/minimapicons/RESEARCH.md b/src/minimapicons/RESEARCH.md new file mode 100644 index 0000000..c37de17 --- /dev/null +++ b/src/minimapicons/RESEARCH.md @@ -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//SavedVariables/.lua (account-wide) + 4. Loads WTF/Account////SavedVariables/.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////SavedVariables/MinimapIcons.lua` exists on disk +5. Re-login, verify toggles persisted diff --git a/src/minimapicons/addon/MinimapIcons.lua b/src/minimapicons/addon/MinimapIcons.lua index a49dcf4..30cefd0 100644 --- a/src/minimapicons/addon/MinimapIcons.lua +++ b/src/minimapicons/addon/MinimapIcons.lua @@ -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() diff --git a/src/minimapicons/addon/MinimapIcons.toc b/src/minimapicons/addon/MinimapIcons.toc index 2e4cc19..7274d89 100644 --- a/src/minimapicons/addon/MinimapIcons.toc +++ b/src/minimapicons/addon/MinimapIcons.toc @@ -2,5 +2,6 @@ ## Title: MinimapIcons ## Notes: Minimap tracking spell dropdown ## Version: 1.0 +## SavedVariablesPerCharacter: UI_MinimapIcons MinimapIcons.lua \ No newline at end of file