Reorder minimap tracking categories and update addon system docs
This commit is contained in:
+112
-75
@@ -14,38 +14,52 @@ The addon's .lua code still runs (served via `loadFileDetour`), but it's loaded
|
||||
|
||||
```
|
||||
HandleLogin (0x0046afb0)
|
||||
-> SetupAddonProcessing (0x0051c740)
|
||||
-> ProcessAddonDirectory (0x0051c760)
|
||||
-> EnumerateDirectoryWithCallback("Interface\\AddOns\\", ...) -- filesystem scan
|
||||
-> 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 if exists
|
||||
2. Loads Bindings.xml via preloadFileWithFlags (does NOT go through our file hook)
|
||||
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
|
||||
-> [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 | ? | Calls ProcessAddonDirectory |
|
||||
| 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) | Parses TOC, registers addon. RET (no stack cleanup) |
|
||||
| 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 |
|
||||
| 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)
|
||||
|
||||
@@ -57,8 +71,9 @@ InitializeGameInterface (0x0048fbf0)
|
||||
+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 flag (## Secure)
|
||||
+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
|
||||
@@ -70,55 +85,104 @@ Dynamic arrays (each: capacity/count/data_ptr/growth at 3 dword intervals):
|
||||
+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 |
|
||||
|------------|------|---------|
|
||||
| 0x00be1b6c | PTR_00be1b6c | Addon linked list head |
|
||||
| 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 |
|
||||
| 0x00be1b60 | PTR_00be1b60 | Hash table control structure |
|
||||
|
||||
### Fix: Hook SetupAddonProcessing
|
||||
### 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
|
||||
|
||||
`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: WeirdUtils_MinimapIconsSettings`
|
||||
3. Register the addon in the hash table with the variable name at +0x78/7c/80
|
||||
## Addon Security System
|
||||
|
||||
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
|
||||
### 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"
|
||||
|
||||
**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.
|
||||
String table at `PTR_s_SECURE_0085367c` indexed by state value.
|
||||
|
||||
### Current Status (WORKING)
|
||||
### 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\<name>\<name>.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)**
|
||||
|
||||
- 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`)
|
||||
### Hiding Addons -- Approaches Tried
|
||||
|
||||
### Verification Plan
|
||||
**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.
|
||||
|
||||
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/WeirdUtils_MinimapIcons.lua` exists on disk
|
||||
5. Re-login, verify toggles persisted
|
||||
**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.
|
||||
|
||||
## Login Screen Addon List (WIP)
|
||||
**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.
|
||||
|
||||
### 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.
|
||||
### 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
|
||||
|
||||
@@ -126,13 +190,7 @@ 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.
|
||||
Both read from the **same** underlying addon data.
|
||||
|
||||
#### Glue Lua Functions (charselect screen)
|
||||
|
||||
@@ -150,39 +208,18 @@ view, likely populated after `ProcessAddonDirectory` finishes.
|
||||
| 0x0046d990 | SaveAddOns |
|
||||
| 0x0046d9a0 | ResetAddOns |
|
||||
|
||||
### Call Flow
|
||||
### Data Path
|
||||
- `GetAddonCount` (0x0051def0): returns `PTR_00be1b90` (simple count)
|
||||
- `GetAddonByIndex` (0x0051df00): returns `PTR_00be1b94[index]` (flat array of addon ptrs)
|
||||
|
||||
```
|
||||
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.
|
||||
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?** 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.
|
||||
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. **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
|
||||
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.
|
||||
|
||||
@@ -84,28 +84,28 @@ local AMMO_FILTERS = {
|
||||
-- Filtered entries take priority over unfiltered catch-all entries with the same flag.
|
||||
-- "dynamic" filter is resolved at runtime via getFilter().
|
||||
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.8, 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", default = 1 },
|
||||
{ name = "Auctioneer", trackingType = "auctioneer", icon = "Interface\\Minimap\\Tracking\\Auctioneer" },
|
||||
{ name = "Repair", trackingType = "repair", icon = "Interface\\Minimap\\Tracking\\Repair", 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 },
|
||||
{ name = "Profession Trainer", trackingType = "trainer", icon = "Interface\\Minimap\\Tracking\\Profession",
|
||||
getExclude = function() return UnitClass("player") end },
|
||||
{ name = "Brainwasher", trackingType = "brainwasher", icon = "Interface\\Minimap\\Tracking\\Brainwasher", scale = 1.8, default = 1 },
|
||||
{ name = "Innkeeper", trackingType = "innkeeper", icon = "Interface\\Minimap\\Tracking\\Innkeeper", default = 1 },
|
||||
{ name = "Flight Master", trackingType = "flightmaster", icon = "Interface\\Minimap\\Tracking\\FlightMaster" },
|
||||
{ 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", default = 1 },
|
||||
{ name = "Stable Master", trackingType = "stablemaster", icon = "Interface\\Minimap\\Tracking\\StableMaster" },
|
||||
{ name = "Poison Vendor", trackingType = "vendor", icon = "Interface\\Minimap\\Tracking\\Poison",
|
||||
getFilter = function() return getLocaleFilter(POISON_FILTERS) end },
|
||||
{ name = "Trade Goods", trackingType = "vendor", icon = "Interface\\Minimap\\Tracking\\Profession",
|
||||
getFilter = function() return getLocaleFilter(TRADE_FILTERS) end },
|
||||
{ name = "Ammunition", trackingType = "vendor", icon = "Interface\\Minimap\\Tracking\\Ammunition",
|
||||
getFilter = function() return getLocaleFilter(AMMO_FILTERS) end },
|
||||
{ name = "General Vendor", trackingType = "vendor", icon = "Interface\\Minimap\\Tracking\\Food" },
|
||||
{ name = "General Vendor", trackingType = "vendor", icon = "Interface\\Minimap\\Tracking\\Food" },
|
||||
{ name = "Class Trainer", trackingType = "trainer", icon = "Interface\\Minimap\\Tracking\\Class",
|
||||
getFilter = function() return (UnitClass("player")) end },
|
||||
{ name = "Profession Trainer", trackingType = "trainer", icon = "Interface\\Minimap\\Tracking\\Profession",
|
||||
getExclude = function() return UnitClass("player") end },
|
||||
{ name = "Battle Master", trackingType = "battlemaster", icon = "Interface\\Minimap\\Tracking\\BattleMaster" },
|
||||
{ name = "Stable Master", trackingType = "stablemaster", icon = "Interface\\Minimap\\Tracking\\StableMaster" },
|
||||
}
|
||||
|
||||
-- =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user