207 lines
9.1 KiB
Lua
207 lines
9.1 KiB
Lua
-- 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
|