Skip to content

Commit b09fd7b

Browse files
author
mergetest
committed
Fix combat freeze: build-once-leave-it parity with the combat-proven DF_AuraLab
Krathe's DF_AuraLab renders a CustomAuraContainer correctly THROUGH combat with the SAME build sequence as ours — so the freeze was us re-touching the live container in combat (Blizzard drives it; touching it re-enters the secret aura path and silently latches its dirty engine). Diffed the lab vs ours and removed every in-combat container touch: - bindNative: drop the slot.DurationTextBinding:SetTextColorCurve poke on live buttons — dead path (private field, SetDurationText drops the required property arg; port plan §2.8) that mutates Blizzard-owned binding state. Colour-by-time returns via the bucket formatter in P2. The lab's initFrame never touches the binding. - Handle:ApplyStyle: combat-guard — in lockdown, stash _pendingRestyle and return without touching buttons; the regen handler replays it OOC (skipped if a queued rebuild subsumes it). - DriveBuffFactory/DriveDefensiveFactory: SetAlpha/SetShown only on actual state change (cached); the layout-version re-apply block (Rebuild/ApplyStyle/formatter churn on live buttons) gated to OOC — version stays stale in combat, catches up on the first out-of-combat drive. - Icons.lua + the external GetFrame():Hide() sites clear the shown-cache so it can't go stale. Net: in combat a standing container is never touched by any DF path — matches the lab. Unverified.
1 parent e66d915 commit b09fd7b

3 files changed

Lines changed: 84 additions & 29 deletions

File tree

Features/Auras.lua

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2559,7 +2559,15 @@ function DF:DriveBuffFactory(frame, db)
25592559
if not h then return end
25602560

25612561
-- Row-level opacity (legacy per-icon buffAlpha; container-frame children multiply it).
2562-
h:GetFrame():SetAlpha(db.buffAlpha or 1)
2562+
-- BUILD-ONCE-LEAVE-IT: the standing container's frame tree is written ONLY on actual
2563+
-- change, never per-event — the combat-proven DF_AuraLab pattern builds once and lets
2564+
-- Blizzard drive; DriveBuffFactory runs per UNIT_AURA/range tick, so unconditional
2565+
-- writes here would re-touch the live tree many times a second in combat.
2566+
local rowAlpha = db.buffAlpha or 1
2567+
if frame.dfBuffFactoryAlpha ~= rowAlpha then
2568+
frame.dfBuffFactoryAlpha = rowAlpha
2569+
h:GetFrame():SetAlpha(rowAlpha)
2570+
end
25632571

25642572
-- Keep the container on the frame's current unit. OOC retargets immediately; in combat
25652573
-- the factory defers the retarget, so hide the row until regen rather than show the
@@ -2573,11 +2581,20 @@ function DF:DriveBuffFactory(frame, db)
25732581
elseif frame.dfBuffFactoryHidden and not InCombatLockdown() then
25742582
frame.dfBuffFactoryHidden = nil
25752583
end
2576-
h:GetFrame():SetShown(not frame.dfBuffFactoryHidden)
2584+
-- Show/hide only on state change (no per-event SetShown churn on the live tree).
2585+
local rowShown = not frame.dfBuffFactoryHidden
2586+
if frame.dfBuffFactoryShown ~= rowShown then
2587+
frame.dfBuffFactoryShown = rowShown
2588+
h:GetFrame():SetShown(rowShown)
2589+
end
25772590

2578-
-- Apply setting changes only when the layout version actually bumped.
2591+
-- Apply setting changes only when the layout version actually bumped — and only OUT
2592+
-- of combat. In combat the standing container is left completely alone (the lab's
2593+
-- proven pattern: existing containers keep running in combat; every addon-side
2594+
-- re-touch — restyle, rebuild, SetFrameLevel, formatter churn — is a divergence).
2595+
-- The version stays stale so the first OOC drive catches up.
25792596
local ver = DF.auraLayoutVersion or 0
2580-
if frame.dfBuffFactoryVersion ~= ver then
2597+
if frame.dfBuffFactoryVersion ~= ver and not InCombatLockdown() then
25812598
frame.dfBuffFactoryVersion = ver
25822599
local cfg = DF:BuildAuraRowConfig(db, "buff", {
25832600
unit = frame.unit,
@@ -2717,12 +2734,20 @@ function DF:DriveDefensiveFactory(frame, db)
27172734
elseif frame.dfDefFactoryHidden and not InCombatLockdown() then
27182735
frame.dfDefFactoryHidden = nil
27192736
end
2720-
h:GetFrame():SetShown(not frame.dfDefFactoryHidden)
2737+
-- Show/hide only on state change (no per-event SetShown churn on the live tree —
2738+
-- build-once-leave-it, mirrors DriveBuffFactory).
2739+
local rowShown = not frame.dfDefFactoryHidden
2740+
if frame.dfDefFactoryShown ~= rowShown then
2741+
frame.dfDefFactoryShown = rowShown
2742+
h:GetFrame():SetShown(rowShown)
2743+
end
27212744

27222745
-- Re-apply settings only on a layout-version bump (defensive option changes bump it
2723-
-- via UpdateAllDefensiveBars -> InvalidateAuraLayout).
2746+
-- via UpdateAllDefensiveBars -> InvalidateAuraLayout) — and only OUT of combat: the
2747+
-- standing container is never re-touched in lockdown (lab parity); the stale version
2748+
-- catches up on the first OOC drive.
27242749
local ver = DF.auraLayoutVersion or 0
2725-
if frame.dfDefFactoryVersion ~= ver then
2750+
if frame.dfDefFactoryVersion ~= ver and not InCombatLockdown() then
27262751
frame.dfDefFactoryVersion = ver
27272752
local cfg = DF:BuildDefensiveRowConfig(db, frame.unit)
27282753
-- Re-apply the z-order level (honors runtime defensiveIconFrameLevel changes; survives
@@ -2764,6 +2789,7 @@ function DF:UpdateAuras_Enhanced(frame)
27642789
local buffFactoryActive = db.showBuffs and DF:UseFactoryForBuffs(frame, db)
27652790
if frame.buffFactory and not buffFactoryActive then
27662791
frame.buffFactory:GetFrame():Hide()
2792+
frame.dfBuffFactoryShown = false -- keep DriveBuffFactory's shown-cache coherent
27672793
end
27682794

27692795
-- Aura Designer runs when enabled; standard buffs can coexist if showBuffs is on.

Frames/AuraContainer.lua

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -419,15 +419,15 @@ local function bindNative(slot, config)
419419
warnedCurve = true
420420
DF:DebugWarn(DBG, "SetDurationText failed: %s", tostring(err))
421421
end
422-
-- Colour-by-time: call SetTextColorCurve DIRECTLY on the binding with the REQUIRED
423-
-- `property` arg. Blizzard's SetDurationText wrapper omits it (bugged -> text vanishes),
424-
-- so we bypass it — the binding is a plain field on our addon-created button. This is the
425-
-- legacy percent gradient. Guarded; C-side render is unverified (Krathe checks in-game).
426-
if ok and durSpec.colorCurve and slot.DurationTextBinding then
427-
pcall(function()
428-
slot.DurationTextBinding:SetTextColorCurve(durSpec.colorCurve, Enum.DurationTextBindingProperty.RemainingPercent)
429-
end)
430-
end
422+
-- Colour-by-time: the smooth textColorCurve path is DEAD on 68569 (live-tested
423+
-- 2026-07-09, port plan §2.8/§3): SetDurationText forwards SetTextColorCurve(curve)
424+
-- WITHOUT the required `property` arg (no-op), and `button.DurationTextBinding` is a
425+
-- PRIVATE field — NOT on the public object table initializeFrame receives — so the
426+
-- old direct-binding poke here could never fire, and poking Blizzard-owned binding
427+
-- state on a live button is exactly the class of touch the combat-proven DF_AuraLab
428+
-- initFrame avoids. durSpec.colorCurve is accepted-but-inert; colour-by-time ships
429+
-- via the discrete BUCKETS formatter (|cRRGGBB escapes in AddBreakpoint format
430+
-- strings, the NSRT/EnhanceQoL-proven path) in P2.
431431
end
432432

433433
if slot.dfStack and slot.SetApplicationCount and not slot._boundStack then
@@ -870,6 +870,17 @@ function Handle:ApplyStyle(style, layout)
870870
if type(layout) == "table" then
871871
self.config.layout = layout -- optional geometry swap (size/scale/spacing/growth/offsets)
872872
end
873+
-- BUILD-ONCE-LEAVE-IT (combat parity with the proven DF_AuraLab pattern): a live
874+
-- native container's buttons are NEVER re-touched in combat. The lab builds once,
875+
-- lets Blizzard drive, and only restyles on explicit OOC user action; restyling
876+
-- live buttons mid-combat (SetSize/SetPoint/SetTexCoord/SafeSetFont on regions the
877+
-- native driver owns) is a divergence from the combat-proven pattern. Config is
878+
-- already captured above; the restyle replays at regen.
879+
if InCombatLockdown() then
880+
self._pendingRestyle = true
881+
self:_registerRegen()
882+
return
883+
end
873884
-- Row-mode buttons are anchored by the CONTAINER's secure flow layout -- SetPoint-ing
874885
-- them here would fight it (and touches secretwrapped anchor points). Geometry changes
875886
-- go through SetAuraLayout* (P1); only the future "slots" mode hand-anchors. styleButton_regions
@@ -978,6 +989,7 @@ function Handle:Destroy()
978989
return
979990
end
980991
self._pendingOp = nil
992+
self._pendingRestyle = nil
981993
self:_teardownContainer()
982994
end
983995

@@ -1002,19 +1014,29 @@ function Handle:_registerRegen()
10021014
self._handles[h] = nil
10031015
local op = h._pendingOp
10041016
h._pendingOp = nil
1017+
local restyle = h._pendingRestyle
1018+
h._pendingRestyle = nil
10051019
-- pcall each handle's op so one failure can't strand the rest.
10061020
if op == "destroy" then
10071021
pcall(function() h:_teardownContainer() end)
1008-
elseif not h._destroyed and op then
1009-
pcall(function()
1010-
if op == "rebuild" then
1011-
h:_rebuild()
1012-
elseif op == "retarget" then
1013-
if h.backend then h.backend:setUnit(h.config.unit) end
1014-
elseif op == "enable" then
1015-
if h.backend then h.backend:setEnabled(h.config.enabled ~= false) end
1016-
end
1017-
end)
1022+
elseif not h._destroyed then
1023+
if op then
1024+
pcall(function()
1025+
if op == "rebuild" then
1026+
h:_rebuild()
1027+
elseif op == "retarget" then
1028+
if h.backend then h.backend:setUnit(h.config.unit) end
1029+
elseif op == "enable" then
1030+
if h.backend then h.backend:setEnabled(h.config.enabled ~= false) end
1031+
end
1032+
end)
1033+
end
1034+
-- Combat-deferred cosmetic restyle (ApplyStyle hit in lockdown). A
1035+
-- rebuild already styles fresh buttons from the updated config, so
1036+
-- only non-rebuild paths need the explicit OOC re-apply.
1037+
if restyle and op ~= "rebuild" then
1038+
pcall(function() h:ApplyStyle() end)
1039+
end
10181040
end
10191041
end
10201042
end)

Frames/Icons.lua

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1105,14 +1105,20 @@ function DF:UpdateDefensiveBar(frame)
11051105

11061106
-- Check if feature is enabled
11071107
if not db.defensiveIconEnabled then
1108-
if frame.defensiveFactory then frame.defensiveFactory:GetFrame():Hide() end
1108+
if frame.defensiveFactory then
1109+
frame.defensiveFactory:GetFrame():Hide()
1110+
frame.dfDefFactoryShown = false -- keep DriveDefensiveFactory's shown-cache coherent
1111+
end
11091112
frame.defensiveIcon:Hide()
11101113
return
11111114
end
1112-
1115+
11131116
-- Check if unit exists
11141117
if not UnitExists(unit) then
1115-
if frame.defensiveFactory then frame.defensiveFactory:GetFrame():Hide() end
1118+
if frame.defensiveFactory then
1119+
frame.defensiveFactory:GetFrame():Hide()
1120+
frame.dfDefFactoryShown = false
1121+
end
11161122
frame.defensiveIcon:Hide()
11171123
return
11181124
end
@@ -1127,6 +1133,7 @@ function DF:UpdateDefensiveBar(frame)
11271133
-- hide it so the legacy render below can't double up.
11281134
if frame.defensiveFactory then
11291135
frame.defensiveFactory:GetFrame():Hide()
1136+
frame.dfDefFactoryShown = false
11301137
end
11311138

11321139
-- Ensure cache.defensives is populated for this unit. In steady state

0 commit comments

Comments
 (0)