Files
coa-exporter/CoaExporter/Catalogs/Common.lua
T
florian.berthold 166eb1ec5e Common.lua: fall back to player's class when entry.Class is missing
Ascension's `C_CharacterAdvancement.GetEntryByInternalID` returns the
active character's *own* class entries with `Class` empty (the API
treats the player's class as implicit), while still returning `Class`
populated for every other class.

The entry-walk gate `if entry and entry.Class then ...` was therefore
silently dropping every talent of whatever class you ran the export
on. A Venomancer alt would correctly capture the 41 *other* classes'
talents/skills but produce zero rows for Prophet itself, leaving the
db.exil.es Venomancer page perpetually missing icons for Rotting Away,
Lure, Envenomed Weapons, and friends.

Cache UnitClass("player") at the start of C.Run and substitute it for
`entry.Class` whenever that field is missing or empty. The bucketing
key in `info.cls` now matches the localized class string (consistent
with how the other 41 classes show up in CoaExporterCatalog.talents).
2026-05-09 21:11:09 +02:00

240 lines
9.1 KiB
Lua

-- CoaExporter / Catalogs / Common.lua
--
-- Shared engine for catalog dumps. Walks
-- C_CharacterAdvancement.GetAllEntries() in 100-entry batches via OnUpdate
-- and dispatches each entry to every registered collector. Used by
-- Catalogs/Skills.lua and Catalogs/Talents.lua so we only walk the (large)
-- entry list once.
CoaExporter = _G.CoaExporter or {}
_G.CoaExporter = CoaExporter
local AE = CoaExporter
AE.Catalog = AE.Catalog or {}
local C = AE.Catalog
C._collectors = C._collectors or {}
C._isRunning = false
-- Shared SavedVariable for catalog output. Each collector writes its own
-- top-level key (skills, dispels, levelPassives, talents, ...).
CoaExporterCatalog = CoaExporterCatalog or {}
local TalentScanner
local function GetCatalogTooltip(spellId)
if not TalentScanner then
TalentScanner = CreateFrame("GameTooltip", "CoaExpCatalogScanner", nil, "GameTooltipTemplate")
end
local scanner = TalentScanner
scanner:SetOwner(UIParent, "ANCHOR_NONE")
scanner:ClearLines()
local ok = pcall(function() scanner:SetHyperlink("spell:" .. spellId) end)
if not ok or scanner:NumLines() == 0 then return "No description" end
local text = ""
for i = 1, scanner:NumLines() do
local line = _G["CoaExpCatalogScannerTextLeft"..i]
if line then
local t = line:GetText()
if t and t ~= "" then text = text .. t .. "\n" end
end
end
return text
end
C.GetCatalogTooltip = GetCatalogTooltip
local function log(msg)
DEFAULT_CHAT_FRAME:AddMessage("CoaExp catalog: " .. tostring(msg))
end
C._log = log
-- Collectors register a handler that gets called for each valid entry.
-- spec = {
-- name = "skills", -- identifier (also used as SV key)
-- onStart = function(ctx) end, -- reset state
-- onEntry = function(ctx, entry, info) end,
-- onFinish = function(ctx) end, -- write into CoaExporterCatalog[name]
-- }
function C.Register(spec)
assert(spec and spec.name, "catalog collector needs a name")
C._collectors[spec.name] = spec
end
local function activeCollectors(filter)
local out = {}
if filter == nil or filter == "all" then
for _, c in pairs(C._collectors) do out[#out+1] = c end
elseif type(filter) == "string" then
local one = C._collectors[filter]
if one then out[#out+1] = one end
end
table.sort(out, function(a, b) return (a.name or "") < (b.name or "") end)
return out
end
-- One-pass scan. filter: "all", "skills", "talents", or nil.
function C.Run(filter, callback)
if C._isRunning then
log("already running, ignoring")
return false
end
if not C_CharacterAdvancement or not C_CharacterAdvancement.GetAllEntries then
log("ERROR: C_CharacterAdvancement.GetAllEntries missing - are you on Ascension 3.3.5?")
return false
end
local collectors = activeCollectors(filter)
if #collectors == 0 then
log("no collectors matched filter: " .. tostring(filter))
return false
end
-- Ascension's GetEntryByInternalID returns the active character's
-- *own* class entries with `Class` unset (the API treats the player's
-- class as implicit). Cache the player's class so we can substitute
-- it when entry.Class is missing — otherwise an export from a
-- Venomancer alt would correctly capture the 41 *other* classes but
-- silently drop every Venomancer talent at the gate.
local playerLocalizedClass, playerClassFile = UnitClass("player")
playerLocalizedClass = playerLocalizedClass or "Unknown"
playerClassFile = playerClassFile or "UNKNOWN"
local ctx = {
startedAt = date(),
filter = filter or "all",
playerClass = playerLocalizedClass,
playerClassFile = playerClassFile,
}
for _, col in ipairs(collectors) do
if col.onStart then col.onStart(ctx) end
end
local allEntries = C_CharacterAdvancement.GetAllEntries()
local entryKeys = {}
for k in pairs(allEntries) do table.insert(entryKeys, k) end
local total = #entryKeys
local idx = 1
log(string.format(
"scanning %d entries for [%s] (active class: %s/%s)",
total, ctx.filter, playerLocalizedClass, playerClassFile))
C._isRunning = true
local frame = CreateFrame("Frame")
frame:SetScript("OnUpdate", function(self)
if not C._isRunning then
self:SetScript("OnUpdate", nil)
return
end
local batch = 100
local processed = 0
while processed < batch and idx <= total do
local k = entryKeys[idx]
local v = allEntries[k]
local entry
if type(v) == "table" and v.Class then
entry = v
else
local id = tonumber(k) or (type(v) == "table" and v.ID)
if id then entry = C_CharacterAdvancement.GetEntryByInternalID(id) end
end
-- Pre-relax: keep entries whose Class field is missing/empty.
-- They're typically the active character's own class (see the
-- comment near `playerLocalizedClass` above). We bucket them
-- under the player's class string further down.
local entryClass = entry and entry.Class
if entry and (not entryClass or entryClass == "") then
entryClass = playerLocalizedClass
end
if entry and entryClass and entryClass ~= "" then
-- Build a normalized info bag every collector can use.
local spellId = 0
local allSpells = {}
if entry.Spells then
if type(entry.Spells) == "table" then
spellId = tonumber(entry.Spells[1]) or 0
for _, sp in ipairs(entry.Spells) do
table.insert(allSpells, tonumber(sp) or 0)
end
else
spellId = tonumber(entry.Spells) or 0
table.insert(allSpells, spellId)
end
end
local name, _, icon
if spellId > 0 then name, _, icon = GetSpellInfo(spellId) end
local tooltip = GetCatalogTooltip(spellId > 0 and spellId or entry.ID)
if not name or name == "" then
name = tooltip:match("^([^\n]+)") or ("ID:" .. tostring(entry.ID))
end
local tierVal = tonumber(entry.PositionY) or tonumber(entry.Row) or tonumber(entry.y) or 0
local colVal = tonumber(entry.PositionX) or tonumber(entry.Column) or tonumber(entry.x) or tonumber(entry.Col) or 0
local entryIcon = (type(entry.Icon) == "string" and entry.Icon ~= "") and entry.Icon or ""
local hasAnyIcon = (icon and icon ~= "") or entryIcon ~= ""
-- Garbage filter: drop entries that have no spell/coords AND
-- no icon from either source (GetSpellInfo or entry.Icon).
-- entry.Icon catches spec-root talents whose only identity
-- is a server-resolved icon (e.g. Venomancer/Fortitude root).
local valid = true
if tierVal == 0 and colVal == 0 and not hasAnyIcon then valid = false end
if spellId == 0 and not hasAnyIcon then valid = false end
if valid then
local info = {
cls = tostring(entryClass),
tab = tostring(entry.Tab or "General"),
entryType = tostring(entry.Type or "Talent"),
name = name,
spellId = spellId,
allSpells = allSpells,
icon = icon or "",
entryIcon = entryIcon,
tier = tierVal,
col = colVal,
tooltip = tooltip,
}
for _, col in ipairs(collectors) do
if col.onEntry then col.onEntry(ctx, entry, info) end
end
end
end
idx = idx + 1
processed = processed + 1
end
if idx > total then
C._isRunning = false
self:SetScript("OnUpdate", nil)
for _, col in ipairs(collectors) do
if col.onFinish then col.onFinish(ctx) end
end
CoaExporterCatalog._meta = {
lastScanAt = ctx.startedAt,
filter = ctx.filter,
totalEntries = total,
}
log(string.format("done. %d entries processed; /reload to flush SavedVariables.", total))
if callback then callback(ctx) end
else
local pct = math.floor((idx / total) * 100)
if pct > 0 and pct % 10 == 0 and (ctx._lastPct or -1) ~= pct then
ctx._lastPct = pct
log(string.format(" %d%% (%d/%d)", pct, idx, total))
end
end
end)
return true
end
AE._loadedCatalogCommon = true