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
This commit is contained in:
2026-05-07 12:08:23 +02:00
parent cc5a87d2fb
commit 6badf0e7ea
5 changed files with 124 additions and 7 deletions
+1
View File
@@ -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
+103
View File
@@ -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
+16 -6
View File
@@ -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 {})))
+1
View File
@@ -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 },
}},
+3 -1
View File
@@ -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