5 Commits
Author SHA1 Message Date
Brues 004eebc9f7 Coalesce macro/spell rebuilds to cut the login event storm
UPDATE_MACROS and SPELLS_CHANGED each did a full spell + talent + pet +
120-slot action-bar rebuild inline, and both fire several times during
login as the spellbook and macros populate - so the rebuild ran 4-6
times in the first ~2s, most of it duplicated work fanning 120 button
updates out to Blizzard/pfUI/Bongos each pass.

Defer the rebuild instead: UPDATE_MACROS, SPELLS_CHANGED and PLAYER_LOGIN
arm a 0.3s debounce (macroRebuildTime); the update loop runs RebuildMacros
once when the burst settles. RebuildMacros skips the action-bar pass until
`ready` (GetAction early-returns before then, and the +1.5s init timer
builds the bars once anyway), and BAG_UPDATE_DELAYED's bar rebuild is
gated on `ready` too. Net: IndexActionBars runs once at login (the
suppressed init-timer pass) instead of 4-6 times, IndexSpells ~2x instead
of ~5x. Runtime macro edits and bag changes still do a full unsuppressed
rebuild, just debounced.
2026-08-21 21:39:11 -05:00
Brues 63b484b1ea Replace WDB tooltip warmup with GET_ITEM_INFO_RECEIVED handler
ClassicAPI hooks the global GetItemInfo to auto-warm the item cache on a
miss and fires GET_ITEM_INFO_RECEIVED when the async fill lands, so the
tooltip-scan warmup is obsolete: IndexItems's own GetItemInfo calls
already trigger the same warmup, and owned items (bags + equipped) are
priority-prefetched by the engine. The old warmup also assumed the fill
was synchronous, which no longer holds.

Drop DoWDBWarmup and its login scheduling; instead listen for
GET_ITEM_INFO_RECEIVED and run a debounced re-index. IndexItems records
owned itemIDs it could not resolve into pendingItemInfo, and the handler
ignores any fill not in that set (quest DB scans, AH sweeps, chat-link
hovers, inspects) in O(1) so unrelated bursts do not cause reindex churn.
2026-08-21 21:02:59 -05:00
Brues cf47d8caba Use SetSpellByID for tooltip 2026-08-06 00:33:12 -05:00
Brues 71f8f74237 Support #showtooltip spell:<id> and item:<id>
GetSpell/GetItem now resolve explicit spell:/item: ID forms. spell:<id>
prefers the player's spellbook entry via FindSpellBookSlotByID (per-rank
and pet aware) for full cost/cooldown/usability, falling back to an
id-only entry rendered via SetSpellByID for spells not in the book.
item:<id> reuses the existing numeric lookup for location-aware tooltips.

Guard the id-only path against nil spellSlot/cost in TestForActiveAction
and GetActionCooldown so display-only spell references don't crash.

Remove the dead, no-op GetSpellSlotByID stub.
2026-08-06 00:15:19 -05:00
Brues 1a98a05b7e Remove redundant local 'i' declarations
Delete unnecessary local 'i' declarations in Extensions/Mouseover/pfUI.lua (ResolvePfUnit, RegisterPartyScripts, RegisterRaidScripts, RegisterRaidMarkScripts). The for-loop headers already provide a local loop variable, so the explicit locals were redundant and could shadow variables. No functional change.
2026-08-02 15:18:48 -05:00
4 changed files with 126 additions and 94 deletions
+85 -78
View File
@@ -970,7 +970,10 @@ function CleveRoids.TestForActiveAction(actions)
actions.active.oom = (manaToCheck < actions.active.spell.cost) actions.active.oom = (manaToCheck < actions.active.spell.cost)
end end
local start, duration = GetSpellCooldown(actions.active.spell.spellSlot, actions.active.spell.bookType) local start, duration = 0, 0
if actions.active.spell.spellSlot then
start, duration = GetSpellCooldown(actions.active.spell.spellSlot, actions.active.spell.bookType)
end
local onCooldown = (start > 0 and duration > 0) local onCooldown = (start > 0 and duration > 0)
if actions.active.isReactive then if actions.active.isReactive then
@@ -3901,6 +3904,17 @@ function CleveRoids.OnUpdate(self)
-- PERFORMANCE: Single GetTime() call per frame -- PERFORMANCE: Single GetTime() call per frame
local time = GetTime() local time = GetTime()
-- Coalesced macro/spell/action-bar rebuild (armed by UPDATE_MACROS,
-- SPELLS_CHANGED and PLAYER_LOGIN). Debounced so the login burst -
-- SPELLS_CHANGED fires several times as the spellbook populates - collapses
-- into a single rebuild. Runs even before `ready` so spells/talents are
-- indexed before the init timer's first action-bar pass; RebuildMacros
-- itself skips the 120-slot action-bar rebuild until ready.
if CR.macroRebuildTime and time >= CR.macroRebuildTime then
CR.macroRebuildTime = nil
CR.RebuildMacros()
end
-- PERFORMANCE: Early exit if not ready (before any other checks) -- PERFORMANCE: Early exit if not ready (before any other checks)
if not CR.ready then if not CR.ready then
-- Handle initialization timer only when not ready -- Handle initialization timer only when not ready
@@ -3939,10 +3953,21 @@ function CleveRoids.OnUpdate(self)
return return
end end
-- PERFORMANCE: Delayed WDB warmup after login (ensures GetItemInfo works after WDB clear) -- Coalesced re-index after async item data arrives (GET_ITEM_INFO_RECEIVED).
if CR.wdbWarmupTime and time >= CR.wdbWarmupTime then -- ClassicAPI warms the item cache asynchronously, so items that were still
CR.wdbWarmupTime = nil -- uncached during an earlier index pass land here once their
CR.DoWDBWarmup() -- SMSG_ITEM_QUERY_SINGLE response resolves. Bursts are debounced into one
-- re-index via CR.itemInfoReindexTime (armed by the event handler).
if CR.itemInfoReindexTime and time >= CR.itemInfoReindexTime then
CR.itemInfoReindexTime = nil
-- In combat: drop it; PLAYER_LEAVE_COMBAT does a full re-index once safe.
if not UnitAffectingCombat("player") then
CR.lastItemIndexTime = GetTime()
CR.IndexItems()
CR.Actions = {}
CR.Macros = {}
CR.IndexActionBars()
end
end end
-- PERFORMANCE: Cache refresh rate calculation (avoid per-frame division) -- PERFORMANCE: Cache refresh rate calculation (avoid per-frame division)
@@ -4189,11 +4214,7 @@ function GameTooltip.SetAction(self, slot)
local current_spell_data = CleveRoids.GetSpell(action_name) local current_spell_data = CleveRoids.GetSpell(action_name)
if current_spell_data and current_spell_data.id then if current_spell_data and current_spell_data.id then
if current_spell_data.spellSlot and current_spell_data.bookType then GameTooltip:SetSpellByID(current_spell_data.id)
GameTooltip:SetSpell(current_spell_data.spellSlot, current_spell_data.bookType)
else
GameTooltip:SetSpellByID(current_spell_data.id)
end
GameTooltip:Show() GameTooltip:Show()
return return
end end
@@ -4626,7 +4647,7 @@ function GetActionCooldown(slot)
return GetInventoryItemCooldown("player", slotId) return GetInventoryItemCooldown("player", slotId)
end end
if a.spell then if a.spell and a.spell.spellSlot then
return GetSpellCooldown(a.spell.spellSlot, a.spell.bookType) return GetSpellCooldown(a.spell.spellSlot, a.spell.bookType)
elseif a.item then elseif a.item then
if a.item.bagID and a.item.slot then if a.item.bagID and a.item.slot then
@@ -4825,6 +4846,7 @@ CleveRoids.Frame:RegisterEvent("UPDATE_MACROS")
CleveRoids.Frame:RegisterEvent("SPELLS_CHANGED") CleveRoids.Frame:RegisterEvent("SPELLS_CHANGED")
CleveRoids.Frame:RegisterEvent("ACTIONBAR_SLOT_CHANGED") CleveRoids.Frame:RegisterEvent("ACTIONBAR_SLOT_CHANGED")
CleveRoids.Frame:RegisterEvent("BAG_UPDATE_DELAYED") CleveRoids.Frame:RegisterEvent("BAG_UPDATE_DELAYED")
CleveRoids.Frame:RegisterEvent("GET_ITEM_INFO_RECEIVED")
CleveRoids.Frame:RegisterEvent("PLAYER_EQUIPMENT_CHANGED") CleveRoids.Frame:RegisterEvent("PLAYER_EQUIPMENT_CHANGED")
CleveRoids.Frame:RegisterEvent("UNIT_PET") CleveRoids.Frame:RegisterEvent("UNIT_PET")
@@ -4963,59 +4985,13 @@ function CleveRoids.Frame:PLAYER_LOGIN()
CleveRoids.IndexPetSpells() CleveRoids.IndexPetSpells()
CleveRoids.initializationTimer = GetTime() + 1.5 CleveRoids.initializationTimer = GetTime() + 1.5
-- Guarantee a full index (talents + macros + action bars) even if
-- UPDATE_MACROS / SPELLS_CHANGED happen not to fire before the init timer.
-- Coalesces with those events' arming; RebuildMacros runs once for the burst.
CleveRoids.macroRebuildTime = GetTime() + 0.3
-- PERFORMANCE: Initialize event-driven cache states -- PERFORMANCE: Initialize event-driven cache states
CleveRoids._cachedPlayerInCombat = UnitAffectingCombat("player") and true or false CleveRoids._cachedPlayerInCombat = UnitAffectingCombat("player") and true or false
-- Schedule delayed WDB warmup (loads items into client cache via tooltip scan)
-- This ensures GetItemInfo() works for all inventory items after a WDB clear
CleveRoids.wdbWarmupTime = GetTime() + 3.0 -- 3 second delay after login
end
-- PERFORMANCE: WDB warmup - tooltip scan all bag items to ensure they're cached
-- This prevents GetItemInfo() returning nil for items after a WDB clear
function CleveRoids.DoWDBWarmup()
if CleveRoids.wdbWarmupDone then return end
CleveRoids.wdbWarmupDone = true
-- Create a hidden tooltip for scanning if it doesn't exist
local tip = CleveRoidsWDBTip
if not tip then
tip = CreateFrame("GameTooltip", "CleveRoidsWDBTip", UIParent, "GameTooltipTemplate")
tip:SetOwner(WorldFrame, "ANCHOR_NONE")
end
local scanned = 0
-- Scan all bag slots
for bag = 0, 4 do
local slots = GetContainerNumSlots(bag) or 0
for slot = 1, slots do
if C_Container.GetContainerItemID(bag, slot) then
-- Tooltip scan loads the item into WDB
tip:ClearLines()
tip:SetBagItem(bag, slot)
scanned = scanned + 1
end
end
end
-- Scan equipped items
for slot = 1, 19 do
if GetInventoryItemID("player", slot) then
tip:ClearLines()
tip:SetInventoryItem("player", slot)
scanned = scanned + 1
end
end
-- Now trigger a full item index to populate the cache with valid data
if CleveRoids.IndexItems then
CleveRoids.IndexItems()
end
if CleveRoids.debug then
CleveRoids.Print("|cff88ff88[WDB Warmup]|r Scanned " .. scanned .. " items into cache")
end
end end
function CleveRoids.Frame:ADDON_LOADED(addon) function CleveRoids.Frame:ADDON_LOADED(addon)
@@ -5652,35 +5628,44 @@ function CleveRoids.Frame:PLAYER_TARGET_CHANGED()
end end
end end
function CleveRoids.Frame:UPDATE_MACROS() -- Full rebuild of the macro/spell/talent/action-bar index. Invoked from the
-- update loop when macroRebuildTime elapses (armed by UPDATE_MACROS /
-- SPELLS_CHANGED / PLAYER_LOGIN), never inline from an event - so a burst of
-- those events collapses into one rebuild.
function CleveRoids.RebuildMacros()
CleveRoids.currentSequence = nil CleveRoids.currentSequence = nil
-- Explicitly nil tables before re-assignment
CleveRoids.ParsedMsg = nil;
CleveRoids.ParsedMsg = {} CleveRoids.ParsedMsg = {}
CleveRoids.Macros = nil;
CleveRoids.Macros = {} CleveRoids.Macros = {}
CleveRoids.Actions = nil;
CleveRoids.Actions = {} CleveRoids.Actions = {}
CleveRoids.Sequences = nil;
CleveRoids.Sequences = {} CleveRoids.Sequences = {}
CleveRoids.IndexSpells() CleveRoids.IndexSpells()
CleveRoids.IndexTalents() CleveRoids.IndexTalents()
CleveRoids.IndexPetSpells() CleveRoids.IndexPetSpells()
CleveRoids.IndexActionBars()
-- Action-bar indexing is wasted before `ready` (GetAction early-returns
-- while not ready), and the +1.5s init timer rebuilds the bars once anyway.
-- Skipping it here keeps the pre-ready login rebuilds from fanning 120 slot
-- updates out to Blizzard/pfUI/Bongos buttons.
if CleveRoids.ready then
CleveRoids.IndexActionBars()
end
if CleveRoidMacros.realtime == 0 then if CleveRoidMacros.realtime == 0 then
CleveRoids.QueueActionUpdate() CleveRoids.QueueActionUpdate()
end end
end end
function CleveRoids.Frame:UPDATE_MACROS()
-- Debounce: collapse bursts (login, rapid macro edits) into one rebuild.
CleveRoids.macroRebuildTime = GetTime() + 0.3
end
function CleveRoids.Frame:SPELLS_CHANGED() function CleveRoids.Frame:SPELLS_CHANGED()
-- PERFORMANCE: Clear spell caches when spells change (learn new ranks, etc.) -- Clear spell caches immediately (cheap); defer the heavy rebuild.
CleveRoids.spellIdCache = {} CleveRoids.spellIdCache = {}
CleveRoids.spellNameCache = {} CleveRoids.spellNameCache = {}
CleveRoids.Frame:UPDATE_MACROS() CleveRoids.macroRebuildTime = GetTime() + 0.3
end end
function CleveRoids.Frame:ACTIONBAR_SLOT_CHANGED() function CleveRoids.Frame:ACTIONBAR_SLOT_CHANGED()
@@ -5691,6 +5676,23 @@ function CleveRoids.Frame:ACTIONBAR_SLOT_CHANGED()
end end
end end
-- ClassicAPI fires GET_ITEM_INFO_RECEIVED when an async item-cache fill lands
-- (the hooked GetItemInfo auto-warms on a miss). It fires for EVERY fill in the
-- game though - quest DB scans, AH sweeps, chat-link hovers, inspects - so we
-- ignore anything not in pendingItemInfo (items we own that missed the last
-- index pass). That check is O(1) and keeps unrelated bursts free.
function CleveRoids.Frame:GET_ITEM_INFO_RECEIVED()
local pending = CleveRoids.pendingItemInfo
if not (pending and pending[arg1]) then return end
-- Skip during combat; PLAYER_LEAVE_COMBAT re-indexes once it's safe.
if UnitAffectingCombat("player") then return end
-- Arm once per burst; the update loop clears it after re-indexing. Later
-- arrivals re-arm it, guaranteeing the final resolved state is captured.
if not CleveRoids.itemInfoReindexTime then
CleveRoids.itemInfoReindexTime = GetTime() + 0.5
end
end
function CleveRoids.Frame:BAG_UPDATE_DELAYED() function CleveRoids.Frame:BAG_UPDATE_DELAYED()
-- In combat: Skip expensive indexing but still queue icon update -- In combat: Skip expensive indexing but still queue icon update
-- so conditionals like [inbag] re-evaluate (they use live bag APIs) -- so conditionals like [inbag] re-evaluate (they use live bag APIs)
@@ -5707,10 +5709,15 @@ function CleveRoids.Frame:BAG_UPDATE_DELAYED()
CleveRoids.lastItemIndexTime = now CleveRoids.lastItemIndexTime = now
CleveRoids.IndexItems() CleveRoids.IndexItems()
-- Directly clear all relevant caches and force a UI refresh for all buttons. -- Rebuild action bars so item-dependent macro resolution refreshes.
CleveRoids.Actions = {} -- Skipped before `ready` (GetAction early-returns; the +1.5s init timer
CleveRoids.Macros = {} -- builds the bars), which keeps bag-fill events during login from firing
CleveRoids.IndexActionBars() -- a full 120-slot rebuild each time.
if CleveRoids.ready then
CleveRoids.Actions = {}
CleveRoids.Macros = {}
CleveRoids.IndexActionBars()
end
end end
-- Always queue icon update so conditionals like [inbag] re-evaluate -- Always queue icon update so conditionals like [inbag] re-evaluate
-4
View File
@@ -34,7 +34,6 @@ local function ResolvePfUnit(frame, fallbackName)
name = strlower(name) name = strlower(name)
local candidates = { "target", "targettarget", "player", "pet" } local candidates = { "target", "targettarget", "player", "pet" }
local i
for i = 1, 4 do for i = 1, 4 do
table.insert(candidates, "party"..i) table.insert(candidates, "party"..i)
table.insert(candidates, "partypet"..i) table.insert(candidates, "partypet"..i)
@@ -156,7 +155,6 @@ end
function Extension.RegisterPartyScripts() function Extension.RegisterPartyScripts()
if not pfUI or not pfUI.uf or not pfUI.uf.group then return end if not pfUI or not pfUI.uf or not pfUI.uf.group then return end
local i
for i = 0, 4 do for i = 0, 4 do
local frame = pfUI.uf.group[i] local frame = pfUI.uf.group[i]
if frame then if frame then
@@ -184,7 +182,6 @@ end
function Extension.RegisterRaidScripts() function Extension.RegisterRaidScripts()
if not pfUI or not pfUI.uf or not pfUI.uf.raid then return end if not pfUI or not pfUI.uf or not pfUI.uf.raid then return end
local i
for i = 1, 40 do for i = 1, 40 do
local frame = pfUI.uf.raid[i] local frame = pfUI.uf.raid[i]
if frame then if frame then
@@ -336,7 +333,6 @@ end
function Extension.RegisterRaidMarkScripts() function Extension.RegisterRaidMarkScripts()
if not pfUI or not pfUI.raidmarkers or not pfUI.raidmarkers.rows then return end if not pfUI or not pfUI.raidmarkers or not pfUI.raidmarkers.rows then return end
local i
for i = 1, 8 do for i = 1, 8 do
local row = pfUI.raidmarkers.rows[i] local row = pfUI.raidmarkers.rows[i]
if row then if row then
+41
View File
@@ -238,6 +238,14 @@ function CleveRoids.IndexItems()
local items = {} local items = {}
local NUM_BAG_SLOTS = NUM_BAG_SLOTS -- Upvalue for bag constant local NUM_BAG_SLOTS = NUM_BAG_SLOTS -- Upvalue for bag constant
-- Rebuilt each pass: itemIDs the player owns that GetItemInfo couldn't
-- resolve yet (cold cache under ClassicAPI's async warmup). The
-- GET_ITEM_INFO_RECEIVED handler consults this so it only re-indexes for
-- our own uncached items and ignores the flood of unrelated fills (quest
-- DB scans, AH, chat-link hovers, inspects) in O(1).
local pendingItemInfo = {}
CleveRoids.pendingItemInfo = pendingItemInfo
-- PERFORMANCE: Local function references -- PERFORMANCE: Local function references
local GetContainerNumSlots = GetContainerNumSlots local GetContainerNumSlots = GetContainerNumSlots
local GetContainerItemInfo = GetContainerItemInfo local GetContainerItemInfo = GetContainerItemInfo
@@ -281,6 +289,9 @@ function CleveRoids.IndexItems()
if lowerName ~= name then if lowerName ~= name then
items[lowerName] = name items[lowerName] = name
end end
else
-- Owned but not cached yet; wait for GET_ITEM_INFO_RECEIVED.
pendingItemInfo[itemID] = true
end end
end end
end end
@@ -318,6 +329,9 @@ function CleveRoids.IndexItems()
if lowerName ~= name then if lowerName ~= name then
items[lowerName] = name items[lowerName] = name
end end
else
-- Owned but not cached yet; wait for GET_ITEM_INFO_RECEIVED.
pendingItemInfo[itemID] = true
end end
end end
end end
@@ -421,6 +435,27 @@ end
function CleveRoids.GetSpell(text) function CleveRoids.GetSpell(text)
text = CleveRoids.Trim(text) text = CleveRoids.Trim(text)
-- Explicit "spell:<id>" form. If the player knows the spell, reuse its cached
-- spellbook entry (full cost/cooldown/usability). FindSpellBookSlotByID
-- (ClassicAPI) resolves per-rank IDs and pet spells natively and returns the
-- same bookType string CleveRoids.Spells is keyed by. If the spell is not in
-- either book, fall back to an id-only entry that SetAction renders via
-- SetSpellByID; cost=0 keeps the downstream active-action checks nil-safe.
local _, _, spellId = string.find(text, "^spell:(%d+)$")
if spellId then
spellId = tonumber(spellId)
local slot, book = FindSpellBookSlotByID(spellId)
if slot then
local name, rank = GetSpellInfo(slot, book)
local byName = name and CleveRoids.Spells[book] and CleveRoids.Spells[book][name]
if byName then
return (rank and rank ~= "" and byName[rank]) or byName.highest or byName
end
end
return { id = spellId, texture = C_Spell.GetSpellTexture(spellId) or CleveRoids.unknownTexture, cost = 0 }
end
local rs, _, rank = string.find(text, "[^%s]%((Rank %d+)%)$") local rs, _, rank = string.find(text, "[^%s]%((Rank %d+)%)$")
local name = rank and string.sub(text, 1, rs) or text local name = rank and string.sub(text, 1, rs) or text
@@ -508,6 +543,12 @@ end
function CleveRoids.GetItem(text) function CleveRoids.GetItem(text)
if not text or text == "" then return end if not text or text == "" then return end
-- Explicit "item:<id>" form: force resolution by item ID. Reuses the numeric
-- lookup below (equipped -> bags -> GetItemInfo), so location-aware tooltip
-- rendering (SetInventoryItem/SetBagItem) still applies when the player has it.
local _, _, prefixedId = string.find(text, "^item:(%d+)$")
if prefixedId then text = prefixedId end
local Items = CleveRoids.Items local Items = CleveRoids.Items
local item = Items[text] or Items[tostring(text)] local item = Items[text] or Items[tostring(text)]
if not item then if not item then
-12
View File
@@ -117,18 +117,6 @@ local cachedSpellDurations = {}
local spellDurationCacheTime = {} local spellDurationCacheTime = {}
local SPELL_CACHE_DURATION = 0.5 -- Re-scan every 0.5 seconds (haste can change mid-fight) local SPELL_CACHE_DURATION = 0.5 -- Re-scan every 0.5 seconds (haste can change mid-fight)
-- Get a spell's slot in the spellbook by spell ID
local function GetSpellSlotByID(targetSpellID)
local i = 1
while true do
local spellName = GetSpellName(i, BOOKTYPE_SPELL)
if not spellName then break end
-- Note: spell ID matching done via GetSpellTexture comparison if needed
i = i + 1
end
return nil, nil
end
-- Get a spell's slot in the spellbook by name (finds highest rank by default) -- Get a spell's slot in the spellbook by name (finds highest rank by default)
-- If targetRank is specified (e.g., "Rank 5"), finds that specific rank -- If targetRank is specified (e.g., "Rank 5"), finds that specific rank
local function GetSpellSlotByName(targetSpellName, targetRank) local function GetSpellSlotByName(targetSpellName, targetRank)