onupdate and onevent performance updates

This commit is contained in:
Jrc13245
2026-02-09 22:22:10 -05:00
parent 529f4d1e06
commit ec1075b9f4
4 changed files with 395 additions and 103 deletions
+188 -59
View File
@@ -524,16 +524,19 @@ local function AndNotEqualsNumber(t, target)
end
-- PERFORMANCE: Avoid creating wrapper tables for single values
-- PERFORMANCE: All helpers use next() instead of pairs() to avoid iterator closure allocation.
-- In Lua 5.0 (WoW 1.12), pairs() creates a closure each call. These helpers are invoked
-- for every conditional keyword on every macro line during action evaluation.
local function And(t, func)
if type(func) ~= "function" then return false end
-- PERFORMANCE: Handle non-table case without allocation
if type(t) ~= "table" then
return func(t) and true or false
end
for k, v in pairs(t) do
if not func(v) then
return false
end
local k, v = next(t)
while k do
if not func(v) then return false end
k, v = next(t, k)
end
return true
end
@@ -544,10 +547,10 @@ local function Or(t, func)
if type(t) ~= "table" then
return func(t) and true or false
end
for k, v in pairs(t) do
if func(v) then
return true
end
local k, v = next(t)
while k do
if func(v) then return true end
k, v = next(t, k)
end
return false
end
@@ -569,23 +572,28 @@ local function Multi(t, func, conditionals, condition)
if conditionals and conditionals._groups and conditionals._groups[condition] then
local groups = conditionals._groups[condition]
-- All groups must pass (AND between groups)
for _, group in ipairs(groups) do
-- PERFORMANCE: Index-based loop instead of ipairs() to avoid iterator closure
local numGroups = table.getn(groups)
for gi = 1, numGroups do
local group = groups[gi]
local groupPassed = false
local groupOp = group.operator or "OR"
local values = group.values
local numValues = table.getn(values)
if groupOp == "AND" then
-- AND within group: ALL values must match
groupPassed = true
for _, v in ipairs(group.values) do
if not func(v) then
for vi = 1, numValues do
if not func(values[vi]) then
groupPassed = false
break
end
end
else
-- OR within group: ANY value can match
for _, v in ipairs(group.values) do
if func(v) then
for vi = 1, numValues do
if func(values[vi]) then
groupPassed = true
break
end
@@ -606,14 +614,18 @@ local function Multi(t, func, conditionals, condition)
if operatorType == "AND" then
-- AND separator (&): ALL must match
for k, v in pairs(t) do
local k, v = next(t)
while k do
if not func(v) then return false end
k, v = next(t, k)
end
return true
else
-- OR separator (/) [default]: ANY can match
for k, v in pairs(t) do
local k, v = next(t)
while k do
if func(v) then return true end
k, v = next(t, k)
end
return false
end
@@ -637,15 +649,20 @@ local function NegatedMulti(t, func, conditionals, condition)
if conditionals and conditionals._groups and conditionals._groups[condition] then
local groups = conditionals._groups[condition]
-- All groups must pass (AND between groups)
for _, group in ipairs(groups) do
-- PERFORMANCE: Index-based loop instead of ipairs() to avoid iterator closure
local numGroups = table.getn(groups)
for gi = 1, numGroups do
local group = groups[gi]
local groupPassed = false
local groupOp = group.operator or "OR"
local values = group.values
local numValues = table.getn(values)
-- FLIPPED from positive conditionals (De Morgan's law for intuitive behavior)
if groupOp == "AND" then
-- AND separator (&) FLIPPED: ANY negation can pass (missing at least one)
for _, v in ipairs(group.values) do
if func(v) then
for vi = 1, numValues do
if func(values[vi]) then
groupPassed = true
break
end
@@ -653,8 +670,8 @@ local function NegatedMulti(t, func, conditionals, condition)
else
-- OR separator (/) FLIPPED: ALL negations must pass (missing all)
groupPassed = true
for _, v in ipairs(group.values) do
if not func(v) then
for vi = 1, numValues do
if not func(values[vi]) then
groupPassed = false
break
end
@@ -676,14 +693,18 @@ local function NegatedMulti(t, func, conditionals, condition)
-- FLIPPED from positive conditionals (De Morgan's law for intuitive behavior)
if operatorType == "AND" then
-- AND separator (&): ANY negation can pass (missing at least one)
for k, v in pairs(t) do
local k, v = next(t)
while k do
if func(v) then return true end
k, v = next(t, k)
end
return false
else
-- OR separator (/) [default]: ALL negations must pass (missing all)
for k, v in pairs(t) do
local k, v = next(t)
while k do
if not func(v) then return false end
k, v = next(t, k)
end
return true
end
@@ -804,6 +825,8 @@ CleveRoids.AuraCapStatus = {
playerLastUpdate = 0,
-- Target aura cap status (tracked per-GUID)
targetCapStatus = {}, -- [guid] = { buffCapped, debuffCapped, timestamp }
-- PERFORMANCE: Throttle cleanup to avoid O(n) scan on every aura event
lastCleanupTime = 0,
}
-- All-caster aura duration tracking (from AURA_CAST events)
@@ -968,11 +991,19 @@ local function OnAuraCastOther(spellId, casterGuid, targetGuid, effect, effectAu
if not CleveRoids.AllCasterAuraTracking[targetGuid] then
CleveRoids.AllCasterAuraTracking[targetGuid] = {}
end
CleveRoids.AllCasterAuraTracking[targetGuid][spellId] = {
start = now,
duration = durationMs / 1000, -- Convert ms to seconds
casterGuid = casterGuid,
}
-- PERFORMANCE: Reuse existing entry table when possible
local existing = CleveRoids.AllCasterAuraTracking[targetGuid][spellId]
if existing then
existing.start = now
existing.duration = durationMs / 1000
existing.casterGuid = casterGuid
else
CleveRoids.AllCasterAuraTracking[targetGuid][spellId] = {
start = now,
duration = durationMs / 1000, -- Convert ms to seconds
casterGuid = casterGuid,
}
end
-- Debug output when enabled
if CleveRoids.debug then
@@ -986,26 +1017,41 @@ local function OnAuraCastOther(spellId, casterGuid, targetGuid, effect, effectAu
-- Store cap status for this target GUID (if available)
if auraCapStatus then
CleveRoids.AuraCapStatus.targetCapStatus[targetGuid] = {
buffCapped = HasHitFlag(auraCapStatus, AURA_CAP_BUFF_FULL),
debuffCapped = HasHitFlag(auraCapStatus, AURA_CAP_DEBUFF_FULL),
timestamp = now,
}
-- PERFORMANCE: Reuse existing entry table when possible
local entry = CleveRoids.AuraCapStatus.targetCapStatus[targetGuid]
if not entry then
entry = {}
CleveRoids.AuraCapStatus.targetCapStatus[targetGuid] = entry
end
entry.buffCapped = HasHitFlag(auraCapStatus, AURA_CAP_BUFF_FULL)
entry.debuffCapped = HasHitFlag(auraCapStatus, AURA_CAP_DEBUFF_FULL)
entry.timestamp = now
end
-- Cleanup old entries (older than 60 seconds) to prevent memory leak
local count = 0
for guid, data in pairs(CleveRoids.AuraCapStatus.targetCapStatus) do
count = count + 1
-- PERFORMANCE: Throttle cleanup to every 5 seconds instead of every aura event
-- Previously this O(n) scan ran on EVERY aura application (10-50+/sec in raids)
if (now - CleveRoids.AuraCapStatus.lastCleanupTime) < 5 then return end
CleveRoids.AuraCapStatus.lastCleanupTime = now
-- Cleanup old cap status entries (older than 60 seconds)
local guid, data = next(CleveRoids.AuraCapStatus.targetCapStatus)
while guid do
local nextGuid = next(CleveRoids.AuraCapStatus.targetCapStatus, guid)
if now - data.timestamp > 60 then
CleveRoids.AuraCapStatus.targetCapStatus[guid] = nil
end
guid = nextGuid
data = nextGuid and CleveRoids.AuraCapStatus.targetCapStatus[nextGuid]
end
-- Cleanup old aura tracking entries
for guid, spells in pairs(CleveRoids.AllCasterAuraTracking) do
local tGuid, spells = next(CleveRoids.AllCasterAuraTracking)
while tGuid do
local nextTGuid = next(CleveRoids.AllCasterAuraTracking, tGuid)
local hasActive = false
for sid, auraData in pairs(spells) do
local sid, auraData = next(spells)
while sid do
local nextSid = next(spells, sid)
if auraData.start and auraData.duration then
local remaining = auraData.duration + auraData.start - now
if remaining <= 0 then
@@ -1014,11 +1060,14 @@ local function OnAuraCastOther(spellId, casterGuid, targetGuid, effect, effectAu
hasActive = true
end
end
sid = nextSid
auraData = nextSid and spells[nextSid]
end
-- Remove empty target entries
if not hasActive then
CleveRoids.AllCasterAuraTracking[guid] = nil
CleveRoids.AllCasterAuraTracking[tGuid] = nil
end
tGuid = nextTGuid
spells = nextTGuid and CleveRoids.AllCasterAuraTracking[nextTGuid]
end
end
@@ -1051,6 +1100,45 @@ autoAttackFrame:SetScript("OnEvent", function()
end
end
-- Check for aura duration update events (v2.30+)
if API and API.features and API.features.hasAuraDurationEvents then
this:RegisterEvent("BUFF_UPDATE_DURATION_SELF")
this:RegisterEvent("DEBUFF_UPDATE_DURATION_SELF")
end
elseif event == "BUFF_UPDATE_DURATION_SELF" or event == "DEBUFF_UPDATE_DURATION_SELF" then
-- v2.30+: Player aura duration was refreshed
-- arg1 = auraSlot (0-based raw slot index)
local auraSlot = arg1
if auraSlot and API and API.GetPlayerAuraDuration then
local spellId, durationMs, expirationTimeMs = API.GetPlayerAuraDuration(auraSlot)
if spellId and spellId > 0 and durationMs and durationMs > 0 then
local _, playerGUID = UnitExists("player")
if playerGUID then
if not CleveRoids.AllCasterAuraTracking[playerGUID] then
CleveRoids.AllCasterAuraTracking[playerGUID] = {}
end
local durationSec = durationMs / 1000
local now = GetTime()
local expirationSec = expirationTimeMs and expirationTimeMs / 1000
local startTime = expirationSec and (expirationSec - durationSec) or now
CleveRoids.AllCasterAuraTracking[playerGUID][spellId] = {
start = startTime,
duration = durationSec,
casterGuid = playerGUID,
}
if CleveRoids.debug then
local spellName = SpellInfo and SpellInfo(spellId) or "Unknown"
DEFAULT_CHAT_FRAME:AddMessage(string.format(
"|cff88ff88[AuraDurUpdate]|r %s (slot:%d, ID:%d) dur=%.1fs",
spellName, auraSlot, spellId, durationSec
))
end
end
end
end
elseif event == "AUTO_ATTACK_OTHER" then
-- arg1=attackerGuid, arg2=targetGuid, arg3=totalDamage, arg4=hitInfo, arg5=victimState,
-- arg6=subDamageCount, arg7=blockedAmount, arg8=totalAbsorb, arg9=totalResist
@@ -1181,6 +1269,15 @@ function CleveRoids.IsPlayerBuffCapped()
return CleveRoids.AuraCapStatus.playerBuffCapped
end
-- v2.30+ fast path: use raw slot counting via GetPlayerAuraDuration
local API = CleveRoids.NampowerAPI
if API then
local count = API.CountPlayerBuffSlots()
if count then
return count >= 32
end
end
-- Fallback: count player buffs manually
local count = 0
for i = 0, 31 do
@@ -1197,6 +1294,15 @@ function CleveRoids.IsPlayerDebuffCapped()
return CleveRoids.AuraCapStatus.playerDebuffCapped
end
-- v2.30+ fast path: use raw slot counting via GetPlayerAuraDuration
local API = CleveRoids.NampowerAPI
if API then
local count = API.CountPlayerDebuffSlots()
if count then
return count >= 16
end
end
-- Fallback: count player debuffs
local count = 0
for i = 0, 15 do
@@ -2303,17 +2409,25 @@ function CleveRoids.IsPlayerMoving()
end
-- Fallback: use continuously tracked position history from OnUpdate
-- Core.lua tracks position at ~100 Hz with 4-sample history (matches MonkeySpeed's 0.01s).
-- Core.lua tracks position at ~100 Hz with 4-sample circular buffer (matches MonkeySpeed's 0.01s).
-- Movement is detected if ANY consecutive pair of samples shows movement.
-- This gives instant start-of-movement detection (first sample shows delta).
local history = CleveRoids._positionHistory
local count = CleveRoids._posHistoryCount or 0
local headIdx = CleveRoids._posHistoryIndex or 0
if history and table.getn(history) >= 2 then
-- Check all consecutive pairs in history - moving if ANY shows movement
-- This detects start-of-movement immediately when first delta appears
for i = 2, table.getn(history) do
local prev = history[i - 1]
local curr = history[i]
if history and count >= 2 then
-- Iterate consecutive pairs in circular buffer order (oldest to newest)
-- Head is the most recent entry, walk backwards through the ring
local samplesAvail = count
for i = 1, samplesAvail - 1 do
-- prevIdx and currIdx walk from oldest to newest
local currOffset = samplesAvail - i -- e.g., 3,2,1,0 for count=4
local prevOffset = samplesAvail - i + 1 -- e.g., 4,3,2,1
local currIdx = ((headIdx - currOffset - 1) % 4) + 1
local prevIdx = ((headIdx - prevOffset - 1) % 4) + 1
local prev = history[prevIdx]
local curr = history[currIdx]
local dx = curr.x - prev.x
local dy = curr.y - prev.y
local dist = math.sqrt(dx * dx + dy * dy)
@@ -3923,14 +4037,25 @@ function CleveRoids.GetItemCooldown(item)
end
function CleveRoids.ValidatePlayerAuraCount(bigger, amount)
-- Count player buffs by iterating all 32 slots
-- (matches IsPlayerBuffCapped logic for consistency)
local count = 0
for i = 0, 31 do
if GetPlayerBuffTexture(GetPlayerBuff(i, "HELPFUL")) then
count = count + 1
-- Count player buffs
local count
-- v2.30+ fast path: use raw slot counting via GetPlayerAuraDuration
local API = CleveRoids.NampowerAPI
if API then
count = API.CountPlayerBuffSlots()
end
-- Fallback: iterate all 32 slots manually
if not count then
count = 0
for i = 0, 31 do
if GetPlayerBuffTexture(GetPlayerBuff(i, "HELPFUL")) then
count = count + 1
end
end
end
if bigger == 0 then
return count < tonumber(amount)
else
@@ -4670,17 +4795,19 @@ CleveRoids.Keywords = {
end,
equipped = function(conditionals)
local itemsToCheck = {}
-- PERFORMANCE: Or()/And() handle non-table values natively, so pass strings directly
-- instead of wrapping in a temporary table (avoids allocation per evaluation)
local itemsToCheck
-- Case 1: conditionals.equipped is a string (e.g., [equipped]ItemName)
if type(conditionals.equipped) == "string" then
table.insert(itemsToCheck, conditionals.equipped)
itemsToCheck = conditionals.equipped
-- Case 2: conditionals.equipped is a table (e.g., [equipped:Shields])
elseif type(conditionals.equipped) == "table" and table.getn(conditionals.equipped) > 0 then
itemsToCheck = conditionals.equipped
-- Case 3: No value provided, check the action
elseif conditionals.action then
table.insert(itemsToCheck, conditionals.action)
itemsToCheck = conditionals.action
else
return false
end
@@ -4692,17 +4819,19 @@ CleveRoids.Keywords = {
end,
noequipped = function(conditionals)
local itemsToCheck = {}
-- PERFORMANCE: Or()/And() handle non-table values natively, so pass strings directly
-- instead of wrapping in a temporary table (avoids allocation per evaluation)
local itemsToCheck
-- Case 1: conditionals.noequipped is a string (e.g., [noequipped]ItemName)
if type(conditionals.noequipped) == "string" then
table.insert(itemsToCheck, conditionals.noequipped)
itemsToCheck = conditionals.noequipped
-- Case 2: conditionals.noequipped is a table (e.g., [noequipped:Shields])
elseif type(conditionals.noequipped) == "table" and table.getn(conditionals.noequipped) > 0 then
itemsToCheck = conditionals.noequipped
-- Case 3: No value provided, check the action
elseif conditionals.action then
table.insert(itemsToCheck, conditionals.action)
itemsToCheck = conditionals.action
else
return false
end
+33 -18
View File
@@ -3797,10 +3797,9 @@ function CleveRoids.OnUpdate(self)
-- =========================================================================
-- CONTINUOUS POSITION TRACKING (for [moving] fallback without MonkeySpeed)
-- =========================================================================
-- Track player position at ~100 Hz with 4-sample history for snappy movement detection.
-- Track player position at ~100 Hz with 4-sample circular buffer for snappy movement detection.
-- Matches MonkeySpeed's 0.01s detection interval for similar responsiveness.
-- This provides instant start-of-movement detection and quick stop detection.
-- Without this, [moving] only updates when macros execute (stale data).
-- PERFORMANCE: Pre-allocated circular buffer eliminates ~100 table allocs/sec and table.remove shifting.
local posTrackInterval = 0.01 -- 10ms between position samples (100 Hz, matches MonkeySpeed)
local lastPosTime = CleveRoids._positionTrackTime or 0
@@ -3810,24 +3809,40 @@ function CleveRoids.OnUpdate(self)
if UnitPosition then
local x, y = UnitPosition("player")
if x and y then
-- Initialize history buffer if needed
if not CleveRoids._positionHistory then
CleveRoids._positionHistory = {}
end
-- Add new sample to history
-- Initialize circular buffer if needed (pre-allocate 4 entry tables)
local history = CleveRoids._positionHistory
table.insert(history, { x = x, y = y, time = time })
-- Keep only last 4 samples (40ms window at 10ms intervals)
while table.getn(history) > 4 do
table.remove(history, 1)
if not history then
history = {
{ x = 0, y = 0, time = 0 },
{ x = 0, y = 0, time = 0 },
{ x = 0, y = 0, time = 0 },
{ x = 0, y = 0, time = 0 },
}
CleveRoids._positionHistory = history
CleveRoids._posHistoryIndex = 0
CleveRoids._posHistoryCount = 0
end
-- Also maintain legacy vars for any code that uses them directly
if table.getn(history) >= 2 then
CleveRoids._previousPlayerPos = history[table.getn(history) - 1]
CleveRoids._currentPlayerPos = history[table.getn(history)]
-- Advance circular index (1-based, wraps at 4)
local idx = (CleveRoids._posHistoryIndex % 4) + 1
CleveRoids._posHistoryIndex = idx
-- Reuse existing entry (zero allocation)
local entry = history[idx]
entry.x = x
entry.y = y
entry.time = time
-- Track how many samples we have (caps at 4)
if CleveRoids._posHistoryCount < 4 then
CleveRoids._posHistoryCount = CleveRoids._posHistoryCount + 1
end
-- Maintain legacy vars for any code that uses them directly
if CleveRoids._posHistoryCount >= 2 then
local prevIdx = ((idx - 2) % 4) + 1
CleveRoids._previousPlayerPos = history[prevIdx]
CleveRoids._currentPlayerPos = entry
end
end
end
+94 -2
View File
@@ -58,11 +58,22 @@
- GetItemIconTexture(displayInfoId) - Get item texture path from display info ID
- GetSpellIconTexture(spellIconId) - Get spell texture path from spell icon ID
Nameplate GUID (v2.28+):
- CSimpleFrame:GetName(1) returns nameplate unit GUID
Ammo Query (v2.29+):
- GetAmmo() - Returns equipped ammo item ID and total count
Aura Slot Extensions (v2.30+):
- auraSlot parameter on BUFF/DEBUFF_ADDED/REMOVED events (raw 0-based slot)
- BUFF_UPDATE_DURATION_SELF / DEBUFF_UPDATE_DURATION_SELF events
- GetPlayerAuraDuration(auraSlot) - Get duration info for player aura by slot
Settings Integration:
- Reads from NampowerSettings addon when available
- Falls back to CVars when addon not present
Current version: v2.27.0
Current version: v2.30.0
]]
local _G = _G or getfenv(0)
@@ -197,6 +208,17 @@ API.VERSION_REQUIREMENTS = {
-- v2.27+ - Texture lookup functions
["GetItemIconTexture"] = { 2, 27, 0, "GetItemIconTexture" },
["GetSpellIconTexture"] = { 2, 27, 0, "GetSpellIconTexture" },
-- v2.28+ - Nameplate GUID via CSimpleFrame:GetName(1)
["NameplateGUID"] = { 2, 28, 0 },
-- v2.29+ - Ammo query
["GetAmmo"] = { 2, 29, 0, "GetAmmo" },
-- v2.30+ - Aura slot extensions
["AuraSlotParameter"] = { 2, 30, 0 },
["AuraDurationEvents"] = { 2, 30, 0 },
["GetPlayerAuraDuration"] = { 2, 30, 0, "GetPlayerAuraDuration" },
}
-- Check if a specific feature is available
@@ -315,6 +337,17 @@ local function InitializeFeatures()
f.hasGetItemIconTexture = API.HasFeature("GetItemIconTexture")
f.hasGetSpellIconTexture = API.HasFeature("GetSpellIconTexture")
-- v2.28+ Nameplate GUID
f.hasNameplateGUID = API.HasFeature("NameplateGUID")
-- v2.29+ Ammo query
f.hasGetAmmo = API.HasFeature("GetAmmo")
-- v2.30+ Aura slot extensions
f.hasAuraSlotParameter = API.HasFeature("AuraSlotParameter")
f.hasAuraDurationEvents = API.HasFeature("AuraDurationEvents")
f.hasGetPlayerAuraDuration = API.HasFeature("GetPlayerAuraDuration")
-- Runtime detection for enhanced spell functions (verify by testing)
if f.hasEnhancedSpellFunctions and GetSpellTexture then
local success, result = pcall(function()
@@ -1897,7 +1930,7 @@ API.CAST_TYPE = {
}
-- Buff/debuff event names (v2.18+)
-- Parameters: guid, slot, spellId, stackCount, auraLevel (v2.20+)
-- Parameters: guid, slot, spellId, stackCount, auraLevel (v2.20+), auraSlot (v2.30+, raw 0-based)
API.AURA_EVENTS = {
"BUFF_ADDED_SELF",
"BUFF_REMOVED_SELF",
@@ -1925,6 +1958,14 @@ API.AURA_CAP_STATUS = {
BOTH_FULL = 3,
}
-- Aura duration update event names (v2.30+)
-- Fires when a buff/debuff duration is refreshed on the player
-- Parameters: auraSlot (0-based raw slot index)
API.AURA_DURATION_EVENTS = {
"BUFF_UPDATE_DURATION_SELF", -- Player buff duration refreshed
"DEBUFF_UPDATE_DURATION_SELF", -- Player debuff duration refreshed
}
-- Auto-attack event names (v2.24+, requires NP_EnableAutoAttackEvents=1)
-- Parameters: attackerGuid, targetGuid, totalDamage, hitInfo, victimState,
-- subDamageCount, blockedAmount, totalAbsorb, totalResist
@@ -2763,6 +2804,57 @@ function API.QueueScript(script, priority)
return false
end
-- Get equipped ammo ID and total count (v2.29+)
-- Returns: ammoId, ammoCount (or nil, nil if unavailable)
function API.GetAmmo()
if not API.features.hasGetAmmo or not _G.GetAmmo then
return nil, nil
end
return _G.GetAmmo()
end
-- Get player aura duration info by raw slot index (v2.30+)
-- auraSlot: 0-based raw aura slot (0-31 = buffs, 32-47 = debuffs)
-- Returns: spellId, durationMs, expirationTimeMs (or nil if unavailable)
function API.GetPlayerAuraDuration(auraSlot)
if not API.features.hasGetPlayerAuraDuration or not _G.GetPlayerAuraDuration then
return nil, nil, nil
end
return _G.GetPlayerAuraDuration(auraSlot)
end
-- Count occupied player buff slots using GetPlayerAuraDuration (v2.30+)
-- Returns: count of occupied buff slots (0-32), or nil if unavailable
function API.CountPlayerBuffSlots()
if not API.features.hasGetPlayerAuraDuration or not _G.GetPlayerAuraDuration then
return nil
end
local count = 0
for i = 0, 31 do
local spellId = _G.GetPlayerAuraDuration(i)
if spellId and spellId > 0 then
count = count + 1
end
end
return count
end
-- Count occupied player debuff slots using GetPlayerAuraDuration (v2.30+)
-- Returns: count of occupied debuff slots (0-16), or nil if unavailable
function API.CountPlayerDebuffSlots()
if not API.features.hasGetPlayerAuraDuration or not _G.GetPlayerAuraDuration then
return nil
end
local count = 0
for i = 32, 47 do
local spellId = _G.GetPlayerAuraDuration(i)
if spellId and spellId > 0 then
count = count + 1
end
end
return count
end
-- Clear spell type cache (call on spec change)
function API.ClearSpellTypeCache()
API.spellTypeCache = {}
+80 -24
View File
@@ -4759,6 +4759,7 @@ ev:SetScript("OnEvent", function()
local slot = arg2
local spellId = arg3
local stacks = arg4 or 1
local auraSlot = arg6 -- v2.30+: raw 0-based aura slot index (stable, no shifting)
if not guid or not slot or not spellId then return end
guid = CleveRoids.NormalizeGUID(guid)
@@ -4767,28 +4768,44 @@ ev:SetScript("OnEvent", function()
if not spellName then return end
local _, playerGUID = UnitExists("player")
local isOurs = lib.ownDebuffs[guid] and lib.ownDebuffs[guid][spellName] ~= nil
-- Update slot info in ownDebuffs if this is our debuff
if lib.ownDebuffs[guid] and lib.ownDebuffs[guid][spellName] then
if isOurs then
lib.ownDebuffs[guid][spellName].slot = slot
if auraSlot then
lib.ownDebuffs[guid][spellName].auraSlot = auraSlot
end
end
-- Update allSlots for slot tracking
lib.allSlots[guid] = lib.allSlots[guid] or {}
lib.allSlots[guid][slot] = {
spellName = spellName,
casterGuid = playerGUID, -- Default to player, updated if other caster known
isOurs = lib.ownDebuffs[guid] and lib.ownDebuffs[guid][spellName] ~= nil,
}
-- Check allAuraCasts for caster info if not ours
-- Determine caster GUID
local casterGuid = playerGUID -- Default to player
if lib.allAuraCasts[guid] and lib.allAuraCasts[guid][spellName] then
for casterGuid, _ in pairs(lib.allAuraCasts[guid][spellName]) do
lib.allSlots[guid][slot].casterGuid = casterGuid
for cGuid, _ in pairs(lib.allAuraCasts[guid][spellName]) do
casterGuid = cGuid
break
end
end
-- v2.30+: Use stable auraSlot for slotOwnership (no shifting needed)
if auraSlot then
lib.slotOwnership[guid] = lib.slotOwnership[guid] or {}
lib.slotOwnership[guid][auraSlot] = {
casterGuid = casterGuid,
spellName = spellName,
spellId = spellId,
isOurs = isOurs,
}
end
-- Legacy: Update allSlots for slot tracking (pre-v2.30 or fallback)
lib.allSlots[guid] = lib.allSlots[guid] or {}
lib.allSlots[guid][slot] = {
spellName = spellName,
casterGuid = casterGuid,
isOurs = isOurs,
}
elseif event == "DEBUFF_REMOVED_OTHER" then
-- Skip if pfUI enhanced tracking is active
if lib.hasPfUIEnhanced then return end
@@ -4796,6 +4813,7 @@ ev:SetScript("OnEvent", function()
local guid = arg1
local slot = arg2
local spellId = arg3
local auraSlot = arg6 -- v2.30+: raw 0-based aura slot index (stable, no shifting)
if not guid or not slot then return end
guid = CleveRoids.NormalizeGUID(guid)
@@ -4812,11 +4830,16 @@ ev:SetScript("OnEvent", function()
lib.ownSlots[guid][slot] = nil
end
-- Remove from allSlots and shift slots down
-- v2.30+: Remove from stable slotOwnership (no shifting needed!)
if auraSlot and lib.slotOwnership[guid] then
lib.slotOwnership[guid][auraSlot] = nil
end
-- Legacy: Remove from allSlots and shift slots down (pre-v2.30 or fallback)
if lib.allSlots[guid] and lib.allSlots[guid][slot] then
lib.allSlots[guid][slot] = nil
-- Shift slots down
-- Shift slots down (only needed for legacy slot tracking)
local maxSlot = 0
for s in pairs(lib.allSlots[guid]) do
if s > maxSlot then maxSlot = s end
@@ -4849,6 +4872,9 @@ ev:SetScript("OnEvent", function()
if lib.allSlots[guid] then
lib.allSlots[guid] = nil
end
if lib.slotOwnership[guid] then
lib.slotOwnership[guid] = nil
end
if lib.allAuraCasts[guid] then
lib.allAuraCasts[guid] = nil
end
@@ -7980,7 +8006,8 @@ function CleveRoids.ClearReactiveProc(spellName)
end
-- Parse combat log for reactive ability triggers
function CleveRoids.ParseReactiveCombatLog()
-- lowerMsg: optional pre-lowercased message from dispatcher (avoids redundant string.lower allocation)
function CleveRoids.ParseReactiveCombatLog(lowerMsg)
if not arg1 then return end
local message = arg1
@@ -7991,7 +8018,8 @@ function CleveRoids.ParseReactiveCombatLog()
end
-- PERFORMANCE: Quick keyword check - most messages won't match
local lowerMsg = lower(message)
-- Use pre-computed lowerMsg if provided by dispatcher, otherwise compute it
lowerMsg = lowerMsg or lower(message)
if not (strfind(lowerMsg, "dodge") or strfind(lowerMsg, "parry") or strfind(lowerMsg, "block")) then
return
end
@@ -8420,7 +8448,8 @@ function CleveRoids.CheckResistState(resistType)
end
-- Parse combat log for resist messages
local function ParseResistCombatLog()
-- lowerMsg: optional pre-lowercased message from dispatcher (avoids redundant string.lower allocation)
local function ParseResistCombatLog(lowerMsg)
local message = arg1 -- Formatted message (localized)
if not message then return end
@@ -8429,7 +8458,8 @@ local function ParseResistCombatLog()
if string.len(message) < 15 then return end
-- PERFORMANCE: Quick keyword check before expensive pattern matching
local lowerMsg = string.lower(message)
-- Use pre-computed lowerMsg if provided by dispatcher, otherwise compute it
lowerMsg = lowerMsg or string.lower(message)
if not string.find(lowerMsg, "resist") then return end
-- Get current target info for matching
@@ -8603,19 +8633,45 @@ unifiedCombatLogFrame:RegisterEvent("RAW_COMBATLOG")
unifiedCombatLogFrame:SetScript("OnEvent", function()
if event ~= "RAW_COMBATLOG" then return end
-- 1. Debuff fade learning (for duration auto-learning)
HandleDebuffFade()
-- PERFORMANCE: Dispatcher-level triage to avoid calling handlers that will immediately return.
-- Each handler previously did its own nil/length/keyword checks independently, resulting in
-- redundant work and unnecessary function call overhead. Now we do one pass of keyword checks
-- and only call relevant handlers. Also shares string.lower() between reactive and resist handlers.
local msg = arg1
local raw = arg2
-- 1. Debuff fade learning (checks raw/arg2 for "fades from")
if raw and string.len(raw) >= 12 and find(raw, "fades from") then
HandleDebuffFade()
end
-- All remaining handlers need arg1
if not msg then return end
local msgLen = string.len(msg)
if msgLen < 8 then return end
-- 2. Immunity detection (immune/reflect/evade from combat log)
ParseImmunityCombatLog()
if find(msg, "immune") or find(msg, "reflect") or find(msg, "evade") then
ParseImmunityCombatLog()
end
-- 3. "Afflicted by" detection for hidden CC (e.g., Pounce stun)
ParseAfflictedCombatLog()
if find(msg, "afflicted by") then
ParseAfflictedCombatLog()
end
-- PERFORMANCE: Compute lowerMsg once, share between reactive and resist handlers
-- Previously each handler called string.lower() independently (2 string allocations)
local lowerMsg = lower(msg)
-- 4. Reactive ability procs (always run - AUTO_ATTACK events only cover white swings,
-- yellow ability dodges like Mortal Strike/Heroic Strike come through combat log)
CleveRoids.ParseReactiveCombatLog()
if strfind(lowerMsg, "dodge") or strfind(lowerMsg, "parry") or strfind(lowerMsg, "block") then
CleveRoids.ParseReactiveCombatLog(lowerMsg)
end
-- 5. Resist tracking
ParseResistCombatLog()
if msgLen >= 15 and strfind(lowerMsg, "resist") then
ParseResistCombatLog(lowerMsg)
end
end)