feat(catalog): wire up Icons collector (TOC + router); fix MysticScrollProbe nil crash
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
-- CoaExporter / Catalogs / Icons.lua
|
||||
--
|
||||
-- Captures the realm-server-resolved icon paths the MoA UI actually
|
||||
-- renders, which neither CharacterAdvancementData.json nor any client
|
||||
-- DBC carries:
|
||||
--
|
||||
-- 1. Per-entry icon (`entry.Icon`) — the talent-grid node icon. The
|
||||
-- Skills/Talents collectors above use GetSpellInfo's icon, which
|
||||
-- is empty for CoA-custom entries that don't back to a real spell
|
||||
-- (every spec-root Talent like ID 31202 "Fortitude Venomancer" =
|
||||
-- the Venomancer beetle node, plus ~3800 other CoA-custom rows).
|
||||
-- `entry.Icon` is what the realm gives the UI; we capture both.
|
||||
--
|
||||
-- 2. Per-(class, spec) sidebar tab icon — the small icon on each
|
||||
-- vertical spec tab in the Character Advancement window, fetched
|
||||
-- via `C_ClassInfo.GetSpecInfo(classFile, specFile).SpecFilename`.
|
||||
-- Same source the UI itself uses (see Ascension's
|
||||
-- Ascension_CharacterAdvancement/Templates/CAGate.lua:179).
|
||||
--
|
||||
-- Output (in CoaExporterCatalog):
|
||||
-- iconByEntryId = { [entryId] = "icon-name" }
|
||||
-- specInfoByFileString = { [<FILE_STRING>] = {
|
||||
-- [<TabName>] = { Name=..., SpecFilename=... } } }
|
||||
--
|
||||
-- Class-keyspace: every spec is keyed by the WoW classFile (UPPERCASE,
|
||||
-- the second return of UnitClass) like "PROPHET", "MONK", "FLESHWARDEN" —
|
||||
-- the same key C_ClassInfo.GetSpecInfo wants. Avoids the entry.Class →
|
||||
-- file_string alias mess (Venomancer → PROPHET, Templar → MONK, …) by
|
||||
-- never recording entry.Class as a primary key.
|
||||
--
|
||||
-- For sidebar icons we also brute-force `KNOWN_CLASS_FILES × KNOWN_TABS`
|
||||
-- regardless of what the entry-walk found this session — Ascension's
|
||||
-- C_CharacterAdvancement.GetAllEntries() is class-scoped (only returns
|
||||
-- entries the active character can reach), so a Venomancer alt may not
|
||||
-- see Templar entries and vice-versa. C_ClassInfo.GetSpecInfo is
|
||||
-- keyed (class, spec) directly though, so we can ask about every CoA
|
||||
-- combination without the entry-walk needing to have surfaced it.
|
||||
--
|
||||
-- iconByEntryId is still entry-walk gated (we need entry.ID + entry.Icon),
|
||||
-- so per-talent icons accumulate across runs as you rotate characters.
|
||||
|
||||
CoaExporter = _G.CoaExporter or {}
|
||||
_G.CoaExporter = CoaExporter
|
||||
local AE = CoaExporter
|
||||
local C = AE.Catalog
|
||||
|
||||
-- All file_strings in coa-db `class.file_string`. Vanilla 10 + DK + 21
|
||||
-- Ascension custom. The "HERO" pseudo-class is intentionally omitted —
|
||||
-- it's not a real class and GetSpecInfo wouldn't have data for it.
|
||||
local KNOWN_CLASS_FILES = {
|
||||
-- vanilla
|
||||
"WARRIOR", "PALADIN", "HUNTER", "ROGUE", "PRIEST",
|
||||
"DEATHKNIGHT", "SHAMAN", "MAGE", "WARLOCK", "DRUID",
|
||||
-- Ascension CoA custom (class.file_string)
|
||||
"BARBARIAN", "WITCHDOCTOR", "DEMONHUNTER", "WITCHHUNTER", "STORMBRINGER",
|
||||
"FLESHWARDEN", "GUARDIAN", "MONK", "SONOFARUGAL", "RANGER",
|
||||
"CHRONOMANCER", "NECROMANCER", "PYROMANCER", "CULTIST", "STARCALLER",
|
||||
"SUNCLERIC", "TINKER", "PROPHET", "REAPER", "WILDWALKER", "SPIRITMAGE",
|
||||
}
|
||||
|
||||
-- Every internal Tab name seen in CharacterAdvancementTabTypes.dbc
|
||||
-- (94 unique values). Hardcoding lets us probe specs that the current
|
||||
-- character's GetAllEntries doesn't surface — Venomancer's
|
||||
-- Fortitude/Stalking/Vizier/Venom in particular.
|
||||
local KNOWN_TABS = {
|
||||
"Affliction", "Ancestry", "Animation", "Arcane", "Archery", "Arms",
|
||||
"Assassination", "AstralWarfare", "Balance", "BeastMastery",
|
||||
"Blessings", "Blood", "Boltslinger", "Brewing", "Brutality", "Bulwark",
|
||||
"Combat", "Corruption", "Darkness", "Death", "Defiance", "Demonology",
|
||||
"Destruction", "Discipline", "Displacement", "Domination", "Draconic",
|
||||
"Duality", "Dueling", "Elemental", "Enhancement", "Felblood", "Feral",
|
||||
"Ferocity", "Fighting", "Fire", "Firearms", "Fleshweaver", "Fortitude",
|
||||
"Frost", "Fury", "Geomancy", "Gifts", "Gladiator", "Godblade",
|
||||
"Hellfire", "Holy", "Hydromancy", "Incineration", "Influence",
|
||||
"Inquisition", "Inspiration", "Invention", "Life", "Lightning",
|
||||
"Marksmanship", "Mechanics", "Moonbow", "MountainKing", "Packleader",
|
||||
"Piety", "Primal", "Protection", "Reaping", "Restoration",
|
||||
"Retribution", "Riftblade", "Rime", "Runes", "Runic", "Seraphim",
|
||||
"Shadow", "Shadowhunting", "Slaying", "Soul", "Stalking", "Subtlety",
|
||||
"Survival", "Tactics", "Tides", "Time", "Unholy", "Valkyr", "Venom",
|
||||
"Vizier", "Voodoo", "War", "Wind", "WitchKnight",
|
||||
}
|
||||
|
||||
-- Map entry.Class (the in-game advancement string) -> classFile, for the
|
||||
-- entry-walk stream. Mirrors load_moa_talents.py's CLASS_NAME_TO_FILE_STRING.
|
||||
-- Used only to bucket entry icons; the spec-info brute force iterates
|
||||
-- KNOWN_CLASS_FILES directly so this only needs to cover what we actually
|
||||
-- see in entry.Class strings.
|
||||
local CLASS_NAME_TO_FILE_STRING = {
|
||||
Warrior = "WARRIOR",
|
||||
Paladin = "PALADIN",
|
||||
Hunter = "HUNTER",
|
||||
Rogue = "ROGUE",
|
||||
Priest = "PRIEST",
|
||||
DeathKnight = "DEATHKNIGHT",
|
||||
Shaman = "SHAMAN",
|
||||
Mage = "MAGE",
|
||||
Warlock = "WARLOCK",
|
||||
Druid = "DRUID",
|
||||
Barbarian = "BARBARIAN",
|
||||
WitchDoctor = "WITCHDOCTOR",
|
||||
DemonHunter = "DEMONHUNTER",
|
||||
Felsworn = "DEMONHUNTER",
|
||||
WitchHunter = "WITCHHUNTER",
|
||||
Stormbringer = "STORMBRINGER",
|
||||
KnightOfXoroth = "FLESHWARDEN",
|
||||
Fleshwarden = "FLESHWARDEN",
|
||||
Guardian = "GUARDIAN",
|
||||
Monk = "MONK",
|
||||
Templar = "MONK",
|
||||
SonOfArugal = "SONOFARUGAL",
|
||||
Bloodmage = "SONOFARUGAL",
|
||||
Ranger = "RANGER",
|
||||
Chronomancer = "CHRONOMANCER",
|
||||
Necromancer = "NECROMANCER",
|
||||
Pyromancer = "PYROMANCER",
|
||||
Cultist = "CULTIST",
|
||||
Starcaller = "STARCALLER",
|
||||
SunCleric = "SUNCLERIC",
|
||||
Tinker = "TINKER",
|
||||
Prophet = "PROPHET",
|
||||
Venomancer = "PROPHET",
|
||||
Reaper = "REAPER",
|
||||
Wildwalker = "WILDWALKER",
|
||||
Primalist = "WILDWALKER",
|
||||
SpiritMage = "SPIRITMAGE",
|
||||
Runemaster = "SPIRITMAGE",
|
||||
}
|
||||
|
||||
local function tablen(t)
|
||||
if not t then return 0 end
|
||||
local n = 0
|
||||
for _ in pairs(t) do n = n + 1 end
|
||||
return n
|
||||
end
|
||||
|
||||
C.Register({
|
||||
name = "icons",
|
||||
|
||||
onStart = function(_)
|
||||
-- Don't wipe — accumulate across characters/runs so the user can
|
||||
-- /coae catalog icons after each Reborn class swap and end up with
|
||||
-- a complete catalog without re-collecting what was already done.
|
||||
CoaExporterCatalog.iconByEntryId = CoaExporterCatalog.iconByEntryId or {}
|
||||
CoaExporterCatalog.specInfoByFileString = CoaExporterCatalog.specInfoByFileString or {}
|
||||
-- Drop the legacy field if it exists from earlier runs — single
|
||||
-- source of truth keeps the loader simple.
|
||||
CoaExporterCatalog.specInfoByClassSpec = nil
|
||||
end,
|
||||
|
||||
onEntry = function(_, entry, info)
|
||||
local id = tonumber(entry.ID) or entry.ID
|
||||
local serverIcon = (type(entry.Icon) == "string" and entry.Icon ~= "") and entry.Icon or nil
|
||||
local fallback = (info.icon and info.icon ~= "") and info.icon or nil
|
||||
local pick = serverIcon or fallback
|
||||
if id and pick then
|
||||
CoaExporterCatalog.iconByEntryId[id] = pick
|
||||
end
|
||||
end,
|
||||
|
||||
onFinish = function(_)
|
||||
local resolved, attempted = 0, 0
|
||||
|
||||
if C_ClassInfo and C_ClassInfo.GetSpecInfo then
|
||||
for _, classFile in ipairs(KNOWN_CLASS_FILES) do
|
||||
for _, tab in ipairs(KNOWN_TABS) do
|
||||
attempted = attempted + 1
|
||||
local upperSpec = string.upper(tab)
|
||||
local ok, r = pcall(C_ClassInfo.GetSpecInfo, classFile, upperSpec)
|
||||
if ok and r and r.SpecFilename and r.SpecFilename ~= "" then
|
||||
CoaExporterCatalog.specInfoByFileString[classFile] =
|
||||
CoaExporterCatalog.specInfoByFileString[classFile] or {}
|
||||
CoaExporterCatalog.specInfoByFileString[classFile][tab] = {
|
||||
Name = r.Name or tab,
|
||||
SpecFilename = r.SpecFilename,
|
||||
}
|
||||
resolved = resolved + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local totalIcons = tablen(CoaExporterCatalog.iconByEntryId)
|
||||
local totalClasses, totalSpecs = 0, 0
|
||||
for _, tabs in pairs(CoaExporterCatalog.specInfoByFileString) do
|
||||
totalClasses = totalClasses + 1
|
||||
totalSpecs = totalSpecs + tablen(tabs)
|
||||
end
|
||||
|
||||
if C_ClassInfo and C_ClassInfo.GetSpecInfo then
|
||||
C._log(string.format(
|
||||
"icons: brute-forced %d (class,spec) probes, %d resolved this run; "
|
||||
.. "cumulative: %d entry icons, %d specs across %d classes",
|
||||
attempted, resolved, totalIcons, totalSpecs, totalClasses))
|
||||
else
|
||||
C._log(string.format(
|
||||
"icons: %d entry icons cumulatively (C_ClassInfo.GetSpecInfo unavailable - sidebar icons skipped)",
|
||||
totalIcons))
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
-- Exposed for the loader (load_moa_icons.py) to keep the entry.Class
|
||||
-- alias table in lockstep with this addon. Not used in-game.
|
||||
AE._iconsClassNameToFileString = CLASS_NAME_TO_FILE_STRING
|
||||
AE._loadedCatalogIcons = true
|
||||
@@ -18,6 +18,7 @@ Collectors\MysticScrollProbe.lua
|
||||
Catalogs\Common.lua
|
||||
Catalogs\Skills.lua
|
||||
Catalogs\Talents.lua
|
||||
Catalogs\Icons.lua
|
||||
|
||||
UI\ExportFrame.lua
|
||||
Core.lua
|
||||
|
||||
@@ -65,7 +65,7 @@ function AE.CollectMysticScrollProbe()
|
||||
slotData.spellID = spellID
|
||||
local name, _, icon = GetSpellInfo(spellID)
|
||||
slotData.name = name
|
||||
slotData.icon = icon and icon:match("Interface\\Icons\\(.+)"):lower() or nil
|
||||
slotData.icon = icon and (icon:match("Interface\\Icons\\(.+)") or icon):lower() or nil
|
||||
slotData.tooltip_lines = TipToLines(function(t)
|
||||
t:SetHyperlink("spell:" .. spellID)
|
||||
end)
|
||||
|
||||
@@ -378,7 +378,7 @@ CoaExporter commands:
|
||||
|
||||
/coae export all|talents|spellbook|gear|enchants
|
||||
/coae export mdgear|mdenchants|mdspellbook|md (full wiki)
|
||||
/coae catalog all|skills|talents
|
||||
/coae catalog all|skills|talents|icons
|
||||
/coae catalog dispels [class]|passives [class]|status
|
||||
/coae scrolls scan|export|reset|status
|
||||
/coae sv on|off (SavedVariables for character export)
|
||||
@@ -408,7 +408,7 @@ local function HandleCatalog(rest)
|
||||
local sub, arg = rest:match("^(%S+)%s*(.*)$")
|
||||
sub = sub or rest
|
||||
|
||||
if sub == "all" or sub == "skills" or sub == "talents" then
|
||||
if sub == "all" or sub == "skills" or sub == "talents" or sub == "icons" then
|
||||
if not AE.Catalog or not AE.Catalog.Run then
|
||||
DEFAULT_CHAT_FRAME:AddMessage("CoaExporter: catalog module not loaded")
|
||||
return
|
||||
@@ -432,7 +432,7 @@ local function HandleCatalog(rest)
|
||||
tostring(meta.lastScanAt or "never"), tostring(meta.filter or "-"),
|
||||
nSkills, nDispels, nPassives, nTalents))
|
||||
else
|
||||
DEFAULT_CHAT_FRAME:AddMessage("CoaExporter: /coae catalog [all|skills|talents|dispels|passives|status]")
|
||||
DEFAULT_CHAT_FRAME:AddMessage("CoaExporter: /coae catalog [all|skills|talents|icons|dispels|passives|status]")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -71,9 +71,12 @@ once per account; results persist across sessions and characters.
|
||||
### Game-data catalog (for db.exil.es / talent-calc)
|
||||
|
||||
```
|
||||
/coae catalog all One pass: dump skills + talents
|
||||
/coae catalog all One pass: dump skills + talents + icons
|
||||
/coae catalog skills Dump skills/dispels/passives only
|
||||
/coae catalog talents Dump talent-tree nodes only
|
||||
/coae catalog icons Dump server-resolved per-entry icons +
|
||||
per-(class, spec) sidebar tab icons via
|
||||
C_ClassInfo.GetSpecInfo (for db.exil.es)
|
||||
|
||||
/coae catalog dispels [class] Print dispel summary (or for one class)
|
||||
/coae catalog passives [class]Print level-passive summary (or for one class)
|
||||
@@ -117,7 +120,7 @@ SavedVariables
|
||||
| `CoaExporterSaved` | `/coae export …` when sv = on | Latest character export, keyed by `realm:name` |
|
||||
| `CoaExporterConfig` | `/coae sv on/off` | Just `enableSavedVariables` for now |
|
||||
| `CoaExporterScrollCache` | `/coae scrolls scan` | Mystic scroll item tooltips |
|
||||
| `CoaExporterCatalog` | `/coae catalog …` | `skills`, `dispels`, `levelPassives`, `talents`, `_meta` |
|
||||
| `CoaExporterCatalog` | `/coae catalog …` | `skills`, `dispels`, `levelPassives`, `talents`, `iconByEntryId`, `specInfoByClassSpec`, `_meta` |
|
||||
|
||||
Layout
|
||||
------
|
||||
@@ -137,7 +140,8 @@ CoaExporter/
|
||||
├── Catalogs/ (game-data dumps for the wiki/calc)
|
||||
│ ├── Common.lua (one entry-walk; fans out to collectors)
|
||||
│ ├── Skills.lua
|
||||
│ └── Talents.lua
|
||||
│ ├── Talents.lua
|
||||
│ └── Icons.lua (entry.Icon + C_ClassInfo.GetSpecInfo)
|
||||
├── UI/ExportFrame.lua
|
||||
└── Core.lua (slash router)
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user