feat(catalog): wire up Icons collector (TOC + router); fix MysticScrollProbe nil crash

This commit is contained in:
2026-05-29 10:43:54 +02:00
parent a57bc77de6
commit 7e5c58d1ca
5 changed files with 218 additions and 7 deletions
+206
View File
@@ -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
+1
View File
@@ -18,6 +18,7 @@ Collectors\MysticScrollProbe.lua
Catalogs\Common.lua
Catalogs\Skills.lua
Catalogs\Talents.lua
Catalogs\Icons.lua
UI\ExportFrame.lua
Core.lua
+1 -1
View File
@@ -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)
+3 -3
View File
@@ -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
+7 -3
View File
@@ -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)
```