12 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
Brues 64e3f93042 Specifically disable pfUI mouseover hooks when unit attribute exists 2026-08-02 15:11:46 -05:00
Brues 0c86ed67e9 Revert "Drop redundant pfUI mouseover frame hooks; pfUI sets native mouseover"
This reverts commit c5bc69560e.
2026-08-02 15:07:35 -05:00
Brues 60c1b7f235 dont bother hooking into brues-code pfUI 2026-08-01 20:21:37 -05:00
Brues 8c10353567 removed useless GetSpellRecField checks 2026-07-30 20:41:23 -05:00
Brues fcd206756d Replace equipment cache with C_Item.IsEquippedItem
HasGearEquipped now wraps ClassicAPI's native slot-walk, which
short-circuits on first match. Removes BuildEquipmentCache, the
_equipped* cache tables/invalidation, and the InvalidateEquipmentCache
call from PLAYER_EQUIPMENT_CHANGED. [equipped] reads live engine state,
so no cache staleness surface remains; also gains item-link support.
2026-07-30 20:35:39 -05:00
Brues 8d86389a03 Remove dead IndexEquippedItems (no callers) 2026-07-30 20:28:39 -05:00
Brues 8c85ce572a Scope PLAYER_EQUIPMENT_CHANGED to the changed slot
Replace the full IndexItems + action-bar rebuild with a single-slot
IndexEquipSlot using the event's arg1 (slot) and arg2 (hasCurrent),
and drop the now-pointless throttle/deferral. Bag-side deltas remain
covered by BAG_UPDATE_DELAYED; [equipped] runs off its own cache.
2026-07-30 20:28:05 -05:00
5 changed files with 414 additions and 318 deletions
+5 -115
View File
@@ -197,102 +197,6 @@ local function IsSharedDebuffByIdOrName(lib, spellID, debuffName)
return false
end
-- PERFORMANCE: Equipment cache for HasGearEquipped (avoids 19-slot scan per call)
-- Invalidated on UNIT_INVENTORY_CHANGED via CleveRoids.InvalidateEquipmentCache()
-- Enhanced with Nampower v2.18+ GetEquippedItems when available
local _equippedItemIDs = {} -- [slot] = itemID (number)
local _equippedItemNames = {} -- [slot] = itemName (lowercase string)
local _equipmentCacheValid = false
-- Track if we've warned about GetEquippedItems errors (warn once per session)
local _getEquippedItemsErrorWarned = false
local function BuildEquipmentCache()
if _equipmentCacheValid then return end
_equipmentCacheValid = true
-- Clear old data
for i = 1, 19 do
_equippedItemIDs[i] = nil
_equippedItemNames[i] = nil
end
local string_find = string.find
local string_lower = string.lower
-- Try Nampower GetEquippedItems for faster enumeration
-- Requires v2.22+ because earlier versions (e.g., v2.19.1) have internal bug
local API = CleveRoids.NampowerAPI
local hasValidNampower = API and API.HasMinimumVersion and API.HasMinimumVersion(2, 22, 0)
if hasValidNampower and GetEquippedItems then
-- Use pcall to catch any internal Nampower errors and fall back gracefully
local success, result = pcall(GetEquippedItems, "player")
if not success then
-- Log the error once per session for debugging
if not _getEquippedItemsErrorWarned then
_getEquippedItemsErrorWarned = true
local errMsg = tostring(result)
if CleveRoids.Print then
CleveRoids.Print("|cffff6600Warning:|r GetEquippedItems failed: " .. errMsg)
CleveRoids.Print("Using fallback equipment detection. Consider updating Nampower.")
end
end
-- Fall through to manual enumeration
elseif result and type(result) == "table" then
local usedNampower = false
for nampowerSlot, itemInfo in pairs(result) do
-- Nampower uses 0-indexed slots, WoW API uses 1-indexed
-- tonumber() handles both string and numeric keys from different Nampower versions
-- Skip non-numeric keys (metadata fields, etc.)
local slotNum = tonumber(nampowerSlot)
if slotNum and type(itemInfo) == "table" and itemInfo.itemId then
local slot = slotNum + 1
-- itemInfo must be a table to access .itemId (userdata from some Nampower versions is not indexable)
if slot >= 1 and slot <= 19 then
_equippedItemIDs[slot] = itemInfo.itemId
usedNampower = true
-- Get item name via Nampower API or GetItemInfo
local itemName = API and API.GetItemName and API.GetItemName(itemInfo.itemId)
if not itemName then
itemName = GetItemInfo(itemInfo.itemId)
end
if itemName then
_equippedItemNames[slot] = string_lower(itemName)
end
end
end
end
if usedNampower then
return -- Done with Nampower path
end
-- Fall through to manual enumeration if Nampower returned userdata items
end
end
-- Fallback: manual slot enumeration via ClassicAPI (id + decorated name),
-- no link string built. C_Item.GetItemName carries random-suffix decoration
-- and falls back to the base name internally, so it replaces the old
-- bracket-name / GetItemInfo two-step in a single call.
for slot = 1, 19 do
local id = GetInventoryItemID("player", slot)
if id then
_equippedItemIDs[slot] = id
local name = C_Item.GetItemName({ equipmentSlotIndex = slot })
if name then
_equippedItemNames[slot] = string_lower(name)
end
end
end
end
-- Invalidate equipment cache (call on UNIT_INVENTORY_CHANGED)
function CleveRoids.InvalidateEquipmentCache()
_equipmentCacheValid = false
end
-- ============================================================================
-- PERFORMANCE: Unified item location lookup using CleveRoids.Items cache
-- Returns: { type="inventory"|"bag", inventoryID=N } or { type="bag", bag=N, slot=N }
@@ -2060,26 +1964,12 @@ function CleveRoids.CancelAura(auraName)
return false
end
-- ClassicAPI's C_Item.IsEquippedItem walks the 19 equipment slots natively and
-- short-circuits on the first match, so it replaces the old Lua-side equipment
-- cache entirely -- no per-call scan, no invalidation. Accepts itemID, item
-- link, or (case-insensitive, decorated) name.
function CleveRoids.HasGearEquipped(gearId)
if not gearId then return false end
-- PERFORMANCE: Build/refresh equipment cache if needed
BuildEquipmentCache()
-- Handle both numeric IDs and string IDs like "5196"
local wantId = tonumber(gearId)
local wantName = (type(gearId) == "string" and not wantId) and string.lower(gearId) or nil
-- PERFORMANCE: Use cached data instead of scanning all slots
for slot = 1, 19 do
if wantId and _equippedItemIDs[slot] == wantId then
return true
end
if wantName and _equippedItemNames[slot] == wantName then
return true
end
end
return false
return (gearId and C_Item.IsEquippedItem(gearId)) or false
end
+101 -127
View File
@@ -970,7 +970,10 @@ function CleveRoids.TestForActiveAction(actions)
actions.active.oom = (manaToCheck < actions.active.spell.cost)
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)
if actions.active.isReactive then
@@ -3901,6 +3904,17 @@ function CleveRoids.OnUpdate(self)
-- PERFORMANCE: Single GetTime() call per frame
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)
if not CR.ready then
-- Handle initialization timer only when not ready
@@ -3939,10 +3953,21 @@ function CleveRoids.OnUpdate(self)
return
end
-- PERFORMANCE: Delayed WDB warmup after login (ensures GetItemInfo works after WDB clear)
if CR.wdbWarmupTime and time >= CR.wdbWarmupTime then
CR.wdbWarmupTime = nil
CR.DoWDBWarmup()
-- Coalesced re-index after async item data arrives (GET_ITEM_INFO_RECEIVED).
-- ClassicAPI warms the item cache asynchronously, so items that were still
-- uncached during an earlier index pass land here once their
-- 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
-- PERFORMANCE: Cache refresh rate calculation (avoid per-frame division)
@@ -3974,25 +3999,6 @@ function CleveRoids.OnUpdate(self)
CR.EnsureSendChatMessageHook()
end
-- Process deferred equipment index updates (for throttled UNIT_INVENTORY_CHANGED)
-- PERFORMANCE: Skip check entirely if no pending update
local pendingTime = CR.equipIndexPendingTime
if pendingTime and not UnitAffectingCombat("player") then
if (time - (CR.lastEquipIndexTime or 0)) >= 0.2 then
CR.lastEquipIndexTime = time
CR.equipIndexPendingTime = nil
CR.lastItemIndexTime = time
CR.IndexItems()
CR.Actions = {}
CR.Macros = {}
CR.IndexActionBars()
if CRM.realtime == 0 then
CR.QueueActionUpdate()
end
end
end
-- PERFORMANCE: Check for expired reactive procs only if we have any
-- Use statically allocated removal buffer to avoid per-frame allocation
local reactiveProcs = CR.reactiveProcs
@@ -4208,11 +4214,7 @@ function GameTooltip.SetAction(self, slot)
local current_spell_data = CleveRoids.GetSpell(action_name)
if current_spell_data and current_spell_data.id then
if current_spell_data.spellSlot and current_spell_data.bookType then
GameTooltip:SetSpell(current_spell_data.spellSlot, current_spell_data.bookType)
else
GameTooltip:SetSpellByID(current_spell_data.id)
end
GameTooltip:SetSpellByID(current_spell_data.id)
GameTooltip:Show()
return
end
@@ -4645,7 +4647,7 @@ function GetActionCooldown(slot)
return GetInventoryItemCooldown("player", slotId)
end
if a.spell then
if a.spell and a.spell.spellSlot then
return GetSpellCooldown(a.spell.spellSlot, a.spell.bookType)
elseif a.item then
if a.item.bagID and a.item.slot then
@@ -4844,6 +4846,7 @@ CleveRoids.Frame:RegisterEvent("UPDATE_MACROS")
CleveRoids.Frame:RegisterEvent("SPELLS_CHANGED")
CleveRoids.Frame:RegisterEvent("ACTIONBAR_SLOT_CHANGED")
CleveRoids.Frame:RegisterEvent("BAG_UPDATE_DELAYED")
CleveRoids.Frame:RegisterEvent("GET_ITEM_INFO_RECEIVED")
CleveRoids.Frame:RegisterEvent("PLAYER_EQUIPMENT_CHANGED")
CleveRoids.Frame:RegisterEvent("UNIT_PET")
@@ -4982,59 +4985,13 @@ function CleveRoids.Frame:PLAYER_LOGIN()
CleveRoids.IndexPetSpells()
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
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
function CleveRoids.Frame:ADDON_LOADED(addon)
@@ -5671,35 +5628,44 @@ function CleveRoids.Frame:PLAYER_TARGET_CHANGED()
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
-- Explicitly nil tables before re-assignment
CleveRoids.ParsedMsg = nil;
CleveRoids.ParsedMsg = {}
CleveRoids.Macros = nil;
CleveRoids.Macros = {}
CleveRoids.Actions = nil;
CleveRoids.Actions = {}
CleveRoids.Sequences = nil;
CleveRoids.Sequences = {}
CleveRoids.IndexSpells()
CleveRoids.IndexTalents()
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
CleveRoids.QueueActionUpdate()
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()
-- PERFORMANCE: Clear spell caches when spells change (learn new ranks, etc.)
-- Clear spell caches immediately (cheap); defer the heavy rebuild.
CleveRoids.spellIdCache = {}
CleveRoids.spellNameCache = {}
CleveRoids.Frame:UPDATE_MACROS()
CleveRoids.macroRebuildTime = GetTime() + 0.3
end
function CleveRoids.Frame:ACTIONBAR_SLOT_CHANGED()
@@ -5710,6 +5676,23 @@ function CleveRoids.Frame:ACTIONBAR_SLOT_CHANGED()
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()
-- In combat: Skip expensive indexing but still queue icon update
-- so conditionals like [inbag] re-evaluate (they use live bag APIs)
@@ -5726,10 +5709,15 @@ function CleveRoids.Frame:BAG_UPDATE_DELAYED()
CleveRoids.lastItemIndexTime = now
CleveRoids.IndexItems()
-- Directly clear all relevant caches and force a UI refresh for all buttons.
CleveRoids.Actions = {}
CleveRoids.Macros = {}
CleveRoids.IndexActionBars()
-- Rebuild action bars so item-dependent macro resolution refreshes.
-- Skipped before `ready` (GetAction early-returns; the +1.5s init timer
-- builds the bars), which keeps bag-fill events during login from firing
-- a full 120-slot rebuild each time.
if CleveRoids.ready then
CleveRoids.Actions = {}
CleveRoids.Macros = {}
CleveRoids.IndexActionBars()
end
end
-- Always queue icon update so conditionals like [inbag] re-evaluate
@@ -5740,31 +5728,21 @@ function CleveRoids.Frame:BAG_UPDATE_DELAYED()
end
function CleveRoids.Frame:PLAYER_EQUIPMENT_CHANGED()
-- PERFORMANCE: Invalidate equipment cache for HasGearEquipped
if CleveRoids.InvalidateEquipmentCache then
CleveRoids.InvalidateEquipmentCache()
end
-- arg1 = inventory slot that changed, arg2 = hasCurrent (slot now holds an item)
local slot, hasCurrent = arg1, arg2
-- In combat: Skip ALL processing - EquipBagItem already handles cache invalidation
-- This eliminates lag from IndexEquippedItems during rapid gear swapping
-- In combat: skip the slot reindex to avoid lag during rapid gear swapping.
-- [equipped] reads live engine state (C_Item.IsEquippedItem) so it stays
-- correct regardless; the Items table self-heals on the next BAG_UPDATE.
if UnitAffectingCombat("player") then
return
end
-- Out of combat: Full indexing with throttle
local now = GetTime()
if (now - (CleveRoids.lastEquipIndexTime or 0)) < 0.2 then
CleveRoids.equipIndexPendingTime = now
return
end
CleveRoids.lastEquipIndexTime = now
CleveRoids.equipIndexPendingTime = nil
CleveRoids.lastItemIndexTime = now
CleveRoids.IndexItems()
CleveRoids.Actions = {}
CleveRoids.Macros = {}
CleveRoids.IndexActionBars()
-- Out of combat: reindex only the one slot that changed (the bag side of any
-- swap is covered by BAG_UPDATE_DELAYED). No action-bar rebuild is needed -
-- gear swaps don't change what's on the bars; QueueActionUpdate re-evaluates
-- [equipped:...] icons/conditionals.
CleveRoids.IndexEquipSlot(slot, hasCurrent)
if CleveRoidMacros.realtime == 0 then
CleveRoids.QueueActionUpdate()
@@ -5930,11 +5908,9 @@ function CleveRoids.Frame:SPELL_QUEUE_EVENT()
queueType = eventCode,
queueTime = GetTime()
}
if GetSpellRecField then
local name = C_Spell.GetSpellName(spellId)
if name then
CleveRoids.queuedSpell.spellName = name
end
local name = C_Spell.GetSpellName(spellId)
if name then
CleveRoids.queuedSpell.spellName = name
end
-- BUGFIX: Update casting state when spell is queued (for [casting] conditional)
if CleveRoids.UpdateCastingState then
@@ -5971,11 +5947,9 @@ function CleveRoids.Frame:SPELL_CAST_EVENT()
targetGuid = targetGuid,
timestamp = GetTime()
}
if GetSpellRecField then
local name = C_Spell.GetSpellName(spellId)
if name then
CleveRoids.lastCastSpell.spellName = name
end
local name = C_Spell.GetSpellName(spellId)
if name then
CleveRoids.lastCastSpell.spellName = name
end
-- Track pending cast for SPELL_GO correlation (reactive ability detection)
+249 -16
View File
@@ -2,18 +2,15 @@
Author: Dennis Werner Garske (DWG) / brian / Mewtiny
License: MIT License
pfUI integration. pfUI's own unitframes now set the native mouseover unit
(Nampower SetMouseoverUnit, via pfUI.uf.OnEnter bound in pfUI.uf:EnableScripts
on every unitframe), so [@mouseover]/[mouseover] resolve against pfUI frames
through the native "mouseover" token -- every consumer checks UnitExists(
"mouseover") before the CleveRoids.mouseoverUnit fallback, so no per-frame
hooking is needed here anymore. What remains is the two things pfUI doesn't
cover:
- Raid-marker rows: NOT unitframes (never go through EnableScripts), so pfUI
sets no mouseover for them. Hooked so hovering registers "mark1".."mark8"
via CleveRoids.mouseoverUnit.
- /pfcast: wrapped so its argument runs through CleveRoids conditionals.
Fixes pfUI mouseover issues by:
- Using a unique source key per pfUI frame (e.g., "pfui:party3", "pfui:raid7")
- Pairing Set/Clear with the same per-frame key
- Resolving a real UnitID when .unit isn't set
- Properly hooking party group[0] (your own party slot) with a safe closure and defaulting to "player"
]]
if pfPlayer and pfPlayer.GetAttribute and pfPlayer:GetAttribute('unit') == 'player' then return end
local _G = _G or getfenv(0)
local CleveRoids = _G.CleveRoids or {}
@@ -37,7 +34,6 @@ local function ResolvePfUnit(frame, fallbackName)
name = strlower(name)
local candidates = { "target", "targettarget", "player", "pet" }
local i
for i = 1, 4 do
table.insert(candidates, "party"..i)
table.insert(candidates, "partypet"..i)
@@ -101,15 +97,242 @@ local function PfClear(frame)
end
end
-- PLAYER
function Extension.RegisterPlayerScripts()
if not pfUI or not pfUI.uf or not pfUI.uf.player then return end
local frame = pfUI.uf.player
local onEnterFunc = frame:GetScript("OnEnter")
local onLeaveFunc = frame:GetScript("OnLeave")
frame:SetScript("OnEnter", function()
PfSet(this, "player")
if onEnterFunc then onEnterFunc(this) end
end)
frame:SetScript("OnLeave", function()
PfClear(this)
if onLeaveFunc then onLeaveFunc(this) end
end)
end
-- TARGET
function Extension.RegisterTargetScripts()
if not pfUI or not pfUI.uf or not pfUI.uf.target then return end
local frame = pfUI.uf.target
local onEnterFunc = frame:GetScript("OnEnter")
local onLeaveFunc = frame:GetScript("OnLeave")
frame:SetScript("OnEnter", function()
PfSet(this, "target")
if onEnterFunc then onEnterFunc(this) end
end)
frame:SetScript("OnLeave", function()
PfClear(this)
if onLeaveFunc then onLeaveFunc(this) end
end)
end
-- TARGETTARGET
function Extension.RegisterTargetTargetScripts()
if not pfUI or not pfUI.uf or not pfUI.uf.targettarget then return end
local frame = pfUI.uf.targettarget
local onEnterFunc = frame:GetScript("OnEnter")
local onLeaveFunc = frame:GetScript("OnLeave")
frame:SetScript("OnEnter", function()
PfSet(this, "targettarget")
if onEnterFunc then onEnterFunc(this) end
end)
frame:SetScript("OnLeave", function()
PfClear(this)
if onLeaveFunc then onLeaveFunc(this) end
end)
end
-- PARTY (pfUI.uf.group[0..4]) -- include 0 to cover your own party slot
function Extension.RegisterPartyScripts()
if not pfUI or not pfUI.uf or not pfUI.uf.group then return end
for i = 0, 4 do
local frame = pfUI.uf.group[i]
if frame then
-- bind loop index for closures (Vanilla-safe)
local idx = i
local onEnterFunc = frame:GetScript("OnEnter")
local onLeaveFunc = frame:GetScript("OnLeave")
frame:SetScript("OnEnter", function()
-- For group[0] (your own party frame), default to "player"
local defaultUnit = (idx == 0) and "player" or nil
PfSet(this, defaultUnit)
if onEnterFunc then onEnterFunc(this) end
end)
frame:SetScript("OnLeave", function()
PfClear(this)
if onLeaveFunc then onLeaveFunc(this) end
end)
end
end
end
-- RAID (pfUI.uf.raid[1..40])
function Extension.RegisterRaidScripts()
if not pfUI or not pfUI.uf or not pfUI.uf.raid then return end
for i = 1, 40 do
local frame = pfUI.uf.raid[i]
if frame then
local onEnterFunc = frame:GetScript("OnEnter")
local onLeaveFunc = frame:GetScript("OnLeave")
frame:SetScript("OnEnter", function()
PfSet(this)
if onEnterFunc then onEnterFunc(this) end
end)
frame:SetScript("OnLeave", function()
PfClear(this)
if onLeaveFunc then onLeaveFunc(this) end
end)
end
end
end
-- FOCUS
function Extension.RegisterFocusScripts()
if not pfUI or not pfUI.uf or not pfUI.uf.focus then return end
local frame = pfUI.uf.focus
local onEnterFunc = frame:GetScript("OnEnter")
local onLeaveFunc = frame:GetScript("OnLeave")
frame:SetScript("OnEnter", function()
PfSet(this) -- ResolvePfUnit handles focus emulation
if onEnterFunc then onEnterFunc(this) end
end)
frame:SetScript("OnLeave", function()
PfClear(this)
if onLeaveFunc then onLeaveFunc(this) end
end)
end
-- FOCUSTARGET (if your pfUI build provides it)
function Extension.RegisterFocusTargetScripts()
if not pfUI or not pfUI.uf or not pfUI.uf.focustarget then return end
local frame = pfUI.uf.focustarget
local onEnterFunc = frame:GetScript("OnEnter")
local onLeaveFunc = frame:GetScript("OnLeave")
frame:SetScript("OnEnter", function()
PfSet(this)
if onEnterFunc then onEnterFunc(this) end
end)
frame:SetScript("OnLeave", function()
PfClear(this)
if onLeaveFunc then onLeaveFunc(this) end
end)
end
-- PETTARGET (if your pfUI build provides it)
function Extension.RegisterPetTargetScripts()
if not pfUI or not pfUI.uf or not pfUI.uf.pettarget then return end
local frame = pfUI.uf.pettarget
local onEnterFunc = frame:GetScript("OnEnter")
local onLeaveFunc = frame:GetScript("OnLeave")
frame:SetScript("OnEnter", function()
PfSet(this)
if onEnterFunc then onEnterFunc(this) end
end)
frame:SetScript("OnLeave", function()
PfClear(this)
if onLeaveFunc then onLeaveFunc(this) end
end)
end
-- TARGETTARGETTARGET (if your pfUI build provides it)
function Extension.RegisterTargetTargetTargetScripts()
if not pfUI or not pfUI.uf or not pfUI.uf.targettargettarget then return end
local frame = pfUI.uf.targettargettarget
local onEnterFunc = frame:GetScript("OnEnter")
local onLeaveFunc = frame:GetScript("OnLeave")
frame:SetScript("OnEnter", function()
PfSet(this)
if onEnterFunc then onEnterFunc(this) end
end)
frame:SetScript("OnLeave", function()
PfClear(this)
if onLeaveFunc then onLeaveFunc(this) end
end)
end
-- PARTYTARGET (party1target..party4target, plus player's target)
function Extension.RegisterPartyTargetScripts()
if not pfUI or not pfUI.uf then return end
-- This helper function is used to hook any given frame.
local function hookFrame(frame, defaultUnit)
if not frame then return end
local onEnterFunc = frame:GetScript("OnEnter")
local onLeaveFunc = frame:GetScript("OnLeave")
frame:SetScript("OnEnter", function()
PfSet(this, defaultUnit)
-- We remove the call to the original onEnterFunc to prevent overwritten tooltips.
end)
frame:SetScript("OnLeave", function()
PfClear(this)
if onLeaveFunc then onLeaveFunc(this) end
end)
end
-- This helper function is specifically for party member targets (1-4)
local function hookPartyMemberTarget(i, frame)
if not frame then return end
local defaultUnit = "party" .. i .. "target"
hookFrame(frame, defaultUnit)
end
-- Case A: Hook dedicated arrays for party members 1-4
if pfUI.uf.grouptarget then
for i = 1, 4 do hookPartyMemberTarget(i, pfUI.uf.grouptarget[i]) end
end
if pfUI.uf.partytarget then
for i = 1, 4 do hookPartyMemberTarget(i, pfUI.uf.partytarget[i]) end
end
-- Case B: Hook child target frames for party members 1-4
if pfUI.uf.group then
for i = 1, 4 do
local g = pfUI.uf.group[i]
if g and g.target then hookPartyMemberTarget(i, g.target) end
end
end
--- START OF FIX to include party0target ---
-- Case C: Specifically find and hook the player's own target frame (group[0].target)
if pfUI.uf.group and pfUI.uf.group[0] and pfUI.uf.group[0].target then
-- The player's target UnitID is always "target", not "party0target".
hookFrame(pfUI.uf.group[0].target, "target")
end
--- END OF FIX ---
end
-- RAID MARKERS (pfUI raidmarkers module)
-- Rows are plain Buttons with label="mark" and id=1-8. They have no OnEnter/OnLeave
-- by default, so [mouseover] macros are blind to them. We hook each row so hovering
-- registers "mark1".."mark8" through the normal priority system. pfUI's own
-- SetMouseoverUnit path only covers unitframes, so this stays.
-- registers "mark1".."mark8" through the normal priority system.
function Extension.RegisterRaidMarkScripts()
if not pfUI or not pfUI.raidmarkers or not pfUI.raidmarkers.rows then return end
local i
for i = 1, 8 do
local row = pfUI.raidmarkers.rows[i]
if row then
@@ -147,7 +370,17 @@ function Extension.HookPfCast()
end
function Extension.PLAYER_ENTERING_WORLD()
if not pfUI then return end
if not pfUI or not pfUI.uf then return end
Extension.RegisterPlayerScripts()
Extension.RegisterTargetScripts()
Extension.RegisterTargetTargetScripts()
Extension.RegisterPartyScripts()
Extension.RegisterPartyTargetScripts()
Extension.RegisterRaidScripts()
Extension.RegisterFocusScripts()
Extension.RegisterFocusTargetScripts()
Extension.RegisterPetTargetScripts()
Extension.RegisterTargetTargetTargetScripts()
Extension.RegisterRaidMarkScripts()
Extension.HookPfCast()
end
+57 -42
View File
@@ -156,53 +156,27 @@ end
-- Lightweight equipment-only indexing for combat situations
-- Updates existing cache rather than rebuilding it
function CleveRoids.IndexEquippedItems()
local items = CleveRoids.Items or {}
for inventoryID = 1, 19 do
local itemID = GetInventoryItemID("player", inventoryID)
if itemID then
local name, link, _, _, itemType, itemSubType, _, _, texture = GetItemInfo(itemID)
if name then
local count = GetInventoryItemCount("player", inventoryID)
if not items[name] then
items[name] = {
inventoryID = inventoryID,
id = itemID,
name = name,
count = count,
texture = texture,
link = link,
}
items[itemID] = name
local lowerName = string.lower(name)
if lowerName ~= name then
items[lowerName] = name
end
else
-- Update existing entry with current equipment state
items[name].inventoryID = inventoryID
items[name].count = count
end
end
else
-- Slot is now empty - clear inventoryID from any item that was there
-- This is handled lazily by GetItem() fallback, so we skip expensive iteration
end
end
CleveRoids.lastGetItem = nil
CleveRoids.Items = items
end
-- PERFORMANCE: Index a single equipment slot instead of all 20
-- Use when we know exactly which slot changed (e.g., from EquipBagItem)
function CleveRoids.IndexEquipSlot(inventoryID)
-- Use when we know exactly which slot changed (e.g., from EquipBagItem or
-- PLAYER_EQUIPMENT_CHANGED). hasCurrent is the event's arg2 (does the slot now
-- hold an item); pass false to skip the API probe on a slot we know is empty.
function CleveRoids.IndexEquipSlot(inventoryID, hasCurrent)
if not inventoryID then return end
local items = CleveRoids.Items or {}
local itemID = GetInventoryItemID("player", inventoryID)
-- Clear any stale inventoryID still pointing at this slot: the item that was
-- here is now unequipped or swapped out (its real location comes from the
-- paired BAG_UPDATE rebuild). Equip changes are user-paced, so this table
-- scan is off the hot path.
for _, entry in pairs(items) do
if type(entry) == "table" and entry.inventoryID == inventoryID then
entry.inventoryID = nil
end
end
-- hasCurrent == false (arg2) => slot is now empty, nothing to add.
local itemID = hasCurrent ~= false and GetInventoryItemID("player", inventoryID)
if itemID then
local name, itemLink, _, _, itemType, itemSubType, _, _, texture = GetItemInfo(itemID)
if name then
@@ -264,6 +238,14 @@ function CleveRoids.IndexItems()
local items = {}
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
local GetContainerNumSlots = GetContainerNumSlots
local GetContainerItemInfo = GetContainerItemInfo
@@ -307,6 +289,9 @@ function CleveRoids.IndexItems()
if lowerName ~= name then
items[lowerName] = name
end
else
-- Owned but not cached yet; wait for GET_ITEM_INFO_RECEIVED.
pendingItemInfo[itemID] = true
end
end
end
@@ -344,6 +329,9 @@ function CleveRoids.IndexItems()
if lowerName ~= name then
items[lowerName] = name
end
else
-- Owned but not cached yet; wait for GET_ITEM_INFO_RECEIVED.
pendingItemInfo[itemID] = true
end
end
end
@@ -447,6 +435,27 @@ end
function CleveRoids.GetSpell(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 name = rank and string.sub(text, 1, rs) or text
@@ -534,6 +543,12 @@ end
function CleveRoids.GetItem(text)
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 item = Items[text] or Items[tostring(text)]
if not item then
+2 -18
View File
@@ -117,18 +117,6 @@ local cachedSpellDurations = {}
local spellDurationCacheTime = {}
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)
-- If targetRank is specified (e.g., "Rank 5"), finds that specific rank
local function GetSpellSlotByName(targetSpellName, targetRank)
@@ -247,7 +235,6 @@ function CleveRoids.GetChannelDurationFromTooltipByID(spellID)
end
-- Fallback: resolve name and scan tooltip
if not GetSpellRecField then return nil end
local spellName = C_Spell.GetSpellName(spellID)
if not spellName then return nil end
return CleveRoids.GetSpellDurationFromTooltip(spellName)
@@ -7105,10 +7092,7 @@ function CleveRoids.ApplySetBonusModifier(spellID, baseDuration)
local modifiedDuration = modifier.modifier(baseDuration)
if modifiedDuration ~= baseDuration and CleveRoids.debug then
local spellName = "Unknown"
if GetSpellRecField then
spellName = C_Spell.GetSpellName(spellID) or spellName
end
local spellName = C_Spell.GetSpellName(spellID) or "Unknown"
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff00ffff[Set Bonus Modifier]|r %s (ID:%d): %.1fs -> %.1fs (%d/%d pieces)",
spellName, spellID, baseDuration, modifiedDuration,
@@ -7585,7 +7569,7 @@ local function GetSpellSchool(spellName, spellID)
end
-- If we only have spellID but no name, try to get name from GetSpellRecField
if spellID and not spellName and GetSpellRecField then
if spellID and not spellName then
spellName = C_Spell.GetSpellName(spellID)
end