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")