nameplates: let each plate watch its own unit

The central handler registered UNIT_AURA, UNIT_FLAGS and the four
UNIT_SPELLCAST_* globally, so every unit event in the world woke it for a
"^nameplate" prefix test, and each of the four branches that survived the test
then resolved the plate back out of arg1 -- UnitGUID + plateByGuid, or
GetNamePlateForUnit.

Register them per plate instead, on the plate's own frame against its own
token. The token is the subscription, so the event only reaches the plate it
concerns and `this` is already that plate: the aura branch is now one
assignment where it used to be a prefix test, an API call and a table lookup.

This is also what makes the approach viable at all. A central listener would
have had to name every token it might care about, and nameplate slots have no
cap -- Events.cpp keeps them in a vector that grows by push_back, with only a
"<80 even in AV-scale scenes" comment for sizing. Any nameplate1..N list would
have been a guess that fails silently in exactly the crowded scenes where
plates matter.

Lifecycle: NAME_PLATE_UNIT_ADDED points the registrations at the new token,
replacing the previous unit rather than stacking, since RegisterUnitEvent over
an already-filtered registration swaps the units. NAME_PLATE_UNIT_REMOVED
unregisters, which is required -- freed slots are reused, and a stale token
would feed the frame another unit's events.

PLAYER_LOGOUT now tears down the plates too. It silenced only the central
frame before; leaving forty plate frames dispatching through logout is the
crash 132 that branch exists to prevent.
This commit is contained in:
Brues
2026-09-08 12:02:38 -05:00
parent 73f4999004
commit ab98bb7ab5
+59 -59
View File
@@ -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
@@ -498,13 +495,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 +510,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 +594,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 +634,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 +651,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 +732,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)