Files
coa-exporter/CoaExporter/Catalogs/Common.lua
T
florian.berthold a57bc77de6 Common.lua: surface playerClass in CoaExporterCatalog._meta
Diagnostic so we can tell whether the patched scan actually ran
(if _meta.playerClass is non-Unknown, /reload picked up the new file)
without having to inspect the chat log.
2026-05-09 21:19:24 +02:00

242 lines
9.2 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,
playerClass = ctx.playerClass,
playerClassFile = ctx.playerClassFile,
}
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