Skip to content

fix(damagemeters): cancel a staggered window build that a rebuild supersedes - #1254

Open
paulbuechner wants to merge 1 commit into
EllesmereGaming:mainfrom
paulbuechner:fix/dm-cancel-superseded-window-build
Open

fix(damagemeters): cancel a staggered window build that a rebuild supersedes#1254
paulbuechner wants to merge 1 commit into
EllesmereGaming:mainfrom
paulbuechner:fix/dm-cancel-superseded-window-build

Conversation

@paulbuechner

Copy link
Copy Markdown

DISCLAIMER: AI was used to create this PR (Claude Fable 5 Max)

What does this PR do?

Fixes duplicate damage meter windows that appear after login and show up as
frozen "ghost" meters at combat start.

The login build creates windows one per frame: CreateNextWindow re-arms itself
through C_Timer.After(0). _EDM_Apply() does the opposite, tearing everything
down and rebuilding synchronously. When an apply lands while the login build is
still in flight, the two overlap: the staggered build's remaining steps assign
_windows[i] over the entries the rebuild has just created. Those window tables
are dropped from _windows, but their frames stay parented and shown. The user
is left with a second meter that never updates, which becomes obvious 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 during login is enough.

The fix is a build generation counter. CreateNextWindow captures the generation
when its build starts and drops out if a newer one has taken over; _EDM_Apply
bumps the counter before tearing down, superseding any build still running.
Because a superseded build can now be cut off before reaching 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.

Nothing changes when the two never overlap, which is the normal login.

How this was discovered

It surfaced as duplicate meters while running NaowhUI's EllesmereUI profile
addon, so the first assumption was that it belonged downstream: NaowhUI writes
profile keys during login and each write triggered a meter rebuild, which is
exactly the apply-lands-mid-build case. I worked around it on that side by
deferring the rebuild.

NaowhUI then shipped its own guard in 1.0.6 (NaowhUI.RefreshDamageMeter() in
NaowhUI_Core.lua), better than the workaround: it debounces the writes so one
toggle drives a single rebuild, and it polls for the last window's globally named
frame instead of guessing a delay, capped at about 5s.

The duplicates still came back at combat start, which is what pointed at
EllesmereUI. A downstream guard can only cover its own trigger; _EDM_Apply() is
called directly from EllesmereUI_Profiles.lua and EllesmereUI_SpecOverrides.lua
with no equivalent protection, so the race stays reachable from inside
EllesmereUI regardless of what any addon does. Hence this fix here rather than
there.

Worth reading if you want a second account of the same race: the comment above
RefreshDamageMeter in NaowhUI_Core.lua (~line 311) describes it
independently, including a consequence I had not traced. They report that the
stranded window's close button removes the live window's saved slot, so the
window is, in their words, "gone for good on the next reload". If that holds,
this is a data-loss bug and not only a cosmetic one.

Two notes for anyone reproducing or writing similar guards:

  • The EllesmereUIDMFrame1..5 globals are not a reliable guide to which windows
    are live. CreateFrame with an existing name does not rebind the global, so
    after the first build those globals stay non-nil even for frames that have been
    torn down, while the live windows exist only in the private _windows table.
    Diagnostics keyed off the globals will report the wrong frames, and a readiness
    probe keyed off them can read "built" while a build is still in flight. With
    the generation counter in place, downstream addons should not need such a probe
    at all.
  • Several plausible-looking fixes are dead ends: calling _EDM_Apply() from its
    own hook, tracking orphans by reference (it cannot see frames leaked before its
    own baseline), or hiding/reparenting frames found through those globals, which
    tears down the working meters instead.

How was it tested?

Live retail, Interface: 120001.

  • Reproduced first: duplicate window present after /reload, and a frozen ghost
    meter reappearing at combat start.
  • With the change: repeated /reload cycles, profile switches and spec-override
    swaps, entering and leaving combat each time. No duplicate windows, and
    #_windows matches windowCount after each apply.
  • Tooltip on first hover and the in-combat refresh ticker both still come up when
    a rebuild supersedes the login build (the two closing steps moved into
    _EDM_Apply).
  • Carried across 8.7.4 through 8.7.7 in daily play, with NaowhUI's own profile
    addon installed and actively rebuilding the meter.

Screenshots

Checklist

  • New settings default OFF (no behavior change without opt-in) - N/A, no new settings; bug fix only
  • Zero cost while disabled: no events registered, no polling, no hooks doing work, no frames built - N/A, nothing added to disable
  • Cheap while enabled: event-driven (no polling, no timer-based logic, no per-frame allocations) - one integer compare per staggered build step; the counter is written only when a build starts or is superseded
  • No writes onto Blizzard-owned frames (weak-table pattern used); HookScript/hooksecurefunc only, never SetScript on Blizzard frames - N/A, touches only EllesmereUI's own window tables
  • Tested in-game, works on live retail; no load errors on the 12.1 PTR client

…ersedes

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant