@@ -68,10 +68,7 @@ local DBG = "AURACONTAINER"
6868-- taint, native dispel reject) logs ONCE, not once per button.
6969local warnedCurve , warnedBorder , warnedNativeDispel = false , false , false
7070local warnedRestyle , warnedRefresh , warnedMouse = false , false , false
71- local warnedNoContainer = false
72- -- Per-handle key-namespace counter: multiple consumers (buff, defensive, ...) share the ONE
73- -- secure AuraContainer that lives on each unit button, so their group/slot keys must be unique.
74- local _handleSeq = 0
71+ local warnedCreate = false
7572
7673-- ============================================================
7774-- CAPABILITY DETECTION (the version gate + PTR-4 feature gates)
568565-- per button (in lazy batches of 10) to style it. isNativeSlots = true. Row layout is the
569566-- container's flow layout (SetAuraLayout* translation lands in P1); overlay slots are
570567-- addon-anchored via the button AddAuraSlot returns.
568+ --
569+ -- Each backend owns its OWN plain container (Krathe's proven ContainerOverlay pattern —
570+ -- reference ContainerOverlay.lua buildOverlay): insecure CreateFrame is combat-legal for the
571+ -- aura pipeline (taint.log-proven; the earlier freeze was unrelated secret-value compares,
572+ -- since fixed). One container per consumer = one flow layout per row (independent
573+ -- positioning) and a trivial recreate-on-structural-change teardown. The container is
574+ -- STANDING: built once from config, then Blizzard drives it — no per-UNIT_AURA touches.
571575-- ============================================================
572576local NativeBackend = {}
573577NativeBackend .__index = NativeBackend
@@ -578,35 +582,37 @@ end
578582
579583function NativeBackend :isNativeSlots () return true end
580584
581- -- Order: SetUnit -> AddAuraGroup/AddAuraSlot(each filter, initializeFrame) -> SetEnabled
582- -- LAST. SetEnabled gates aura-event registration (IsVisible() and IsEnabled()); without
583- -- the LAST enable the row renders once then goes permanently stale.
585+ -- Order (Krathe's ContainerOverlay.lua buildOverlay, proven live in combat on 68569):
586+ -- CreateFrame("AuraContainer", nil, ours, "CustomAuraContainerTemplate") -> SetAllPoints ->
587+ -- SetUnit -> AddAuraGroup/AddAuraSlot(each filter, initializeFrame) -> SetEnabled LAST.
588+ -- SetEnabled gates aura-event registration (IsVisible() and IsEnabled()); without the LAST
589+ -- enable the row renders once then goes permanently stale. After build the container is
590+ -- STANDING — Blizzard drives it; DF touches it again only on a structural rebuild, a unit
591+ -- retarget, or teardown.
584592function NativeBackend :build ()
585593 local handle = self .handle
586594 local config = handle .config
587- -- ACQUIRE the SECURE container the SecureGroupHeader created on the unit button (via the
588- -- auraContainerTemplate attribute). An addon-created container is tainted → its aura
589- -- Show/Hide/create are BLOCKED in combat (renders OOC, freezes in combat). This one is
590- -- Blizzard-created → untainted → combat-safe. It's SHARED per button across consumers
591- -- (buff/defensive/...), so keys are namespaced and we never destroy it.
592- local button = handle .parentButton
593- local c = button and button .AuraContainer
594- if not c then
595- if not warnedNoContainer then
596- warnedNoContainer = true
597- DF :DebugWarn (DBG , " no secure AuraContainer on the unit button (auraContainerTemplate unset on the header, or a non-header frame) — unit=%s" , tostring (config .unit ))
595+
596+ -- Never stand up a container in combat: in-lockdown create/enable is a hard client
597+ -- error pcall can't catch (ContainerOverlay gotcha 1). Every caller already gates this
598+ -- (Create / _rebuild / the regen handler); a stray path defers instead of dying.
599+ if InCombatLockdown () then handle :_deferRebuild (); return end
600+
601+ -- OUR OWN plain per-consumer container, parented to the handle's anchor frame. Insecure
602+ -- creation is fine — taint.log proved the old combat freeze was unrelated secret-value
603+ -- compares (Config.lua SafeSetFont / Auras.lua legacy scan), both fixed — and this exact
604+ -- plain-create pattern runs live in combat in the AD ContainerOverlay PoC.
605+ local ok , c = pcall (CreateFrame , " AuraContainer" , nil , handle .frame , " CustomAuraContainerTemplate" )
606+ if not ok or not c then
607+ if not warnedCreate then
608+ warnedCreate = true
609+ DF :DebugWarn (DBG , " CreateFrame(AuraContainer) failed: %s" , tostring (c ))
598610 end
599611 self .container = nil
600612 return
601613 end
602614 self .container = c
603- -- Position the (secure) container over the unit button. The SecureGroupHeader creates it
604- -- with NO anchors and NO size, so without this its flow-laid buttons never resolve a rect
605- -- and never draw. taint.log CONFIRMED this base-Frame call does NOT taint the container —
606- -- the combat freeze was unrelated secret-value compares (Config.lua:773 / Auras.lua:1300),
607- -- now fixed. OOC only (build runs OOC / on regen). config.layout → inbound SetAuraLayout*
608- -- lands in P1, but anchoring the container to the button is DF's job regardless.
609- if not InCombatLockdown () then pcall (function () c :SetAllPoints (button ); c :Show () end ) end
615+ c :SetAllPoints (handle .frame )
610616 if type (config .unit ) == " string" then pcall (function () c :SetUnit (config .unit ) end ) end
611617
612618 -- Fresh generation: buttons are created in lazy batches (of 10) as needed, so a slot's
@@ -616,85 +622,72 @@ function NativeBackend:build()
616622 handle ._gen = (handle ._gen or 0 ) + 1
617623 local initFn = handle :_makeInitializeFrame (handle ._gen )
618624
619- -- Park the previous generation's groups (topology is add-only — no RemoveAuraGroup), then
620- -- add this generation under per-handle + per-gen namespaced keys (no collision with other
621- -- consumers sharing this container, no collision with our own parked-but-not-removed keys).
622- self :_parkGroups ()
623- self .groupKeys = {}
624- local keyPrefix = handle ._keyBase .. " _g" .. handle ._gen .. " _"
625+ -- Declare one AuraGroup per filter (row) / one AuraSlot per filter (overlay). The
626+ -- container is exclusively ours, so keys need no cross-consumer namespacing.
625627 local filters = normalizeFilters (config .filter )
626628 local maxCount = handle :_slotCount ()
627629 local isOverlay = config .mode == " overlay"
628630 for i , f in ipairs (filters ) do
629631 if AuraUtil and AuraUtil .IsValidFilterString and not AuraUtil .IsValidFilterString (f ) then
630632 DF :DebugWarn (DBG , " filter rejected by IsValidFilterString: %s (group skipped)" , tostring (f ))
631633 else
632- local key = keyPrefix .. i
634+ local key = " df " .. i
633635 if isOverlay then
634- local ok , btn = pcall (function () return c :AddAuraSlot (key , f , { initializeFrame = initFn }) end )
635- if ok and btn then self . groupKeys [ key ] = " slot " ; pcall (function () btn :SetAllPoints (button ) end )
636- elseif not ok then DF :DebugWarn (DBG , " AddAuraSlot failed: %s" , tostring (btn )) end
636+ local okSlot , btn = pcall (function () return c :AddAuraSlot (key , f , { initializeFrame = initFn }) end )
637+ if okSlot and btn then pcall (function () btn :SetAllPoints (handle . frame ) end )
638+ elseif not okSlot then DF :DebugWarn (DBG , " AddAuraSlot failed: %s" , tostring (btn )) end
637639 else
638- local ok , err = pcall (function () c :AddAuraGroup (key , f , { maxFrameCount = maxCount , initializeFrame = initFn }) end )
639- if ok then self .groupKeys [key ] = " group"
640- else DF :DebugWarn (DBG , " AddAuraGroup failed: %s" , tostring (err )) end
640+ local okGroup , err = pcall (function () c :AddAuraGroup (key , f , { maxFrameCount = maxCount , initializeFrame = initFn }) end )
641+ if not okGroup then DF :DebugWarn (DBG , " AddAuraGroup failed: %s" , tostring (err )) end
641642 end
642643 end
643644 end
644645
645- -- No SetEnabled call: the header-created container defaults enabled=true, and AddAuraGroup
646- -- triggers UpdateEventRegistrations, so it registers UNIT_AURA on its own. Calling the
647- -- base SetEnabled from insecure code would taint it (see the SetAllPoints note above).
646+ -- SetEnabled LAST — after the groups/slots + filters are declared (ContainerOverlay.lua
647+ -- gotcha 2). This is what arms the parse + UNIT_AURA registration.
648+ pcall ( function () c : SetEnabled ( config . enabled ~= false ) end )
648649
649- pcall (function ()
650- DF :Debug (DBG , " built (native) unit=%s mode=%s groups=%d" ,
651- tostring (config .unit ), tostring (config .mode or " row" ), # filters )
652- end )
653- end
654-
655- -- Park (don't remove — topology is add-only) this backend's groups by zeroing their frame
656- -- count, so a rebuild/teardown releases their buttons without destroying the shared container.
657- function NativeBackend :_parkGroups ()
658- local c = self .container
659- if c and self .groupKeys then
660- for key , kind in pairs (self .groupKeys ) do
661- pcall (function ()
662- if kind == " group" and c .SetAuraGroupMaxFrameCount then
663- c :SetAuraGroupMaxFrameCount (key , 0 )
664- elseif kind == " slot" and c .SetAuraSlotFilterString then
665- c :SetAuraSlotFilterString (key , " HELPFUL|!HELPFUL" ) -- match-nothing: parks the slot
666- end
667- end )
668- end
669- end
670- self .groupKeys = nil
650+ DF :Debug (DBG , " built (native) unit=%s mode=%s groups=%d" ,
651+ tostring (config .unit ), tostring (config .mode or " row" ), # filters )
671652end
672653
673654function NativeBackend :setUnit (unit )
674655 if self .container and type (unit ) == " string" then pcall (function () self .container :SetUnit (unit ) end ) end
675656end
676657
658+ -- Callers combat-gate this (Handle:_applyEnabled defers to regen in lockdown) — enabling
659+ -- a container in combat is forbidden, same class of op as creating one.
677660function NativeBackend :setEnabled (on )
678- -- No-op: the shared container is enabled by default and must NOT be driven by the base
679- -- SetEnabled from insecure code (taints it → combat block). We hide OUR row by parking our
680- -- groups (maxFrameCount 0), not by disabling the whole container.
661+ local c = self .container
662+ if c then pcall (function () c :SetEnabled (on and true or false ) end ) end
681663end
682664
683665-- 68569: UpdateAllAuras() is an addon-callable dirty-mark (processed next OnUpdate while
684- -- visible) — the real refresh. Fall back to the Hide/Show bounce only if it's absent.
666+ -- visible) — the real refresh. The Hide/Show bounce fallback is legal again (the container
667+ -- is ours alone), but only out of combat (Show re-arms the parse, same class as enable).
685668function NativeBackend :refresh ()
686669 local c = self .container
687670 if not c then return end
688671 if type (c .UpdateAllAuras ) == " function" then
689672 pcall (function () c :UpdateAllAuras () end )
673+ elseif not InCombatLockdown () then
674+ pcall (function () c :Hide (); c :Show () end )
690675 end
691- -- (no Hide/Show fallback: the container is shared + button-owned — never bounce it)
692676end
693677
694- -- The container is OWNED BY THE UNIT BUTTON (secure header created it) and SHARED across
695- -- consumers — never Hide/disable/destroy it. Just park OUR groups and drop our ref.
678+ -- The container is OURS (per-consumer): teardown mirrors ContainerOverlay's teardownEntry —
679+ -- disable, drop its buttons, hide, release the ref. The next build creates a fresh container
680+ -- (topology is add-only — no RemoveAuraGroup/Slot — so recreate IS the sanctioned removal).
681+ -- Callers gate teardown out of combat (Destroy/_rebuild defer to regen in lockdown).
696682function NativeBackend :teardown ()
697- self :_parkGroups ()
683+ local c = self .container
684+ if c then
685+ pcall (function () c :SetEnabled (false ) end )
686+ if type (c .RemoveAllAuraFrames ) == " function" then
687+ pcall (function () c :RemoveAllAuraFrames () end )
688+ end
689+ pcall (function () c :Hide () end )
690+ end
698691 self .container = nil
699692end
700693
@@ -921,14 +914,10 @@ function Handle:SetSort(sort)
921914 -- TODO(PTR-4): route to the managed backend's sort setter (rule + direction).
922915end
923916
924- -- Force a re-scan of the container. There is NO addon-callable Refresh() on b8f90f2a:
925- -- container:UpdateAllAuras() is an empty stub on the inbound handle (the real refresh
926- -- lives on the private mixin, reached only via OnShow/OnHide/OnEnabledChanged/
927- -- OnUnitChanged), so the sanctioned trigger is a Hide();Show() bounce -> OnShow -> the
928- -- secure refresh, with no filter rebuild or invalid-unit blip [Krathe, source-confirmed
929- -- b8f90f2a]. Use on a dynamic-unit consumer (target/focus/mouseover) when the underlying
930- -- unit changes but the token does not. (In-combat bounce safety is on the PTR audit
931- -- list; a real Refresh() / wired UpdateAllAuras is a PTR-4 candidate.)
917+ -- Force a re-scan of the container. 68569: UpdateAllAuras() is an addon-callable
918+ -- dirty-mark (processed on the next OnUpdate while visible) — the real refresh. Use on
919+ -- a dynamic-unit consumer (target/focus/mouseover) when the underlying unit changes but
920+ -- the token does not. Falls back to an out-of-combat Hide/Show bounce if absent.
932921function Handle :Refresh ()
933922 if not self .backend then return end
934923 local ok , err = pcall (function () self .backend :refresh () end )
@@ -1116,11 +1105,8 @@ function AuraContainer:Create(parent, config)
11161105 local h = setmetatable ({ config = cfg , buttons = {} }, Handle )
11171106 AuraContainer ._handles = AuraContainer ._handles or setmetatable ({}, { __mode = " k" })
11181107 AuraContainer ._handles [h ] = true -- weak-keyed registry so a dropped handle GCs (else rebuild-forever on test toggle)
1119- -- The parent IS the secure unit button; its .AuraContainer (created securely by the
1120- -- SecureGroupHeader's auraContainerTemplate attribute) is what we render into.
1121- h .parentButton = parent
1122- _handleSeq = _handleSeq + 1
1123- h ._keyBase = " df" .. _handleSeq
1108+ -- h.frame is the plain anchor frame DF positions; the backend parents its OWN
1109+ -- CustomAuraContainer to it (per-consumer container — Krathe's ContainerOverlay pattern).
11241110 h .frame = CreateFrame (" Frame" , nil , parent )
11251111 -- Both modes: h.frame occupies the unit-frame rect (row layout anchors are relative
11261112 -- to it; overlay covers it). To reposition: h:ClearAllPoints() then h:SetPoint(...).
0 commit comments