From 18c9e3b68848edf355977b8c43e7a5f7a95fe0a4 Mon Sep 17 00:00:00 2001 From: Krathe Date: Mon, 14 Sep 2026 00:05:55 +0100 Subject: [PATCH] Slot park: track the CF guard against the last ASK, not the OOC-confirmed memo Two 5.3.2 reports (2026-09-13): a player who dies and takes a battle res has every Aura Designer indicator vanish until the fight is over. The death latch parks each of the unit's slots with the maxDuration=0 candidate-filter lock and the rez restores the live candidates. Both pushes are value-tracked, because SetAuraSlotCandidateFilters has no engine-side equality guard and reparses on every call. The guard compared against _cfPushed, which is recorded only out of lockdown so that the audits never believe a push a secure setter may have refused quietly. A death is in combat. So the park push landed -- the setter does work in combat; the helper gate measured deferred=0 across a raid -- and went unrecorded, and the rez's dark-to-live pass compared the live stash against the pre-combat live stash, found them identical by table identity, and skipped. The engine kept the park on every indicator until the regen replay cleared the memo. The comment on the guard said dark always records the park constant so the transition can never compare equal; that was true out of combat and false exactly when the latch fires. _cfWant now records every attempt, in or out of lockdown, and is what the guard tests. _cfPushed keeps its meaning for the slot audit and the AD dump. The regen replay clears both, so a quietly refused combat push is still re-pushed fresh. The birth seed, ApplyTuning's live push and the helper gate's direct push record the ask too, so none of them is followed by a redundant reparse from _pushFilter. Same guard is in v5.3.2 stable and in main. (cherry picked from commit e4b6bc0b105711cc6c7d5414f2b2512b0a20eb20) --- DandersFrames/Frames/AuraContainer.lua | 44 +++++++++++++++++++++----- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/DandersFrames/Frames/AuraContainer.lua b/DandersFrames/Frames/AuraContainer.lua index 3368c6ed..ce6ccebc 100644 --- a/DandersFrames/Frames/AuraContainer.lua +++ b/DandersFrames/Frames/AuraContainer.lua @@ -7933,7 +7933,9 @@ function AuraContainer:AcquireSlot(frame, slotKey, spec) -- value-tracked push in _pushFilter must not redundantly re-push (and reparse) -- them on the first live pass. Seeded with the DECLARED value (through _cf()), so a -- slot born helper-gated tracks the dead map it actually declared. See SLOT_PARK_CF. + -- Both memos: the guard tests _cfWant, the audits read _cfPushed (see _pushFilter). handle._cfPushed = declaredCF + handle._cfWant = declaredCF -- ☠ SEED THE DEATH LATCH — it is edge-driven, and a slot born AFTER the edge hears -- nothing. SetUnitDeathLatched loops the registries at the transition; a slot -- created later (indicator re-enabled beside a ghost) starts unlatched and renders @@ -8149,17 +8151,34 @@ function SlotHandle:_pushFilter() -- stored live candidates (nil clears — a slot with no live CFs must not keep the -- park). Value-tracked because SetAuraSlotCandidateFilters has NO engine-side -- equality guard — every call clears the slot's candidates and reparses — so this - -- pushes only on a real transition. _cfPushed is recorded only out of lockdown: a - -- secure setter can refuse QUIETLY in combat, and the regen replay (which clears - -- _cfPushed) must re-push then. The dark->live push doubles as the unlatch BOUNCE - -- the death-latch clear wants (the standing parse predates the death): dark always - -- records the non-nil park constant, so the transition can never compare equal and - -- skip. + -- pushes only on a real transition. The dark->live push doubles as the unlatch + -- BOUNCE the death-latch clear wants (the standing parse predates the death). + -- + -- ☠☠ TRACKED AGAINST WHAT WAS LAST ASKED FOR (_cfWant), NOT WHAT WAS CONFIRMED + -- (_cfPushed) — and the difference was the 5.3.2 battle-res bug (2026-09-13, + -- two reports: "if somebody dies and gets a BR their auras vanish till the fight + -- is over"). _cfPushed is recorded only out of lockdown, on purpose: a secure + -- setter can refuse QUIETLY in combat, so the audits must not believe a combat + -- push. But this guard USED to compare against it, and a death is nearly always + -- in combat -- so the park push landed (SetAuraSlotCandidateFilters works in + -- combat; the helper gate proved it, deferred=0 across a raid) and went + -- UNRECORDED, and the rez's dark->live pass then compared the live stash against + -- the pre-combat live stash, found them identical by table identity, and SKIPPED. + -- The engine kept the maxDuration=0 park on every AD indicator until the regen + -- replay cleared the memo. The comment here claimed "dark always records the park + -- constant, so the transition can never compare equal" -- true out of combat and + -- false exactly when a death latch fires. A fix whose comment asserts a false + -- mechanism: this is the class. + -- ⇒ _cfWant records every ATTEMPT, in or out of lockdown, so the transition test + -- is about intent and always sees the park it just asked for. _cfPushed keeps its + -- meaning for the slot audit and the AD dump, and the regen replay clears BOTH so + -- a quietly-refused combat push is still re-pushed fresh. -- Through _cf(), never the raw stash: the wake push after a park/latch clears must -- not un-gate a helper-owned slot whose Power Infusion gate is still dark. _cf() -- returns the stash (already caster-locked at write) or the helper dead map. local cfWant = dark and SLOT_PARK_CF or self:_cf() - if self._cfPushed ~= cfWant then + if self._cfWant ~= cfWant then + self._cfWant = cfWant local okCF = pcall(c.SetAuraSlotCandidateFilters, c, self.key, cfWant) if okCF and not InCombatLockdown() then self._cfPushed = cfWant end ok = ok and okCF @@ -8306,6 +8325,7 @@ function SlotHandle:ApplyTuning(filter, candidateFilters, sortMethod, sortDirect if candidatesChanged and not (self.parked or self._deathLatched or slotVisDark(self)) then local cfOut = self:_cf() pcall(c.SetAuraSlotCandidateFilters, c, self.key, cfOut) + self._cfWant = cfOut if not InCombatLockdown() then self._cfPushed = cfOut end end -- ☠ UNCONDITIONAL. What must reach the engine is the park/latch state, and a pass @@ -8338,7 +8358,11 @@ function SlotHandle:_replayTuning() -- _cfPushed makes _pushFilter's value-tracked CF half push FRESH, choosing -- park or live from the state at drain time (the same collapse-to-one-push -- rule the filter string already follows). + -- ⚠ BOTH MEMOS. _cfWant is the one the guard actually tests (see _pushFilter); + -- clearing only the confirmed record would leave a combat-refused push looking + -- already-asked-for, and the drain would skip the very push it exists to make. self._cfPushed = nil + self._cfWant = nil self:_pushFilter() if self._lastSortMethod ~= nil then pcall(c.SetAuraSlotSortMethod, c, self.key, self._lastSortMethod, @@ -8392,7 +8416,11 @@ function SlotHandle:_applyHelperGate() registerSlotRegen(self) return true end - pcall(c.SetAuraSlotCandidateFilters, c, self.key, self:_cf()) + local cfOut = self:_cf() + pcall(c.SetAuraSlotCandidateFilters, c, self.key, cfOut) + -- Record the ask, so _pushFilter's value-tracked half (which every tuning pass + -- ends in) does not immediately re-push -- and reparse -- the same map. + self._cfWant = cfOut return true end