unitframes: let UpdateVisibility own the event subscriptions

Every unit frame registered fifteen unit events globally and then sorted them
out per event:

  arg1 == this.label .. this.id or (UnitGUID and arg1 == UnitGUID(this.label .. this.id))

A string concat -- and on a miss a second concat plus a UnitGUID call -- for
every frame, for every unit event fired by anything in the world. Forty-five
frames in a raid, on every health tick of every raider, to conclude "not mine".

UpdateVisibility is the single place a frame's label and id are ever assigned,
and it already pushed the result to SetAttribute("unit", ...). It now caches
the unitstr and points the subscriptions at it, so there is one hook rather
than a set of transitions to catch, and visibilityscan re-running it five
times a second makes it self-healing. A frame that is not in use drops its
unit events entirely.

EnableEvents no longer registers them: a registration keeps its kind, so
plain-registering first would make every later RegisterUnitEvent a no-op.

UNIT_PET and UNIT_HAPPINESS stay plain deliberately. 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.

The 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 clause was only ever a duplicate wake. It dates from b7a0912e, before
ClassicAPI's TokenObserver made synthetic tokens first-class event sources.

Two teardown paths needed help. UnregisterAllEvents drops the filters with the
registrations, so the cached unit has to be cleared or the next
UpdateVisibility believes they are still in place. And PLAYER_LOGOUT silences
the frame while visibilityscan keeps ticking, which would re-register
everything it just dropped -- the crash 132 that branch exists to prevent -- so
the frame now takes itself off the scan list.
This commit is contained in:
Brues
2026-09-08 15:26:06 -05:00
parent ab98bb7ab5
commit 39840c10fd
+78 -19
View File
@@ -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