-- 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 = { [] = { -- [] = { 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