From 6badf0e7eaab373d802665facf4786171f2da44f Mon Sep 17 00:00:00 2001 From: Florian Berthold Date: Thu, 7 May 2026 12:08:23 +0200 Subject: [PATCH] Add per-character spellbook collector New Collectors/Spellbook.lua walks GetNumSpellTabs() and emits one entry per tab with name, texture, offset, numSpells, and a spells[] array of { slot, name, rank, spellID, icon }. 3.3.5/Ascension API differences are handled defensively: name comes from GetSpellBookItemInfo or vanilla GetSpellName; spellID is parsed out of GetSpellLink (3.3.5 doesn't return it from GetSpellBookItemInfo); icon falls back to GetSpellTexture if GetSpellInfo doesn't have it yet. Wiring: - CoaExporter.toc: load Collectors/Spellbook.lua after Talents - Core.lua: AssembleExport() includes spellbook for all + spellbook - Core.lua: HandleExport() accepts /coae export spellbook - Core.lua: debug output shows tab + spell counts - UI/ExportFrame.lua: "Spellbook" button in the Character section --- CoaExporter/CoaExporter.toc | 1 + CoaExporter/Collectors/Spellbook.lua | 103 +++++++++++++++++++++++++++ CoaExporter/Core.lua | 22 ++++-- CoaExporter/UI/ExportFrame.lua | 1 + README.md | 4 +- 5 files changed, 124 insertions(+), 7 deletions(-) create mode 100644 CoaExporter/Collectors/Spellbook.lua diff --git a/CoaExporter/CoaExporter.toc b/CoaExporter/CoaExporter.toc index 04e9f1c..d959055 100644 --- a/CoaExporter/CoaExporter.toc +++ b/CoaExporter/CoaExporter.toc @@ -9,6 +9,7 @@ Util\Json.lua Data\ScrollCatalog.lua Collectors\Talents.lua +Collectors\Spellbook.lua Collectors\Gear.lua Collectors\Enchants.lua Collectors\MysticScrolls.lua diff --git a/CoaExporter/Collectors/Spellbook.lua b/CoaExporter/Collectors/Spellbook.lua new file mode 100644 index 0000000..1ba8f79 --- /dev/null +++ b/CoaExporter/Collectors/Spellbook.lua @@ -0,0 +1,103 @@ +-- CoaExporter - Spellbook collector +-- +-- Walks the player's spell book (BOOKTYPE_SPELL) tab by tab and emits +-- name + rank + spellID + icon for every learned spell. Pet book is +-- skipped — it's not part of the build/guide use-case. +-- +-- 3.3.5 / Ascension API quirks handled defensively: +-- - GetSpellBookItemInfo(slot, "spell") returns (name, subName) on +-- vanilla 3.3.5 and (name, rank, [stuff]) on newer/private servers. +-- We treat the second return as a rank string regardless. +-- - GetSpellBookItemInfo doesn't return a spellID on 3.3.5. The reliable +-- way to get the ID is to parse it out of GetSpellLink(slot, "spell") +-- which yields "|cff71d5ff|Hspell:NNNN|h[Name]|h|r". +-- - Some entries can be category headers / flyouts; if no link or +-- spellID, we still record the name (it might be a known-spell that +-- just doesn't link, e.g. some passives). + +CoaExporter = _G.CoaExporter or {} +_G.CoaExporter = CoaExporter +local AE = CoaExporter + +local BOOKTYPE = (BOOKTYPE_SPELL or "spell") + +local function safeCall(fn, ...) + if type(fn) ~= "function" then return nil end + local ok, a, b, c = pcall(fn, ...) + if not ok then return nil end + return a, b, c +end + +local function spellIdFromLink(link) + if not link or link == "" then return 0 end + local id = string.match(link, "spell:(%d+)") + return tonumber(id) or 0 +end + +local function iconPath(icon) + if not icon or icon == "" then return "" end + local p = icon:match("Interface\\Icons\\(.+)") or icon + return tostring(p):lower() +end + +function AE.CollectSpellbook() + local out = { tabs = {}, totalSpells = 0 } + + if type(GetNumSpellTabs) ~= "function" then + out.error = "GetNumSpellTabs not available" + return out + end + + local numTabs = GetNumSpellTabs() or 0 + for tabIndex = 1, numTabs do + local tabName, tabTexture, offset, numSpells = safeCall(GetSpellTabInfo, tabIndex) + offset = offset or 0 + numSpells = numSpells or 0 + + local tab = { + index = tabIndex, + name = tabName or ("Tab " .. tabIndex), + texture = iconPath(tabTexture), + offset = offset, + numSpells = numSpells, + spells = {}, + } + + for slot = offset + 1, offset + numSpells do + local name, subOrRank = safeCall(GetSpellBookItemInfo, slot, BOOKTYPE) + if (not name or name == "") and type(GetSpellName) == "function" then + -- vanilla 3.3.5 fallback + name, subOrRank = safeCall(GetSpellName, slot, BOOKTYPE) + end + + local link = safeCall(GetSpellLink, slot, BOOKTYPE) + local spellID = spellIdFromLink(link) + + local icon + if spellID > 0 then + local _, _, ic = GetSpellInfo(spellID) + icon = ic + end + if (not icon) and type(GetSpellTexture) == "function" then + icon = safeCall(GetSpellTexture, slot, BOOKTYPE) + end + + if name and name ~= "" then + table.insert(tab.spells, { + slot = slot, + name = name, + rank = subOrRank or "", + spellID = spellID, + icon = iconPath(icon), + }) + out.totalSpells = out.totalSpells + 1 + end + end + + table.insert(out.tabs, tab) + end + + return out +end + +AE._loadedSpellbook = true diff --git a/CoaExporter/Core.lua b/CoaExporter/Core.lua index 158dbef..441ad5a 100644 --- a/CoaExporter/Core.lua +++ b/CoaExporter/Core.lua @@ -47,6 +47,9 @@ function AE:AssembleExport(which) if wantAll or which == "talents" then if AE.CollectTalents then out.talents = SafeCall(AE.CollectTalents) or {} end end + if wantAll or which == "spellbook" then + if AE.CollectSpellbook then out.spellbook = SafeCall(AE.CollectSpellbook) or {} end + end if wantAll or which == "gear" then if AE.CollectGear then out.gear = SafeCall(AE.CollectGear) or {} end end @@ -331,7 +334,7 @@ end local HELP = [[ CoaExporter commands: -/coae export all|talents|gear|enchants +/coae export all|talents|spellbook|gear|enchants /coae export mdgear|mdenchants|md (full wiki) /coae catalog all|skills|talents /coae catalog dispels [class]|passives [class]|status @@ -349,7 +352,7 @@ local function HandleExport(rest) AE:ShowExport(AE:GenerateMarkdownEnchants(), "CoaExporter - Markdown Enchants (Ctrl+C)") elseif rest == "md" or rest == "mdfull" or rest == "wiki" then AE:ShowExport(AE:GenerateMarkdownFull(), "CoaExporter - Wiki Markdown (Ctrl+C)") - elseif rest == "all" or rest == "talents" or rest == "gear" or rest == "enchants" then + elseif rest == "all" or rest == "talents" or rest == "spellbook" or rest == "gear" or rest == "enchants" then AE:Export(rest) else DEFAULT_CHAT_FRAME:AddMessage("CoaExporter: unknown export target '" .. tostring(rest) .. "'") @@ -442,10 +445,10 @@ local function HandleDebug() add(string.format("- AddOn: %s", tostring(ADDON_NAME))) add(string.format("- UI: %s", type(_G.CoaExporter_ShowExportFrame) == "function" and "yes" or "no")) add(string.format("- JSON: %s", type(_G.CoaExporter_Json_Encode) == "function" and "yes" or "no")) - add(string.format("- Loaded: talents=%s gear=%s enchants=%s scrolls=%s probe=%s catCommon=%s catSkills=%s catTalents=%s", - tostring(AE._loadedTalents or false), tostring(AE._loadedGear or false), - tostring(AE._loadedEnchants or false), tostring(AE._loadedMysticScrolls or false), - tostring(AE._loadedMysticScrollProbe or false), + add(string.format("- Loaded: talents=%s spellbook=%s gear=%s enchants=%s scrolls=%s probe=%s catCommon=%s catSkills=%s catTalents=%s", + tostring(AE._loadedTalents or false), tostring(AE._loadedSpellbook or false), + tostring(AE._loadedGear or false), tostring(AE._loadedEnchants or false), + tostring(AE._loadedMysticScrolls or false), tostring(AE._loadedMysticScrollProbe or false), tostring(AE._loadedCatalogCommon or false), tostring(AE._loadedCatalogSkills or false), tostring(AE._loadedCatalogTalents or false))) @@ -456,6 +459,13 @@ local function HandleDebug() else add("Talents: MISSING") end + if type(AE.CollectSpellbook) == "function" then + local sb = SafeCall(AE.CollectSpellbook) or {} + add(string.format("Spellbook: OK, tabs=%d, spells=%d", + #(sb.tabs or {}), sb.totalSpells or 0)) + else + add("Spellbook: MISSING") + end if type(AE.CollectGear) == "function" then local g = SafeCall(AE.CollectGear) or {} add(string.format("Gear: OK, slots=%d", #(g.slots or {}))) diff --git a/CoaExporter/UI/ExportFrame.lua b/CoaExporter/UI/ExportFrame.lua index 73046b1..b8293b1 100644 --- a/CoaExporter/UI/ExportFrame.lua +++ b/CoaExporter/UI/ExportFrame.lua @@ -28,6 +28,7 @@ local function buildSections() { title = "Character", items = { { "All", function() local ae = ce(); if ae then ae:Export("all") end end }, { "Talents", function() local ae = ce(); if ae then ae:Export("talents") end end }, + { "Spellbook", function() local ae = ce(); if ae then ae:Export("spellbook") end end }, { "Gear", function() local ae = ce(); if ae then ae:Export("gear") end end }, { "Enchants", function() local ae = ce(); if ae then ae:Export("enchants") end end }, }}, diff --git a/README.md b/README.md index 897131b..5d89281 100644 --- a/README.md +++ b/README.md @@ -44,8 +44,9 @@ Primary slash: `/coae`. Aliases: `/coaexp`, `/ascx`, `/asxc`. ### Per-character export ``` -/coae export all JSON of talents + gear + mystic enchants +/coae export all JSON of talents + spellbook + gear + mystic enchants /coae export talents JSON, talents only +/coae export spellbook JSON, full spellbook (per tab, with spellID + rank + icon) /coae export gear JSON, gear only /coae export enchants JSON, mystic enchants only @@ -127,6 +128,7 @@ CoaExporter/ ├── Data/ScrollCatalog.lua (auto-generated from AtlasLootAscension) ├── Collectors/ (per-character data) │ ├── Talents.lua +│ ├── Spellbook.lua │ ├── Gear.lua │ ├── Enchants.lua │ ├── MysticScrolls.lua