a57bc77de6
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.
242 lines
9.2 KiB
Lua
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
|