From ab98bb7ab513167c45c33a1b52b8a071b67a7c5d Mon Sep 17 00:00:00 2001 From: Brues <5278969+brues-code@users.noreply.github.com> Date: Tue, 8 Sep 2026 12:02:38 -0500 Subject: [PATCH] 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. --- modules/nameplates.lua | 118 ++++++++++++++++++++--------------------- 1 file changed, 59 insertions(+), 59 deletions(-) diff --git a/modules/nameplates.lua b/modules/nameplates.lua index 6c784b32..078ee466 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 @@ -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)