166eb1ec5e
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).
240 lines
9.1 KiB
Lua
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
|