Skip to content

Fix party Dispels settings writing to the shared raid values - #1256

Open
dfrisone wants to merge 3 commits into
EllesmereGaming:mainfrom
dfrisone:party-dispels-section-leak
Open

Fix party Dispels settings writing to the shared raid values#1256
dfrisone wants to merge 3 commits into
EllesmereGaming:mainfrom
dfrisone:party-dispels-section-leak

Conversation

@dfrisone

@dfrisone dfrisone commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Reported on 8.7.5: choosing custom party Dispels settings also applied them to the raid frames, "except for border".

Cause

Two independent things decide how a control on the Party tab behaves, and they could disagree.

Whether you can edit it - the party tab reuses the raid section builders and draws a "Synced with Raid Settings" blocking overlay per section, sized from that section's onSection(key, startY, endY) y-range. So a control is editable whenever the section whose header it is drawn under is unsynced.

Which value it writes - SSet consults ns._PARTY_KEY_SECTION[key] and only writes party_<key> if that section is custom.

The Dispellable Debuff Location row and its cog are drawn under DISPELS but were filed under debuffDisplay. With Dispels unsynced and Debuff Display still synced, the overlay allowed the edit but the write routed through debuffDisplay and landed on the shared raid key.

Frame Border was the one setting that did not leak because dispelBorderSize was already filed correctly. That exception is what identified the cause: the write path was fine, the per-key routing was not.

Change

Every control drawn under the DISPELS header now files under dispels:

  • The five dispellableDebuff* keys (location, growth direction, size, offset X/Y) moved from debuffDisplay.
  • dispelClockBorder, dispelClockExtraBorder, dispelIconBorderSize and dispelOverlayPosition were in no section at all, so they could never be party-overridden. Same symptom, now fixed.

Notes

No migration needed and no existing override changes meaning. Writing a party_dispellableDebuff* value already required Dispels to be unsynced to reach the control past the overlay, so every stored override stays live under the new section. The four previously unmapped keys have no stored party values at all, and their runtime reads already resolve through the party proxy.

dispelOverlayPosition applies when the private aura container re-registers, exactly as it did for raid before this change.

Testing

luac -p passes. Verified in game with Dispels unsynced and Debuff Display left synced:

  • Dispellable Debuff Location set differently on each tab, neither carries over.
  • The location cog (icon size, growth direction, offsets), Dispel Clock Border with its extra border, and Private Dispel Overlay Position all stay party-only and persist.
  • Frame Border and Type Icon Position, which were already routed correctly, are unchanged.

Update after review

Two additions since the description above:

  • dispelIconBorderSize now has an explicit default. ReloadPartyFrames temp-swaps party overrides onto db.profile and restores from a table populated with saved[key] = raw[key]. A nil raid value stores no entry, so the restore skips that key and the party value stays on the shared raid key permanently. Every other key in the section has a default, which NewDB merges in at load; this one relied on an or 2 read fallback and only became reachable as a party override in this PR.
  • A migration was added. The original "no migration needed" claim was wrong: writing one of these values required both sections custom, but re-syncing Dispels afterwards only deleted keys mapped to dispels, and these were mapped to debuffDisplay. So an override could survive as live under a custom Debuff Display with Dispels synced, and would flip state under the new mapping. rf_dispellable_debuff_party_section_v1 clears the override whenever its live/dormant state would flip, leaving the normal both-custom case untouched.

Note for merging alongside #1257: both branches append a migration at the end of EllesmereUI_Migration.lua, so git will likely flag a conflict there. Keeping both blocks is the correct resolution; the two migrations are independent.

Migration testing

Exercised two ways.

Offline: a harness loads this file, registers the full chain, and runs the real RunRegisteredMigrations() over synthetic profiles covering every gating combination. Both-custom keeps the override, both-synced keeps it, and each single-sided flip clears it. Zero errors across the whole chain.

In game: a profile in the flip state (Dispels custom, Debuff Display synced, all five dispellableDebuff* overrides stored) was loaded and logged out. Result on disk afterwards: all five keys cleared, party_dispelClockBorder / dispelClockExtraBorder / dispelBorderSize / dispelIconPosition / dispelOverlayPosition untouched, migration stamped once, EllesmereUI._migrationErrors empty.

dragon energy GIF

The party tab reuses the raid section builders and blocks each synced
section with an overlay sized from that section's y-range, so a control is
editable whenever the section it is DRAWN under is unsynced. Which value it
writes is decided separately, by ns._PARTY_KEY_SECTION. When the two
disagree the control is editable on the party tab but writes the shared
raid key, so the edit shows up on the Frames tab and on the raid frames.

Every control under the DISPELS header now files under "dispels":

- The Dispellable Debuff Location row and its cog (location, growth,
  size, offset X/Y) were filed under "debuffDisplay".
- Dispel Clock Border, its Extra Border Size, the 12.1 Debuff Icon Border
  and Private Dispel Overlay Position were in no section at all, so they
  could never be party-overridden.

Frame Border was unaffected because dispelBorderSize was already filed
correctly, which is why it was the one setting that did not leak.

No existing override changes meaning: writing a party_dispellableDebuff*
value already required "dispels" to be unsynced to reach the control, so
every one of them stays live under the new section. The four previously
unmapped keys have no stored party values at all, and their runtime reads
already resolve through the party proxy.
ReloadPartyFrames temp-swaps party overrides onto db.profile and restores
from a table populated with saved[key] = raw[key]. A nil raid value stores
no entry, so the restore loop skips that key and the party value stays on
the shared raid key permanently.

Every other key in the section has a default, which NewDB merges into the
profile at load, so their raid value is never nil in memory.
dispelIconBorderSize relied on an `or 2` read fallback instead, and it only
became reachable as a party override in the previous commit. Store the same
value as a real default so the swap always has something to restore.
The no-migration claim was wrong. Writing one of these values required both
Dispels and Debuff Display to be custom, but re-syncing Dispels afterwards
only deleted keys mapped to "dispels", and these were mapped to
"debuffDisplay". So a stored override could survive as LIVE under a custom
Debuff Display with Dispels synced, and would go dormant under the new
mapping. The mirror case, dormant under a synced Debuff Display and live
once Dispels is custom, would switch on and change how party frames look.

Clear the override whenever its live/dormant state flips. Flipping to live
is the case that must be neutralized, and clearing preserves exactly what
party renders today. Flipping to dormant renders the same either way, and
clearing stops a zombie value reviving the next time Dispels is unsynced.
An override live under both mappings is left untouched.
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