diff --git a/Conditionals.lua b/Conditionals.lua index ce4c8e6..2e9e4bc 100644 --- a/Conditionals.lua +++ b/Conditionals.lua @@ -835,6 +835,23 @@ function CleveRoids.FindAllCasterAuraByName(targetGuid, searchName) if not targetData then return nil, nil end local now = GetTime() + + -- Direct spell ID lookup: O(1) hash table access instead of iterating all entries + local searchID = tonumber(searchName) + if searchID then + local auraData = targetData[searchID] + if auraData and auraData.start and auraData.duration then + local remaining = auraData.duration + auraData.start - now + if remaining > 0 then + return remaining, auraData.casterGuid + else + targetData[searchID] = nil + end + end + return nil, nil + end + + -- Name-based search (existing behavior) local searchLower = string.lower(searchName) for spellId, auraData in pairs(targetData) do @@ -2997,7 +3014,9 @@ function CleveRoids.ValidateAura(unit, args, isbuff) end -- PERFORMANCE: Cache lowercased search name to avoid repeated string.lower calls - local searchName = args.name and _string_lower(args.name) + -- Support spell ID matching: [mybuff:17941] matches by spell ID instead of name + local searchID = args.name and tonumber(args.name) + local searchName = not searchID and args.name and _string_lower(args.name) or nil -- Primary search: BUFFS if isbuff==true, DEBUFFS if isbuff==false while true do @@ -3020,12 +3039,20 @@ function CleveRoids.ValidateAura(unit, args, isbuff) if not texture then break end - if current_spellID and searchName then - -- PERFORMANCE: Use cached lowercase spell name lookup - local lowerName = GetLowercaseSpellName(current_spellID) - if lowerName and lowerName == searchName then - found = true - break + if current_spellID then + if searchID then + -- Spell ID matching: [mybuff:17941] + if current_spellID == searchID then + found = true + break + end + elseif searchName then + -- PERFORMANCE: Use cached lowercase spell name lookup + local lowerName = GetLowercaseSpellName(current_spellID) + if lowerName and lowerName == searchName then + found = true + break + end end end @@ -3033,7 +3060,7 @@ function CleveRoids.ValidateAura(unit, args, isbuff) end -- Overflow handling: when searching DEBUFFS on non-players, also scan BUFFS - if not isbuff and not isPlayer and not found and searchName then + if not isbuff and not isPlayer and not found and (searchID or searchName) then i = 1 while true do local texture @@ -3044,11 +3071,18 @@ function CleveRoids.ValidateAura(unit, args, isbuff) if not texture then break end if current_spellID then - -- PERFORMANCE: Use cached lowercase spell name lookup - local lowerName = GetLowercaseSpellName(current_spellID) - if lowerName and lowerName == searchName then - found = true - break + if searchID then + if current_spellID == searchID then + found = true + break + end + elseif searchName then + -- PERFORMANCE: Use cached lowercase spell name lookup + local lowerName = GetLowercaseSpellName(current_spellID) + if lowerName and lowerName == searchName then + found = true + break + end end end @@ -3154,12 +3188,15 @@ function CleveRoids.ValidateUnitDebuff(unit, args) -- Convert underscores to spaces for matching (e.g., "Thunder_Clap" -> "Thunder Clap") args.name = string.gsub(args.name, "_", " ") + -- Support spell ID matching: [debuff:9904] matches by spell ID instead of name + local searchID = tonumber(args.name) + local found = false local texture, stacks, spellID, remaining local i -- PERFORMANCE: For non-SuperWoW, early return if no texture registered - if not CleveRoids.hasSuperwow and not CleveRoids.auraTextures[args.name] then + if not searchID and not CleveRoids.hasSuperwow and not CleveRoids.auraTextures[args.name] then return false end @@ -3176,7 +3213,13 @@ function CleveRoids.ValidateUnitDebuff(unit, args) if not guid then return false end -- PERFORMANCE: Use cached spell name -> ID mapping instead of iterating every call - local matchingSpellIDs = GetSpellIDsForName(args.name) or {} + -- For spell ID input, use the ID directly instead of name->ID lookup + local matchingSpellIDs + if searchID then + matchingSpellIDs = {searchID} + else + matchingSpellIDs = GetSpellIDsForName(args.name) or {} + end -- Check tracking table for ANY rank of this spell: Did player cast this? Is timer valid? if matchingSpellIDs and table.getn(matchingSpellIDs) > 0 then @@ -3272,9 +3315,14 @@ function CleveRoids.ValidateUnitDebuff(unit, args) if not tex then break end if debuffSpellID then - -- PERFORMANCE: Use cached spell name lookup - local baseName, fullName = GetSpellNames(debuffSpellID) - if baseName and (baseName == args.name or fullName == args.name) then + local matched = false + if searchID then + matched = (debuffSpellID == searchID) + else + local baseName, fullName = GetSpellNames(debuffSpellID) + matched = baseName and (baseName == args.name or fullName == args.name) + end + if matched then -- IMPORTANT: Only use fallback for SHARED debuffs -- Personal debuffs must come from tracking table (caster check) local isShared = lib and lib.IsPersonalDebuff and lib:IsPersonalDebuff(debuffSpellID) == false @@ -3298,9 +3346,14 @@ function CleveRoids.ValidateUnitDebuff(unit, args) if not tex then break end if buffSpellID then - -- PERFORMANCE: Use cached spell name lookup - local baseName, fullName = GetSpellNames(buffSpellID) - if baseName and (baseName == args.name or fullName == args.name) then + local matched = false + if searchID then + matched = (buffSpellID == searchID) + else + local baseName, fullName = GetSpellNames(buffSpellID) + matched = baseName and (baseName == args.name or fullName == args.name) + end + if matched then -- IMPORTANT: Only use fallback for SHARED debuffs local isShared = lib and lib.IsPersonalDebuff and lib:IsPersonalDebuff(buffSpellID) == false if isShared then @@ -3324,8 +3377,12 @@ function CleveRoids.ValidateUnitDebuff(unit, args) texture, stacks, spellID, remaining = CleveRoids.GetPlayerAura(i, false) if not texture then break end - if CleveRoids.hasSuperwow then - -- PERFORMANCE: Use cached spell name lookup + if searchID then + if spellID == searchID then + found = true + break + end + elseif CleveRoids.hasSuperwow then local baseName, fullName = GetSpellNames(spellID) if baseName and (baseName == args.name or fullName == args.name) then found = true @@ -3345,8 +3402,12 @@ function CleveRoids.ValidateUnitDebuff(unit, args) texture, stacks, spellID, remaining = CleveRoids.GetPlayerAura(i, true) if not texture then break end - if CleveRoids.hasSuperwow then - -- PERFORMANCE: Use cached spell name lookup + if searchID then + if spellID == searchID then + found = true + break + end + elseif CleveRoids.hasSuperwow then local baseName, fullName = GetSpellNames(spellID) if baseName and (baseName == args.name or fullName == args.name) then found = true diff --git a/Core.lua b/Core.lua index ec809e7..613280f 100644 --- a/Core.lua +++ b/Core.lua @@ -5558,6 +5558,15 @@ function CleveRoids.Frame:SPELL_CAST_EVENT() -- Track pending cast for SPELL_GO correlation (reactive ability detection) -- Keyed by spellId so concurrent casts don't overwrite each other CleveRoids.pendingCasts = CleveRoids.pendingCasts or {} + + -- Clean up consumed entries older than 5 seconds (lightweight, runs per-cast) + local cleanupTime = GetTime() - 5 + for id, entry in pairs(CleveRoids.pendingCasts) do + if entry.consumed and entry.consumedAt and entry.consumedAt < cleanupTime then + CleveRoids.pendingCasts[id] = nil + end + end + CleveRoids.pendingCasts[spellId] = { castType = castType, targetGuid = targetGuid, diff --git a/Utility.lua b/Utility.lua index 175f6cb..0ac6afb 100644 --- a/Utility.lua +++ b/Utility.lua @@ -8003,11 +8003,11 @@ function CleveRoids.ParseReactiveCombatLog() local _, targetGUID = UnitExists("target") -- Check each reactive ability's trigger patterns + -- NOTE: Overpower (enemy_dodge) ALWAYS uses combat log text parsing, even when SPELL_GO + -- events are available. SPELL_GO only provides hit/miss COUNTS, not miss TYPES, so it + -- cannot distinguish dodge (Overpower proc) from parry/block/resist (no Overpower proc). for spellName, config in pairs(reactivePatterns) do - -- Skip outgoing dodge patterns when SPELL_GO_SELF handles yellow attack miss detection - if CleveRoids.usingSpellGoEvents and config.type == "enemy_dodge" then - -- Overpower handled by SPELL_GO_SELF (hit/miss binary, no text parsing needed) - elseif CleveRoids.reactiveSpells and CleveRoids.reactiveSpells[spellName] then + if CleveRoids.reactiveSpells and CleveRoids.reactiveSpells[spellName] then for _, pattern in ipairs(config.patterns) do if strfind(message, pattern) then -- Found a trigger event (works in any stance) @@ -8016,7 +8016,6 @@ function CleveRoids.ParseReactiveCombatLog() CleveRoids.SetReactiveProc(spellName, duration, guid) -- For Overpower (enemy dodge), also update LastSwing so [lastswing:dodge] works - -- (Only reached when SPELL_GO is NOT available - combat log fallback) if config.type == "enemy_dodge" and CleveRoids.LastSwing then CleveRoids.LastSwing.timestamp = GetTime() CleveRoids.LastSwing.victimState = VICTIMSTATE_DODGE_REACTIVE @@ -8198,28 +8197,36 @@ if GetNampowerVersion then end end --- Check for Nampower v2.25+ SPELL_GO events (replaces combat log for yellow attack miss detection) +-- Check for Nampower v2.25+ SPELL_GO events (LastSwing tracking for yellow attack misses) +-- NOTE: SPELL_GO only provides hit/miss counts, not miss types, so Overpower still needs +-- combat log text parsing. SPELL_GO is used as a gate to avoid parsing combat log on every +-- successful spell hit - only parse when SPELL_GO reports a miss. local hasSpellGoEvents = false if GetNampowerVersion then local npMajor, npMinor = GetNampowerVersion() if npMajor > 2 or (npMajor == 2 and npMinor >= 25) then hasSpellGoEvents = true - CleveRoids.usingSpellGoEvents = true - reactiveFrame:RegisterEvent("SPELL_GO_SELF") if CleveRoids.debug then - DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nampower]|r Using SPELL_GO events for yellow attack miss detection (v2.25+)") + DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nampower]|r Using SPELL_GO events for yellow attack LastSwing tracking (v2.25+)") end end end -- Register combat log events for reactive abilities --- When SPELL_GO + AUTO_ATTACK are both available, outgoing dodges are handled natively; --- combat log only needed for incoming avoidance (Riposte/Revenge from enemy abilities) -if hasAutoAttackEvents and hasSpellGoEvents then +-- Outgoing dodge detection (Overpower) uses combat log text parsing because SPELL_GO only +-- provides hit/miss counts, not miss types (dodge vs parry vs block vs resist). However, +-- with SPELL_GO available, CHAT_MSG_SPELL_SELF_DAMAGE parsing is gated behind a miss window +-- so we skip parsing on every successful spell hit (see OnEvent handler below). +-- AUTO_ATTACK events handle white swing avoidance; combat log handles yellow ability dodges. +if hasAutoAttackEvents then + -- Incoming avoidance (Riposte/Revenge) reactiveFrame:RegisterEvent("CHAT_MSG_COMBAT_CREATURE_VS_SELF_MISSES") reactiveFrame:RegisterEvent("CHAT_MSG_COMBAT_CREATURE_VS_SELF_HITS") + -- Outgoing dodge detection (Overpower) - gated by SPELL_GO miss window when available + reactiveFrame:RegisterEvent("CHAT_MSG_COMBAT_SELF_MISSES") + reactiveFrame:RegisterEvent("CHAT_MSG_SPELL_SELF_DAMAGE") else -- Full combat log fallback reactiveFrame:RegisterEvent("RAW_COMBATLOG") @@ -8263,10 +8270,11 @@ reactiveFrame:SetScript("OnEvent", function() end -- ======================================================================== - -- NAMPOWER v2.25+ SPELL_GO EVENTS (yellow attack hit/miss detection) + -- NAMPOWER v2.25+ SPELL_GO EVENTS (yellow attack LastSwing tracking) -- ======================================================================== -- Two-step: SPELL_CAST_EVENT (success=1) → SPELL_GO_SELF (hit/miss outcome) - -- Replaces combat log text parsing for outgoing yellow ability misses. + -- Updates LastSwing state for yellow ability misses. Overpower detection still uses + -- combat log text parsing (SPELL_GO only has hit/miss counts, not types like dodge). -- SPELL_GO_SELF params: itemId, spellId, casterGuid, targetGuid, castFlags, numTargetsHit, numTargetsMissed if event == "SPELL_GO_SELF" then local spellId = arg2 @@ -8276,12 +8284,16 @@ reactiveFrame:SetScript("OnEvent", function() -- Step 1: Validate against SPELL_CAST_EVENT (confirms player-initiated, successful cast) local pending = CleveRoids.pendingCasts and CleveRoids.pendingCasts[spellId] - if not pending then - return -- No matching SPELL_CAST_EVENT with success=1, skip + if not pending or pending.consumed then + return -- No matching SPELL_CAST_EVENT with success=1, or already processed end - -- Clear the pending cast (consumed) - CleveRoids.pendingCasts[spellId] = nil + -- Mark pending cast as consumed with timestamp instead of deleting immediately. + -- Other handlers (libdebuff SPELL_GO, AURA_CAST, ComboPointTracker) on different + -- frames may still need to read this data. Stale entries are cleaned up on next + -- SPELL_CAST_EVENT for the same spellId (overwritten) or expire naturally. + pending.consumed = true + pending.consumedAt = GetTime() -- Skip channels and targeting spells (not melee/ranged attacks) -- CastType: NORMAL=1, NON_GCD=2, ON_SWING=3, CHANNEL=4, TARGETING=5, TARGETING_NON_GCD=6 @@ -8289,7 +8301,11 @@ reactiveFrame:SetScript("OnEvent", function() return end - -- Step 2: Check hit/miss outcome - binary check covers dodge, parry, resist, etc. + -- Step 2: Check hit/miss outcome for LastSwing tracking + -- NOTE: SPELL_GO only provides hit/miss COUNTS, not miss TYPES (dodge vs parry vs + -- block vs resist). Overpower detection is handled by combat log text parsing which + -- can distinguish "dodged" from other avoidance types. We only update LastSwing here + -- as a generic miss indicator (combat log overwrites with specific type if dodge). if numMissed >= 1 and numHit == 0 then -- Use current target GUID if SPELL_GO targetGuid is empty local procTarget = targetGuid @@ -8301,22 +8317,9 @@ reactiveFrame:SetScript("OnEvent", function() end end - -- Overpower: enemy avoided our yellow attack - if CleveRoids.reactiveSpells and CleveRoids.reactiveSpells["Overpower"] then - CleveRoids.SetReactiveProc("Overpower", 4.0, procTarget) - CleveRoids.QueueActionUpdate() - - if CleveRoids.debug then - local spellName = SpellInfo and SpellInfo(spellId) or tostring(spellId) - DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cff00ff00[SPELL_GO]|r Overpower proc - %s missed (hit=%d, miss=%d, castType=%s)", - spellName, numHit, numMissed, tostring(pending.castType)) - ) - end - end - -- Update LastSwing for yellow miss (victimState unknown from SPELL_GO, use UNAFFECTED) -- Don't overwrite AUTO_ATTACK data from the same frame (on-swing abilities fire both) + -- Combat log handler will overwrite with specific type (dodge/parry/etc.) if available if CleveRoids.LastSwing and CleveRoids.LastSwing.timestamp ~= GetTime() then CleveRoids.LastSwing.timestamp = GetTime() CleveRoids.LastSwing.hitInfo = HITINFO_MISS @@ -8327,21 +8330,46 @@ reactiveFrame:SetScript("OnEvent", function() CleveRoids.LastSwing.resistAmount = 0 CleveRoids.LastSwing.targetGuid = procTarget end + + -- Gate combat log parsing: set miss window so CHAT_MSG_SPELL_SELF_DAMAGE + -- only calls ParseReactiveCombatLog when we know a yellow miss occurred. + -- This avoids parsing every successful spell hit message. + CleveRoids._yellowMissWindow = GetTime() + + if CleveRoids.debug then + local spellName = SpellInfo and SpellInfo(spellId) or tostring(spellId) + DEFAULT_CHAT_FRAME:AddMessage( + string.format("|cff00ff00[SPELL_GO]|r Yellow miss detected - %s (hit=%d, miss=%d, castType=%s) - deferring to combat log for type", + spellName, numHit, numMissed, tostring(pending.castType)) + ) + end end return end -- ======================================================================== - -- COMBAT LOG FALLBACK FOR REACTIVE ABILITIES + -- COMBAT LOG TEXT PARSING FOR REACTIVE ABILITIES -- ======================================================================== - -- Only used for incoming avoidance (Riposte/Revenge) from enemy abilities. - -- Outgoing dodges handled by SPELL_GO_SELF (v2.25+) or AUTO_ATTACK (white swings). - if event == "RAW_COMBATLOG" or - event == "CHAT_MSG_COMBAT_SELF_MISSES" or - event == "CHAT_MSG_SPELL_SELF_DAMAGE" or - event == "CHAT_MSG_COMBAT_CREATURE_VS_SELF_MISSES" or - event == "CHAT_MSG_COMBAT_CREATURE_VS_SELF_HITS" then + -- Handles: Overpower (outgoing dodge), Riposte (incoming parry), Revenge (incoming avoid). + -- SPELL_GO provides hit/miss counts but not types, so combat log is still needed to + -- distinguish dodge (Overpower) from parry/block/resist. But we only parse when SPELL_GO + -- told us a miss occurred, avoiding overhead on every successful spell hit. + if event == "CHAT_MSG_SPELL_SELF_DAMAGE" then + -- Yellow ability avoidance (e.g., "Your Mortal Strike was dodged by Target") + -- With SPELL_GO events: only parse when a yellow miss was recently detected. + -- Without SPELL_GO: always parse (fallback for older Nampower). + if hasSpellGoEvents then + local missWindow = CleveRoids._yellowMissWindow + if not missWindow or (GetTime() - missWindow) > 1.0 then + return -- No recent yellow miss from SPELL_GO, skip parsing + end + end + CleveRoids.ParseReactiveCombatLog() + elseif event == "RAW_COMBATLOG" or + event == "CHAT_MSG_COMBAT_SELF_MISSES" or + event == "CHAT_MSG_COMBAT_CREATURE_VS_SELF_MISSES" or + event == "CHAT_MSG_COMBAT_CREATURE_VS_SELF_HITS" then CleveRoids.ParseReactiveCombatLog() end end)