@@ -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