mirror of
https://github.com/brues-code/SuperCleveRoidMacros.git
synced 2026-09-16 03:38:00 +00:00
onupdate and onevent performance updates
This commit is contained in:
+188
-59
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user