feat: add spell book tab API for SpellBookFrame addon compatibility

Implement GetNumSpellTabs, GetSpellTabInfo, GetSpellBookItemInfo, and
GetSpellBookItemName — the core functions SpellBookFrame.lua needs to
organize known spells into class skill line tabs.

Tabs are built lazily from knownSpells grouped by SkillLineAbility.dbc
mappings (category 7 = class). A "General" tab collects spells not in
any class skill line. Tabs auto-rebuild when the spell count changes.

Also adds SpellBookTab struct and getSpellBookTabs() to GameHandler.
This commit is contained in:
Kelsi 2026-03-22 15:40:40 -07:00
parent f29ebbdd71
commit 5086520354
3 changed files with 150 additions and 0 deletions

View file

@ -830,6 +830,14 @@ public:
void togglePetSpellAutocast(uint32_t spellId);
const std::unordered_set<uint32_t>& getKnownSpells() const { return knownSpells; }
// Spell book tabs — groups known spells by class skill line for Lua API
struct SpellBookTab {
std::string name;
std::string texture; // icon path
std::vector<uint32_t> spellIds; // spells in this tab
};
const std::vector<SpellBookTab>& getSpellBookTabs();
// ---- Pet Stable ----
struct StabledPet {
uint32_t petNumber = 0; // server-side pet number (used for unstable/swap)
@ -3443,6 +3451,8 @@ private:
std::unordered_map<uint32_t, std::string> skillLineNames_;
std::unordered_map<uint32_t, uint32_t> skillLineCategories_;
std::unordered_map<uint32_t, uint32_t> spellToSkillLine_; // spellID -> skillLineID
std::vector<SpellBookTab> spellBookTabs_;
bool spellBookTabsDirty_ = true;
bool skillLineDbcLoaded_ = false;
bool skillLineAbilityLoaded_ = false;
static constexpr size_t PLAYER_EXPLORED_ZONES_COUNT = 128;

View file

@ -1396,6 +1396,86 @@ static int lua_IsSpellKnown(lua_State* L) {
return 1;
}
// --- Spell Book Tab API ---
// GetNumSpellTabs() → count
static int lua_GetNumSpellTabs(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { lua_pushnumber(L, 0); return 1; }
lua_pushnumber(L, gh->getSpellBookTabs().size());
return 1;
}
// GetSpellTabInfo(tabIndex) → name, texture, offset, numSpells
// tabIndex is 1-based; offset is 1-based global spell book slot
static int lua_GetSpellTabInfo(lua_State* L) {
auto* gh = getGameHandler(L);
int tabIdx = static_cast<int>(luaL_checknumber(L, 1));
if (!gh || tabIdx < 1) {
lua_pushnil(L); return 1;
}
const auto& tabs = gh->getSpellBookTabs();
if (tabIdx > static_cast<int>(tabs.size())) {
lua_pushnil(L); return 1;
}
// Compute offset: sum of spells in all preceding tabs (1-based)
int offset = 0;
for (int i = 0; i < tabIdx - 1; ++i)
offset += static_cast<int>(tabs[i].spellIds.size());
const auto& tab = tabs[tabIdx - 1];
lua_pushstring(L, tab.name.c_str()); // name
lua_pushstring(L, tab.texture.c_str()); // texture
lua_pushnumber(L, offset); // offset (0-based for WoW compat)
lua_pushnumber(L, tab.spellIds.size()); // numSpells
return 4;
}
// GetSpellBookItemInfo(slot, bookType) → "SPELL", spellId
// slot is 1-based global spell book index
static int lua_GetSpellBookItemInfo(lua_State* L) {
auto* gh = getGameHandler(L);
int slot = static_cast<int>(luaL_checknumber(L, 1));
if (!gh || slot < 1) {
lua_pushstring(L, "SPELL");
lua_pushnumber(L, 0);
return 2;
}
const auto& tabs = gh->getSpellBookTabs();
int idx = slot; // 1-based
for (const auto& tab : tabs) {
if (idx <= static_cast<int>(tab.spellIds.size())) {
lua_pushstring(L, "SPELL");
lua_pushnumber(L, tab.spellIds[idx - 1]);
return 2;
}
idx -= static_cast<int>(tab.spellIds.size());
}
lua_pushstring(L, "SPELL");
lua_pushnumber(L, 0);
return 2;
}
// GetSpellBookItemName(slot, bookType) → name, subName
static int lua_GetSpellBookItemName(lua_State* L) {
auto* gh = getGameHandler(L);
int slot = static_cast<int>(luaL_checknumber(L, 1));
if (!gh || slot < 1) { lua_pushnil(L); return 1; }
const auto& tabs = gh->getSpellBookTabs();
int idx = slot;
for (const auto& tab : tabs) {
if (idx <= static_cast<int>(tab.spellIds.size())) {
uint32_t spellId = tab.spellIds[idx - 1];
const std::string& name = gh->getSpellName(spellId);
lua_pushstring(L, name.empty() ? "Unknown" : name.c_str());
lua_pushstring(L, ""); // subName/rank
return 2;
}
idx -= static_cast<int>(tab.spellIds.size());
}
lua_pushnil(L);
return 1;
}
static int lua_GetSpellCooldown(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { lua_pushnumber(L, 0); lua_pushnumber(L, 0); return 2; }
@ -3915,6 +3995,10 @@ void LuaEngine::registerCoreAPI() {
{"IsAddonMessagePrefixRegistered", lua_IsAddonMessagePrefixRegistered},
{"CastSpellByName", lua_CastSpellByName},
{"IsSpellKnown", lua_IsSpellKnown},
{"GetNumSpellTabs", lua_GetNumSpellTabs},
{"GetSpellTabInfo", lua_GetSpellTabInfo},
{"GetSpellBookItemInfo", lua_GetSpellBookItemInfo},
{"GetSpellBookItemName", lua_GetSpellBookItemName},
{"GetSpellCooldown", lua_GetSpellCooldown},
{"GetSpellPowerCost", lua_GetSpellPowerCost},
{"IsSpellInRange", lua_IsSpellInRange},

View file

@ -23033,6 +23033,62 @@ void GameHandler::loadSkillLineAbilityDbc() {
}
}
const std::vector<GameHandler::SpellBookTab>& GameHandler::getSpellBookTabs() {
// Rebuild when spell count changes (learns/unlearns)
static size_t lastSpellCount = 0;
if (lastSpellCount == knownSpells.size() && !spellBookTabsDirty_)
return spellBookTabs_;
lastSpellCount = knownSpells.size();
spellBookTabsDirty_ = false;
spellBookTabs_.clear();
static constexpr uint32_t SKILLLINE_CATEGORY_CLASS = 7;
// Group known spells by class skill line
std::map<uint32_t, std::vector<uint32_t>> bySkillLine;
std::vector<uint32_t> general;
for (uint32_t spellId : knownSpells) {
auto slIt = spellToSkillLine_.find(spellId);
if (slIt != spellToSkillLine_.end()) {
uint32_t skillLineId = slIt->second;
auto catIt = skillLineCategories_.find(skillLineId);
if (catIt != skillLineCategories_.end() && catIt->second == SKILLLINE_CATEGORY_CLASS) {
bySkillLine[skillLineId].push_back(spellId);
continue;
}
}
general.push_back(spellId);
}
// Sort spells within each group by name
auto byName = [this](uint32_t a, uint32_t b) {
return getSpellName(a) < getSpellName(b);
};
// "General" tab first (spells not in a class skill line)
if (!general.empty()) {
std::sort(general.begin(), general.end(), byName);
spellBookTabs_.push_back({"General", "Interface\\Icons\\INV_Misc_Book_09", std::move(general)});
}
// Class skill line tabs, sorted by name
std::vector<std::pair<std::string, std::vector<uint32_t>>> named;
for (auto& [skillLineId, spells] : bySkillLine) {
auto nameIt = skillLineNames_.find(skillLineId);
std::string tabName = (nameIt != skillLineNames_.end()) ? nameIt->second : "Unknown";
std::sort(spells.begin(), spells.end(), byName);
named.emplace_back(std::move(tabName), std::move(spells));
}
std::sort(named.begin(), named.end(), [](const auto& a, const auto& b) { return a.first < b.first; });
for (auto& [name, spells] : named) {
spellBookTabs_.push_back({std::move(name), "Interface\\Icons\\INV_Misc_Book_09", std::move(spells)});
}
return spellBookTabs_;
}
void GameHandler::categorizeTrainerSpells() {
trainerTabs_.clear();