# 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) -- __fastcall(ECX=mgr_ptr), RET -> ShutdownAddonSystem (0x0051fa40) -- clears old state -> ProcessAddonDirectory (0x0051c760) -- filesystem scan -> EnumerateDirectoryWithCallback("Interface\\AddOns\\", ...) -> ProcessAddonFilePath (0x0051c910) -- per .toc file found -> LoadAddonTOC (0x0051c9b0) -- parses TOC, registers addon in hash table -- reads TOC via LoadFileWithTextureResourceFallback (hookable!) -> [our hook: callLoadAddonTOC per non-hidden embedded addon] 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 via preloadFileWithFlags (does NOT go through our file hook) 3. Loads WTF/Account//SavedVariables/.lua (account-wide) 4. Loads WTF/Account////SavedVariables/.lua (per-character) 5. FireLuaEvent(0x1ad) -- ADDON_LOADED event -> [our hook: for hidden addons, callLoadFileListWithIncludes + callLoadUIBindingsFromFile] -> [our hook: for all addons with Bindings.xml, callLoadUIBindingsFromFile] -> SaveAddonVariables (0x0051f650) -- called from World_HandlePlayerLogin (0x00490bd0) on logout ``` ### Key Functions | Address | Name | Convention | Notes | |------------|----------------------------|------------|-------| | 0x0051c740 | SetupAddonProcessing | __fastcall(ECX=mgr_ptr), RET | Calls ShutdownAddonSystem + 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), RET | Parses TOC, registers addon in hash table | | 0x0051d410 | ProcessAddonInterface | __fastcall(ECX=addonData) | Reads .pub file for secure addons | | 0x0051d4c0 | ProcessAddonURL | __fastcall(ECX=addonData) | Reads .url file | | 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 | | 0x0064b3f0 | String_ComputeHash | **__stdcall(string)**, RET 0x4 | NOT fastcall! Reads from [EBP+8]. Returns hash in EAX | | 0x0051def0 | GetAddonCount | cdecl | Returns PTR_00be1b90 | | 0x0051df00 | GetAddonByIndex | __fastcall(ECX=index) | Returns PTR_00be1b94[index] | | 0x0051e3e0 | IsAddonSecure | __fastcall(ECX=name) | Returns addon[0x30] byte (from .pub parsing) | | 0x0051e470 | ValidateCommandAccess | __fastcall(ECX=name, EDX=acctName, stack=flag) | Checks addon enabled state | | 0x0051e780 | EnableAddonWithDependencies | __fastcall(ECX=name, ...) | Validates + enables addon | | 0x0051e980 | GetAddonSecurityString | __fastcall(ECX=state) | Returns PTR_s_SECURE_0085367c[state] | | 0x0051e990 | GetAddonState | __fastcall(ECX=name) | Returns addon[9] (+0x24) from hash table | ### 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) +0x24: u32 security state (0=SECURE, 1=INSECURE, 2=BANNED) +0x26-0x2f: hash table for Title/Notes/Author/Version metadata +0x28: u8 secure TOC flag (## Secure) -- triggers .pub validation +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 +0xC1: u8 .pub type byte (from ParseAddonInterfaceData) +0xC2: [256]u8 .pub key data (256 bytes) +0x1C2: [16]u8 MD5 hash for signature validation ``` ### Key Globals | Address | Name | Purpose | |------------|------|---------| | 0x00be1b60 | PTR_00be1b60 | Hash table control structure | | 0x00be1b64 | PTR_00be1b64 | Linked list base for traversal (node+4 offset) | | 0x00be1b6c | PTR_00be1b6c | Addon linked list head | | 0x00be1b7c | PTR_00be1b7c | Hash table buckets array | | 0x00be1b84 | PTR_00be1b84 | Hash table mask (0xffffffff = uninitialized) | | 0x00be1b90 | PTR_00be1b90 | Flat addon count (for GetAddonCount) | | 0x00be1b94 | PTR_00be1b94 | Flat addon array (for GetAddonByIndex) | | 0x00be1bd8 | PTR_00be1bd8 | Saved variables state list head | ### Hash Table Structure - Buckets at PTR_00be1b7c, mask at PTR_00be1b84 - Each bucket is 12 bytes: `[+0] next_offset, [+4] ?, [+8] list_head_ptr` - Node traversal: `next = *(node + bucket.next_offset + 4)` - Lookup: compute hash via `String_ComputeHash`, bucket = `mask & hash`, walk chain comparing hash then name ## Addon Security System ### Security States (+0x24) - **0 = SECURE** -- Blizzard addon, hidden from addon list, always loaded - **1 = INSECURE** -- Normal third-party addon, shown in list - **2 = BANNED** -- Failed signature validation, shown as "Banned" String table at `PTR_s_SECURE_0085367c` indexed by state value. ### Signature Validation Flow When `## Secure: 1` is in the TOC: 1. `LoadAddonTOC` sets `+0x28` secure flag, then calls `ProcessAddonInterface` + `ProcessAddonURL` 2. `ProcessAddonInterface` (0x0051d410) reads `Interface\AddOns\\.pub` via `LoadFileWithTextureResourceFallback` 3. `.pub` file must be exactly **257 bytes**: 1 byte type + 256 bytes public key 4. Stored at addon struct `+0xC1` (type) and `+0xC2` (key data) 5. Later in `LoadAddonRecursive`, MD5 hash of addon files is compared against signature 6. If no valid `.pub` or signature mismatch: security state set to **2 (BANNED)** ### Hiding Addons -- Approaches Tried **Approach 1: `## Secure: 1` in TOC** -- FAILED Sets secure flag but without a valid `.pub` file, `ProcessAddonInterface` runs and the addon ends up as BANNED (state 2). Cannot forge a valid `.pub` without Blizzard's private key. **Approach 2: Patch +0x24 after LoadAddonTOC** -- NOT VERIFIED After calling `LoadAddonTOC`, look up addon in hash table and write 0 to `+0x24`. Issue: `String_ComputeHash` is `__stdcall` NOT `__fastcall` -- initial crash was caused by calling it via `hook.fastcall` which passed the string pointer in ECX instead of on the stack. Fixed to use direct function pointer cast with `callconv(sc)`. The write logic itself was not verified to work at runtime before being removed. **Approach 3: Skip LoadAddonTOC for hidden addons** -- CURRENT Hidden addons (`hidden = true` in EmbedModule) are NOT registered via `LoadAddonTOC` at all. They don't appear in the addon hash table, so the game doesn't know about them. Their files are loaded directly via `callLoadFileListWithIncludes` in `loadAddonsDetour`, same as the original pre-SavedVariables approach. This means they are invisible in the addon list but also cannot have SavedVariables. ### Current Implementation Two-tier addon loading controlled by `EmbedModule.hidden`: **Visible addons** (`hidden = false`): - Registered via `LoadAddonTOC` in `setupAddonsDetour` (hook on SetupAddonProcessing) - Appear in charselect addon list as INSECURE (normal) - Game handles file loading, SavedVariables, ADDON_LOADED events - Bindings.xml loaded explicitly (preloadFileWithFlags bypass) **Hidden addons** (`hidden = true`): - NOT registered via `LoadAddonTOC` -- invisible in addon list - Files loaded directly via `callLoadFileListWithIncludes` in `loadAddonsDetour` - Bindings.xml loaded explicitly - No SavedVariables support (no addon hash table entry) | Addon | Hidden | |-------|--------| | WeirdUtils_Screenshot | yes | | WeirdUtils_Interact | no | | WeirdUtils_Outline | no | | WeirdUtils_WorldMarkers | yes | | WeirdUtils_LogSessions | no | | WeirdUtils_MinimapIcons | no | ### Future: Proper Addon Hiding To revisit Approach 2 (patching +0x24 to SECURE after registration), need to: 1. Verify `markAddonSecure` hash table traversal with debug logging 2. Confirm `hook.writeMem` actually writes to the addon struct 3. Check if `GetAddonState` is the only thing the UI reads, or if there are other security-related fields that also need patching 4. Investigate whether generating a valid `.pub` + MD5 signature is feasible (likely needs Blizzard's private key, but the validation algorithm is in-binary) ## Login Screen Addon List ### 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. #### 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 | ### Data Path - `GetAddonCount` (0x0051def0): returns `PTR_00be1b90` (simple count) - `GetAddonByIndex` (0x0051df00): returns `PTR_00be1b94[index]` (flat array of addon ptrs) These are a flat indexed view, separate from the hash table (PTR_00be1b7c) and linked list (PTR_00be1b6c). Registered addons appear in the list after `SetupAddonProcessing`. ### Open Questions 1. **When is PTR_00be1b90/PTR_00be1b94 populated?** Need to find xrefs to determine who writes to it and when relative to our hook. 2. **Enable/Disable persistence**: `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.