From 6ca724611b83cd867c4334bfbf83e69d8f1bf553 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20B=C3=BCchner?= Date: Sat, 8 Aug 2026 00:34:13 +0200 Subject: [PATCH] fix(damagemeters): cancel a staggered window build that a rebuild supersedes The login build creates windows one per frame (CreateNextWindow re-arms itself through C_Timer.After(0)), while _EDM_Apply() tears everything down and rebuilds synchronously. When an apply lands during login the two overlap: the staggered build's remaining steps assign _windows[i] over the entries the rebuild just created. Those window tables are dropped from _windows, but their frames stay parented and shown, so the user is left with a duplicate meter that never updates. It reads as a ghost at combat start, when the live windows begin refreshing and the orphan sits there frozen. This does not need a third-party addon to trigger. EllesmereUI_Profiles.lua and EllesmereUI_SpecOverrides.lua both call _EDM_Apply() directly, so a profile apply or a spec override arriving while the login build is still in flight is enough. A build generation counter closes it. CreateNextWindow captures the generation at the start of its build and drops out if a newer one has taken over, and _EDM_Apply bumps the counter before tearing down, which supersedes any build still running. Because a superseded build can now be cut off before it reaches its closing steps, _EDM_Apply performs them itself: EnsureTooltipFrame() and the in-combat ticker restart, which the staggered build otherwise does after its last window. No added cost: one integer compare per staggered step, and the counter is only written when a build starts or is superseded. Behaviour is unchanged when the two never overlap, which is the normal login. --- .../EllesmereUIDamageMeters.lua | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/EllesmereUIDamageMeters/EllesmereUIDamageMeters.lua b/EllesmereUIDamageMeters/EllesmereUIDamageMeters.lua index d587bf64..0e4829c1 100644 --- a/EllesmereUIDamageMeters/EllesmereUIDamageMeters.lua +++ b/EllesmereUIDamageMeters/EllesmereUIDamageMeters.lua @@ -418,6 +418,14 @@ local _inEncounter = false -- true between ENCOUNTER_START and ENCOUNTER_E local _playerGUID local _windows = {} -- array of active window tables ns._windows = _windows + +-- Bumped whenever a build of the windows starts or _EDM_Apply() supersedes one. The +-- login build is staggered across frames, so a rebuild arriving while it is still in +-- flight would otherwise let the pending steps assign _windows[i] over entries the +-- rebuild had just created -- abandoning those frames while they stay parented and +-- visible. A staggered step checks this before doing any work and drops out if a newer +-- build has taken over. +local _buildGen = 0 ns._DM_TYPE_NAMES = DM_TYPE_NAMES ------------------------------------------------------------------------------- @@ -5242,7 +5250,12 @@ initFrame:SetScript("OnEvent", function(self) cfg.windowCount = winCount for i = winCount + 1, MAX_WINDOWS do cfg.windows[i] = nil end local winIdx = 0 + _buildGen = _buildGen + 1 + local myBuildGen = _buildGen local function CreateNextWindow() + -- A rebuild has superseded this staggered build; carrying on would overwrite the + -- windows it created and leave those frames orphaned but visible. + if myBuildGen ~= _buildGen then return end winIdx = winIdx + 1 if winIdx > winCount then -- All windows exist: register them with the core unlock mode system @@ -5263,6 +5276,9 @@ initFrame:SetScript("OnEvent", function(self) -- Profile swap rebuild: tear down all windows and recreate from new profile _G._EDM_Apply = function() + -- Supersede any staggered build still in flight, so its remaining steps do not + -- assign over the windows created below and orphan them + _buildGen = _buildGen + 1 -- Destroy existing windows (frame cleanup only, don't touch DB) for i = #_windows, 1, -1 do local w = _windows[i] @@ -5299,10 +5315,15 @@ initFrame:SetScript("OnEvent", function(self) ns.RegisterDMUnlock() -- Recreate standalone timer if enabled if c.standaloneTimer then CreateSATimer() end + -- Pre-create tooltip frame so first hover doesn't pay creation cost. This and the + -- ticker below are the staggered build's closing steps, repeated here because + -- this rebuild may have superseded that build before it reached them. + EnsureTooltipFrame() -- Update tooltip scale if _ttFrame then local sc = (c.hoverTooltipScale or 100) / 100 _ttFrame:SetScale(sc) end + if _inCombat and not _sharedTicker then StartSharedTicker() end end end)