diff --git a/API_Check.lua b/API_Check.lua index df5170e6..45c8c2bc 100644 --- a/API_Check.lua +++ b/API_Check.lua @@ -13,7 +13,7 @@ do -- on. There pfUI does load, and will throw wherever it reaches for something -- the installed DLL doesn't have yet -- the popup names the cause so those -- errors aren't a mystery. - local PFUI_CLASSIC_API_MIN = 11304 -- (X*10000 + Y*100 + Z) + local PFUI_CLASSIC_API_MIN = 11500 -- (X*10000 + Y*100 + Z) local PFUI_CLASSIC_API_LATEST = PFUI_CLASSIC_API_MIN local PFUI_CLASSIC_API_WEBSITE = "https://github.com/brues-code/ClassicAPI" local PFUI_CLASSIC_API_LATEST_URL = PFUI_CLASSIC_API_WEBSITE .. "/releases/latest" diff --git a/api/config.lua b/api/config.lua index 7e917a07..f7d74e68 100644 --- a/api/config.lua +++ b/api/config.lua @@ -645,7 +645,6 @@ function pfUI:LoadConfig() pfUI:UpdateConfig("bars", nil, "animation", "zoomfade") pfUI:UpdateConfig("bars", nil, "animmode", "keypress") pfUI:UpdateConfig("bars", nil, "animalways", "0") - pfUI:UpdateConfig("bars", nil, "macroscan", "1") pfUI:UpdateConfig("bars", nil, "reagents", "1") pfUI:UpdateConfig("bars", nil, "hunterbar", "0") pfUI:UpdateConfig("bars", nil, "pagemasteralt", "0") diff --git a/api/unitframes.lua b/api/unitframes.lua index 56c8cb86..677b99e1 100644 --- a/api/unitframes.lua +++ b/api/unitframes.lua @@ -314,7 +314,8 @@ function pfUI.uf:UpdateVisibility() end local unitstr = ("%s%s"):format(self.label or "", self.id or "") - self:SetAttribute("unit", unitstr ~= "" and unitstr or nil) + self.unitstr = unitstr ~= "" and unitstr or nil + self:SetAttribute("unit", self.unitstr) local visibility = ("[target=%s,exists] show; hide"):format(unitstr) -- Group frames are redundant when the group is already shown as a raid grid: @@ -344,6 +345,15 @@ function pfUI.uf:UpdateVisibility() self.visible = nil end + -- This is the single place a frame's unit is ever assigned, so it is also the + -- place its subscriptions follow it to: the engine then delivers only this + -- unit's events, and OnEvent compares against the cached string instead of + -- rebuilding label..id -- and calling UnitGUID -- for every unit event fired + -- by anything, anywhere. A frame that is not in use drops them entirely; the + -- roster events that would bring it back are registered plainly, and + -- visibilityscan re-runs this every 0.2s regardless, so it recovers on its own. + self:RegisterUnitEvents(visibility ~= "hide" and self.unitstr or nil) + -- vanilla visibility if self.unitname then self:Show() @@ -624,11 +634,14 @@ function pfUI.uf:UpdateConfig() f.feedbackText:ClearAllPoints() f.feedbackText:SetPoint("CENTER", f.portrait, "CENTER") end - f:RegisterEvent("UNIT_COMBAT") + f.combatfeedback = true else f.feedbackText:Hide() - f:UnregisterEvent("UNIT_COMBAT") + f.combatfeedback = nil end + -- RegisterUnitEvents owns UNIT_COMBAT; clearing the cached unit makes the + -- next UpdateVisibility re-run it against the new combatfeedback state. + f.eventunit = nil f.hpLeftText:SetFontObject(GameFontWhite) f.hpLeftText:SetFont(fontname, fontsize, fontstyle) @@ -918,6 +931,9 @@ function pfUI.uf:UpdateConfig() f:UpdateFrameSize() else f:UnregisterAllEvents() + -- that dropped the unit filters along with the registrations, so the cache + -- has to go too or the next UpdateVisibility believes they are still set + f.eventunit = nil f:Hide() end end @@ -959,6 +975,10 @@ function pfUI.uf.OnEvent() this:UnregisterAllEvents() this:SetScript("OnEvent", nil) this:SetScript("OnUpdate", nil) + -- visibilityscan is a separate frame and keeps ticking, so leaving this one + -- on its list would have UpdateVisibility re-register the unit events we + -- just dropped -- straight back into the crash 132 this branch prevents + visibilityscan.frames[this] = nil return end @@ -1025,8 +1045,12 @@ function pfUI.uf.OnEvent() this.update_aura = true elseif this.label == "pet" and event == "UNIT_HAPPINESS" then this.update_full = true - -- UNIT_XXX Events - elseif arg1 and (arg1 == this.label .. this.id or (UnitGUID and arg1 == UnitGUID(this.label .. this.id))) then + -- UNIT_XXX Events. RegisterUnitEvents means arg1 can only be this frame's own + -- unit; the compare is kept for the case the filter sits out, which is when + -- arg1 is not a string. The old GUID alternative is gone: these events fire + -- once per token that resolves to the unit AND once with the raw GUID, so the + -- token form always arrives and the GUID form was only ever a duplicate wake. + elseif arg1 and arg1 == this.unitstr then if event == "UNIT_PORTRAIT_UPDATE" or event == "UNIT_MODEL_CHANGED" then this.update_portrait = true elseif event == "UNIT_AURA" then @@ -1251,25 +1275,56 @@ function pfUI.uf.OnUpdate() end end +-- The unit events whose arg1 is the frame's OWN unit -- everything the OnEvent +-- routes through its "UNIT_XXX Events" branch. These are not registered here: +-- UpdateVisibility owns them, because it owns the frame's unit (below). +-- +-- UNIT_PET and UNIT_HAPPINESS are deliberately absent. Their branches key on +-- the frame's label, and UNIT_PET's arg1 is the pet's OWNER ("player" for a +-- "pet" frame), so filtering them by the frame's own unit would drop them. +local UNIT_EVENTS = { + "UNIT_DISPLAYPOWER", + "UNIT_HEALTH", "UNIT_MAXHEALTH", + "UNIT_MANA", "UNIT_MAXMANA", + "UNIT_RAGE", "UNIT_MAXRAGE", + "UNIT_ENERGY", "UNIT_MAXENERGY", + "UNIT_FOCUS", + "UNIT_PORTRAIT_UPDATE", "UNIT_MODEL_CHANGED", + "UNIT_FACTION", + "UNIT_AURA", -- frame=buff, frame=debuff +} + +-- Point this frame's unit-event subscriptions at `unitstr`, or drop them when +-- the frame has no unit. Cheap to call repeatedly: it no-ops unless the unit +-- actually changed, which matters because visibilityscan runs UpdateVisibility +-- for every frame five times a second. +function pfUI.uf:RegisterUnitEvents(unitstr) + if self.eventunit == unitstr then return end + self.eventunit = unitstr + + for i = 1, table.getn(UNIT_EVENTS) do + if unitstr then + self:RegisterUnitEvent(UNIT_EVENTS[i], unitstr) + else + self:UnregisterEvent(UNIT_EVENTS[i]) + end + end + + -- UNIT_COMBAT rides along only while the frame draws combat feedback text. + -- UpdateConfig clears eventunit when it toggles that, so the next + -- UpdateVisibility re-runs this. + if unitstr and self.combatfeedback then + self:RegisterUnitEvent("UNIT_COMBAT", unitstr) + else + self:UnregisterEvent("UNIT_COMBAT") + end +end + function pfUI.uf:EnableEvents() local f = self f:RegisterEvent("PLAYER_ENTERING_WORLD") f:RegisterEvent("PLAYER_LOGOUT") - f:RegisterEvent("UNIT_DISPLAYPOWER") - f:RegisterEvent("UNIT_HEALTH") - f:RegisterEvent("UNIT_MAXHEALTH") - f:RegisterEvent("UNIT_MANA") - f:RegisterEvent("UNIT_MAXMANA") - f:RegisterEvent("UNIT_RAGE") - f:RegisterEvent("UNIT_MAXRAGE") - f:RegisterEvent("UNIT_ENERGY") - f:RegisterEvent("UNIT_MAXENERGY") - f:RegisterEvent("UNIT_FOCUS") - f:RegisterEvent("UNIT_PORTRAIT_UPDATE") - f:RegisterEvent("UNIT_MODEL_CHANGED") - f:RegisterEvent("UNIT_FACTION") - f:RegisterEvent("UNIT_AURA") -- frame=buff, frame=debuff f:RegisterEvent("PLAYER_AURAS_CHANGED") -- label=player && frame=buff f:RegisterEvent("PLAYER_EQUIPMENT_CHANGED") -- label=player && frame=buff (ClassicAPI: weapon-enchant buffs) f:RegisterEvent("PARTY_MEMBERS_CHANGED") -- label=party, frame=leaderIcon @@ -1387,6 +1442,7 @@ function pfUI.uf:CreateUnitFrame(unit, id, config, tick) f.UpdateConfig = pfUI.uf.UpdateConfig f.EnableScripts = pfUI.uf.EnableScripts f.EnableEvents = pfUI.uf.EnableEvents + f.RegisterUnitEvents = pfUI.uf.RegisterUnitEvents f.EnableClickCast = pfUI.uf.EnableClickCast f.GetColor = pfUI.uf.GetColor @@ -1503,6 +1559,9 @@ function pfUI.uf:CreateUnitFrame(unit, id, config, tick) f:UpdateFrameSize() else f:UnregisterAllEvents() + -- that dropped the unit filters along with the registrations, so the cache + -- has to go too or the next UpdateVisibility believes they are still set + f.eventunit = nil f:Hide() end diff --git a/env/translations_deDE.lua b/env/translations_deDE.lua index 108601bb..b0492a27 100644 --- a/env/translations_deDE.lua +++ b/env/translations_deDE.lua @@ -670,7 +670,6 @@ pfUI_translation["deDE"] = { ["Scale"] = nil, ["Scale Border On HiDPI Displays"] = nil, ["Scaling"] = nil, - ["Scan Macros For Spells"] = nil, ["Screen Edge Glow Intensity"] = nil, ["Screen Resolution"] = nil, ["Screenshot"] = nil, diff --git a/env/translations_enUS.lua b/env/translations_enUS.lua index 4b0bfb90..64feeea7 100644 --- a/env/translations_enUS.lua +++ b/env/translations_enUS.lua @@ -683,7 +683,6 @@ pfUI_translation["enUS"] = { ["Scale"] = nil, ["Scale Border On HiDPI Displays"] = nil, ["Scaling"] = nil, - ["Scan Macros For Spells"] = nil, ["Screen Edge Glow Intensity"] = nil, ["Screen Resolution"] = nil, ["Screenshot"] = nil, diff --git a/env/translations_esES.lua b/env/translations_esES.lua index 0bb0fc3d..dade5647 100644 --- a/env/translations_esES.lua +++ b/env/translations_esES.lua @@ -670,7 +670,6 @@ pfUI_translation["esES"] = { ["Scale"] = "Escala", ["Scale Border On HiDPI Displays"] = "Escalar los bordes en las pantallas con DPI alto", ["Scaling"] = "Escalada", - ["Scan Macros For Spells"] = nil, ["Screen Edge Glow Intensity"] = "Intensidad de brillo en los bordes de la pantalla", ["Screen Resolution"] = "Resolución de pantalla", ["Screenshot"] = "Captura de pantalla", diff --git a/env/translations_frFR.lua b/env/translations_frFR.lua index c8f250bf..b041ae74 100644 --- a/env/translations_frFR.lua +++ b/env/translations_frFR.lua @@ -670,7 +670,6 @@ pfUI_translation["frFR"] = { ["Scale"] = "Échelle", ["Scale Border On HiDPI Displays"] = "Échelle de bordure sur les écrans HiDPI", ["Scaling"] = "Mise à l'échelle", - ["Scan Macros For Spells"] = nil, ["Screen Edge Glow Intensity"] = nil, ["Screen Resolution"] = "Résolution d'écran", ["Screenshot"] = "Imprime écran", diff --git a/env/translations_koKR.lua b/env/translations_koKR.lua index bb05b818..1a8f7799 100644 --- a/env/translations_koKR.lua +++ b/env/translations_koKR.lua @@ -670,7 +670,6 @@ pfUI_translation["koKR"] = { ["Scale"] = nil, ["Scale Border On HiDPI Displays"] = nil, ["Scaling"] = nil, - ["Scan Macros For Spells"] = nil, ["Screen Edge Glow Intensity"] = nil, ["Screen Resolution"] = "화면 해상도", ["Screenshot"] = nil, diff --git a/env/translations_ruRU.lua b/env/translations_ruRU.lua index 595ed9b8..c2a14231 100644 --- a/env/translations_ruRU.lua +++ b/env/translations_ruRU.lua @@ -670,7 +670,6 @@ pfUI_translation["ruRU"] = { ["Scale"] = "Масштаб", ["Scale Border On HiDPI Displays"] = "Масштабировать границы на HiDPI мониторах", ["Scaling"] = "Масштаб интерфейса", - ["Scan Macros For Spells"] = nil, ["Screen Edge Glow Intensity"] = "Интенсивность свечения на краях экрана", ["Screen Resolution"] = "Разрешение экрана", ["Screenshot"] = "Снимок экрана", diff --git a/env/translations_zhCN.lua b/env/translations_zhCN.lua index 6e3467fe..670bbd00 100644 --- a/env/translations_zhCN.lua +++ b/env/translations_zhCN.lua @@ -670,7 +670,6 @@ pfUI_translation["zhCN"] = { ["Scale"] = "比例", ["Scale Border On HiDPI Displays"] = "缩放高DPI显示器上的边框", ["Scaling"] = "UI缩放", - ["Scan Macros For Spells"] = "扫描宏命令中的法术", ["Screen Edge Glow Intensity"] = "屏幕边缘发光强度", ["Screen Resolution"] = "屏幕分辨率", ["Screenshot"] = "屏幕截图", diff --git a/env/translations_zhTW.lua b/env/translations_zhTW.lua index 6438d0cd..698155cd 100644 --- a/env/translations_zhTW.lua +++ b/env/translations_zhTW.lua @@ -670,7 +670,6 @@ pfUI_translation["zhTW"] = { ["Scale"] = "比例", ["Scale Border On HiDPI Displays"] = nil, ["Scaling"] = nil, - ["Scan Macros For Spells"] = nil, ["Screen Edge Glow Intensity"] = nil, ["Screen Resolution"] = "螢幕解析度", ["Screenshot"] = nil, diff --git a/init/modules.xml b/init/modules.xml index eeecf935..8189e378 100644 --- a/init/modules.xml +++ b/init/modules.xml @@ -70,7 +70,6 @@ - diff --git a/libs/libhealth.lua b/libs/libhealth.lua index 4060557e..f1bc01d0 100644 --- a/libs/libhealth.lua +++ b/libs/libhealth.lua @@ -10,8 +10,8 @@ local libhealth = CreateFrame("Frame") libhealth.enabled = true libhealth.reqhit = 4 libhealth.reqdmg = 5 -libhealth:RegisterEvent("UNIT_HEALTH") -libhealth:RegisterEvent("UNIT_COMBAT") +libhealth:RegisterUnitEvent("UNIT_HEALTH", "target") +libhealth:RegisterUnitEvent("UNIT_COMBAT", "target") libhealth:RegisterEvent("PLAYER_TARGET_CHANGED") libhealth:RegisterEvent("PLAYER_ENTERING_WORLD") libhealth:SetScript("OnEvent", function() diff --git a/libs/libpredict.lua b/libs/libpredict.lua index 27fa98b4..2943f233 100644 --- a/libs/libpredict.lua +++ b/libs/libpredict.lua @@ -1134,7 +1134,7 @@ libpredict.sender:RegisterEvent("SPELL_HEAL_BY_SELF") libpredict.sender:RegisterEvent("SPELL_HEAL_BY_OTHER") -- populates foreignCache for other healers -- force cache updates -libpredict.sender:RegisterEvent("UNIT_INVENTORY_CHANGED") +libpredict.sender:RegisterUnitEvent("UNIT_INVENTORY_CHANGED", "player") libpredict.sender:RegisterEvent("SKILL_LINES_CHANGED") -- Shared cleanup helper for failed/interrupted casts diff --git a/modules/actionbar.lua b/modules/actionbar.lua index 896ee80f..8d95b32a 100644 --- a/modules/actionbar.lua +++ b/modules/actionbar.lua @@ -360,68 +360,6 @@ pfUI:RegisterModule("actionbar", function () end end - local function ButtonMacroScan(self) - if self.bar > 10 then return end - if not self.scanmacro then return end - if pfUI.bars.skip_macro then return end - - -- SuperCleveRoidMacros: for macros it manages, leave spellslot/booktype unset - -- so the button's icon, cooldown, and tooltip flow through the hooked - -- GetActionTexture / GetActionCooldown / GameTooltip:SetAction and follow the - -- active conditional dynamically, instead of being frozen to the first - -- statically-scanned spell. - if CleveRoids and CleveRoids.IsManagedAction and CleveRoids.IsManagedAction(self.id) then - self.spellslot, self.booktype, self.spellID = nil, nil, nil - return - end - - local kind, slot = GetActionInfo(self.id) - self.spellslot, self.booktype, self.spellID = nil, nil, nil - if kind == 'macro' then - local name, _, body = GetMacroInfo(slot) - - if name and body then - local match - - for line in gfind(body, "[^%\n]+") do - _, _, match = string.find(line, '^#showtooltip (.+)') - - -- allow the user to disable the scan - if match and strfind(match, "disable") then - return - end - - if not match then - -- add support to specify custom tooltips via: - -- /run --showtooltip SPELLNAME - _, _, match = string.find(line, '%-%-showtooltip (.+)') - end - - if not match then - _, _, match = string.find(line, '^/cast (.+)') - end - - if not match then - _, _, match = string.find(line, '^/pfcast (.+)') - end - - if not match then - _, _, match = string.find(line, '^/pfmouse (.+)') - end - - if not match then - _, _, match = string.find(line, 'CastSpellByName%(%"(.+)%"%)') - end - - if match then - self.spellslot, self.booktype, self.spellID = select(7, libspell.GetSpellInfo(match)) - if self.spellslot and self.spellslot > 0 then return end - end - end - end - end - end - local function ButtonEnter(self) self = self or this @@ -690,7 +628,6 @@ pfUI:RegisterModule("actionbar", function () local function ButtonFullUpdate(button) if not button then return end - ButtonMacroScan(button) ButtonSlotUpdate(button) ButtonRangeUpdate(button) ButtonUsableUpdate(button) @@ -807,10 +744,28 @@ pfUI:RegisterModule("actionbar", function () -- create the main event and update handler for pfUI actionbars local bars = CreateFrame("Frame", "pfActionBar", UIParent) - for event in pairs(special_events) do bars:RegisterEvent(event) end - for event in pairs(global_events) do bars:RegisterEvent(event) end - for event in pairs(aura_events) do bars:RegisterEvent(event) end - for event in pairs(pet_events) do bars:RegisterEvent(event) end + + -- The only unit events in the tables above; both concern the player alone. + -- A registration keeps its kind, so these have to go in unit-filtered from + -- the start -- RegisterUnitEvent over a plain registration stays plain. + local event_units = { + ["UNIT_INVENTORY_CHANGED"] = "player", + ["UNIT_PET"] = "player", + } + + local function RegisterBarEvent(event) + local unit = event_units[event] + if unit then + bars:RegisterUnitEvent(event, unit) + else + bars:RegisterEvent(event) + end + end + + for event in pairs(special_events) do RegisterBarEvent(event) end + for event in pairs(global_events) do RegisterBarEvent(event) end + for event in pairs(aura_events) do RegisterBarEvent(event) end + for event in pairs(pet_events) do RegisterBarEvent(event) end -- refresh actionbar buttons on event bars:SetScript("OnEvent", BarsEvent) @@ -1148,12 +1103,7 @@ pfUI:RegisterModule("actionbar", function () f.count:SetJustifyH("RIGHT") f.count:SetJustifyV("BOTTOM") - -- macro spell scan (disabled when macro addons are loaded) - if C.bars.macroscan == "0" or pfUI:MacroAddonsLoaded() then - f.scanmacro, f.spellslot, f.booktype = nil, nil, nil - else - f.scanmacro = true - end + f.scanmacro, f.spellslot, f.booktype = nil, nil, nil -- range glow color f.rangeColor = GetStringColorObject(C.bars.rangecolor) diff --git a/modules/buff.lua b/modules/buff.lua index beff5dc3..4bd730e5 100644 --- a/modules/buff.lua +++ b/modules/buff.lua @@ -164,7 +164,7 @@ pfUI:RegisterModule("buff", function () pfUI.buff = CreateFrame("Frame", "pfGlobalBuffFrame", UIParent) pfUI.buff:RegisterEvent("PLAYER_AURAS_CHANGED") pfUI.buff:RegisterEvent("PLAYER_EQUIPMENT_CHANGED") - pfUI.buff:RegisterEvent("UNIT_MODEL_CHANGED") + pfUI.buff:RegisterUnitEvent("UNIT_MODEL_CHANGED", "player") pfUI.buff:RegisterEvent("BUFF_UPDATE_DURATION_SELF") pfUI.buff:RegisterEvent("DEBUFF_UPDATE_DURATION_SELF") pfUI.buff:SetScript("OnEvent", function() diff --git a/modules/castbar.lua b/modules/castbar.lua index fb998cc1..d52e7ba0 100644 --- a/modules/castbar.lua +++ b/modules/castbar.lua @@ -322,15 +322,21 @@ pfUI:RegisterModule("castbar", function () -- casts only ever fire arg1=="player" -- when the bar's unit resolves to the -- player (target=self). PLAYER_TARGET/FOCUS_CHANGED re-polls so a unit -- already mid-cast when it becomes the target/focus still shows. - cb:RegisterEvent("UNIT_SPELLCAST_START") - cb:RegisterEvent("UNIT_SPELLCAST_STOP") - cb:RegisterEvent("UNIT_SPELLCAST_FAILED") - cb:RegisterEvent("UNIT_SPELLCAST_INTERRUPTED") - cb:RegisterEvent("UNIT_SPELLCAST_DELAYED") - cb:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED") - cb:RegisterEvent("UNIT_SPELLCAST_CHANNEL_START") - cb:RegisterEvent("UNIT_SPELLCAST_CHANNEL_STOP") - cb:RegisterEvent("UNIT_SPELLCAST_CHANNEL_UPDATE") + -- Filter to this bar's unit, plus "player" for the target/focus bars: the + -- player's own casts only ever fire arg1=="player", so a self-targeted cast + -- has to reach them too (nil for the player bar itself, which the filter + -- skips). This only narrows what arrives -- the arg1/UnitIsUnit test below + -- still decides whether the bar acts on it. + local selfunit = unitstr ~= "player" and "player" or nil + cb:RegisterUnitEvent("UNIT_SPELLCAST_START", unitstr, selfunit) + cb:RegisterUnitEvent("UNIT_SPELLCAST_STOP", unitstr, selfunit) + cb:RegisterUnitEvent("UNIT_SPELLCAST_FAILED", unitstr, selfunit) + cb:RegisterUnitEvent("UNIT_SPELLCAST_INTERRUPTED", unitstr, selfunit) + cb:RegisterUnitEvent("UNIT_SPELLCAST_DELAYED", unitstr, selfunit) + cb:RegisterUnitEvent("UNIT_SPELLCAST_SUCCEEDED", unitstr, selfunit) + cb:RegisterUnitEvent("UNIT_SPELLCAST_CHANNEL_START", unitstr, selfunit) + cb:RegisterUnitEvent("UNIT_SPELLCAST_CHANNEL_STOP", unitstr, selfunit) + cb:RegisterUnitEvent("UNIT_SPELLCAST_CHANNEL_UPDATE", unitstr, selfunit) if unitstr == "target" then cb:RegisterEvent("PLAYER_TARGET_CHANGED") elseif unitstr == "focus" then diff --git a/modules/combopoints.lua b/modules/combopoints.lua index 09c52a20..2704c302 100644 --- a/modules/combopoints.lua +++ b/modules/combopoints.lua @@ -52,7 +52,7 @@ pfUI:RegisterModule("combopoints", function () -- combo if class == "DRUID" or class == "ROGUE" then local combo = CreateFrame("Frame") - combo:RegisterEvent("UNIT_COMBO_POINTS") + combo:RegisterUnitEvent("UNIT_COMBO_POINTS", "player") combo:RegisterEvent("PLAYER_COMBO_POINTS") combo:RegisterEvent("PLAYER_TARGET_CHANGED") combo:RegisterEvent("PLAYER_ENTERING_WORLD") diff --git a/modules/energytick.lua b/modules/energytick.lua index ad7d7fab..ce787798 100644 --- a/modules/energytick.lua +++ b/modules/energytick.lua @@ -1,20 +1,82 @@ -local function getAdjustedTickTimer() - local adjustedEnergyTick = 2 +-- One server clock drives every power: Player::RegenerateAll fires every +-- REGEN_TIME_FULL (2s), re-arms with `+=`, and is never reset by casting. The +-- five-second rule (SetLastManaUse on any mana-costing cast) changes what a tick +-- pays, never when it lands; mp5 and the player's MOD_MANA_REGEN_INTERRUPT share +-- still come in. That share can't be computed here -- item sources are equip +-- auras absent from the buff list and m_modManaRegenInterrupt is never sent -- +-- so the spark shows it instead: dim through the window until a tick lands. +-- +-- The sweep free-runs on that clock and phase-locks to observed gains. A gain +-- mid-sweep (Illumination, Judgement of Wisdom, potions, a Mana Spring totem on +-- its own phase) is not the tick and never moves it. - -- Check rogue talents and compute energy tick timing reduction for Combat spec (1.18.0 Blade Rush Talent) - if UnitClassBase("player") == "ROGUE" then - local _, _, _, _, currRank = GetTalentInfo(2, 16) - local bladeRushRank = currRank or 0 +local FIVE_SECOND_RULE = 5 - if bladeRushRank > 0 then - local agility = UnitStat("player", 2) -- 2 is agility stat index - local reductionPerAgi = 0.0006 * bladeRushRank -- 0.0006 for rank 1, 0.0012 for rank 2 - local totalReduction = agility * reductionPerAgi - adjustedEnergyTick = adjustedEnergyTick - totalReduction +-- gains farther than this from the predicted boundary are not the tick +local TICK_TOLERANCE = .25 + +-- arrival jitter; a tick inside this band confirms the sweep rather than +-- re-anchoring it, or the spark hitches at every wrap +local TICK_JITTER = .08 + +-- Player::RegenerateAll: +-- mod = GetTotalAuraModifier(SPELL_AURA_MOD_ENERGY_REGEN_TIME) +-- if mod > 0 then mod = mod * agility / 10 end +-- m_regenTimer += max(1, REGEN_TIME_FULL - mod) -- milliseconds +local REGEN_TIME_FULL = 2 +local ENERGY_REGEN_TIME_AURA = 217 -- SPELL_AURA_MOD_ENERGY_REGEN_TIME + +-- fixed magnitude is basePoints + baseDice (stored 11 -> 12); a die above 1 is +-- a roll the client can't know, so it counts as nothing rather than a guess +local amountCache = {} +local function auraAmount(spellID) + local amount = amountCache[spellID] + if amount then return amount end + amount = 0 + local effects = C_Spell.GetSpellEffectInfo(spellID) -- nil for an id with no record + if effects then + for i = 1, 3 do + local fx = effects[i] + if fx.auraName == ENERGY_REGEN_TIME_AURA and fx.dieSides <= 1 then + amount = fx.basePoints + fx.baseDice + break + end end end + amountCache[spellID] = amount + return amount +end - return adjustedEnergyTick +-- A passive is in effect exactly while known (current rank only, never in the +-- buff list); anything castable or cast on us counts only while it is up. +local function getEnergyRegenTimeMod() + local sum = 0 + for _, spellID in ipairs(C_SpellBook.GetPlayerSpellsByAura(ENERGY_REGEN_TIME_AURA)) do + if C_Spell.IsSpellPassive(spellID) then + sum = sum + auraAmount(spellID) + end + end + for i = 1, 32 do + local spellID = select(10, C_UnitAuras.UnitAura("player", i, "HELPFUL")) + if not spellID then break end + sum = sum + auraAmount(spellID) + end + return sum +end + +-- cleared on SPELLS_CHANGED (passives) and PLAYER_AURAS_CHANGED (buffs), and +-- recomputed by the next tick that asks. Agility stays live: it's one call. +local energyRegenTimeMod + +local function getAdjustedTickTimer() + if not energyRegenTimeMod then + energyRegenTimeMod = getEnergyRegenTimeMod() + end + if energyRegenTimeMod == 0 then return REGEN_TIME_FULL end + + -- ms on the server, seconds here; the 1ms floor is the server's and this is a divisor + local reduction = energyRegenTimeMod * UnitStat("player", 2) / 10000 + return math.max(0.001, REGEN_TIME_FULL - reduction) end pfUI:RegisterModule("energytick", function() @@ -22,14 +84,53 @@ pfUI:RegisterModule("energytick", function() return end + -- inside the module body on purpose: C is on pfUI.env, not _G + local function getBarWidth() + return C.unitframes.player.pwidth ~= "-1" and C.unitframes.player.pwidth or C.unitframes.player.width + end + + -- was this gain the regen tick? if so, re-anchor the sweep on it + local function lockTick(frame) + local now, period = GetTime(), getAdjustedTickTimer() + + if frame.start then + -- signed distance to the nearest predicted boundary + local err = mod(now - frame.start, period) + if err > period / 2 then err = err - period end + + if math.abs(err) <= TICK_TOLERANCE then + -- correct only what lies beyond normal jitter + if err > TICK_JITTER then + frame.start = frame.start + (err - TICK_JITTER) + elseif err < -TICK_JITTER then + frame.start = frame.start + (err + TICK_JITTER) + end + frame.max, frame.rejected = period, nil + return true + end + + -- two rejected gains one period apart are the real clock: relock to it + local periodic = frame.rejected and math.abs(now - frame.rejected - period) <= TICK_TOLERANCE + if not periodic then + frame.rejected = now + return false + end + end + + frame.start, frame.max, frame.rejected = now, period, nil + return true + end + local energytick = CreateFrame("Frame", nil, pfUI.uf.player.power.bar) energytick:SetAllPoints(pfUI.uf.player.power.bar) energytick:RegisterEvent("PLAYER_ENTERING_WORLD") - energytick:RegisterEvent("UNIT_DISPLAYPOWER") - energytick:RegisterEvent("UNIT_ENERGY") - energytick:RegisterEvent("UNIT_MANA") - energytick:RegisterEvent("CHAT_MSG_SPELL_SELF_BUFF") - energytick:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_SELF_BUFFS") + energytick:RegisterUnitEvent("UNIT_DISPLAYPOWER", "player") + energytick:RegisterUnitEvent("UNIT_ENERGY", "player") + energytick:RegisterUnitEvent("UNIT_MANA", "player") + energytick:RegisterUnitEvent("UNIT_SPELLCAST_SUCCEEDED", "player") + energytick:RegisterUnitEvent("UNIT_SPELLCAST_CHANNEL_STOP", "player") + energytick:RegisterEvent("SPELLS_CHANGED") + energytick:RegisterEvent("PLAYER_AURAS_CHANGED") energytick:SetScript("OnEvent", function() if UnitPowerType("player") == Enum.PowerType.Mana and C.unitframes.player.manatick == "1" then @@ -42,44 +143,52 @@ pfUI:RegisterModule("energytick", function() this:Hide() end - -- Filter nur eigene Energy-Gewinne von Talents/Buffs - if event == "CHAT_MSG_SPELL_SELF_BUFF" or event == "CHAT_MSG_SPELL_PERIODIC_SELF_BUFFS" then - if string.find(arg1, "You gain") and string.find(arg1, "Energy from") then - this.ignoreNextGain = true - end + if event == "SPELLS_CHANGED" or event == "PLAYER_AURAS_CHANGED" then + energyRegenTimeMod = nil return end if event == "PLAYER_ENTERING_WORLD" then - this.lastMana = UnitPower("player") + this.lastPower = UnitPower("player") + end + + -- the rule arms on the cast (Spell::TakePower: mana powerType, cost > 0), + -- not on a mana drop -- Mana Burn lowers mana without arming it + if event == "UNIT_SPELLCAST_SUCCEEDED" and arg1 == "player" then + local cost = C_Spell.GetSpellPowerCost(arg3) + cost = cost and cost[1] + if cost and cost.type == Enum.PowerType.Mana and cost.cost > 0 then + this.fsrSpell, this.fsrEnd = arg3, GetTime() + FIVE_SECOND_RULE + this.fsrGain = nil + end + return + end + + -- Unit::Update won't expire the rule while the spending spell still channels + if event == "UNIT_SPELLCAST_CHANNEL_STOP" and arg1 == "player" then + if this.fsrSpell and this.fsrSpell == arg3 then + this.fsrEnd, this.fsrGain = GetTime() + FIVE_SECOND_RULE, nil + end + return end if (event == "UNIT_MANA" or event == "UNIT_ENERGY") and arg1 == "player" then - this.currentMana = UnitPower("player") - local diff = 0 - if this.lastMana then - diff = this.currentMana - this.lastMana + local power = UnitPower("player") + local diff = this.lastPower and (power - this.lastPower) or 0 + this.lastPower = power + + -- only a gain can be the tick; a spend never touches the phase + if diff > 0 and lockTick(this) then + -- a tick inside the window proves regen continues through it + if this.fsrEnd and this.fsrEnd > GetTime() then + this.fsrGain = true + end end - if this.mode == "MANA" and diff < 0 then - this.target = 5 - elseif this.mode == "MANA" and diff > 0 then - if UnitPower("player") >= UnitPowerMax("player") then - this.start = nil - this.spark:SetAlpha(0) - this:Hide() - elseif this.max ~= 5 and diff > (this.badtick and this.badtick * 1.2 or 5) then - this.target = 2 - else - this.badtick = diff - end - elseif this.mode == "ENERGY" and diff >= 0 then - if not this.ignoreNextGain then - this.target = getAdjustedTickTimer() - end - this.ignoreNextGain = false + -- phase is kept while hidden; OnUpdate catches up by whole periods + if this.mode == "MANA" and power >= UnitPowerMax("player") then + this:Hide() end - this.lastMana = this.currentMana end end) @@ -90,37 +199,53 @@ pfUI:RegisterModule("energytick", function() end this.tick = GetTime() + 0.020 -- ~50 FPS - if this.target then - this.start, this.max = GetTime(), this.target - this.target = nil - this.spark:SetAlpha(1) - this:Show() + -- five-second rule drains to nothing + local remaining = this.fsrEnd and (this.fsrEnd - GetTime()) or 0 + if this.mode == "MANA" and remaining > 0 then + this.fsrbar:SetWidth(getBarWidth() * remaining / FIVE_SECOND_RULE) + this.fsrbar:Show() + else + this.fsrSpell, this.fsrEnd, this.fsrGain = nil, nil, nil + this.fsrbar:Hide() end if not this.start then + this.spark:SetAlpha(0) + return + end + + if this.mode == "MANA" and UnitPower("player") >= UnitPowerMax("player") then + this.spark:SetAlpha(0) return end this.current = GetTime() - this.start + -- roll over by whole periods, not from now: restarting bakes frame + -- overshoot into the phase as drift the lock then has to chase if this.current > this.max then - -- Don't restart tick timer if mana is full - if this.mode == "MANA" and UnitPower("player") >= UnitPowerMax("player") then - this.start = nil - this.spark:SetAlpha(0) - return - end - this.start, this.max, this.current = GetTime(), getAdjustedTickTimer(), 0 + this.start = this.start + this.max * math.floor(this.current / this.max) + this.max = getAdjustedTickTimer() + this.current = GetTime() - this.start end - local pos = (C.unitframes.player.pwidth ~= "-1" and C.unitframes.player.pwidth or C.unitframes.player.width) - * (this.current / this.max) + -- dim while the rule is up and nothing has ticked inside it yet + this.spark:SetAlpha((remaining > 0 and not this.fsrGain) and .4 or 1) + if not C.unitframes.player.pheight then return end + + local pos = getBarWidth() * (this.current / this.max) this.spark:SetPoint("LEFT", pos - ((C.unitframes.player.pheight + 5) / 2), 0) end) + energytick.fsrbar = energytick:CreateTexture(nil, "ARTWORK") + energytick.fsrbar:SetTexture(1, 1, 1, .15) + energytick.fsrbar:SetPoint("TOPLEFT", 0, 0) + energytick.fsrbar:SetPoint("BOTTOMLEFT", 0, 0) + energytick.fsrbar:Hide() + energytick.spark = energytick:CreateTexture(nil, "OVERLAY") energytick.spark:SetTexture("Interface\\CastingBar\\UI-CastingBar-Spark") energytick.spark:SetHeight(C.unitframes.player.pheight + 15) @@ -133,4 +258,4 @@ pfUI:RegisterModule("energytick", function() energytick.spark:SetWidth(C.unitframes.player.pheight + 5) hookUpdateConfig(pfUI.uf.player) end -end) \ No newline at end of file +end) diff --git a/modules/gui.lua b/modules/gui.lua index 7ebe729a..8dcd85e2 100644 --- a/modules/gui.lua +++ b/modules/gui.lua @@ -2556,9 +2556,6 @@ pfUI:RegisterModule("gui", function () CreateConfig(U["bars"], T["Button Animation"], C.bars, "animation", "dropdown", pfUI.gui.dropdowns.actionbuttonanimations) CreateConfig(U["bars"], T["Button Animation Trigger"], C.bars, "animmode", "dropdown", pfUI.gui.dropdowns.animationmode) CreateConfig(U["bars"], T["Show Animation On Hidden Bars"], C.bars, "animalways", "checkbox") - if not pfUI:MacroAddonsLoaded() then - CreateConfig(U["bars"], T["Scan Macros For Spells"], C.bars, "macroscan", "checkbox", nil, nil, nil, nil) - end CreateConfig(U["bars"], T["Show Reagent Count"], C.bars, "reagents", "checkbox") CreateConfig(U["bars"], T["Highlight Equipped Items"], C.bars, "showequipped", "checkbox") CreateConfig(U["bars"], T["Equipped Item Color"], C.bars, "eqcolor", "color") @@ -3029,8 +3026,7 @@ pfUI:RegisterModule("gui", function () CreateGUIEntry(T["Components"], T["Modules"], function() table.sort(pfUI.modules) for i,m in pairs(pfUI.modules) do - -- skip gui and macrotweak when macro addons are loaded - if m ~= "gui" and not (m == "macrotweak" and pfUI:MacroAddonsLoaded()) then + if m ~= "gui" then -- create disabled entry if not existing and display pfUI:UpdateConfig("disabled", nil, m, "0") CreateConfig(nil, T["Disable Module"] .. " " .. m, C.disabled, m, "checkbox") diff --git a/modules/macrotweak.lua b/modules/macrotweak.lua deleted file mode 100644 index 9169a5bc..00000000 --- a/modules/macrotweak.lua +++ /dev/null @@ -1,67 +0,0 @@ -pfUI:RegisterModule("macrotweak", function () - local conflictAddons = { "Supermacro", "SuperCleveRoidMacros", "UltimaMacros" } - local disabled = false - - for _, addon in pairs(conflictAddons) do - local name = addon - EventUtil.ContinueOnAddOnLoaded(name, function() - if not disabled then - DEFAULT_CHAT_FRAME:AddMessage("|cff33ffccpfUI|r: " .. name .. " found, macrotweak disabled.") - end - disabled = true - end) - end - - -- do not write macro calls into chat input history - -- (install once: _AddHistoryLine is our backup slot and is nil until we set it) - if not ChatFrameEditBox._AddHistoryLine then - local userinput - ChatFrameEditBox._AddHistoryLine = ChatFrameEditBox.AddHistoryLine - ChatFrameEditBox.AddHistoryLine = function(self, text) - if disabled then return ChatFrameEditBox._AddHistoryLine(self, text) end - if not userinput and text and string.find(text, "^/run(.+)") then return end - if not userinput and string.find(text, "^/script(.+)") then return end - if not userinput and string.find(text, "^/cast(.+)") then return end - ChatFrameEditBox._AddHistoryLine(self, text) - end - - local OnEnter = ChatFrameEditBox:GetScript("OnEnterPressed") - ChatFrameEditBox:SetScript("OnEnterPressed", function(a1,a2,a3,a4) - userinput = true - OnEnter(a1,a2,a3,a4) - userinput = nil - end) - end - - -- make sure #showtooltip inside macros won't be sent - local hookSendChatMessage = SendChatMessage - function _G.SendChatMessage(msg, ...) - if disabled then return hookSendChatMessage(msg, unpack(arg)) end - if msg and string.find(msg, "^#showtooltip ") then return end - hookSendChatMessage(msg, unpack(arg)) - end - - -- add /use and /equip to the macro api: - -- https://wowwiki.fandom.com/wiki/Making_a_macro - -- supported arguments: - -- /use - -- /use - -- /use - pfUI.api.RegisterSlashCommand("PFUSE", { "/equip" , "/use", "/pfequip", "/pfuse" }, function (msg) - if not msg or msg == "" then return end - local bag, slot, _ - if string.find(msg, "%d+%s+%d+") then - _, _, bag, slot = string.find(msg, "(%d+)%s+(%d+)") - elseif string.find(msg, "%d+") then - _, _, slot = string.find(msg, "(%d+)") - else - bag, slot = FindItem(msg) - end - - if bag and slot then - UseContainerItem(bag, slot) - elseif not bag and slot then - UseInventoryItem(slot) - end - end) -end) \ No newline at end of file diff --git a/modules/marktracking.lua b/modules/marktracking.lua index c06c53f4..4736495e 100644 --- a/modules/marktracking.lua +++ b/modules/marktracking.lua @@ -1,12 +1,9 @@ pfUI:RegisterModule("marktracking", function () - if not UnitExists("mark1") and not UnitExists("mark8") then - if not pcall(function() UnitExists("mark1") end) then return end - end - local rawborder, border = GetBorderSize() local markerOrder = { 8, 7, 6, 5, 4, 3, 2, 1 } -- skull, cross, square, moon, triangle, diamond, circle, star - local markerTokens = {} + local markerTokens = {} -- [i] = "markN" + local markerIndex = {} -- ["markN"] = i, for the event handler's arg1 local markerConfigKeys = { "raidmarkercolor_star", @@ -22,6 +19,7 @@ pfUI:RegisterModule("marktracking", function () local markerColors = {} for i, markKey in ipairs(markerConfigKeys) do markerTokens[i] = "mark" .. i + markerIndex[markerTokens[i]] = i local r, g, b, a = GetStringColor(C.unitframes[markKey]) markerColors[i] = { tonumber(r), tonumber(g), tonumber(b), tonumber(a) } end @@ -73,8 +71,7 @@ pfUI.marktracking = CreateFrame("Frame", "pfMarkTracking", UIParent) else pfUI.marktracking:SetPoint("TOP", UIParent, "CENTER", 0, 0) end - pfUI.marktracking:SetWidth(TOTAL_ROW_WIDTH) - pfUI.marktracking:SetHeight(8 * (ROW_HEIGHT + 1) + border * 2 - 1) + pfUI.marktracking:SetSize(TOTAL_ROW_WIDTH, 8 * (ROW_HEIGHT + 1) + border * 2 - 1) pfUI.marktracking:Hide() CreateBackdrop(pfUI.marktracking) @@ -284,27 +281,50 @@ pfUI.marktracking = CreateFrame("Frame", "pfMarkTracking", UIParent) -- Event-driven scanner frame local scanner = CreateFrame("Frame") + -- Fallback poll: catches units that come into range AFTER a marker was set + -- (no event fires for that case, so we need this safety net). UpdateDisplay + -- is a full eight-row rebuild, so it exists only while grouped -- raid markers + -- are a group feature and there is nothing to discover alone. The group events + -- below start and cancel it, so outside a group there is no timer queued at + -- all rather than one waking every second to return early. + -- + -- Deliberately NOT keyed on a mark being visible: a marker set on a unit that + -- is out of range shows no row, and that is exactly what this poll catches. + local poll + local function UpdatePoll() + local grouped = IsInGroup() + if grouped and not poll then + poll = C_Timer.NewTicker(FALLBACK_INTERVAL, UpdateDisplay) + elseif not grouped and poll then + poll:Cancel() + poll = nil + end + end + -- RAID_TARGET_UPDATE: a raid marker was set/cleared -> full refresh -- PLAYER_ENTERING_WORLD: login/reload/zone -> full refresh - -- UNIT_HEALTH/UNIT_MAXHEALTH: ClassicAPI fires these per token; with the mark - -- tokens observed they arrive as arg1 == "markN", so we refresh just that - -- one row (UpdateRow) instead of rescanning all eight. + -- PARTY_MEMBERS_CHANGED/RAID_ROSTER_UPDATE: joined or left a group -> the rows + -- can change, and the fallback poll starts or stops with it + -- UNIT_HEALTH/UNIT_MAXHEALTH: filtered to the eight mark tokens, so arg1 is + -- always "markN" and we refresh just that row (UpdateRow) instead of + -- rescanning all eight. scanner:RegisterEvent("RAID_TARGET_UPDATE") scanner:RegisterEvent("PLAYER_ENTERING_WORLD") - scanner:RegisterEvent("UNIT_HEALTH") - scanner:RegisterEvent("UNIT_MAXHEALTH") + scanner:RegisterEvent("PARTY_MEMBERS_CHANGED") + scanner:RegisterEvent("RAID_ROSTER_UPDATE") + scanner:RegisterUnitEvent("UNIT_HEALTH", "mark1", "mark2", "mark3", "mark4", "mark5", "mark6", "mark7", "mark8") + scanner:RegisterUnitEvent("UNIT_MAXHEALTH", "mark1", "mark2", "mark3", "mark4", "mark5", "mark6", "mark7", "mark8") scanner:SetScript("OnEvent", function() if event == "UNIT_HEALTH" or event == "UNIT_MAXHEALTH" then - -- arg1 is the token; "markN" -> N, nil for any non-mark token. - local i = arg1 and tonumber(string.match(arg1, "^mark(%d)")) + -- arg1 is one of the eight tokens we registered for, so this is a lookup + -- rather than a parse -- string.match would allocate a capture and + -- tonumber would parse it, on every health tick of every marked unit. + local i = arg1 and markerIndex[arg1] if i then UpdateRow(i) end return end + if event ~= "RAID_TARGET_UPDATE" then UpdatePoll() end UpdateDisplay() end) - - -- Fallback poll: catches units that come into range AFTER a marker was set - -- (no event fires for that case, so we need this safety net) - C_Timer.NewTicker(FALLBACK_INTERVAL, UpdateDisplay) end) \ No newline at end of file diff --git a/modules/minimap.lua b/modules/minimap.lua index 7351c225..0783a7b7 100644 --- a/modules/minimap.lua +++ b/modules/minimap.lua @@ -225,7 +225,7 @@ pfUI:RegisterModule("minimap", function () pfUI.minimap.pvpicon = CreateFrame("Frame", nil, pfUI.minimap) pfUI.minimap.pvpicon:Hide() pfUI.minimap.pvpicon:RegisterEvent("UPDATE_FACTION") - pfUI.minimap.pvpicon:RegisterEvent("UNIT_FACTION") + pfUI.minimap.pvpicon:RegisterUnitEvent("UNIT_FACTION", "player") pfUI.minimap.pvpicon:SetFrameStrata("HIGH") pfUI.minimap.pvpicon:SetSize(16, 16) pfUI.minimap.pvpicon:SetAlpha(.5) diff --git a/modules/nameplates.lua b/modules/nameplates.lua index 63394b0a..450dda63 100644 --- a/modules/nameplates.lua +++ b/modules/nameplates.lua @@ -5,10 +5,7 @@ pfUI:RegisterModule("nameplates", function () -- Local function references for performance local GetTime = GetTime local UnitName = UnitName - local UnitClass = UnitClass - local UnitLevel = UnitLevel local UnitIsPlayer = UnitIsPlayer - local UnitIsDead = UnitIsDead local UnitAffectingCombat = UnitAffectingCombat local UnitIsUnit = UnitIsUnit local UnitCanAssist = UnitCanAssist @@ -162,6 +159,24 @@ pfUI:RegisterModule("nameplates", function () cfg.debuffanim = tonumber(C.nameplates.debuffanim) or 0 cfg.debufftext = tonumber(C.nameplates.debufftext) or 1 + -- Throttle delays, resolved once instead of per plate per tick. + -- libthrottle:Get walks the saved-variable table, a defaults fallback and a + -- preset table, and can build a "_custom" key -- the per-plate + -- OnUpdate was calling it one or two times for every visible plate, a + -- hundred times a second, just to decide it had nothing to do. + -- + -- cfg.throttle_min is the floor across all four. No plate can ever be due + -- sooner than that, so the update can bail on it before working out which + -- category it actually belongs to. + cfg.throttle_target = pfUI.throttle:Get("nameplates_target") + cfg.throttle_mass = pfUI.throttle:Get("nameplates_mass") + cfg.throttle_normal = pfUI.throttle:Get("nameplates") + cfg.throttle_castbar = pfUI.throttle:Get("nameplates_castbar") + cfg.throttle_min = cfg.throttle_target + if cfg.throttle_mass < cfg.throttle_min then cfg.throttle_min = cfg.throttle_mass end + if cfg.throttle_normal < cfg.throttle_min then cfg.throttle_min = cfg.throttle_normal end + if cfg.throttle_castbar < cfg.throttle_min then cfg.throttle_min = cfg.throttle_castbar end + -- Rebuild offtanks lookup table offtanks = {} for k, v in pairs({strsplit("#", C.nameplates.combatofftanks)}) do @@ -490,7 +505,7 @@ local nameplates = CreateFrame("Frame", "pfNameplates", UIParent) nameplates:RegisterEvent("PLAYER_ENTERING_WORLD") nameplates:RegisterEvent("PLAYER_TARGET_CHANGED") nameplates:RegisterEvent("PLAYER_LOGOUT") -nameplates:RegisterEvent("UNIT_COMBO_POINTS") +nameplates:RegisterUnitEvent("UNIT_COMBO_POINTS", "player") nameplates:RegisterEvent("PLAYER_COMBO_POINTS") nameplates:RegisterEvent("ZONE_CHANGED_NEW_AREA") nameplates:RegisterEvent("RAID_ROSTER_UPDATE") @@ -498,13 +513,10 @@ nameplates:RegisterEvent("PARTY_MEMBERS_CHANGED") nameplates:RegisterEvent("NAME_PLATE_CREATED") nameplates:RegisterEvent("NAME_PLATE_UNIT_ADDED") nameplates:RegisterEvent("NAME_PLATE_UNIT_REMOVED") -nameplates:RegisterEvent("UNIT_AURA") -nameplates:RegisterEvent("UNIT_FLAGS") nameplates:RegisterEvent("UPDATE_MOUSEOVER_UNIT") -nameplates:RegisterEvent("UNIT_SPELLCAST_START") -nameplates:RegisterEvent("UNIT_SPELLCAST_CHANNEL_START") -nameplates:RegisterEvent("UNIT_SPELLCAST_STOP") -nameplates:RegisterEvent("UNIT_SPELLCAST_CHANNEL_STOP") +-- UNIT_AURA / UNIT_FLAGS / UNIT_SPELLCAST_* are registered per plate, on the +-- plate's own frame, against its own token -- see OnCreate and +-- NAME_PLATE_UNIT_ADDED. nameplates:RegisterEvent("PLAYER_GUILD_UPDATE") nameplates:SetScript("OnEvent", function() @@ -516,6 +528,15 @@ nameplates:RegisterEvent("PLAYER_GUILD_UPDATE") if nameplates.mouselook then nameplates.mouselook:SetScript("OnUpdate", nil) end + -- The plates hold their own unit subscriptions now, so silencing this + -- frame alone would leave them dispatching through logout -- exactly what + -- this branch exists to prevent. + for plate in pairs(registry) do + if plate.nameplate then + plate.nameplate:UnregisterAllEvents() + plate.nameplate:SetScript("OnEvent", nil) + end + end return elseif event == "PLAYER_GUILD_UPDATE" and arg1 == 'player' then @@ -591,6 +612,16 @@ nameplates:RegisterEvent("PLAYER_GUILD_UPDATE") local guid = UnitGUID(arg1) plate.nameplate.cachedGuid = guid plate.nameplate.unit = arg1 + + -- Point this plate's own subscriptions at the token it just took. On a + -- recycled frame these replace the previous unit rather than stacking: + -- RegisterUnitEvent on an already-filtered registration swaps the units. + plate.nameplate:RegisterUnitEvent("UNIT_AURA", arg1) + plate.nameplate:RegisterUnitEvent("UNIT_FLAGS", arg1) + plate.nameplate:RegisterUnitEvent("UNIT_SPELLCAST_START", arg1) + plate.nameplate:RegisterUnitEvent("UNIT_SPELLCAST_CHANNEL_START", arg1) + plate.nameplate:RegisterUnitEvent("UNIT_SPELLCAST_STOP", arg1) + plate.nameplate:RegisterUnitEvent("UNIT_SPELLCAST_CHANNEL_STOP", arg1) plate.nameplate.creatureType = nil -- recompute for the new unit plate.nameplate.totemIcon = nil plate.nameplate.totemSpell = nil @@ -621,14 +652,9 @@ nameplates:RegisterEvent("PLAYER_GUILD_UPDATE") if plate and plate.nameplate and plate.nameplate.cachedGuid == guid then plate.nameplate.cachedGuid = nil plate.nameplate.unit = nil - end - end - - elseif event == "UNIT_FLAGS" then - if arg1 and strfind(arg1, "^nameplate") then - local plate = C_NamePlate.GetNamePlateForUnit(arg1) - if plate and plate.nameplate then - plate.nameplate.eventcache = true + -- Drop the subscriptions with the token: this slot is now free and the + -- next plate to take it would otherwise feed this frame its events. + plate.nameplate:UnregisterAllEvents() end end @@ -643,48 +669,6 @@ nameplates:RegisterEvent("PLAYER_GUILD_UPDATE") if pn then pn.eventcache = true end end - elseif event == "UNIT_SPELLCAST_START" or event == "UNIT_SPELLCAST_CHANNEL_START" then - -- ClassicAPI fires UNIT_SPELLCAST_* per unit token, including the caster's - -- "nameplateN". The payload has no timing, so poll it (PollCastInfo picks - -- cast vs channel) and cache -- only for a unit we have a plate for, so - -- the table stays bounded to on-screen casters. - if arg1 and strfind(arg1, "^nameplate") then - local guid = UnitGUID(arg1) - local plate = guid and plateByGuid[guid] - if plate then - castState[guid] = PollCastInfo(arg1) - if castState[guid] then - plate.castUpdate = true -- bypass the throttle so the bar shows now - end - end - end - - elseif event == "UNIT_SPELLCAST_STOP" or event == "UNIT_SPELLCAST_CHANNEL_STOP" then - -- Cast/channel ended (natural, interrupted, or cancelled -- the poll fires - -- STOP for all three). Clear the cached cast and refresh its plate. - if arg1 and strfind(arg1, "^nameplate") then - local guid = UnitGUID(arg1) - if guid and castState[guid] then - castState[guid] = nil - local plate = plateByGuid[guid] - if plate then plate.castUpdate = true end - end - end - - elseif event == "UNIT_AURA" then - -- ClassicAPI: fires with arg1 == "nameplateN" when a unit's aura set - -- changes (add/remove/modify). Flag the matching plate so OnUpdate does a - -- fresh C_UnitAuras read next tick instead of waiting on the 0.5s - -- throttle -- covers expirations, dispels, refreshes, and stack changes - -- in one event. Guard on the token prefix (UNIT_AURA also fires for - -- target/party/raid). - if arg1 and strfind(arg1, "^nameplate") then - local plate = C_NamePlate.GetNamePlateForUnit(arg1) - if plate and plate.nameplate then - plate.nameplate.auraUpdate = true - end - end - elseif event == "PLAYER_TARGET_CHANGED" then frameState.targetGuid = UnitGUID('target') -- Flag the target's plate for update @@ -766,6 +750,40 @@ nameplates:RegisterEvent("PLAYER_GUILD_UPDATE") nameplate.cache = {} nameplate.original = {} + -- Each plate watches its own unit. With RegisterUnitEvent the token IS the + -- subscription, so there is no central listener sifting every unit event in + -- the world for a "^nameplate" prefix and then resolving the plate back out + -- of arg1 -- the event arrives only at the plate it concerns, and `this` is + -- already that plate. NAME_PLATE_UNIT_ADDED points the registration at the + -- new token; _REMOVED drops it, which matters because freed slots are + -- reused and a stale token would feed this frame another unit's events. + nameplate:SetScript("OnEvent", function() + if event == "UNIT_AURA" then + -- a fresh C_UnitAuras read next tick rather than waiting out the 0.5s + -- throttle -- covers expiry, dispels, refreshes and stack changes + this.auraUpdate = true + elseif event == "UNIT_FLAGS" then + this.eventcache = true + elseif event == "UNIT_SPELLCAST_START" or event == "UNIT_SPELLCAST_CHANNEL_START" then + -- the payload carries no timing, so poll it (PollCastInfo picks cast + -- vs channel) + local guid = this.cachedGuid + if guid then + castState[guid] = PollCastInfo(this.unit) + if castState[guid] then + this.castUpdate = true -- bypass the throttle so the bar shows now + end + end + elseif event == "UNIT_SPELLCAST_STOP" or event == "UNIT_SPELLCAST_CHANNEL_STOP" then + -- ended: natural, interrupted or cancelled -- the poll fires STOP for all + local guid = this.cachedGuid + if guid and castState[guid] then + castState[guid] = nil + this.castUpdate = true + end + end + end) + -- create shortcuts for all known elements and disable them nameplate.original.healthbar, nameplate.original.castbar = parent:GetChildren() DisableObject(nameplate.original.healthbar) @@ -1409,6 +1427,16 @@ nameplates:RegisterEvent("PLAYER_GUILD_UPDATE") -- cachedGuid is maintained by NAME_PLATE_UNIT_ADDED / _REMOVED events. + -- Cheap gate first. The central loop calls this for every visible plate ~100 + -- times a second, and classifying the plate below costs two C calls, a cast + -- lookup and a throttle resolution -- all of it wasted on a plate that is + -- throttled to 10fps. cfg.throttle_min is the floor across every category, + -- so nothing that would have updated can be turned away here; the real + -- category-specific throttle is still applied after the classification. + -- Event flags bypass both gates, as before. + local hasEventUpdate = nameplate.eventcache or nameplate.auraUpdate or nameplate.castUpdate or nameplate.targetUpdate or nameplate.comboUpdate + if not hasEventUpdate and (nameplate.lasttick or 0) + cfg.throttle_min > now then return end + -- PERF: Intelligent throttling based on target/castbar status and plate count -- Use GUID comparison as primary target detection: instant, immune to alpha transitions, -- and immediately correct on de-target (unlike istarget which updates one tick later) @@ -1435,25 +1463,24 @@ nameplates:RegisterEvent("PLAYER_GUILD_UPDATE") end end + -- Resolved in CacheConfig, so these are table reads rather than a walk + -- through the saved variables and preset tables. local throttle if target then - throttle = pfUI.throttle:Get("nameplates_target") + throttle = cfg.throttle_target elseif visiblePlateCount > 20 then - throttle = pfUI.throttle:Get("nameplates_mass") + throttle = cfg.throttle_mass else - throttle = pfUI.throttle:Get("nameplates") + throttle = cfg.throttle_normal end -- Non-target plates with active castbar use the castbar throttle - if isCastingNonTarget then - local cbThrottle = pfUI.throttle:Get("nameplates_castbar") - if cbThrottle < throttle then throttle = cbThrottle end + if isCastingNonTarget and cfg.throttle_castbar < throttle then + throttle = cfg.throttle_castbar end - -- Check for pending event updates (these bypass throttle for immediate response) - local hasEventUpdate = nameplate.eventcache or nameplate.auraUpdate or nameplate.castUpdate or nameplate.targetUpdate or nameplate.comboUpdate - - -- Event updates bypass throttle + -- The category-specific gate. hasEventUpdate was read above, before the + -- classification, and still bypasses the throttle. if not hasEventUpdate and (nameplate.lasttick or 0) + throttle > now then return end nameplate.lasttick = now @@ -1655,10 +1682,9 @@ nameplates:RegisterEvent("PLAYER_GUILD_UPDATE") -- engine framerate, decoupled from central loop). Only update non-target castbars here. local isTargetPlate = target or nameplate.istarget or (nameplate.health and nameplate.health.zoomed) if cfg.showcastbar and not cfg.targetcastbar and not isTargetPlate then - local cbThrottle = pfUI.throttle:Get("nameplates_castbar") - if visiblePlateCount > 20 then - local massThrottle = pfUI.throttle:Get("nameplates_mass") - if massThrottle > cbThrottle then cbThrottle = massThrottle end + local cbThrottle = cfg.throttle_castbar + if visiblePlateCount > 20 and cfg.throttle_mass > cbThrottle then + cbThrottle = cfg.throttle_mass end if (nameplate.castbar_tick or 0) + cbThrottle <= now then nameplate.castbar_tick = now diff --git a/modules/panel.lua b/modules/panel.lua index 6cb38aa8..6db455d8 100644 --- a/modules/panel.lua +++ b/modules/panel.lua @@ -486,7 +486,7 @@ pfUI:RegisterModule("panel", function() do -- Ammo local widget = CreateFrame("Frame", "pfPanelWidgetAmmo", UIParent) widget:RegisterEvent("PLAYER_ENTERING_WORLD") - widget:RegisterEvent("UNIT_INVENTORY_CHANGED") + widget:RegisterUnitEvent("UNIT_INVENTORY_CHANGED", "player") widget:RegisterEvent("BAG_UPDATE_DELAYED") widget.Tooltip = function() if GetInventoryItemQuality("player", 0) then diff --git a/modules/swingtimer.lua b/modules/swingtimer.lua index 2c868131..77674945 100644 --- a/modules/swingtimer.lua +++ b/modules/swingtimer.lua @@ -816,7 +816,7 @@ pfUI:RegisterModule("swingtimer", function () events:RegisterEvent("PLAYER_REGEN_DISABLED") events:RegisterEvent("PLAYER_REGEN_ENABLED") events:RegisterEvent("ACTIONBAR_SLOT_CHANGED") - events:RegisterEvent("UNIT_DIED") + events:RegisterUnitEvent("UNIT_DIED", UnitGUID("player")) events:RegisterEvent("SPELL_QUEUE_EVENT") events:RegisterEvent("START_AUTOATTACK") events:RegisterEvent("STOP_AUTOATTACK") diff --git a/modules/xpbar.lua b/modules/xpbar.lua index c8565913..4e42f8e5 100644 --- a/modules/xpbar.lua +++ b/modules/xpbar.lua @@ -363,9 +363,9 @@ end b:EnableMouse(true) b:RegisterEvent("FACTION_STANDING_CHANGED") - b:RegisterEvent("UNIT_PET") - b:RegisterEvent("UNIT_LEVEL") - b:RegisterEvent("UNIT_PET_EXPERIENCE") + b:RegisterUnitEvent("UNIT_PET", "player") + b:RegisterUnitEvent("UNIT_LEVEL", "player") + b:RegisterUnitEvent("UNIT_PET_EXPERIENCE", "player", "pet") b:RegisterEvent("PLAYER_ENTERING_WORLD") b:RegisterEvent("UPDATE_EXHAUSTION") b:RegisterEvent("PLAYER_XP_UPDATE")