Files
TotemBar/core/spellindex.lua
T
2026-08-17 15:19:36 +02:00

186 lines
7.3 KiB
Lua

-- TotemBar - core/spellindex.lua
-- A single cached name -> spellbook-slot index, replacing the four
-- separate linear GetSpellName(i, BOOKTYPE_SPELL) scans that used to be
-- duplicated across ui.lua (FindSpellIndexByName), core/cast.lua
-- (findSpellIndexByName), core/manacost.lua (findHighestRankSlot) and
-- core/known.lua (scanSpellbook/highestKnownRank). Each of those walked
-- the WHOLE spellbook on every call; SPELL_UPDATE_COOLDOWN/cast events
-- fire several of them per totem drop, so a 4-totem-drop macro press
-- could re-walk the book several times in one frame (perf review
-- 2026-07-16, TotemBar finding #14: ui.lua:210 + cast.lua:175 +
-- manacost.lua:116).
--
-- buildSpellIndex() below is PURE (no WoW API) and offline-tested: given
-- a flat array of {name=, rank=} entries in spellbook slot order (array
-- index N == what GetSpellName(N, BOOKTYPE_SPELL) returned for slot N),
-- it returns a name -> record table where each record is:
-- bookIndex - the FIRST (lowest) slot with this name. Mirrors the
-- old FindSpellIndexByName/findSpellIndexByName
-- first-match behavior (icon/cooldown/"is it known"
-- lookups don't care which rank's slot they read --
-- GetSpellTexture/GetSpellCooldown are identical across
-- ranks of the same totem).
-- maxRankIndex - the slot of the HIGHEST parsed rank. Mirrors the old
-- manacost.lua findHighestRankSlot: this is the slot
-- CastSpellByName actually casts and the one
-- tooltip/mana-cost scans must read. Falls back to the
-- LAST matching slot if no entry's rank string for this
-- name ever parsed a number (matches the old
-- fallback -- ranks are listed ascending, so the last
-- slot is still the best guess).
-- maxRank - the highest parsed rank NUMBER, or nil if none of
-- this name's entries carried a parseable rank. Mirrors
-- the old known.lua highestKnownRank() return value.
TotemBar = TotemBar or {}
function TotemBar.buildSpellIndex(entries)
local idx = {}
if not entries then
return idx
end
for i = 1, table.getn(entries) do
local e = entries[i]
local name = e and e.name
if name then
local rec = idx[name]
if not rec then
rec = { bookIndex = i, maxRankIndex = i, maxRank = nil }
idx[name] = rec
end
local _, _, numStr = string.find(e.rank or "", "(%d+)")
local num = numStr and tonumber(numStr)
if num and (not rec.maxRank or num > rec.maxRank) then
rec.maxRank = num
rec.maxRankIndex = i
elseif not rec.maxRank then
-- No rank number has parsed for this name in any entry
-- seen so far; keep sliding the fallback to the LAST
-- matching slot (mirrors the old findHighestRankSlot
-- fallback for names whose rank string never parses).
rec.maxRankIndex = i
end
end
end
return idx
end
-- ===== WoW-API layer (not offline-executed) =====
-- Guarded by CreateFrame's presence, same as manacost.lua's own event
-- block, so dofile-ing this module under plain Lua (offline tests)
-- never errors.
local cachedEntries = nil -- flat array {name=, rank=}, 1..n, book order
local cachedIndex = nil -- name -> {bookIndex, maxRankIndex, maxRank}
local function scanLiveSpellbook()
local entries = {}
local i = 1
while true do
local name, rank = GetSpellName(i, BOOKTYPE_SPELL)
if not name then
break
end
entries[i] = { name = name, rank = rank }
i = i + 1
end
return entries
end
local function ensureBuilt()
if cachedIndex then
return
end
cachedEntries = scanLiveSpellbook()
cachedIndex = TotemBar.buildSpellIndex(cachedEntries)
end
-- Drops the cache; the next accessor call below rebuilds it from a
-- fresh scan (lazy -- nothing re-scans until actually asked for). Wired
-- to SPELLS_CHANGED below; also safe to call manually.
function TotemBar.invalidateSpellIndex()
cachedEntries = nil
cachedIndex = nil
end
-- Returns the cached name -> record index, building it lazily on first
-- access. Lazy-build matters at login/reload: the spellbook
-- (GetSpellName/GetSpellTexture) isn't reliably populated as early as
-- ADDON_LOADED fires (see ui.lua's own SPELLS_CHANGED comment) -- an
-- eager build at file-load time could cache an empty book.
function TotemBar.getSpellIndex()
ensureBuilt()
return cachedIndex
end
-- How many spellbook slots the cached scan holds (0 when the book was empty
-- at scan time). Lets a caller tell "this spell is provably NOT known" apart
-- from "we have no usable spellbook data yet" -- the book isn't reliably
-- populated at login, and the cache is built lazily, so a nil findSpellIndex
-- alone can't distinguish the two. Used by core/cast.lua's recordCast.
function TotemBar.spellbookEntryCount()
ensureBuilt()
return table.getn(cachedEntries)
end
-- Mirrors old FindSpellIndexByName (ui.lua) / findSpellIndexByName
-- (cast.lua): first-match spellbook slot, or nil if unknown.
function TotemBar.findSpellIndex(name)
if not name then
return nil
end
local rec = TotemBar.getSpellIndex()[name]
return rec and rec.bookIndex
end
-- Mirrors old manacost.lua findHighestRankSlot: the slot of the
-- highest-known rank, or nil if unknown.
function TotemBar.findHighestRankSlot(name)
if not name then
return nil
end
local rec = TotemBar.getSpellIndex()[name]
return rec and rec.maxRankIndex
end
-- Mirrors old known.lua highestKnownRank: the highest known rank
-- NUMBER, or nil if unknown / no rank string ever parsed.
function TotemBar.highestKnownRank(name)
if not name then
return nil
end
local rec = TotemBar.getSpellIndex()[name]
return rec and rec.maxRank
end
-- Mirrors old known.lua scanSpellbook: flat array of every spellbook
-- entry's name, ONE PER SLOT (a multi-rank totem appears once per rank
-- it has) -- ShowFlyout's knownTotems() filter and PrintScan's raw dump
-- both rely on that shape. The by-name index above intentionally
-- collapses ranks together, so this stays a separate view over the same
-- cached scan rather than something derived from the index.
function TotemBar.scanSpellbook()
ensureBuilt()
local names = {}
for i = 1, table.getn(cachedEntries) do
names[i] = cachedEntries[i].name
end
return names
end
-- SPELLS_CHANGED is the event this codebase already relies on elsewhere
-- (manacost.lua's manaCache, ui.lua's icon refresh) for "spellbook
-- changed" -- fires on learning a new spell/rank and on talent changes.
-- LEARNED_SPELL_IN_TAB was checked and does not exist as a 1.12 client
-- event (not in this project's confirmed WoW 1.12 event list; it's a
-- later-expansion API) -- SPELLS_CHANGED alone is the correct signal.
if CreateFrame then
local siEvents = CreateFrame("Frame", "TotemBarSpellIndexEventFrame", UIParent)
siEvents:RegisterEvent("SPELLS_CHANGED")
siEvents:SetScript("OnEvent", function()
if event == "SPELLS_CHANGED" then
TotemBar.invalidateSpellIndex()
end
end)
end