From 73166ce4af8faf4f783c9d596f6bbfdb647a9643 Mon Sep 17 00:00:00 2001 From: Dusk-92 Date: Thu, 3 Sep 2026 11:14:02 +0200 Subject: [PATCH] Reduce debuff tracking hot-path overhead --- libs/libdebuff.lua | 117 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 93 insertions(+), 24 deletions(-) diff --git a/libs/libdebuff.lua b/libs/libdebuff.lua index f6cfaaa..18948a8 100644 --- a/libs/libdebuff.lua +++ b/libs/libdebuff.lua @@ -17,7 +17,13 @@ if GetLocale() == "ruRU" then end local libdebuff = CreateFrame("Frame", "ShaguTweaksDebuffsScanner", UIParent) -local scanner = libtipscan:GetScanner("libdebuff") +local scanner +local function GetScanner() + if not scanner then + scanner = libtipscan:GetScanner("libdebuff") + end + return scanner +end local _, class = UnitClass("player") local lastspell @@ -81,11 +87,12 @@ function libdebuff:AddPending(unit, unitlevel, effect, duration) if not unit then return end if not L["debuffs"][effect] then return end + duration = duration or libdebuff:GetDuration(effect) if duration > 0 and libdebuff.pending[3] ~= effect then libdebuff.pending[1] = unit libdebuff.pending[2] = unitlevel or 0 libdebuff.pending[3] = effect - libdebuff.pending[4] = duration or libdebuff:GetDuration(effect) + libdebuff.pending[4] = duration end end @@ -110,7 +117,7 @@ function libdebuff:RevertLastAction() libdebuff:UpdateUnits() end -function libdebuff:AddEffect(unit, unitlevel, effect, duration) +function libdebuff:AddEffect(unit, unitlevel, effect, duration, deferUpdate) if not unit or not effect then return end unitlevel = unitlevel or 0 if not libdebuff.objects[unit] then libdebuff.objects[unit] = {} end @@ -125,12 +132,22 @@ function libdebuff:AddEffect(unit, unitlevel, effect, duration) libdebuff.objects[unit][unitlevel][effect].start = GetTime() libdebuff.objects[unit][unitlevel][effect].duration = duration or libdebuff:GetDuration(effect) - libdebuff:UpdateUnits() + if not deferUpdate then + libdebuff:UpdateUnits() + end end --- scan for debuff application -libdebuff:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_DAMAGE") -libdebuff:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_CREATURE_DAMAGE") +-- ClassicAPI exposes aura identity and timing directly. The old periodic +-- combat-text parser only exists to reconstruct off-target applications on +-- older ClassicAPI builds; current ClassicAPI does not register those noisy +-- events at all. +if not (API and API.aurapositional and API.UnitDebuff) then + libdebuff:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_DAMAGE") + libdebuff:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_CREATURE_DAMAGE") +end + +-- These events still support the legacy estimated-duration fallback when an +-- exact expiration time is unavailable. libdebuff:RegisterEvent("CHAT_MSG_SPELL_FAILED_LOCALPLAYER") libdebuff:RegisterEvent("CHAT_MSG_SPELL_SELF_DAMAGE") libdebuff:RegisterEvent("PLAYER_TARGET_CHANGED") @@ -183,25 +200,40 @@ libdebuff:SetScript("OnEvent", function() -- Add Missing Buffs by Iteration elseif ( event == "UNIT_AURA" and arg1 == "target" ) or event == "PLAYER_TARGET_CHANGED" then + local unit = UnitName("target") + if not unit then return end + + local unitlevel = UnitLevel("target") or 0 + local changed = false + for i=1, 16 do local effect, rank, texture, stacks, dtype, duration, timeleft = libdebuff:UnitDebuff("target", i) -- abort when no further debuff was found - if not texture then return end + if not texture then break end - if texture and effect and effect ~= "" then - -- don't overwrite existing timers - local unitlevel = UnitLevel("target") or 0 - local unit = UnitName("target") + if effect and effect ~= "" then + -- Don't overwrite existing timers. Defer the expensive target-frame + -- refresh until the complete aura pass is done. if not libdebuff.objects[unit] or not libdebuff.objects[unit][unitlevel] or not libdebuff.objects[unit][unitlevel][effect] then - libdebuff:AddEffect(unit, unitlevel, effect) + libdebuff:AddEffect(unit, unitlevel, effect, nil, true) + changed = true end end end + if changed then + libdebuff:UpdateUnits() + end + -- Update Pending Spells elseif event == "CHAT_MSG_SPELL_FAILED_LOCALPLAYER" or event == "CHAT_MSG_SPELL_SELF_DAMAGE" then - -- Remove pending spell + -- Most combat messages cannot affect libdebuff. Avoid running the whole + -- miss/resist/immune pattern set unless there is state to repair. + if not libdebuff.pending[3] and not (lastspell and lastspell.start_old) then + return + end + for _, msg in pairs(libdebuff.rp) do local effect = cmatch(arg1, msg) if effect and libdebuff.pending[3] == effect then @@ -215,28 +247,64 @@ libdebuff:SetScript("OnEvent", function() end end elseif event == "SPELLCAST_STOP" then - QueueFunction(libdebuff.PersistPending) + if libdebuff.pending[3] then + QueueFunction(libdebuff.PersistPending) + end end end) -- Gather Data by User Actions hooksecurefunc("CastSpell", function(id, bookType) - local effect = GetSpellName(id, bookType) - local _, rank = libspell.GetSpellInfo(id, bookType) + local effect, rank = GetSpellName(id, bookType) + if not effect or not L["debuffs"][effect] then return end + local duration = libdebuff:GetDuration(effect, rank) libdebuff:AddPending(UnitName("target"), UnitLevel("target"), effect, duration) end) hooksecurefunc("CastSpellByName", function(effect, target) - local _, rank = libspell.GetSpellInfo(effect) - local duration = libdebuff:GetDuration(effect, rank) - libdebuff:AddPending(UnitName("target"), UnitLevel("target"), effect, duration) + if not effect then return end + + -- Strip an explicit rank before checking the duration database. Reject + -- ordinary damage/heal casts before doing any spell metadata work. + local _, _, baseEffect, explicitRank = string.find(effect, "(.+)%((.+)%)") + local name = baseEffect or effect + if not L["debuffs"][name] then return end + if libdebuff.pending[3] == name then return end + + local rank = explicitRank + if not rank then + _, rank = libspell.GetSpellInfo(name) + end + + local duration = libdebuff:GetDuration(name, rank) + libdebuff:AddPending(UnitName("target"), UnitLevel("target"), name, duration) end) hooksecurefunc("UseAction", function(slot, target, button) + -- Macros are intentionally left to CastSpellByName: conditional macros can + -- select a different spell than their first /cast line. + if API and API.actioninfo and API.GetActionInfo then + local actionType, actionID = API.GetActionInfo(slot) + if actionType == "macro" or actionType == "item" then return end + + if actionType == "spell" and actionID then + if not IsCurrentAction(slot) then return end + local effect, rank = API.GetSpellInfo(actionID) + if not effect or not L["debuffs"][effect] then return end + local duration = libdebuff:GetDuration(effect, rank) + libdebuff:AddPending(UnitName("target"), UnitLevel("target"), effect, duration) + return + elseif actionType then + return + end + end + if GetActionText(slot) or not IsCurrentAction(slot) then return end - scanner:SetAction(slot) - local effect, rank = scanner:Line(1) + local tip = GetScanner() + tip:SetAction(slot) + local effect, rank = tip:Line(1) + if not effect or not L["debuffs"][effect] then return end local duration = libdebuff:GetDuration(effect, rank) libdebuff:AddPending(UnitName("target"), UnitLevel("target"), effect, duration) end) @@ -262,8 +330,9 @@ function libdebuff:UnitDebuff(unit, id) end if texture and (not effect or effect == "") then - scanner:SetUnitDebuff(unit, id) - effect = scanner:Line(1) or "" + local tip = GetScanner() + tip:SetUnitDebuff(unit, id) + effect = tip:Line(1) or "" end if libdebuff.objects[unitname] and libdebuff.objects[unitname][unitlevel] and libdebuff.objects[unitname][unitlevel][effect] then