Conversation
Krathe, 2026-09-08: "it's not very clear how to use it or even how to find it.
It's under AD any buff and it's settings are confusing and don't merge well…
Even the main settings and the added indicator are split."
Both complaints came from the same decision. The helper's records must live in
the Any Buff pool -- the pool decides a record's caster filter before anything
else, and the helper watches OTHER people's cooldowns -- and the card was put
next to the records so the button and its result sat together. Reasonable in
code, and it also decided where the user had to LOOK: priest-only, four levels
deep, behind a tab that is not the default, with the behaviour in one panel and
the appearance of what it created in a different card further down the page.
★ THE CARD DOES NOT HAVE TO LIVE WHERE THE RECORDS LIVE. The pool constraint is
real and unchanged; the location was never required by it. So the feature moves
and the plumbing stays exactly where it was.
* New page: Auras > Power Infusion Helper, a SIBLING of the Aura Designer
rather than a mode of it -- this is a feature with its own rules, and
sitting in the same category is also what gives it the designer's chrome
for nothing. AuraDesigner/UI/PIHelperPage.lua composes the parts Cards.lua
already owns (S.BuildPIHelperCard / S.PIHelperSections /
S.BuildPIHelperPane); not one builder was rewritten.
* Priest-gated through CreateSubTab's own fourth argument, so a non-priest
never has the entry built rather than seeing it greyed.
* NOT gated on the helper existing. A page that appears only once you have
added the helper is a page you cannot use to add it -- the exact trap this
move exists to undo.
* Both old mounts removed: the classic pane in the Effects head area
(Cards.lua) and the popout row band (Rows.lua, 215 lines). Each site keeps
a DO-NOT-REMOUNT note saying where it went and, more importantly, what did
NOT change -- so nobody re-derives the pool rule from an empty space.
⚠ THE WIDTH DANCE IS THE ONLY GENUINELY NEW MACHINERY, and it is worth knowing
about. GUI/Panel.lua's Add records layoutHeight/layoutCol and nothing else; the
actual SetWidth happens later, in PageRefreshStates' layout pass. But
S.BuildPIHelperPane needs a real width AT BUILD TIME -- the card measures its
wrapped description against it and falls back to 320 without one, which is
right for the 260px popout pane and wrong by half on a full-width page. So the
page builds lazily on the first size it is given, re-measures only when the
width actually moves, then publishes the measured height and asks for one more
layout pass. That cannot loop: the rebuild is gated on WIDTH and the second
pass changes only HEIGHT, with a re-entrancy flag as a belt.
Teardown goes through GUI._trashFrame, the same way BuildPage retires its own
children -- a rebuild that left the old column parented here would stack two
sets of controls writing the same settings.
Verification: 170 files parse. No new globals anywhere -- Cards.lua and
Rows.lua each LOST one (UnitClass, select) as the class checks left them, and
the new file reads only CreateFrame/DandersFrames/UIParent/UnitClass/math/
pcall. Every identifier defined inside the deleted 215-line band was checked
for use after it: none escaped. Load order is safe because the CreateSubTab
call runs inside DF._SetupGUIPagesPart3 at GUI-build time, long after
PIHelperPage.lua has loaded. All files CRLF, no BOM, blob-to-blob.
⚠ Not verified in game. Two increments still to come, both agreed: hiding the
helper's records from the designer's own lists, and per-signal appearance plus
a preview on the new page.
(cherry picked from commit 0fa0e84ae7d184ddff427b6f9acd4fc41cc35130)
…ns the signal
Two halves of the same complaint, both from 2026-09-08: "Anything added should
show just on the PI helper page and not in AD itself" and "the main settings
and the added indicator are split".
THE RECORDS LEAVE THE DESIGNER'S LISTS
A record carrying a `pihSignal` mark belongs to the helper. Left in the
designer's own lists it reads as a stray indicator the user does not remember
making, and deleting one there silently half-dismantles a feature configured
somewhere else entirely.
* CollectAllEffects (Active Indicators, both layouts) now takes an opts table
and drops helper-owned rows unless asked for them.
☠ AN OPTION, NOT A HARD SKIP: the helper's own page wants exactly these
rows -- it is the one surface where they ARE the subject -- so the filter is
the caller's choice, and the PIH_SignalLabel derivation below it stays live
rather than becoming unreachable code that still looks maintained.
* Layout Groups: a new P.VisibleLayoutGroups, used by the classic list, its
hasGroups gate, and the popout layout's list.
☠ FILTERED AT THE DISPLAY SITES AND NOT IN CurrentLayoutGroups, and that
distinction is the whole reason the helper still works: two of that
function's callers are LOGIC, not display -- GetIndicatorLayoutGroup
resolves an indicator's owning group and DeleteLayoutGroup removes one by
id. Filtering at the source would have made the helper's own icon group
unreachable and undeletable by its own Remove path. The store stays whole.
THE COLOUR JOINS THE SIGNAL
A signal's behaviour was set on the helper panel and its colour on the
generated indicator's card further down the page, with nothing saying the two
belonged together. That division was consistency with the designer, where
appearance lives on effect rows -- on a page whose only subject IS the helper
it has nothing left to justify it. Each signal row now carries its own colour.
* Bound straight to the record, so no accessor pair was needed: the colour
already lives at cfg[pihColorKey(surface)] and CreateColorPicker writes
through a table+key like every other colour in the addon.
* Re-fetched per build rather than captured. A surface swap changes both the
record and the key -- a border keeps its colour under BorderColor -- so a
captured binding would write to the record the signal had just left.
* Offered ONLY where a colour exists. An Icon has none (pihPlace says so when
it declines to carry one) and "None" has no record at all; a swatch on
either is a control that writes where nothing reads, the same class of
lying control this feature was moved away from.
* Writes end at P.PIH_Apply + pihRefresh, the chokepoint every other helper
mutation already uses -- the table alone would leave the frames on the old
colour until something unrelated repainted them.
Verification: 170 files parse, no global changes in any of the five files, all
CRLF, no BOM, blob-to-blob.
⚠ Not verified in game.
⚠ THE PREVIEW IS NOT DONE, and deliberately not guessed at. CreateFramePreview
is reusable standalone (the scope-card tiles already do it, with thumb +
placement=false), so the plumbing is not the obstacle -- opts.Paint is, because
what a helper preview should PAINT is a design question I would be inventing an
answer to: one frame per signal, or one frame showing whichever signal wins the
contested surface. Worth asking rather than building twice.
(cherry picked from commit a70c4dc993f2543eba0c24c96daf9b992e392451)
Three asks from 2026-09-08, after checking what the standalone PIHelper addon actually does: its entire visual vocabulary is a glow on the raid frame plus optional duration text. Nothing placed. That is the shape this was drifting towards anyway. SQUARE IS RETIRED AS A HELPER SURFACE (schema 3) The helper answers "who should I infuse", and that is a STATE OF THE FRAME, not a thing placed on it. Border, health bar, background and the two texts are all the frame saying something about the unit; a Square is a coloured block at a coordinate, which is a PLACEMENT decision -- and placement is the Aura Designer's job, not this feature's. ⚠ MIGRATED, NOT DELETED. Someone running a Square helper would otherwise open the panel to a signal reading "None" with no explanation, which is indistinguishable from their settings having been lost. pihSweep step 4 converts it to Border, carrying the colour across, because Border is the nearest honest equivalent -- a contended surface, like the Square was competing for attention. ⚠ The square HANDLING code stays (pihPlace, pihCreateSignal, pihDeleteSignal all still know the type). It is what the migration runs through, and a half-removed surface is how a legacy record becomes unreachable. ⭐ ICON SURVIVES, NARROWED TO ONE MEANING, and is relabelled to say it: "Power Infusion icon". It earns its place because it answers something no colour can -- on the already-infused signal it is the buff you would be about to waste, on the cooldown signal it is the spell you are being told to cast. A picture of Power Infusion means Power Infusion; a gold border means whatever the user decided gold means today. "Icon" was accurate and told them nothing -- it read as a sibling of Square, i.e. another placement, which is the exact reading this cut removes. AN ENABLE TICK, NOT AN ADD/REMOVE CARD "Add" and "Remove" were the implementation talking. Literally true -- the helper creates and deletes AD records -- but that is plumbing the user was never meant to know about, and on a page whose whole subject IS the helper, a card offering to add the thing you came here for is a step with nothing on the other side of it. It now reads like the designer's own enable: same banner, same styled check button, same left-aligned label. ⚠ Settings still survive the round trip, as they already did: unticking routes to PIH_Remove, which STASHES every customisation, and PIH_Create lays the stash back over fresh defaults. The tick just stops making it look like a destructive act. ⚠ The handler reads the POOL, not the box. A double click or a profile switch landing mid-build would otherwise create a second helper or remove one that is already gone. The explaining sentence moved onto the banner and stays visible while the tick is OFF -- which is exactly when someone needs to read it. A REAL PREVIEW CreateFramePreview, the same factory the designer mounts in both its layouts and the scope-card tiles already use standalone. Reusing it is the only way this page can promise the picture matches the live frame; a second renderer is a second thing to drift. ☠ PAINTED FROM THE HELPER'S OWN RECORDS. RefreshPreviewEffects now takes an optional pool, defaulting to exactly what it always used, and the page hands it S.PIH_PreviewPool -- the SAME cfg tables, filtered to those carrying a helper mark. The Any Buff pool also holds the user's unrelated work, and a preview on a page about one feature that quietly renders another's effects is worse than no preview. ⚠ S.framePreview is a singleton and this takes it. Safe for the same reason the designer's two layouts can both assign it: one options page renders at a time, and whichever builds next claims it back. Four now-orphaned locale strings removed rather than left for translators to work on text nobody will ever see. Verification: 170 files parse, no global changes in any file, all CRLF, no BOM. ⚠ Not verified in game. (cherry picked from commit e7738df9c6eff7467016222fd8d4099983868ece)
…, Icon out Krathe, 2026-09-08, with the designer's page beside it for comparison: "the preview should be the same with a side by side, settings on right side preview on left, the page should look almost exactly like AD… right side should be the PI helper options with triggers then indicators". THE SPLIT A 50/50 column: preview left, settings right, mirroring the designer's own splitContainer -- including its 6px gutter, borrowed rather than picked so three split panels in this addon do not drift to three different gaps. ⚠ MEASURED, NOT ANCHORED TO CENTER. The designer can anchor its halves to a CENTER point because they live inside an island of known size; this column is measured by the page harness after the fact, so the halves take an explicit width off the host's real width -- which is why nothing builds until the first size arrives. ⚠ The column is as tall as its TALLER half. Sizing to the settings alone would clip the preview whenever the helper is off and the panel is two rows high; sizing to the preview alone would clip the settings in every other case. ⚠ No border on the left panel -- the preview container draws the visible dim border, and one on both stacks into a brighter doubled line. The designer's own note, and the reason it says it. TRIGGERS, THEN INDICATORS The old grouping opened with "What to Show" -- the answer -- and buried the question two boxes down, which is why the panel read as a pile of settings rather than one decision followed by another. Now: Triggers (roles + the cooldown gate), Classes and Cooldowns, Indicators (the signal rows), Sound. ⚠ THE GATE MOVED. "Hide the helper while your own Power Infusion is on cooldown" is not a display choice -- it is a condition on whether the helper has anything to say at all, which is what a trigger is. ⚠ Classes and Cooldowns is a SIBLING of Triggers, not a child: t.group draws a box, and a box inside a box for a thirteen-row list reads as a nested pane rather than a fold. ICON OUT, SQUARE BACK (schema 4) ☠ THIS REVERSES THE PREVIOUS COMMIT, AND THE REVERSAL IS THE POINT. I cut Square as "a placement, which is the designer's job" and kept Icon as "the one picture that carries meaning". That was SHAPE reasoning. Krathe's is SCOPE reasoning: "we don't need to track each buff just the fact someone has popped a CD and we highlight in some form." An icon shows a specific buff's artwork, so offering one implies the helper tracks WHICH cooldown each player popped -- detail the feature never delivers. Scope wins over shape, and the swap is recorded in both files so it does not later read as drift. ⚠ Schema 3 (Square -> Border) is deliberately NOT preserved and its number is burned rather than reused. It existed for part of one afternoon on one developer's client, so nothing in the wild ran it; reviving its inverse would mean tracking which of two contradictory migrations a profile had seen. A profile that DID run it has borders where it had squares -- a colour on a different surface, not lost work. ⚠ Schema 4 migrates Icon -> Square: the nearest thing that still marks the same unit in the same place. An Icon carries no colour, so the square falls back to the signal's default, which is what pihPlace already does with a nil carry. S.PIHelperSections is removed -- it adapted the bodies for the popout row band, and that band went when the helper got its own page. The per-section split is KEPT: it is what let the page reorder Triggers before Indicators without touching a single control. Three locale strings retired with the shapes that used them. Verification: 170 files parse, no global changes, every L[] key resolved, no orphaned key still referenced, all CRLF, no BOM. ⚠ Not verified in game. ⚠ Still open: "indicators you can add like you would on AD" is implemented as the existing one-surface-per-signal dropdown offering exactly that list. Whether you meant MULTIPLE indicators per signal is a data-model question -- pihFound is keyed by signal and assumes one record each -- and I have not guessed at it. (cherry picked from commit 6fa0fe9bd9ab154969595074c51033cae7c7c77a)
The left half rendered as a transparent gap with the game world showing through (Krathe, screenshot 2026-09-08). My bug, and a circular one. ☠ WITHOUT opts.thumb, CreateFramePreview ANCHORS ITS CONTAINER TO ALL FOUR CORNERS OF THE PARENT -- so the canvas takes its height FROM the panel it is built into. I built the preview into a 1px panel and then set that panel's height FROM the preview. The two resolved against each other at 1px, so nothing was drawn and the empty panel showed the world behind it. The designer never hits this: its leftPanel is anchored TOPLEFT+BOTTOMLEFT inside an island that already has a size, so the height exists before the canvas asks for it. ⇒ Decide the height, THEN build into it. The settings half is measured first, the column takes the larger of that and the canvas's own wanted height, and the preview is created last into a panel that already has a size. ⚠ AND THE FLOOR IS THE CANVAS'S OWN NUMBER, not a constant I picked. P.CanvasWantedHeight exists for precisely this case -- its comment says "the host must size the band BEFORE calling the builder that creates it" -- and it derives from the frame height and the live preview scale, so the canvas keeps fitting when either moves. My first patch used a flat 260, which would have been wrong the moment anyone touched the scale slider. It matters most in the state a new user sees first: with the helper switched off the settings are one banner tall, and a canvas sized to that cannot show a unit frame at all. Verification: 170 files parse, no global changes, CRLF, no BOM. ⚠ Not verified in game. (cherry picked from commit b6a77aab30ac452f55c322e75100c2bad32cecab)
… as tabs
Three from the screenshot, 2026-09-08.
THE PREVIEW WAS STRANDED IN THE MIDDLE
☠ The mock frame is SetPoint("CENTER", container, "CENTER") and the container
fills its parent -- so a left panel stretched to match a long settings column
centres the frame halfway down THAT column, which is why it sat near the bottom
with a screen of empty space above it. The designer never shows this because
its two halves are the same height BY CONSTRUCTION: they fill an island sized
to the viewport and the settings side scrolls inside it. This page is a
measured column in a scrolling page, so the halves are independent -- only the
CONTENT height is shared, and the left panel takes the CANVAS's height.
⚠ That height is the canvas's own number, not one I picked: the non-compact
floor, or the height that fits the frame at the live preview scale plus the
30px the band form saves by putting the scale behind a glyph -- the file's own
accounting, quoted at the call site. Whichever is larger.
THE MISSING HEADER AND SLIDER
It was built with compact = true, which is the BAND form: label hidden, and the
scale behind a glyph in the corner rather than a slider row. That is right for
a popout band and wrong for a page asked to look like the designer. Now
non-compact, with placement = false -- the one thing suppressed, because that
flag gates the nine anchor dots, the drag hint and the three instruction rows,
all of which are about POSITIONING an indicator. Nothing here can be dragged.
It does not gate the slider.
TRIGGERS AND EFFECTS ARE TABS NOW
☠ The split is the feature's two questions, and they are answered at different
times. TRIGGERS -- roles, the cooldown gate, which classes, which spells -- is
set up once, carefully. EFFECTS -- surface, colour, sound -- gets fiddled with.
One column holding both meant scrolling past the thirteen-row class list every
time you wanted to nudge a colour.
* GUI:StyleButton's `tab` mode, the same the designer's right panel uses, so
this reads as the same kind of page rather than a lookalike.
* ☠ The active tab lives on S, not in a local: this builder re-runs on every
rebuild, and a local would drop the user back to Triggers every time they
changed anything on Effects.
* The enable banner sits ABOVE the tabs -- it turns the whole feature on, so
it cannot live inside one of the two things it governs.
* Sound moved to Effects: it is another way of being told the same thing.
* S.BuildPIHelperPane becomes S.BuildPIHelperBody(parent, {tab=...}) -- one
tab's worth of settings, card no longer included.
AND THE WAY IN TO THE SPELL LIST
The class ticks say WHICH classes are watched; the filter behind them holds the
individual cooldowns, and nothing on the page said that list existed. Triggers
now carries a line and a button through to the Filter Designer. A LINK, not a
second copy of the picker -- one place edits these, and a rebuilt picker here
would be a second answer to "is this spell watched".
⚠ CAUGHT BY THE _ENV DIFF, NOT BY luac: reworking the height flow left `h` as a
nil GLOBAL in the layout feedback block, which parses clean and would have
thrown on the first rebuild. It is `colH` now. This is the third time that
check has earned its place.
Verification: 170 files parse; the only new global anywhere is `ipairs` for the
tab loop; every L[] key resolves; no stale BuildPIHelperPane reference outside
one historical note; all CRLF, no BOM.
⚠ Not verified in game.
(cherry picked from commit 16601288ceadb577bed63276c5573ee5f4ce3ae3)
… to a tooltip
Four from 2026-09-08, all of them the panel saying things the wrong way round.
ROLES, NOT "NEVER SHOW ON"
DPS / Tanks / Healers, ticked to WATCH. Tanks and Healers off by default.
☠ THE STORE STAYS AN EXCLUSION SET, and that is deliberate rather than lazy.
helperExcludedRoles fails OPEN on a missing entry -- a group with no assigned
roles reads "no role" for everyone and nothing is hidden, which is the safe
direction. A positive store would invert that: an empty table would mean
"watch nobody" and the feature would go dark on any group without role
assignments. So the inversion is in the UI only.
⭐ AND THE OLD DEFAULTS ALREADY ARE THE NEW ONES. The store ships
{ TANK, HEALER } excluded, which through this inversion reads as DPS on, Tanks
and Healers off -- exactly what was asked for. No migration, and nobody's saved
choice changes meaning.
⚠ DPS was never offered before, which made "watch damage dealers" an invisible
always-on rather than a choice. DAMAGER is a real UnitGroupRolesAssigned token
(Core.lua's GetUnitRole passes it through), so unticking it excludes the same
way the other two do.
THE GATE, ALSO POSITIVELY
"Show even if your Power Infusion is on cooldown", off by default. It was the
one control in a box of enables that turned a SUPPRESSION on, which made the
box read inconsistently. Same trick as the roles: stored value untouched,
still defaults to gating, so it ships unticked and means what it always meant.
TWO LINKS TO THE FILTER DESIGNER -- ONE OF THEM MINE, AND THE WORSE ONE
pihAddClasses ALREADY ended with a Filter Designer button, and it uses
GUI:OpenFilterInDesigner, which switches the page AND scrolls to, selects and
pulses the cooldown filter. I added a bare SelectTab beside it without
checking. The comment on the existing one says exactly why mine was wrong: a
hand-written jump "landed you on the page with nothing indicated, which is
indistinguishable from a broken link". Mine is gone.
☠ Check for the affordance before adding it. The section was already complete;
I read its first half and built against the gap I assumed was there.
THE BANNER'S SENTENCE IS A TOOLTIP
It is read once, by someone deciding whether to switch the feature on, and
after that it is a line of prose in front of the settings on every visit. The
hover is the whole banner row, not the 16px box -- a tooltip on a checkbox the
size of a full stop is a tooltip nobody finds.
Verification: 170 files parse, no global changes, no orphaned key still
referenced, all CRLF, no BOM.
⚠ Not verified in game.
⚠ Still open, and the larger half of the request: Effects adding like the
designer's tiles with several indicators per signal (border + health + square
at once), and a generic square/bar that marks "needs PI" without naming the
cooldown. That is the data-model change -- pihFound is keyed by signal and
assumes ONE record each -- so it is its own commit rather than tacked onto this.
(cherry picked from commit d3f17bffa2a6f4df8ff65ddccf1de45b2af312f2)
Krathe, 2026-09-08: "the effects should add with the same button's as we use on
AD... we should be able to add multiple like border + health + square/bar etc.
Just like AD."
☠ THE STORE ALREADY ALLOWED THIS. A pool record holds many frame-level effects
and many placed instances, so "border AND health bar AND a square" was always
expressible. Two things said otherwise: pihFound wrote each hit over the last
into out[signal], and pihCreateSignal refused any second add with "already on".
Both were one-surface assumptions layered over a store that never had one.
* pihFoundAll returns EVERY hit per signal, sorted by surface. pihFound keeps
its old one-per-signal shape over the top, so the eleven existing consumers
are untouched.
⚠ SORTED, because the pool walk is hash order -- "the primary hit" used to
be whichever the iterator reached last. Harmless with one; three effects
reshuffling on every redraw is not.
⚠ Its own rank table rather than PIH_SURFACE_ORDER: that local is declared
~400 lines below, so naming it would compile as a nil GLOBAL -- the
"declared below its first caller" trap this file documents elsewhere.
* The create gate is per SURFACE. It still refuses a duplicate of the same
one: two borders on a record cannot both exist, and two identical squares
would be an invisible double only the store can see.
* pihDeleteSignal takes an optional surface. Without it, unchanged behaviour.
* New: PIH_AddSurface / PIH_RemoveSurface / PIH_SurfacesOf, all ending at
PIH_Apply + pihRefresh like every other helper mutation.
THE ROW BECOMES A LIST
Each signal now shows what it displays -- one entry per surface, with that
surface's colour swatch where it has one and a "Remove Border" / "Remove Health
Bar" button naming what it acts on -- then a row of buttons for what it could
also show. No "None" entry: removing the last effect IS what none meant, and an
empty list says so without a word for it.
⚠ THE CLASH WARNING IS PER EFFECT NOW. It read the signal's single surface;
with several it has to ask about the row it is drawn under, or a border clash
would be reported beside the health bar.
⚠ Two nil GLOBALS caught by the _ENV diff, neither visible to luac: `surface`
(the icons-only note still read the dropdown's variable) and, earlier, the rank
table. Fourth and fifth this week.
☠ AND I PARAPHRASED TWO LOCALE STRINGS WHILE MOVING THE CLASH BLOCK, inventing
L["Stack borders"] and a shortened warning -- keys that do not exist, which
compile fine and render BLANK. The originals are restored verbatim. Copy locale
lines; do not retype them.
⚠ PIH_SurfaceOptions and PIH_SurfaceOf are now unreferenced, and PIH_SetSurface
keeps only its own definition. Left in place rather than removed in the same
commit: PIH_SetSurface owns the swap machinery (pihSurfaceTakenBy, pihCapture,
pihPlace) and pihPlace is what schema 4's migration runs through. Untangling
that is its own job, not a tail-end cleanup.
Verification: 170 files parse, no new globals, every L[] key in the new block
checked against the locale file, all CRLF, no BOM.
⚠ Not verified in game.
⚠ Still to come, and asked for: the PI icon showing Power Infusion's own
artwork rather than the tracked buff's, and a generic square/bar with optional
"PI Me" text. Those change what the surfaces DRAW; this commit changes how many
of them a signal can have.
(cherry picked from commit b1b42390ef83dc3d302a283a9290d2a26dafa669)
Krathe, 2026-09-08, with three screenshots side by side: "what have you done...
It should basically function exactly as AD. Just without the My buffs /
Debuffs / Any Buff tabs and AD only stuff."
☠ I BUILT A LOOKALIKE TWICE AND IT WAS WRONG BOTH TIMES -- first a surface
dropdown, then a stack of plain grey buttons -- while the real thing sat one
call away. The helper's records ARE Aura Designer records; there was never a
reason to draw a second, worse version of the designer's own list.
The whole of AD's Effects tab is three lines:
local yPos = S.BuildEffectsHeadArea(parent, -10)
local effects = CollectAllEffects()
for _, e in ipairs(filtered) do yPos = S.CreateEffectCard(parent, yPos, e) end
So the helper now uses those parts directly:
* ADD AN EFFECT: GUI:CreateChoiceCardGroup for the route -- the designer asks
route -> spell -> type, and there is no spell to choose here (the cooldown
filter IS the spell), so it asks signal -> type. Two cards, then the
designer's own tile grid: CreateFrameTile + PaintEffectOnThumb, the same
miniature pictures and tooltips, three to a row like the add flow.
* ACTIVE INDICATORS: S.CreateEffectCard per effect -- the same expandable
shell, type badge, eye toggle and delete the designer draws, because it IS
that list. CollectAllEffects({ includePIH = true }) is its own collector
asked for the rows it normally hides from itself.
* Types the signal already holds are left out of the grid, and sound has no
tile: it is a property of the helper, not a surface.
⚠ S.activeBuffTab IS SET TO "other" AROUND THE BUILD AND RESTORED AFTER, and
that is not a hack. S.CreateEffectCard reads PoolKeyPrefix() and IsOtherTab()
off it, to key its expand state and decide whether a spec applies. The helper's
records genuinely live in the Any Buff pool, so pointing it there while we build
is telling the shared builder the truth. Restored immediately, because the
designer's own page reads the same field.
☠ AND THE ICON LISTS WOULD HAVE VANISHED. Cooldowns / Trinkets / Potions /
Racials lived inside t.signalRow, and the Effects tab stopped calling it the
moment the designer's tiles took over -- which would have removed four of
Maelareth's settings (DanderBot#263) from the UI as a silent side effect of a layout
change. They are lifted into pihAddIconLists and drawn under TRIGGERS, which is
where they belonged anyway: each tick decides which category of buff COUNTS,
not how anything is drawn.
t.signalRow is removed, with a note at its site saying where each of its four
jobs went.
Verification: 170 files parse, no new globals against the session baseline,
every L[] key in the new code checked against the locale file, all CRLF, no BOM.
⚠ Not verified in game.
⚠ Still not done, and it is the part that needs the surfaces themselves to
change rather than the panel around them: the Icon showing Power Infusion's own
artwork instead of the tracked buff's, and a generic square/bar with optional
"PI Me" text.
(cherry picked from commit 745d180e66e0d444bbb4d247696c95ad30c43b96)
Krathe's own answer, and the right one: "maybe we put it as a tab at the top of
AD next to Any Buff... and then the Power Infusion Helper in the menu just
instant links to that."
☠ I BUILT A COPY OF THE AURA DESIGNER THREE TIMES. A surface dropdown, then a
stack of grey buttons, then choice cards and a tile grid -- each one a worse
version of something sitting twenty lines away, and each one corrected in
review. The mistake was never a widget choice: it was treating the helper as a
thing BESIDE the designer when it is a POOL INSIDE it.
My Buffs | Debuffs | Any Buff | Power Infusion Helper
Every surface the designer owns now works on it unchanged, because they all
route through four accessors in AuraDesigner/UI/Options.lua:
* CurrentAuraPool -> a filtered view of the other pool, helper records only.
☠ SAFE BECAUSE THE VALUES ARE THE LIVE TABLES: consumers either iterate it
or take CurrentAuraPool()[name] and mutate the record they get back, and
both reach the real cfg through the shared reference.
* CurrentAuraPoolWrite -> the REAL pool. A new record must land in the store,
not in the temporary view -- the one thing a filtered read cannot carry.
* IsOtherTab -> true for it. That question is really "any caster's buffs,
shared across specs", which is exactly what the helper watches; answering
no would give it My Buffs' caster filter, the one pool where its records
are guaranteed to match nothing.
* PoolKeyPrefix -> shares "other:", because it is the same record. A prefix of
its own would key one card two ways, so expanding it on one tab would read
as collapsed on the other.
* VisibleLayoutGroups -> the test INVERTS on this tab. Elsewhere a helper
group is somebody else's business; here it is the only business. One rule,
read both ways.
The pool tab itself is one appended entry in PoolDefs -- priest-gated, and
appended rather than inserted so the other three keep the positions people
already know. Both layouts get it: they share S.BuildPoolStrip.
TRIGGERS ARE THE GLOBAL SUB-TAB
Every pool's Global tab holds what applies to the whole pool rather than to one
effect, which is precisely what roles, classes, icon lists and the cooldown
gate are. Krathe: "Triggers where people pick WHAT will show the effect... Then
HOW it shows the effects." WHAT lives there; HOW is the Effects tab, which is
the designer's own and needed nothing added to it.
The enable tick leads it, because it governs everything below and has to be
reachable while the helper is OFF -- the state a new priest arrives in.
PIHelperPage.lua: 292 lines -> 69
It is the DOOR now and nothing else: set the pool, call
DF.BuildAuraDesignerPage. The nav entry survives because the tab is four levels
deep and a priest should not have to know the helper lives inside the designer
-- which was the original complaint. Everything else it had grown is deleted,
with a note at the top saying it must never grow a page again, and why.
⚠ The tab is set BEFORE the build and deliberately NOT restored: the user is
now looking at that pool, and putting it back would leave the strip showing
Power Infusion Helper as active while the surfaces below rendered My Buffs.
Verification: 170 files parse, no global changes in any file, all CRLF, no BOM.
⚠ Not verified in game.
⚠ The two signals (burst / infused) still exist underneath. Collapsing them to
one -- "someone is worth infusing", with "already has it" as a trigger rather
than a second signal -- is the next thing, and it is a data change rather than
a layout one.
(cherry picked from commit 59f531f55705c7f0bd90f7555c0e010b04ff8f40)
Krathe: "selecting the PI helper in the menu is totally fucked up and the trigger/effects are not showing." Both mine, both from pointing a second nav entry at a builder that assumed it had only one. 1. THE FULL BUILD CLOBBERED THE POOL BuildAuraDesignerPage's teardown sets S.activeBuffTab = "my" unconditionally. The helper's page set the pool and then called that builder -- so the pool was reset before anything rendered, and the helper's entry opened on My Buffs with none of its controls on screen. ☠ AND IT WAS INTERMITTENT, WHICH IS WHY IT READ AS "TOTALLY BROKEN" RATHER THAN "WRONG TAB": a REVISIT takes the reuse path, which leaves the pool alone. Same click, two behaviours, depending on whether the island already existed. ⇒ S.pendingBuffTab: a one-shot the builder CONSUMES at the point it would otherwise have defaulted. Requested, not assigned. Cleared on use, so a plain visit to the designer still opens on My Buffs exactly as before. 2. TWO PAGES WERE SHARING ONE ISLAND S.mainFrame is built into whichever page asked for it, and the reuse guard REPARENTS it to whoever asks next. With the designer's page and the helper's page both calling the builder, one island got handed back and forth while each page's harness still believed it owned the widgets it had Add'd around it -- which is the sub-tab strip drawn over the Template row in the screenshot. ⇒ S.mainFrameOwner. A different page is a different build, exactly like a mode switch, so it takes the full-build path -- which hides and unparents the old island first and leaves nothing stranded on the page we came from. Compared by identity, not name: the harness can rebuild a page object, and a stale reference must read as "different" rather than matching a dead frame. ☠ WHAT I SHOULD HAVE DONE: read what BuildAuraDesignerPage does to shared state BEFORE pointing a second caller at it. I checked that the pool accessors routed correctly and stopped there -- the builder's own teardown, twenty lines further down the same function, resets three of the fields I was setting. Verification: 170 files parse, no new globals, all CRLF. ⚠ Not verified in game. (cherry picked from commit e3b99f290b5ea3a0242a8ad8e1aa84d4045a86eb)
Three reports, one root cause each. 1. THE BLANK PAGE. Two nav entries both called the designer's builder, and that builder has ONE island (S.mainFrame) to parent. Giving each page its own build stopped them overlapping and started them blanking instead: the page harness does not re-run a builder on a cache hit (Panel.lua RefreshCached), so whichever page did not build last showed nothing at all. ⇒ Only the Aura Designer's page owns the island now. The Power Infusion Helper's nav row is a LINK -- it asks for the pool, drops the designer's cache and selects it -- which is what the design said from the start: "the Power Infusion helper in the menu just instant links to that." The page object survives as a stub with a banner and a button, because the settings search can still select any tab by name. WHAT I SHOULD HAVE DONE: the previous commit fixed one consequence of two pages sharing an island without asking whether they could share one at all. 2. THE SUB-TABS. On the helper's pool the strip is Triggers, then Effects, and no Layout Groups. "Global" IS "Triggers" -- relabelled, not a new tab, so SwitchTab and sixty call sites need no special case. Layout Groups goes because a layout group is a container of live aura icons and the helper has no per-spell display to arrange; leaving the tab would be a door back to the bug in (3). One definition (P.SubTabDefs) drives both layouts. 3. THE STUCK "PI Helper - Cooldown icons". A Filter Group of live cooldown icons, switched on by a tick captioned "Icons" filed under "Classes and Cooldowns" -- moved, renamed and finally removed, while its output kept drawing with nothing left that could reach it. ⇒ Retired outright (schema 5). pihSweep deletes every pihSignal group and the stash behind it, and both designer builders run the sweep for a priest so it does not wait for the right tab to be opened. The three amplifier ticks stay -- they are genuine triggers -- and now write into the ONE list the effects match on, so ticking Trinkets makes a trinket proc fire them. AND THE EFFECTS TAB IS THE DESIGNER'S. It lists helper rows (includePIH on this pool -- without it the collector filtered out every row the tab exists to show) and adds through the designer's own tiles, with the spell step removed because the cooldown list IS the spell. Icon comes back pinned to Power Infusion's own artwork rather than the trigger's, which answers the scope objection that cut it in schema 4: iconSpec.staticSpellID reaches a placed indicator, and bindNative skips Blizzard's SetIcon when one is set so the engine cannot repaint our art with the matched aura's. Bar joined the placed branch, where it always belonged -- it was writing a frame-level key nothing reads and could never have drawn. Verified: 170 files parse, no new globals in any edited file except PlaySound and SOUNDKIT in Auras.lua (both real), all CRLF, blob-to-blob diffs targeted. NOT verified in game. (cherry picked from commit 6cdbd9c115b3beeb217290510842c94b2563db9b)
The sweep found the retired cooldown-icon group and then failed to delete it,
silently, so it kept drawing. Krathe: "I'm still seeing PI helper - Cooldowns
on my AD despite it not being an option anymore."
A FINDER THAT DOES NOT DEPEND ON THE POOL NEEDS A DELETER THAT DOES NOT EITHER.
pihAnyIconGroup reads adDB.otherLayoutGroups directly -- the helper's groups
are always there. P.DeleteLayoutGroup resolves its store through
CurrentLayoutGroups(), which is pool-routed off S.activeBuffTab. Moving the
sweep to page-build time in the previous commit meant it now runs with the pool
still set to whatever the last session left, usually My Buffs -- so the delete
searched the SPEC store, found no such id, and removed nothing. It has no
return value, so the loop could not tell; my "refuse to spin" guard then turned
the failure into a clean break.
Two fixes, because the mismatch had two halves:
· P.RemoveOtherLayoutGroupByID -- explicit store, clears the expand key,
RETURNS whether it removed anything. Both the sweep and PIH_Remove use it,
and both now warn instead of breaking quietly. PIH_Remove had the identical
latent fault; it was only ever safe because it happened to run from an
other-routed tab, which is a property of the caller, not of the code.
· pihSweep pins the pool for its whole body. Step 4's Icon -> Square
migration writes through EnsureAuraConfig, which is pool-routed too -- so
an unswept profile could have had its migrated squares created into the
spec pool. Same class of bug, one step earlier, not yet reported.
This also mattered more than it looks: with Layout Groups gone from the
helper's sub-tabs, a marked group is listed on no tab at all, so the sweep is
the only thing that can remove one. A silent no-op there is unrecoverable.
WHAT I SHOULD HAVE DONE: when I moved pihSweep to a new call site, checked
which of the things it calls resolve their store from the open tab. Two of them
do, and the one I hit is the one Krathe reported twice.
Verified: 170 files parse, no new globals, CRLF intact. NOT verified in game.
(cherry picked from commit d2c1463593e04dbebe990f590287ceffc893f633)
I read Krathe's SavedVariables instead of guessing again. The groups were
never in the store any of my three attempts searched.
profiles/Healer New/auraDesignerPresets/Base/
layoutGroups/HolyPriest -> EIGHT "PI Helper - Cooldown icons"
auras/HolyPriest/@Custom:cf9 -> one marked icon indicator
otherAuras/@Custom:cf12 -> one marked icon indicator <- the only
one in the store every finder looks in
WHY THEY ARE THERE. The old icon tick called P.CreateLayoutGroup, which is
pool-routed off S.activeBuffTab, and the panel it lived on was mounted in
S.BuildEffectsHeadArea -- drawn on EVERY pool's Effects tab, not only Any Buff,
whatever the comment beside it claimed. Ticking it on My Buffs created a group
in the SPEC store; the finder then could not see it, reported icons as off, and
the next tick made another one. Eight times.
NOTHING COULD REACH THEM. Not the helper (wrong store), not the Layout Groups
tab (VisibleLayoutGroups hides marked groups from every pool but the helper's),
not the sweep. A record no control can see is a record no control can turn off.
· pihPurgeStrayMarks walks EVERY group store (otherLayoutGroups + each
layoutGroups[spec], legacy flat array handled) and every aura pool, removing
all marked groups and any marked effect outside adDB.otherAuras. A spec-pool
helper effect cannot work -- poolFilter answers HELPFUL|PLAYER before it
consults othersOnly, so it asks for other people's cooldowns cast by me --
and no list shows it, so it renders nothing, lists nowhere, deletes never.
· pihIconGroup / pihAnyIconGroup / RemoveOtherLayoutGroupByID are gone. All
three were one-store-by-id, which is the shape that failed three times.
· Schema 6, because schema 5 stamped itself and deleted nothing -- the stamp
says "swept" on profiles that were not.
· The sweep now gates on the version it is coming FROM. Step 4 migrates every
marked Icon to a Square, and Icon is a supported surface again in schema 5;
ungated, the bump to 6 would have silently converted an icon the user had
just added. That would have hit Krathe first.
· The count goes in the debug log. A migration that deletes stored records
has to say what it deleted.
CORRECTION: my last two commit messages claimed "CRLF intact". The check I used
(grep -c $'\r$') matches every line in this shell and proves nothing. Cards.lua,
Rows.lua and PIHelperPage.lua were pure LF in the working tree -- git said so in
three warnings I read as noise. Committed blobs were unaffected (autocrlf
normalises to LF either way), so the repository never moved; the working tree is
back to CRLF and the check is now a byte count.
Verified: 170 files parse, no new globals, byte-counted CRLF, blobs unchanged.
NOT verified in game.
(cherry picked from commit 73e6f3507ee8652a21389bccfaf61b8e546d6c7d)
… name the effect Four things, three of them one bug: the island's REUSE path changes nothing but the content. 1. THE STRIPS WERE STALE. The helper's nav row asks for its pool and reopens the designer's page. Its Invalidate drops the HARNESS cache, not the island's, so the build takes the reuse path -- which redraws the tab content and leaves both strips describing the pool the panel was BUILT for. Hence the helper's settings under a strip reading Effects / Layout Groups / Global, with Any Buff still lit. AuraDesigner_RefreshPage now re-syncs the pool tabs (P.SyncPoolTabs, lifted out of SetMainTab) and the sub-tab strip before SwitchTab -- that order, because ApplySubTabStrip decides which buttons exist and SwitchTab decides which is active. 2. ...AND IT NEVER CONSUMED THE REQUEST. S.pendingBuffTab was read only by the full build, so the nav row -- which almost always lands on the reuse path -- left it standing for the next unrelated full build to pick up and pin the designer to the helper's pool for a visit nobody asked it of. 3. THE RAIL SAT ON THE WRONG ROW. The page is the designer's and the row clicked is the helper's; SelectTab lights the row that owns the page, which is true about the page and wrong about the click. GUI.SetNavHighlight makes the same three writes SelectTab's own tail makes, as a verb -- navMarker and C_TEXT are panel locals and a caller reaching for them would be reaching into that file's private state. Nothing about page state changes, and the next click on any row re-runs SelectTab's clear, so it cannot get stuck. 4. THE ROWS NAMED THE TRIGGER, NOT THE EFFECT. "PI Helper - Big cooldown" is what FIRES every helper effect -- the same words on every row, in the space where the row's own identity belongs, and the whole subject of the Triggers tab anyway. It is "PI Helper" now; the TYPE is already on the row as its coloured badge, which is how the designer tells two effects on one spell apart, so the text does not repeat it. The retired second signal keeps its own name -- the badge cannot say it is a different thing, and nothing creates one any more. Verified: 170 files parse, no new globals, byte-counted CRLF, no BOM. NOT verified in game. (cherry picked from commit 27ebd620765c9e1e612326a14038427d0ad509c0)
Krathe: "we don't need the others only setting or even note that on the Active indicators either, we only care about using it on others anyway, it's a pointless option." He is right, and the recipe already agreed with him: pihCreateSignal STAMPS othersOnly on every effect it builds, because a helper watching your OWN cooldowns would be telling you to infuse yourself. So the tick was a control over a value the user does not get to pick, and the row suffix reported a constant as though it were a state -- the same constant-on-every-line fault the "Big cooldown" label had, in the same summary, one field along. P.ShowsOthersOnly = IsOtherTab() and not IsPIHelperTab(), used at the four sites that draw either half (the tick and the suffix, in each layout). One predicate rather than four copies of the same test, because the question is one question: does the user decide the caster rule on this pool? On Any Buff yes -- that pool means "any caster including you" and narrowing it is the whole point. On the helper, no. ⚠ THE STORED VALUE IS UNTOUCHED. othersOnly is what makes the effect correct and the engine still reads it; what goes is the control that pretends it is up for discussion. The infused signal's deliberate inversion (it IS your own cast) is unaffected for the same reason. Layout-group Others Only is left alone: it is gated on IsOtherTab() too, but the helper's pool has no Layout Groups tab to reach it from. Verified: 170 files parse, no new globals, byte-counted CRLF, no BOM. NOT verified in game. (cherry picked from commit bfb090a944d6109deeb9d9c65988006787ee77fa)
Krathe: "on the example for icon it has a ? instead of the PI icon (on the GUI, works fine to actually show PI when their CD was active)." The live half being right is what narrowed it: staticSpellID reaches the button and bindNative leaves the art alone, so frames were already correct. The tile is a different painter. THE QUESTION MARK IS THE DESIGNER'S HONEST PLACEHOLDER. Its add flow asks for a TYPE first and a SPELL second, so at tile-paint time there genuinely is no artwork to show -- the picture is swapped in later through pv.spellIcon once section 1 is answered. ☠ ON THE HELPER'S POOL THERE IS NO SECOND STEP. The cooldown list IS the spell, so nothing ever came back to swap it, and the one tile whose entire subject is a fixed picture was the one showing a question mark. PaintEffectOnThumb takes an optional staticSpellID and paints that spell's texture; the helper's Icon tile passes the same id pihCreateSignal pins, so the tile and the effect it creates cannot disagree. Every other caller passes nothing and is unchanged. ⚠ AND A PINNED TILE DOES NOT PUBLISH pv.spellIcon AT ALL. That field is the handle the add pane swaps through; leaving it set would let the pane repaint Power Infusion with whatever spell was picked -- the one thing pinned art exists to prevent. The pane already guards on the field being present, so it simply skips. Verified: 170 files parse, one new global (C_Spell, guarded at both uses), byte-counted CRLF, no BOM. NOT verified in game. (cherry picked from commit b2b5750282538da95bacc3f1d1a4fd554ac6e305)
Audited every PIH symbol (definition vs uses, comments stripped) and every locale key (orphaned now vs referenced before this rework started at e3b99f29). DEAD, AND DELETED -- the retired surface dropdown and what only it used: P.PIH_SurfaceOf / P.PIH_SurfaceOptions / P.PIH_SetSurface, PIH_SURFACE_ORDER, pihCapture, pihSurfaceTakenBy, P.PIH_RemoveSurface, pihDeleteSignal, and the P.PIH_FoundAll export (the local is live; only the export was unread). They all answered one question -- "which SINGLE surface is this signal on" -- which is why picking an occupied row had to SWAP two signals. A signal holds several surfaces now, added and removed one at a time through the designer's own tiles and cards, so the question has no answer to give. ⚠ pihPlace SURVIVES: pihSweep step 4 still migrates an old Icon to a Square through it, and it is the only reader left. SEVEN ORPHANED LOCALE KEYS, deleted with the comments that described them: the helper's private "ADD AN EFFECT" heading, the two route cards and their descriptions, its empty-state line, "Big cooldown", and the dropdown's swap label. Orphan count for this rework is now zero. (100 pre-existing orphans are not this rework's doing and were left alone.) NOT DEAD -- A REAL BUG THE AUDIT FOUND: Deleting a helper effect through the designer's ✕ never re-derived the engine. pihRefresh's own note records why that matters -- "Danders' review found the resident half left armed, events registered, sound armed, for a helper with nothing in it" -- and every helper mutation used to end at that chokepoint. It stopped being a chokepoint when the Effects tab became the designer's: its ✕ removes the record, runs the AD refresh path, and knows nothing about the helper. So deleting your last PI effect left the watcher and the sound registered for a feature with nothing in it. Exactly the state that review caught, reachable again through a different door. ⇒ P.PIH_ReDerive, called from both layouts' delete after asking whether the doomed effect carried the mark (asked BEFORE the removal -- afterwards there is no config left to ask). FLAGGED, NOT TOUCHED -- for Krathe to decide: P.PIH_ClashOn / PIH_SiblingContends / PIH_SelfContends and their three helpers have no callers. The warning they feed was rendered on the old per-signal rows. The HAZARD is still real -- border and both texts take a single winner, so a helper border and one of the user's own means one silently does not draw -- so deleting a safety warning is not a cleanup. Kept, and marked as dead-but-live- looking so it cannot rot unnoticed. Verified: 170 files parse, no new globals, byte-counted CRLF, no BOM, zero locale orphans from this rework. NOT verified in game. (cherry picked from commit 0e9f7e695a1df294df69f57d09c57e2ca5051667)
Krathe's call, and his border observation was right: "with border they can just offset and be able to show two borders like you can with AD anyway?" -- exactly, and the warning string has always named that escape hatch. Ticking "Give this aura its own border" opts an effect OUT of the contest (collectStackedBorders draws it alongside, sorted by priority), so both rings show. Checked, not assumed: that checkbox and the Priority slider the text version names are both still live controls on the effect card. THE HAZARD, for the record: border, name text and health text resolve through pickWinner, which takes ONE winner per surface from config alone and tears every other candidate down. A helper border under one of the user's own borders simply does not draw, with nothing on screen saying which lost or why. Health bar and background are MULTI and cannot clash -- that is what PIH_CONTENDED encodes. P.PIH_ClashText composes the message; the badge carries it. AttachWarningBadge gained opts.text, because until now every caller had a config warning KEY and this text is composed at render time -- it names the offending effect and how many others contend. A key still wins when both are present (a fact about the SPELL outranks a fact about this configuration), though they cannot collide in practice: a helper record is filter-owned and has no spec entry. ☠ AND IT ASKS ABOUT THE ROW IT IS ON. pihSurfaceTakenBy, pihSiblingContends and P.PIH_SelfContends all went: every one asked through pihFound(), which returns ONE hit per signal -- correct when a signal had a single surface, wrong the day it could hold several. "Does our border contend?" was answered about whichever surface hash order landed on, which might have been the square. The card hands over its own config instead. The sibling term went with them: only one signal is creatable now and one record holds one effect per surface, so "another of OUR signals contends here" is unreachable rather than merely unlikely. ⚠ It vanishes when the remedy is applied -- pihContends runs the real candidacy test on our own cfg first -- because a warning that survives its own fix teaches people to ignore warnings. ⚠ Both strings still say "signal", which is vocabulary the helper's UI no longer uses anywhere. Left as-is rather than minting new keys mid-sweep; flagged. Audit after this: zero unused PIH symbols, zero locale keys orphaned by the rework (the pre-existing orphan count fell 100 -> 96, these strings coming back). 170 files parse, no new globals, byte-counted CRLF, no BOM. NOT verified in game. (cherry picked from commit b19d5a6bc94018457f2b1359e4ad309181245b73)
Krathe: "Triggers should be Role for the DPS/Tanks etc section." The box was named after the TAB it sits on, so the Triggers tab opened with a box captioned Triggers. A heading that repeats its own parent says nothing -- and the one thing it could have said, that this box is the ROLE filter, was the thing it left out. ⚠ THE COOLDOWN GATE STAYS INSIDE IT, and that was already argued rather than decided now: pihAddGateAndNotes' own note says a whole titled group around a single checkbox is more chrome than the setting is worth, and that "Show even if your Power Infusion is on cooldown" says what it does without a header to lean on. It names this exact case as the test for a control living under a heading that does not quite describe it, so the heading changing does not disturb it. L["Triggers"] stays -- it is the sub-tab's own label -- so nothing is orphaned. Verified: 170 files parse, no new globals, zero locale orphans, byte-counted CRLF, no BOM. NOT verified in game. (cherry picked from commit ede2f3e16f0ce0300ac74aecdd7a9643c66af561)
…that are not ours
Krathe: "when I disable the PI tracker, it seems to remove my border effect I
added." It did, and there were two separate ways for that to happen.
1. THE STASH HELD ONE SURFACE PER SIGNAL, AND A SIGNAL CAN HOLD SEVERAL.
retainedCfg[key] was a single {surface, cfg} pair, written when a signal WAS
one effect; the multi-surface work never revisited it, and PIH_Remove walked
pihFound(), which answers one hit per signal. So a border + an icon + a square
went in, ONE was stashed, all three were deleted, and re-enabling rebuilt the
one. The other two were gone with nothing anywhere that remembered them.
⇒ The stash is a list in menu order (pihStashHits), PIH_Create names each
surface explicitly rather than leaving pihCreateSignal's fallback to pick one,
and pihKeptCfg/pihKeptSurfaces read BOTH shapes -- so nobody's retained
customisation is lost by the fix for losing retained customisations.
2. THE DELETE TOOK THE WHOLE RECORD. It nil'd the pool key outright, justified
by "a record here is identified BY a helper spell list, so nothing of the
user's own can be sitting on it". That is false: the designer's From a Filter
flow will hang an effect off the Power Infusion Helper filter, and it lands on
this exact record. Turning the helper off took it with no warning and no
stash.
⇒ Marked effects are removed one at a time. The helper-owned `sound` key --
the actual reason the wholesale delete existed, since the generic effects list
offers no delete button for sound on a filter-owned record -- is cleared
explicitly, and the record goes only once it holds nothing at all.
The enable tick has always promised it "behaves like an enable even though
records really are created and deleted underneath". It only does if the round
trip is lossless; it wasn't.
⚠ Also corrected the result string: it said "removed %d signal(s)" while
counting one per signal. It counts effects now, because that is what it removes.
Verified: 170 files parse, no new globals, zero unused PIH symbols, byte-counted
CRLF, no BOM. NOT verified in game.
(cherry picked from commit 24fde995aa0c362fd50c77547b229e5aad0d255b)
Krathe: "it should function like the rest of AD". The rest of AD writes ONE
boolean -- modeDB.auraDesignerEnabled -- and deletes nothing. The helper had no
enabled flag at all: its existence WAS its records, so "off" had to mean "delete
them", with a stash to fake reversibility. Every bug in this area had that one
root -- a switch pretending to be a switch while actually being a delete.
· adDB.pihelper.enabled is the switch. The records are never touched.
· Engine: a `pihEnabled` file-local, distinct from pihGateEnabled because they
do not collapse -- off means forced DARK, gate-off means forced OPEN, and
both on means the watcher drives. The gate's machinery already meant "dark
and nothing will open it", so this costs one local and three guards: the
watcher, the PLAYER_ENTERING_WORLD re-check, and the readiness ticker (which
exists to REOPEN the gate and would otherwise have undone the switch at 2Hz).
· PIH_SetGateEnabled now early-outs while the feature is off -- both its
branches mean SHOW, and ticking the cooldown option while disabled lit the
helper up.
BACK-COMPAT: `enabled` is absent on every existing profile. The engine reads
`s.enabled ~= false`, so absent is ON -- a profile with helper records had a
working helper and must come back working. The panel's accessor backfills once
from PIH_Exists(), so a profile with NO records comes back off and the tick reads
honestly. ⚠ `enabled` is deliberately NOT in PIH_Settings' default table: seeding
it there would make the backfill unreachable, which is the defaults-before-
migration trap this addon has hit three times.
DELETED, because nothing serves the delete model any more: P.PIH_Create,
P.PIH_Remove, pihStashHits, pihKeptCfg, pihKeptSurfaces, pihRestoreInto,
PIH_RECIPE_OWNED, pihDeepCopy. Net -100 lines.
⚠ TWO CONSEQUENCES, SAID OUT LOUD SO NEITHER IS REDISCOVERED AS A BUG:
· The three "Power Infusion Helper" custom filters are no longer auto-deleted.
PIH_Remove used to drop them once no mark remained anywhere. Nothing removes
them now -- correct, because the records referencing them are no longer
removed either. They are ordinary custom filters, visible and deletable in
the Filter Designer. A lingering list is cruft; a silently deleted one that
an effect still points at is a broken effect.
· adDB.pihelper.retainedCfg survives in old profiles, inert. Not swept: a
migration that deletes data to tidy up is a worse trade than the bytes.
⚠ First enable on a profile with nothing still seeds the default border, and that
is the one place this differs from AD's switch. AD is enabled and then you add
indicators; the helper is a recipe, and a first-ever enable landing on an empty
Effects tab would be a switch with nothing on the other side of it. Only when the
pool holds nothing of ours -- a re-enable finds its records where it left them.
Verified: 170 files parse, no new globals (the _ENV diff caught pihRestoreInto
and pihKeptSurfaces going nil mid-edit), zero unused PIH symbols, zero locale
orphans, byte-counted CRLF, no BOM. NOT verified in game.
(cherry picked from commit 34b6452b8476a35880e9b2fe1f182578a5ac906b)
Krathe: "the PI 'icon' has stacks but PI does not have stacks, you only get 1
charge so that setting should at least be off by default if not hidden."
It was worse than off-by-default: showStacks DEFAULTS TRUE for icons and squares
(AuraDesigner/UI/Options.lua), so every helper marker ever created has been
drawing one. And on the icon it is a number read off whichever cooldown matched,
printed on artwork pinned to Power Infusion -- a count describing a spell the
picture is not of.
· The whole Stack Count GROUP is skipped, not just the tick: Font, Scale,
Outline, Anchor, Offset X/Y and Colour are seven more controls for a number
that is never drawn, and leaving them while unticking one is how a panel
fills up with settings that do nothing.
· HIDDEN, not greyed -- the opposite call from GateSWM two hundred lines up,
deliberately. Show When Missing is greyed WITH A REASON because ticking it
would silently break the cooldown gate and the user needs to know why. Stacks
are not broken, they are irrelevant, and a greyed control invites a "why?"
that has no interesting answer.
· The stored value is forced false as well -- stamped at creation, and schema 7
backfills the markers already out there -- so the record says what the frame
draws rather than the panel hiding a true value.
⚠ Step 7 walks EVERY aura pool, like step 5, because an old marker can be sitting
in a spec pool; pihPurgeStrayMarks documents how they got there. Placed only: a
frame-level effect has no stacks to begin with. Set rather than cleared, because
an absent field means "inherit" and the inherited default is the true one.
What survives on a helper effect and why: Show Duration and Hide Cooldown Swipe
both describe the TRIGGERING cooldown's remaining time, which is how long the
burst window lasts -- genuinely useful. Show When Missing was already greyed with
its reason.
Verified: 170 files parse, no new globals, byte-counted CRLF, no BOM.
NOT verified in game.
(cherry picked from commit 1df60d591dd028f48047e8ed7283a6df44d6b9e2)
Krathe: "let them pick what kind of icon it is and what it includes." Two ticks, first group on the icon card, helper effects only: Show the triggering cooldown's icon -- off = Power Infusion (default) Ignore trinkets, potions and racials -- off = anything that triggered it WHY THE SECOND ONE EXISTS. A placed icon renders ONE slot. With a class cooldown and a trinket proc and a racial running together, which one the engine hands us is not ours to choose and can change between parses -- and the "Also count" ticks make that COMMON rather than rare, because racials and trinkets are things people press ALONGSIDE a cooldown. That is what made them "amplifiers" in the first place. So the ambiguity is worst exactly where those ticks are on, and this is the control that resolves it. ☠ IT RIDES mutedSpellIDs -- THE SAME STORE THE TRACKED IDS TICKS WRITE, not a parallel one. That is why it needs no new render concept: the resolver already narrows a placement by its mutes (narrowByPlacementMutes), already treats "everything muted" as "matches nothing", and the per-ID ticks are the same setting at maximum resolution. A coarse control and a fine control over one store, and unticking clears only OUR ids so a hand-set mute survives. ☠ THE TICK IS DERIVED, NEVER STORED. It reads "are ALL the currently-ticked amplifier ids muted on this record", so ticking Racials ON in Triggers later makes it read false BY ITSELF -- the new ids are not muted and the icon really can show them. A stored boolean would sit there claiming "ignored" while racials appeared. Same doctrine as the class ticks. ☠ THE PICTURE IS staticSpellID's PRESENCE, with no second field recording the choice: the field that does the thing is the field the control reads. Structural, so it rebuilds rather than restyles -- bindNative's SetIcon bind is once per slot and placedStructSig already carries the flag. ⚠ THE ICON ONLY, of all the helper's surfaces. A border or a square looks the same whichever trigger fired, so "which one is this" has no answer to give there. Both verbs take a record rather than assuming one, so it generalises if asked. ⚠ The add tile keeps the Power Infusion artwork -- it is the default, and a tile that changed picture per instance would be describing one effect rather than the type. Only one icon can exist per helper, so the list stays unambiguous. Verified: 170 files parse, no new globals, zero unused PIH symbols, zero locale orphans, byte-counted CRLF, no BOM. NOT verified in game. (cherry picked from commit dd7deee4929d5393bd1729016a41c8c037e090e0)
…part
Krathe: "it's a pre created list by us that should toggle on off and be able to
reset to default if someone ticks something off."
THE TWO KINDS OF CUSTOM FILTER ARE NOT THE SAME THING, and the ✕ was treating
them as if they were. A list the user built: membership IS the truth, so removing
a spell is exactly the right verb -- they put it there. A list WE seeded from a
curated set: removing a spell was destructive, unrecoverable, and there was no
way back to the default. The Power Infusion Helper's cooldown list is the second
kind and had the first kind's controls.
⭐ SO A CURATED FILTER NOW GETS WHAT A PRESET HAS, deliberately in the same shape
so the UI can offer the same controls:
preset curated custom hand-built custom
IsSpellEnabled IsCustomSpellEnabled (none -- keeps the ✕)
SetSpellEnabled SetCustomSpellEnabled
ResetPreset ResetCuratedFilter
· The state lives ON THE FILTER (f.disabled), not in the preset overrides
table: it is per-filter data with the filter's own lifetime, so it travels
with a profile export and dies with a DeleteCustomFilter.
· ABSENT MEANS ENABLED, so every existing custom filter resolves byte-for-byte
as before and only one actually ticked off carries anything.
· f.dfDefaults is the mark AND the answer -- its presence says "we seeded
this", its contents say what default means. One field, so they cannot
disagree. pihEnsureFilter stamps it on every call, not only on create, so a
profile made before the mark existed picks it up.
· ResolveSelection honours the ticks at the ONE chokepoint every custom-filter
expansion already goes through, mirroring the preset arm three lines up.
· The existing Reset button serves both. Shown only when there is something to
undo, which for a curated list means `disabled` is non-empty -- the same
question IsPresetModified asks.
⚠ RESET RESTORES, IT DOES NOT PRUNE. Everything we seeded comes back and every
tick comes back on; a spell the USER added to our list stays. "Reset to default"
is reached for to undo what you turned off -- one that also threw away your own
additions would be the destructive act this exists to replace.
Verified: 170 files parse, one new global (`next`, stdlib), byte-counted CRLF,
no BOM. NOT verified in game.
(cherry picked from commit 6055292d72c02b61e008e67158bf16d559fba05a)
The mark shipped and reached nobody. dfDefaults is what gives our seeded list the
on/off tick and the Reset button, and it is written by pihEnsureFilter -- which
only RUNS when the helper creates or repairs a signal. A helper that already
exists and is not being edited never calls it, so Krathe's list still had the
destructive ✕ and no Reset, one round after I said the stamp would reach him.
WHAT I GOT WRONG: I wrote "pihEnsureFilter stamps it on every call, not only on
create, so a profile made before the mark existed picks it up". True and useless
-- the question is not what the function does when called, it is who calls it. A
mark written by a CREATE path reaches nobody who already has the thing.
· pihSweep step 8 stamps both seeded lists from the same seed functions
pihEnsureFilter would have used. Not from the list's CURRENT contents:
default means what the recipe seeds, so anything the user added since is
theirs and stays out of what a reset restores.
· ...and the sweep now also runs on the FILTER DESIGNER's page build. It ran
only on the Aura Designer's, so opening the Filter Designer first -- exactly
what someone inspecting that list does -- showed the unmarked version.
Schema-stamped and priest-gated, so it is one comparison after the first run.
Verified: 170 files parse, no new globals, byte-counted CRLF, no BOM.
NOT verified in game.
(cherry picked from commit 970cd22311316a4fa7dc60b36b31906a98bdf674)
Krathe: "the number does not change as I tick them on/off."
THREE PLACES COUNTED THE SAME THING, THREE WAYS, AND NONE OF THEM ASKED WHETHER A
SPELL WAS ON. The Filter Designer's left list had a local CustomSpellCount, its
right-hand header called that same local, and R:ListFilters -- which the Buff
Bar's picker reads -- walked the tables itself and set `enabled = n, total = n`
unconditionally. All three were written when membership was the only state a
custom filter had, and none was revisited when the ticks arrived. A count that
ignores the control next to it is worse than no count.
· R:CustomFilterCounts(cfId) is the one counter now, and it honours the ticks.
A hand-built filter answers enabled == total -- IsCustomSpellEnabled is true
with no disabled set -- so no caller branches on which kind of list it is.
· The left row shows "34/39" once something is off and the plain total
otherwise, plus the modified dot a preset row carries
(R:IsCuratedFilterModified, the same question IsPresetModified asks).
· The header does the same: "%d spells" until something is off, then
"%d of %d spells".
· The local CustomSpellCount is gone rather than left beside its replacement.
⚠ An ordinary custom filter's row and header are unchanged in every case, because
enabled and total are equal for a list that cannot have anything switched off.
Verified: 170 files parse, no new globals, byte-counted CRLF, no BOM.
NOT verified in game.
(cherry picked from commit 31f6acb8d656ee67f7c92f70d95a3b765f26e747)
Krathe: "you should select them and it should show the number active next to them
in a similar way to how you pick on the buff bar", and before that the question
that exposed it -- "I assume Cooldowns is our custom filter for PI helper and the
other 3 are normal filters?"
A reasonable assumption, and wrong, which is the panel's fault. "Cooldowns" was a
HEADING above a link, with no tick to say it was on. The other three were
captioned "Also count" -- counted "also" to something the panel never named. All
four were one custom list underneath and nothing said so.
Now: Trigger Filters, four rows, four ticks, the number in each.
☑ Cooldowns 39/45 our curated list
☐ Trinkets 41 trinketsItems
☐ Potions 6 consumables
☐ Racials 4 the four offensive ones, not the category's thirteen
· P.PIH_CooldownsOn / SetCooldownsOn give the base source the tick it never had.
Read off the LIST, like the class ticks -- a stored boolean beside it is a
second copy that drifts the moment anyone edits the filter by hand. Surgical
like pihApplyClass: the seeded ids only, so a spell the user added to our list
survives being switched off and back on.
· Cooldowns' count is enabled/total from R:CustomFilterCounts, so ticking a
spell off in the Filter Designer moves the number here. ⚠ That is a DIFFERENT
question from the row's own tick: a spell ticked off is still IN the list, so
the source stays included. The row shows both, which is why it needs both.
· Racials says 4 because it IS 4. The `racials` category is every racial
ability and nine of its thirteen -- Shadowmeld, Darkflight, Stoneform -- are
the opposite of worth infusing behind, which is why the curated set exists.
· The Filter Designer link moved down with them, so the whole filter block reads
as one thing instead of a heading at the top and three ticks at the bottom.
⚠ STILL ONE CANDIDATE LIST UNDERNEATH, deliberately. A helper effect resolves from
ONE filter ref -- its record key -- and a placed indicator has no union at all, so
giving it one means changing the Factory's resolve path for every indicator in the
addon. The ticks go on meaning "put these spells in the list", which is what they
always did; what changed is that the panel says so, names each source and counts
it. If per-effect sources are ever wanted, that is the Factory change, not this.
☠ The _ENV globals diff earned its keep twice here: the moved block closed over
pihAddClasses' `parent` local, and a multi-line splice left 65 LF lines in a CRLF
file. Both invisible to luac -p.
Verified: 170 files parse, no new globals, zero unused PIH symbols, zero locale
orphans, byte-counted CRLF, no BOM. NOT verified in game.
(cherry picked from commit e56e040793f486a28824a28be12531da1d719822)
…nd the link says what it reaches
Three from one screenshot.
1. MULTIPLE ICONS -- the cooldown-icon group, back on purpose.
☠ A version of this shipped, stuck on Krathe's frames and took three attempts
to delete. Every one of those failures was PLUMBING, not the idea:
· created through the pool-routed CreateLayoutGroup from whatever tab was
open, so it landed in the SPEC store where no finder looked
· switched on by a tick captioned "Icons" buried under Classes and Cooldowns
· the helper had no Layout Groups tab, so nothing could see or configure it
⇒ What is different, point by point, because "we fixed it" is not an argument:
· created DIRECTLY into adDB.otherLayoutGroups -- never through the creator
whose store depends on the open tab
· added by a TILE on the helper's own add grid, beside the surfaces
· the helper's Layout Groups sub-tab is back, and VisibleLayoutGroups
already shows exactly the marked groups there, so it can be moved, sized
and deleted like any other group
· pihPurgeStrayMarks is unchanged and still finds a marked group in ANY
store, so the recovery path that cleaned up the old one still exists
It earns its place because a placed icon is ONE slot showing one arbitrary
match; this shows every cooldown they actually have up, one icon each.
2. THE ICON PICKS ITS PICTURE WITH PICTURES. Krathe: "when you add an icon it
should then have a graphic like we do for the other types to then pick the type
i.e an actual icon or a PI icon?" Right -- those two are as different from each
other as an icon is from a square, and every other choice on that grid is made
by looking at a thumbnail. A SECOND STEP rather than two icon tiles, because a
signal holds one effect per surface: two icon tiles would both be "icon" and
the second could never be added. The tick on the card stays -- it is how you
change your mind without deleting and re-adding.
3. THE FILTER DESIGNER LINK REACHES ALL FOUR LISTS, so it no longer sits under a
"Cooldowns" heading naming one of them, and its note no longer says "single
cooldowns". Krathe: "the cooldowns header/footer of the Filter Designer link is
wrong, as really they can modify all 4 filters in it." The button belongs to
the tick list it follows -- the escape hatch for what those four cannot do --
so it needs no caption between them at all.
Verified: 170 files parse, no new globals, zero unused PIH symbols, zero locale
orphans, byte-counted CRLF, no BOM. NOT verified in game.
(cherry picked from commit cbf38d9c209b1f046dba870ff1d6543ac386433f)
Krathe, reading the layout: "the classes, they only effect the Cooldowns
correct? Maybe we do separate that out."
Correct, and checked rather than assumed: pihApplyClass only ever adds or removes
entries from pihSeedRecords() -- the class-cooldown set -- inside the cooldown
list. No class tick can reach the other three sources: trinkets and potions are
items with no class at all, and racials are tagged class = "ALL". So thirteen
class ticks sitting above a list of four sources said the opposite of the truth.
Classes and Cooldowns 45 <- the count moves to the header
☑ Death Knight … (13)
Untick a class to stop watching its cooldowns.
Additional Filters
☐ Trinkets 41
☐ Potions 6
☐ Racials 4
[ Filter Designer ]
☠ AND THE COOLDOWNS TICK IS GONE, because it was redundant AND harmful.
Redundant: unticking all thirteen classes IS turning class cooldowns off -- the
classes are that source's control. Harmful: both it and the class ticks READ OFF
THE LIST (no stored booleans, deliberately, so nothing can drift), so unticking
the source removed all forty cooldowns, every class tick then read off, and
ticking it back re-added all forty -- silently undoing whichever classes the user
had turned off. A switch that quietly reverts your other choices is worse than no
switch, and the alternative -- remembering the class states -- is exactly the
stored copy of a derived truth the class ticks exist to avoid.
⚠ THE HEADER COUNTS THE SEED SET, NOT THE LIST. The list also holds trinkets,
potions and racials once those are ticked, and counting them under a "Classes and
Cooldowns" header would be a number describing something else. Enabled too: a
spell ticked off in the Filter Designer is in the list and not firing.
⚠ One link, still, at the foot of the second box, and it reaches all four lists.
Verified: 170 files parse, no new globals, zero unused PIH symbols, zero locale
orphans, byte-counted CRLF, no BOM. NOT verified in game.
(cherry picked from commit 55d3719e5308a930689365f0049cb10f992a36be)
…s what it watches
Three reports from Krathe, 2026-09-10.
RACIALS SHOWED 4 AND HAD NO PENCIL. Trinkets and Potions point at preset
categories and so have a list to open; Racials pointed at four spell IDs
written out in Cards.lua, because the `racials` category is EVERY racial and
nine of its thirteen (Shadowmeld, Stoneform, Darkflight) are the opposite of
worth infusing behind. Those four are now the SEED of a curated list of ours,
so the row gets the pencil, a count that moves as you tick, and Reset to
Default -- the same treatment the cooldown list already has, and a racial we
missed can now be added. It is created wherever the cooldown list is, never
from a tick, and pihAmplifierIDs falls back to the seed until it exists, so the
trigger is right before and after.
Two things that came with it:
· Trinkets and Potions counted the CATEGORY's size, which does not move when
a spell is ticked off in the Filter Designer -- while pihAmplifierIDs, which
decides what fires, honours those ticks. Same fault as the cooldown count,
one row over. Both now read enabled/total, collapsing to one number when
nothing is off.
· An amplifier tick can no longer remove a CLASS COOLDOWN from the cooldown
list. The removal universe is "every id an amplifier source could
contribute", and now that Racials is a list the user can put anything in it
-- including a class cooldown, which unticking Racials would then delete
from Triggers with no control admitting to it.
"COOLDOWN ICONS ALLOWS FOR TRINKETS + THE OTHER FILTERS? DON'T SEE THE OPTION."
It does, automatically: pihSyncTriggerExtras writes the amplifier ids INTO the
cooldown list, which is the list the group links. There is no option because
there is nothing to choose -- but nothing on screen said so, and dropping the
Linked Filters block from its card left it silent about its own contents. Its
header now carries "N spells, from your Triggers", off the list's own enabled
count, so it moves when a tick over there does.
THE ICON TILE ASKS WHICH ICON. Cooldown Icons stood on the main grid beside
Border and Square, which put a CONTAINER among a row of EFFECTS -- and that
mismatch is the whole of what kept going wrong with it. Krathe: "for icon it
would be best to have a sub menu so you click Icon then it has PI Icon,
Cooldown Icons, and the other icon choices?" Three answers on two axes -- how
many (one marker, or one per cooldown up) and what picture (always Power
Infusion, or the buff they used); the fourth cell is four identical PI icons in
a row. So: three tiles behind Icon.
· Cooldown Icons gets a thumbnail of THREE icons, drawn at the group's own
geometry. "How many" is the axis prose keeps failing at, and the tile used
to paint a single icon.
· Taken answers are GREYED with the reason, not removed. The tile vanishing
the moment you clicked it was half of why the group felt lost.
· The Icon tile itself survives its own surface being taken, since Cooldown
Icons is still behind it.
AND A TRAP CLOSED BEFORE BUMPING THE SCHEMA. pihPurgeStrayMarks deletes every
pihSignal-marked group in every store -- correct when a marked group could only
be the retired one, and no longer true: the Cooldown Icons group lives in
otherLayoutGroups on purpose. Any schema bump, for any reason, would have
silently deleted it on the next Aura Designer build; it has not bitten only
because the group and schema 8 shipped together. It now exempts the one group
P.PIH_IconGroup resolves to, exactly as the effect half has always exempted
otherAuras.
(cherry picked from commit 9fd6ed6750003783b9298882e8739d8559cc5f06)
Krathe: "no back use X like we do on the other AD effects." The designer's own picker is a few hundred lines above this one (S.BuildEffectsHeadArea's S.effectsPicker arm): a head frame with the question on the left and GUI:CreateCloseButton on the right, captioned there as "the only way out that does not commit to anything". The icon step is the same question in the same place, so it takes the same control -- a Back button under the grid was a second vocabulary for leaving, invented for one grid. The heading is now two shapes rather than one fontstring doing both jobs: step 1 keeps the small-caps dim SECTION CAPTION (nothing to leave), step 2 gets the picker's own head in the picker's font and colour, with the X. L["Back"] goes with it. It was introduced for a tooltip in the designer's add pane, that call site is long gone, and this was its only remaining user -- checked against the dynamic-lookup sites too, since a key can be reached as L[x.name]. The equipment-slot table has a "Back" entry but renders slotName raw, never through L. (cherry picked from commit 69a83e1cece09b95daea8713527bcd511819d473)
…ng them Krathe: "despite the fact those additional filters link to our actual filters, ticking them on actually just adds those to the PI helper filter, so they are now twice on? this is very confusing." He is right, and the reason it could not be fixed with wording: each row has a PENCIL that opens the real list, which promises a reference, and the tick made a copy -- 51 spell IDs lifted out of Trinkets, Potions and Racials into ours, so his list read 91 where it should read 40 and the same ids lived in two places. A custom filter may now name other filters (`f.includes`), folded into the selection by R:ResolveSelection. Our list stays the 40 class cooldowns -- which is what "Edit Cooldowns" has always claimed to open -- and names the other three. IN THE REGISTRY, NOT IN THE AURA DESIGNER, and that is the load-bearing choice. Four paths resolve the helper's list: DF:ResolveADFilterRef for the placed effects, the layout-group path for the Cooldown Icons group, Engine.lua's pihResolvedMap for the sound registrations, and the Buff Bar's own picker if a user selects it there. Folding in any one of them would make one list mean different things in different places, which is worse than the copy it replaces. It is inert for every filter without `includes` -- the caller's table is handed straight back, no allocation -- and it folds ONE level, so no cycle is possible and no guard is needed. The fold runs BEFORE the empty-selection test, not after: a filter whose only content is its includes has no presets and no customs of its own, and that branch fails open to SHOW EVERYTHING. WHAT GOES WITH THE COPY: the removal universe, and yesterday's guard against an amplifier tick deleting a class cooldown that also sat in one of those lists. Neither was defending against anything real -- they were defending against the copy. Also gone: hand-rolling "honour the preset's own ticks". ResolveSelection has always done that for a selected preset, so a trinket switched off in the Filter Designer now stops firing here for free. Schema 10 takes the copied spells back out, fenced by dfDefaults so a seeded cooldown that also sits in a trinket list stays. A hand-added trinket goes too: it is indistinguishable from a copied one in the store, and the include brings it straight back for anyone with that source ticked. The Cooldown Icons card counts P.PIH_WatchedCount now (the list plus each ticked source) rather than the list's own size, sharing one counter with the Triggers rows so the two screens cannot report the same feature differently. foldIncludes unit-tested standalone: identity on a plain filter, no mutation of the caller's table, uncategorised carried, an already-selected preset kept, a self-include a no-op, and never shrinking the selection (which is what lets the fail-open latch below it keep working). (cherry picked from commit 4472b44e6ad06c71516f6a918615556277ede811)
Krathe: "can we get the preview card here to actually show some example icons
instead of ??"
The placeholder was defensible right up to the point it was looked at. The
designer's add flow asks for a TYPE first and a SPELL second, so its tiles
genuinely have no artwork yet and the `?` is honest there -- I argued the same
for these, since the helper's picture really is unknown until a cooldown
matches. But a tile is a picture of what the thing LOOKS like, and three
question marks in a row is a picture of an error. What is unknown here is WHICH
cooldown, never WHETHER there is art.
Both tiles now sample the cooldown seed: Recklessness, Adrenaline Rush and Blade
Flurry, the three lowest ids in it. Their cooldown shows the first; Cooldown
Icons shows all three, so the row that means "one per cooldown they have up"
finally looks like it.
· FROM THE SEED, NOT THE LIVE LIST. The user's list moves with the class
ticks, so sampling it would change these pictures when someone unticked
Warrior. A tile is not a live readout.
· SORTED, so the choice is the same on every client and every build rather
than whatever pairs() happened to say first.
· ONLY IDS THE CLIENT CAN DRAW -- GetSpellTexture returns nil for a spell
whose data is not cached, and one `?` standing among two real icons reads
worse than three of them. A short sample still draws a full-length row.
· SAMPLED ONCE FOR BOTH TILES, so the single icon is the first of the row
rather than an unrelated fourth spell. The tiles differ in HOW MANY; a
second difference in the picture would mean nothing.
(cherry picked from commit a0ca021b49cc66e832fe20315f0c05fead9723a0)
Krathe: "Max icons should default to 4 it's showing blank but seems to look like 8? Icon size is also showing blank on the slider." Both sliders bind the group field directly, so a missing field draws blank -- and the factory falls back to 8 for a filter group's max, which is the 8 he was seeing. The fields were missing because P.PIH_AddIconGroup writes its record as a table literal instead of calling CreateLayoutGroup, and that literal listed the fields it thought a filter group had. `iconSize` and `maxIcons` were not among them; CreateLayoutGroup has always set both. Not calling CreateLayoutGroup is still right -- that function picks its store from the OPEN TAB, the one line that put eight stray groups in Krathe's spec store and took three attempts to clean up. What was wrong was hand-writing the record as well as the store. P.NewLayoutGroupRecord is that record now. Both creators share it and differ only in where they insert; the helper overrides the three things it genuinely means differently (the mark, othersOnly, and TOPRIGHT/LEFT_DOWN so a row of cooldown icons runs away from the unit's name rather than across it). Copying a field list is how they drifted, and there is no longer a field list to copy. Schema 11 fills the gaps on a group that already exists, nil-only, reading the values off NewLayoutGroupRecord rather than typing them -- so the repair step cannot disagree with what a fresh group is made of, which is the fault it exists to repair. Anchor and grow direction are deliberately not in that list: filling those from the shared record would move an existing group. (cherry picked from commit 293eb9943cc810707aa255aa6069160f1f15b407)
Krathe: "too verbose, make it Enable with a tooltip explaining what it does in better english." The label was "Play a sound when someone becomes worth infusing", under a box already captioned Sound Alert -- the feature stated twice, and wrapping to two lines to do it. The explanation moves to where an explanation goes: Sound Alert Plays your chosen sound when a group member's cooldown makes them worth infusing. A table spec rather than a bare string, so the tooltip keeps the BOX's title: ResolveTooltipSpec defaults a string's title to the widget's label, and "Enable" heading its own tooltip tells nobody which setting they are reading about. L["Enable"] and L["Sound Alert"] already existed; the old sentence key is retired. The "Only plays while the helper is showing" note underneath stays -- it is a caveat about when the sound is suppressed, not a description of it. (cherry picked from commit 321f90e93f3c767a331e9bbd104e17c552f1c868)
Krathe: "same with this, less verbose with tooltip and more clear."
"Show even if your Power Infusion is on cooldown" spelled the whole rule out on
the row and wrapped doing it. The rule moves to the tooltip:
Show when I can't infuse
Off: markers appear only while your Power Infusion is ready, so you are
never pointed at someone you cannot infuse. On: they appear whatever your
own cooldown is doing.
Off first, because off is the default -- the reader's first line is the
behaviour they currently have, the same shape as the icon tick's explainer.
NOT "Show while on cooldown", which was shorter still and ambiguous in the one
way that matters: this box is otherwise full of OTHER people's cooldowns, so a
label that does not say whose reads as the tracked one. "I can't infuse" is what
the row has to carry; the rest is explanation.
A bare string tooltip here, so its title is the label -- unlike the sound tick,
which passes a table because "Enable" titles nothing. Both notes that cite this
label as the example of "a control that says what it does without a header to
lean on" are updated to the new wording rather than left quoting a string that
no longer exists.
(cherry picked from commit 218d6624fca0f7c4135546c896cd92d50f3bd83b)
Krathe's wording, verbatim, capital C included. My draft was "Show when I can't infuse" -- shorter, and it identified the cooldown only by implication. Naming the spell says it outright, which is what the row actually has to carry: this box is otherwise full of OTHER people's cooldowns, so a label that does not say which spell reads as the tracked one. Same reason "Show while on cooldown" was never an option. The tooltip is unchanged and still carries the rule. (cherry picked from commit 467b10bf3d8ee278a0ebf1b6ae1292bb935d9145)
Krathe: "markers? it should be effects and the wording itself is not very clear
on the tooltip rethink it."
Both faults were mine. "Effects" is this panel's own word -- what the Effects tab
lists and what ACTIVE INDICATORS holds; "marker" belongs to the raid target icon
and the dispel corner mark, which are other features. And the sentence stated a
CONSEQUENCE of the rule ("so you are never pointed at someone you cannot infuse")
before it had finished stating the rule, in one long line covering both states.
Show when Power Infusion is on Cooldown
Off: the helper's effects only appear while your Power Infusion is off
cooldown.
On: they appear even while it is on cooldown.
One line per state, each a plain sentence, off first because off is the default.
"off cooldown" / "on cooldown" echoes the label rather than reaching for "ready"
-- the tick says on Cooldown, so the explanation says the same words back.
A table with no title rather than a bare string: ResolveTooltipSpec fills a
missing title from the widget's label, which is what this one wants, and a table
is what carries two lines.
Two internal comments describing the same control as a "marker" are corrected
too -- prose that teaches the wrong word is how the wrong word got into a string.
The migration notes about helper markers keep theirs: they describe what was
being reasoned about at the time.
(cherry picked from commit b8fb59c205a495a5b6250323cfea45c08a23de8b)
THE PREVIEW CANVAS NEVER LEARNED THE RULE THE EFFECTS LIST LEARNED. Krathe: "any
buff tab on AD is showing our PI helper indicators, it should not."
CollectAllEffects has hidden helper-owned records from every pool but the
helper's since 2026-09-08; the canvas was written before that and kept painting
them. Three leaks, one cause -- every one a DISPLAY site reading a STORE
accessor:
· the filter-group placeholder loop read CurrentLayoutGroups instead of
VisibleLayoutGroups, so the Cooldown Icons group drew on every pool. That
accessor's own note describes this exact split; this was the site that
ignored it.
· the placed-instance loop iterated CurrentAuraPool without testing the mark,
so every helper icon and square drew on the designer's canvas.
· RefreshPreviewEffects did the same for frame-level effects -- a helper
BORDER painted itself over the Any Buff preview. That was the half with no
card to explain it: the effects list already hid the row, so the colour on
the mock frame came from nowhere the panel would admit to.
P.PIHShowsMark is that rule in one place, read in both directions like
VisibleLayoutGroups: on the helper's tab the marked records are the only ones
that belong. P.PIHVisibleRecord hides frame-level effects PER TYPE KEY, not per
record -- nothing stops a user adding an effect of their own to the helper's
filter record from the Any Buff tab, and "this record is the helper's" would hide
their work with ours. No copy is made when there is nothing to hide.
TWO ICONS AT ONCE. "We can't add Power Infusion and Their CD at the same time,
we should allow this if we can?" We can, and the store always could: pihPlace's
own note says a placed target mints an INSTANCE and instances are per-id. The
refusal was the guard being coarser than its reason -- two frame effects on one
key genuinely cannot coexist, but two icons differing in art are not a duplicate.
The guard now tests the ART, and only for icons; two squares are still a
duplicate.
· The art is decided AT CREATION. P.PIH_AddSurface used to create with the pin
and then walk EVERY icon the signal held clearing staticSpellID -- harmless
with one icon, destructive with two: adding "Their cooldown" would have
unpinned the Power Infusion icon already on the frame.
· A second icon is nudged one icon-width off the first, away from whichever
edge its anchor names. Both take the type's default corner, so without it the
pair arrives perfectly stacked and reads as a click that did nothing.
· Each add tile greys on its own art now instead of the two sharing one state,
and the Icon tile survives until all THREE answers behind it are spent.
Guard, nudge and visibility rule unit-tested standalone: the six add/refuse
combinations for the icon pair, that squares and icons do not block each other,
the offset's sign per anchor, and PIHShowsMark in both directions.
(cherry picked from commit ebfc32389f9cf92b68a8e48715f402243c686507)
Krathe: "in guild groups it would be useful to only have the PI alert for the DPS
you know who should be getting PI instead of every DPS in the raid who uses a CD."
A Players box on the Triggers tab, collapsed, with the count on its header so a
folded box still says whether it is doing anything. Empty means everyone, so
nothing changes for a profile that never opens it.
THE PICKER IS THE PINNED-FRAMES ONE AT HALF THE WIDTH, not a reuse of it: "it can
just be based around it... as long as it looks and functions in the same way, but
is adjusted for the more narrow width". The dual-column widget is 460 wide with
two 224px panes; this column is ~230, so one of its columns alone is the whole
surface. GUI:CreateCompactRosterWidget is the same rows -- role icon, class
colour, themed right-hand button, Add-by-name field -- in ONE list whose button
toggles both ways instead of two panes with an arrow between them.
Deliberately not carried over:
· the role bulk-add buttons. "No need for an auto add function as you can pick
class/role etc anyway" -- the helper narrows by role and class on the same tab
already, so a button adding every DPS would be a third control saying it.
· drag-to-reorder. Pinned frames needs an order because the order IS the layout;
an allowlist is a set, and a hand-sorted set invites the reader to think the
order means something.
· the group number, the first thing that stops fitting. The list is still sorted
by group, so the grouping shows -- it just is not labelled.
Chosen players who are not in the group lead the list, dim and with no role icon
(neither role nor class is knowable from here). They are the whole point of the
name field, and a list that only showed who is present would give no way to see
or remove what you had typed.
THE ENGINE SIDE RIDES RAILS THAT WERE ALREADY THERE. Role exclusion is a per-unit
test consumed at exactly two points in the container plus one verb for the sound
path, pushed by a setter and re-pushed on five roster/role events. The allowlist
is one more setter and one more clause. GROUP_ROSTER_UPDATE is already in that
event set, so a list written before the raid forms takes effect the moment the
named player walks in.
· Both tests fold into ONE verb (helperUnitExcluded), so the container funnel
and the sound path cannot answer differently for one unit. IsHelperRoleExcluded
is renamed to IsHelperUnitExcluded with its single caller: keeping the old name
while widening the answer is how the next reader gets it wrong.
· The list is stored as an ARRAY (the picker's order of entry) and pushed as a
MAP. Empty is nil, never an empty map -- the container reads a present map as
"these players and nobody else", so an empty one would silence the helper for
someone who added two names and removed them again.
· It is cleared on PIH_ApplySaved's RESET branch too, or a list would go on
narrowing a helper that is not there and then narrow the next one built.
· The name test fails open twice: no list means everyone, and a unit whose name
cannot be read is shown rather than hidden.
Keys are "Name-Realm", the same shape the pinned list writes, and RosterSnapshot
now treats an empty-string realm as absent -- UnitName returns the realm only when
it differs from yours, and "Bob-" would match nothing while looking like a name
that should.
Unit-tested standalone: the predicate's fail-open cases (no list, nil unit,
unreadable name), same-realm nil vs empty string, cross-realm, the array-to-map
conversion collapsing empty and junk to nil, and the toggle never mutating the
stored array.
(cherry picked from commit 70fe8b3dd98cb14172a42305d398bbaba2b7e5df)
Krathe: "if I've set it to red it will show orange when over a yellow border...
I'm sure we used to offer up a blend mode for animation?"
DF Chase is DF_ORBIT, and its sparkles were created with a hardcoded
SetBlendMode("ADD"). ADD means "add my colour to whatever is behind me", so the
effect showed its true red over the dark icon in the corner of his frame and
turned orange the moment it crossed the yellow border. Working exactly as
written, and not what a colour picker promises.
He is right that it dates from the effects rework: c4b4e5e, "drop
LibCustomGlow". Each new effect got a blend mode chosen by hand -- DF_ORBIT and
DF_PROC additive, DF_DASH / DF_PIXEL / BLINK / DF_FLASH none at all, which is
BLEND. Same colour picker, two different meanings depending on which effect you
picked, with nothing anywhere admitting to it. That inconsistency is the actual
bug; the orange was the symptom.
No blend mode for animations ever existed -- git log -S finds no such key on any
branch. What exists is Border Blend Mode, which governs the border's own EDGES
and not the effect drawn over them: an easy pair to remember as one control.
Animation Blend Mode now sits with Scale and Frequency, on every effect that
owns textures. DF Pulsate is absent: it has none, it modulates the border's own
edges, so Border Blend Mode already IS its blend mode and a second control would
leave the user watching whichever ran last.
NOBODY'S FRAMES CHANGE. An unset key means "this effect's own default", and
ANIM_BLEND_DEFAULT holds exactly what was hardcoded. The dropdown shows that
state as "Default" and maps it back to nil, so the stored shape is unchanged and
a profile exported before today imports untouched.
Three things that would each have made the setting look broken:
· SetBlendMode moved OUT of every `if not texture then` branch. These pools are
reused across re-applies, so a mode set at birth is the mode the first frame
happened to get -- changing the option would have done nothing until a
reload. Same reason SetVertexColor was already out there.
· animSpecHash carries blendMode. StartAnimation returns early on an unchanged
hash, so without it the new mode would land only when some OTHER tunable
moved -- the "nudge the frequency and it appears" symptom this file already
documents for the driver check.
· The Aura Designer's PLACED_BORDER_KEYS and rawBorderAnimStructTok carry it
too, or an AD border would stay stale until /reload -- and it is structural
there, since a container button's animation groups are frozen at build.
The DF Dash OnUpdate path redraws 96 pooled quads per frame through a function
handed geometry and a colour, so the resolved mode is stashed on the border and
stamped only when it actually changes rather than threaded through four call
layers or written every frame.
The value reaches SetBlendMode, which throws on an unknown string, and it comes
out of SavedVariables -- so it is validated against the four real modes and falls
back to the effect's own default rather than erroring the animation pass.
Unit-tested standalone: every effect's unset default matches what was hardcoded,
each override is honoured, garbage/number/empty/nil all fall back correctly, and
the dropdown's DEFAULT<->nil mapping round-trips and restores the effect default.
(cherry picked from commit e36a679056b9178a5ec24a630740e9c35d98a502)
Krathe: "we should let people toggle cooldowns and the sub filters on/off so they
can pick from any of the 4, by default it should match what they have set on
triggers, but it might be the case they want to trigger from a trinket but only
show a CD etc."
Triggers answers WHEN the helper fires; this answers WHAT the row of icons then
shows. Those are the same question until you have both a marker and a row, and
different once you do.
A SHOW block on the group's card, which is what now stands where the generic
LINKED FILTERS picker was dropped. That slot was emptied because it offered a
filter picker over the helper's own plumbing -- correct, and it left the card
silent about its own contents. It holds the four real sources by name instead.
ABSENT MEANS FOLLOW, which is the whole compatibility story. With no pihSources
the group links the cooldown list and nothing else, and that list's own
`includes` bring in whatever Triggers has ticked -- exactly what it did before,
with no migration and no defaults table.
Touch a tick and it stops inheriting and names all four itself, materialised from
the EFFECTIVE set so the first click changes the one thing clicked and the other
three keep what they were showing a moment earlier. A Follow Triggers button goes
back, by DELETING the key rather than writing today's values into it -- "follow"
has to keep following.
THE FOOTER IS A STATE READOUT, not a caption. Four ticks that match the Triggers
tab look identical whether they are inheriting it or were set by hand to the same
thing, and the difference is whether a later change over there still reaches this
group. One line says which.
selection.noIncludes in the registry is what makes "cooldowns only" sayable at
all: our cooldown list NAMES the other three now, so selecting it would drag them
in behind it. A selection that lists its sources itself opts out of the fold.
Inert for everyone else -- no other selection sets it.
Two things that would have bitten:
· PIH_AddIconGroup wrote filterSelection as a literal. It goes through the
shared builder, so a new group and an edited one cannot disagree about the
shape -- and with no pihSources it writes exactly what the literal did.
· No file-scope table for the four keys. Cards.lua sits at Lua's 200-local
ceiling in its main chunk, so `local PIH_SOURCE_ORDER = {...}` is a compile
error rather than a style question. luac caught it; the note records why.
R:SelectionSignature hashes the RESOLVED map, not the selection's shape, so a
source toggle re-signs and rebuilds without noIncludes needing to be hashed.
Unit-tested standalone: a fresh group follows and sets no noIncludes, the
effective set mirrors Triggers, each override resolves to the right presets and
customs, following again picks up a Triggers value that moved while overridden,
and the first click preserves the other three.
(cherry picked from commit c307bb52f5975215378038067ac47664e04d34d8)
…called Icons Two from Krathe, 2026-09-10. "THE NUMBER OF SPELLS TRACKED DOES NOT SEEM TO UPDATE ON THE SHOW TOGGLES BUT ONLY ON THE TRIGGERS." P.PIH_WatchedCount read P.PIH_Settings() outright, so the header reported what the HELPER fires on while the SHOW block one row beneath it listed what the GROUP shows. Two different questions, only one being asked. The sources are an argument now, defaulting to the Triggers ticks for every other caller. Cooldowns counts as a source rather than an always-on baseline: the group can switch it off, and a count that added the class list regardless would over-report by forty. The wording follows the state. A group that has been given its own set is no longer reporting "from your Triggers", and a header saying so while the block under it says it has stopped following would be the panel contradicting itself one row apart -- so following gets the Triggers phrasing and overridden gets the count alone. "MAYBE IT SHOULD BE CALLED PI HELPER - ICONS NOT COOLDOWNS AS IT CAN BE TRINKETS ETC TOO?" Right, and more so since the SHOW block: the group can now be set to trinkets ONLY, where the old name is not vague but wrong. Schema 12 renames an existing group -- but ONLY if it still holds the exact old string. The card has an editable Group Name field, so anything else is a name the user typed, and renaming that would be the addon overwriting their words to satisfy its own tidiness. The name stays stored data and never L[]: a translated string in a profile is a name that changes when the client's language does. The old name is a literal inside that migration rather than a constant beside the new one, and the schema's own note says why: this file is at Lua's 200-local ceiling in its main chunk, so the second local is a compile error, not a style question. luac refused it twice today; both refusals are now documented where the next person will hit them. Unit-tested standalone: the count for each combination of the four sources, including cooldowns off with amplifiers on, and his current 91. (cherry picked from commit bcb73bc7603b3f223a49f0c44ad8283147a8ae65)
Krathe: "yes build the icon block the same". The icon card had ONE tick for this -- "Ignore trinkets, potions and racials" -- which is the same mechanism at a coarser grain: per-record mutes, applied to the resolved include map by narrowByPlacementMutes. It is now the same four sources the Cooldown Icons group got, so the two cards read alike. NOTHING TO MIGRATE, which is why the old API could simply go rather than being converted. A record saved by that tick carries exactly the mutes these readers read, so "all three ignored" reads back as three ticks off -- no conversion step, no schema bump. P.PIH_IgnoresAmplifiers / P.PIH_SetIgnoreAmplifiers are deleted with the control they served, and the note above their old home says so. IT CAN ONLY SUBTRACT, and the footer says so rather than the panel pretending otherwise. A placed effect is keyed by ONE filter reference, so it cannot carry a selection of its own the way a group can -- it can show less than Triggers watches and never more. Showing more needs a filter of its own, which is what the group is for. A source Triggers has switched off is GREYED with its own reason, not hidden: hiding it would leave the icon card and the group card disagreeing about how many sources this feature has. Class cooldowns is mutable too, which costs forty entries in the record for "show trinkets only". Accepted deliberately -- the alternative is a fourth source the box has to explain the absence of. ☠ THIRD luac REFUSAL TODAY on the 200-local ceiling in Cards.lua. P.PIH_SourceIDs is a table field rather than a `local function` for that reason alone, and says so, because it reads like a style smell and is actually a compile error. Unit-tested standalone: a fresh record reads all four on and writes no mute table, each toggle leaves the other three alone, turning one back on clears an emptied table, cooldowns can be muted away, a legacy "ignore amplifiers" record reads back as three ticks off, and an unwatched source reads off with its availability explaining why. (cherry picked from commit c7f2bd966b3c36a0f3250e7fc64e2c72b8fa95b5)
Krathe: "a placed PI icon should show as PI Helper - PI Icon / Icon / Icons,
right now only the last actually shows."
Helper effects are named "PI Helper" and told apart by their type BADGE -- which
was the fix for the old "PI Helper - Big cooldown" on every row, and is still
right for Border, Square and the rest, each unique on the signal. The badge
stopped being enough the day both icons could exist: it says "Icon" for the one
pinned to Power Infusion and "Icon" for the one showing their cooldown, so the
pair added two changes ago arrives as two rows reading identically. That is
exactly the "somebody deletes the wrong one" case the label's own note warns
about, now reachable.
pihLabel takes the record as well as the signal, and appends a name for the two
icons only -- where it disambiguates, and nowhere else, so no row prints the word
its badge is already showing. ("Icons", the group, is stored data and was renamed
in the previous commit.)
staticSpellID's presence is the art, as everywhere else -- there is no second
field recording the choice. A frame-level cfg carries no `type`, so it can never
reach that branch; only placed instances do.
Unit-tested standalone: pinned and dynamic icons, square and bar falling through
to the badge's word, a frame-level cfg with no type field, a nil cfg, the infused
signal untouched, and an unknown signal still returning nil.
(cherry picked from commit 2190b62267370631ae2823334845f831fb5ecdae)
Krathe: "I could hear the sound trigger but did not see the border or PI icon at
all."
The two consumers of the helper's cooldown list do NOT resolve it the same way:
· the SOUND registrations resolve fresh on every arm (Engine.lua's
pihResolvedMap calls R:ResolveSelection directly);
· the VISUALS go through DF:ResolveADFilterRef, which MEMOISES the resolved
map and only clears when DF.auraLayoutVersion moves.
So a rewrite of `includes` with no version bump leaves the border and the icon
matching an older spell set while the cue plays off the current one -- a trigger
that makes a noise and draws nothing, with no visible cause.
P.PIH_SetAmplifier already invalidated, through pihRefresh. The paths that did
not are the ones nobody clicks: the schema sweep -- which writes these for the
first time on every upgrading profile, in the same pass that strips the 51
copied ids back out -- and the Triggers panel build.
INVALIDATED ONLY ON A REAL CHANGE. This runs on every panel build and every
sweep, so an unconditional bump would re-resolve every filter reference in the
addon each time the tab is opened. The comparison is set-equality on both halves,
which is also what makes the common case (rebuilding the same table) free.
This is a gap introduced with the references yesterday and is worth closing on
its own merits. Whether it is the whole of what Krathe saw is NOT established:
the sound path also checks the per-unit gate at ARM time while the visuals check
it at RENDER time, so a gate that closed after the cue was armed produces the
same symptom by a different route. That one needs the answer to "was your own
Power Infusion on cooldown at that moment", which only he can supply.
Unit-tested standalone: nil<->set transitions invalidate, a preset or custom
added or removed invalidates, and rebuilding an identical table -- every panel
build, every sweep -- does not.
(cherry picked from commit edf80024ba5f4dc376920679710b8c04accc7ff4)
Four reports, four different theories from me, no measurement:
· the border cleared and the PI icon did not;
· the sound played and neither the border nor the icon drew;
· the border and the Icons group drew, the PI icon did not, and the sound
did not play.
Every one of those is a slot whose LAST PUSH disagrees with the gate, and no
field in the readout could show it. It already prints what the gate INTENDS
against what the chokepoint SAYS -- deliberately as two lines, because they are
allowed to differ -- and the sound registrations per frame. It could not see the
third state, which is where all three reports live.
AuraContainer.GetHelperSlotStatus answers it: how many helper slots exist, how
many would be handed the DEAD filter right now, how many are waiting on a
DEFERRED push, and how many are parked. Derived on every call, never stored, so
it cannot drift from what the slots are doing -- the fault every "same config,
different outcome" hunt in this addon has come down to.
`pending` is the field that names a cause. SlotHandle:_applyHelperGate cannot
call a native tuning setter under lockdown, so it defers to
PLAYER_REGEN_ENABLED -- and Power Infusion is pressed in combat by definition. A
slot at pending > 0 while the gate reads OPEN is that deferral, visible for the
first time, and the line is marked bad in exactly that combination.
Reading it against "gate intends":
OPEN + dark 0 + pending 0 -> the slots agree; the fault is elsewhere.
OPEN + pending > 0 -> a deferred push not yet drained.
OPEN + dark > 0 -> a per-unit exclusion (role, or the named player
list), not the gate.
Diagnostic only: nothing about rendering changes. Adding the detector before the
fix is the order this codebase's own rule asks for, and the last four rounds are
what happens without it.
(cherry picked from commit 07b006dcfae86db6f44e9b2c289c3fedafed7dc9)
…with the rest Krathe: "the PI icon does clear when I use PI on them but it seems to take longer than the border/icons and other indicators." Not stuck -- QUEUED. SetAuraSlotCandidateFilters is a dirty-mark, not a repaint: the container re-reads on its next processor pass, which is the unit's next UNIT_AURA or the next OnUpdate while visible. So "stop matching" is a request the engine honours WHEN IT NEXT LOOKS. The frame-level border is painted by DF on the same frame, and the slot waits for traffic that may be a moment or a couple of seconds away -- which is also why it read as intermittent. _applyHelperGate now arms the processor after the push. UpdateAllAuras is itself only a mark, so this collapses the wait to the next frame rather than making it instant; there is no synchronous re-parse to reach for. It runs on the gate edge -- twice per Power Infusion cycle, not per frame and not per aura event -- so the cost is not measurable. pcall(fn, self) rather than the closure form, per this file's own rule recorded at applyGroupTuning's tail. SetHelperGate's note names this exact path as the reason that rule exists. ☠ A COMMENT I NEARLY SHIPPED WAS FALSE. It claimed the combat deferral drains back through this function. It does not: _replayTuning clears _cfPushed and calls _pushFilter, which re-derives park vs live at drain time and pushes candidates itself, never re-entering here. Checked before committing, and the note now says what actually happens -- a gate edge deferred in combat drains with no arm of its own and picks up whatever the combat-exit kick does. _pushFilter is deliberately left alone. Arming there would cover that drain, and also park, restore and the death latch -- a far wider blast radius than the symptom this answers. If the lag is ever seen on those paths it should be its own change with its own testing. (cherry picked from commit ce70a12791ceac125f41390a9c3d4b094816fd07)
Reported from the alpha: helper borders and icons rendering for NON-PRIESTS.
There was no class gate on the resident side at all. Only the Options UI checked
-- DF.IsPIHelperAvailable hides the page and PoolDefs omits the pool tab, on both
layouts -- and hiding the controls does nothing about records that already exist.
A profile shared across an account, an imported preset, or a priest's own profile
opened on an alt all carry the marked records, and the factory renders what the
pool holds. It has never asked whose class it is, and it should not have to.
PIH_ApplySaved now refuses first, for anyone who is not a priest.
☠ DARK, NOT "NO HELPER". The existing `if not s` branch OPENS the gate --
"nothing is left to hide" -- which is right when the pool genuinely holds nothing
and exactly wrong here: a non-priest with marked records needs them SUPPRESSED,
and an open gate renders them. The two cases look alike and mean opposite things,
so this is its own branch rather than another condition on that one. Getting that
backwards would have shipped a fix that reproduced the bug.
PIH_SetEnabled(false) is the lever, not a new one: forced dark that nothing
reopens, the readiness ticker refused, the manual hold released, sound disarmed
through pihSet. Everything a class gate needs was already written and tested --
and it latches, so the watcher, the gate switch and the ticker all decline on
their own existing guards rather than needing new ones.
THE RECORDS ARE NOT TOUCHED. Deleting a priest's work because their alt logged in
would be destroying data over a display question, and the same profile has to
come back intact on the priest. Suppression only.
pihSyncWatcher takes the same test, so five events are not registered on every
alt of every priest who shares a profile for a feature they cannot enable. The
watcher's tick would have declined anyway; not registering is the honest version.
Read at call time rather than at load: UnitClass("player") is not dependable
before login, and this runs on login and on every profile switch. A character's
class cannot change, so there is nothing to re-check afterwards.
(cherry picked from commit 2807248efcc498c6275c31702b113d4f30c93681)
An option Krathe asked for, beside the cooldown gate on the Triggers tab -- same kind of thing (a condition on whether the helper has anything to say at all) and under the same heading, for the same reason that one is: a titled box around a single tick is more chrome than either setting is worth. Off by default and absent when off, so no existing profile changes and there is no defaults entry to keep in step with the engine's own `== true` read. pihShouldShow is now the one answer for every GLOBAL condition, as against helperUnitExcluded which is the per-unit half. Every gate decision goes through it: the readiness ticker, both switches, PLAYER_ENTERING_WORLD and the cooldown-cleared branch. Leaving any of them on pihReadReady would have reopened the gate out of combat the moment a cooldown ticked over. ☠ THE TWO CONDITIONS ARE INDEPENDENT, and the combat test runs FIRST. Someone who has switched the cooldown gate off still means it when they say combat only, so `not pihGateEnabled` can no longer short-circuit to "open" -- both the switch and the PLAYER_ENTERING_WORLD guard had to stop treating it as the only condition. ☠☠ THE EVENT CARRIES THE TRUTH, NOT DF.playerInCombat. That flag is the house source and is correct -- but Core.lua writes it from PLAYER_REGEN_DISABLED / _ENABLED, the same two events this now watches, and handler order between two frames is undefined. Reading it from inside our own handler can see the value from BEFORE the transition and resolve the gate backwards. pihShouldShow takes an optional override; the regen branch passes what the event itself means and every other caller reads the flag, which by then has settled. (Never InCombatLockdown: that is the addon-restriction state, not the player's.) ⚠ BOTH TRANSITIONS. The addon has a standing note that a combat-keyed gate needs a refresh on entering AND leaving, and that leaving is the half people forget. The branch sits ahead of the cooldown guards -- it was below them first, and those return early when the cooldown gate is off, which would have silently excluded exactly the users the independence above is for. Caught by reading the placement back rather than trusting the comment I had just written. Loaded before the gate setters on both paths (PIH_ApplySaved and the panel's PIH_Apply), since both resolve through pihShouldShow; loading it after would settle the gate from the old value and leave it wrong until the next transition. Cleared on the reset branch with the rest, so a stale hold cannot outlive the profile that set it. Unit-tested standalone: the default changes nothing, combat-only hides out of combat and shows in it, the cooldown gate still applies inside combat, the two compose independently with the cooldown gate off, and a stale combat flag at transition time resolves correctly in both directions. (cherry picked from commit 2725640bc6690924b2926449406986786b9900a7)
Krathe: "it's not working in my raid but I could see it on people before my auto
layout kicked in."
Auto layout is a MASS RETARGET. A slot gated dark for unit A -- a tank excluded
by role, or someone off a named-player list -- migrates to unit B and keeps the
DEAD candidate filter it was handed for A. _cf() re-derives at READ time, but
nothing PUSHES after a retarget: the next push only comes on a gate edge, and
there is no reason for one. The verdict travelled with the container.
This is the exact class SetSlotOwnerUnit already re-seeds two other things for.
Death and visibility are re-asked from their registries there because "the new
unit may already be dead, and that edge will never fire again" -- role exclusion
is that sentence with a different noun. It was missed because the gate is DERIVED
rather than stored, which makes it look like it cannot go stale: the derivation
is fresh, the push is not.
Both retarget lanes get it:
· SetSlotOwnerUnit re-pushes each helper slot's candidates (placed icons,
squares, bars);
· Handle:SetUnit re-tunes a helper group. config.unit is updated there, so
recordCandidateFilters would DERIVE correctly -- but the bounce that follows
re-parses against whatever map the container is still holding.
NARROWED TO OUR OWN CONFIGS in both, matching SetHelperGate's own walk:
SetAuraSlotCandidateFilters has no engine-side equality guard, so an unnarrowed
call would re-parse every placed indicator in the addon on every retarget -- and
a raid auto-layout change retargets the whole roster at once.
Lands on top of this morning's processor arm, so the re-derived filter is acted
on next frame rather than at the unit's next aura event -- which on a freshly
retargeted frame could be a long way off, and is the same "long-lived buff nobody
re-casts" hole the slot-retarget kick was written for.
(cherry picked from commit 7cf49acec32bf3973b5f133d452b235d2c5b6dcc)
Krathe's raid: the helper worked, then stopped when the auto layout kicked in at 20 people, and a reload did not bring it back. He spotted the cause himself -- the helper is not on the preset that layout switches to. That part is configuration, not a bug: a raid auto-layout may point its own AD PRESET at something other than the mode base, and his 21-30 layout uses a "Flex 21-30" preset while the helper lives in "Raid". The frames render from the overlay (DF:ResolveAuraDesigner honours it) and it holds no helper, so nothing draws. Nothing here changes that. What IS a bug is that the engine could not tell. pihSettings read GetModeBaseAuraDesigner -- the EDITOR's variant. Presets.lua says so on the function itself: "the ACTIVE designer a mode resolves to right now ... Used by LIVE consumers (SoundEngine, migrations) that must match what's on screen. The EDITOR uses the GetModeBase* variants." The helper is a live consumer and was reading the editor's answer, so with the overlay active it kept finding the helper in the BASE preset and armed the gate, the roles and THE SOUND for a preset that is not on screen. ⇒ "I could hear the sound trigger but did not see the border or PI icon at all" is this, and so is a status readout reporting a healthy helper while nothing renders. The feature could not diagnose itself because its two halves were reading different presets -- which is most of why this took as many rounds as it did. It does not make his helper appear. What it fixes is the engine agreeing with the screen: no helper on the active preset means silent, ungated, and a /df debug pi that SAYS "no helper installed" instead of describing one nobody can see. SoundEngine already used the live variant, which is why the sound followed the frames while the gate did not. (cherry picked from commit dc1c5f711215f7339d2a63310d4c390f4cc6cc13)
…e it Krathe: "I've added a list of players and it was showing the effects on other players not just them." P.PIH_Apply pushed the roles, the gate, the feature switch, the sound and the watcher -- and not the player list. Only Engine:PIH_ApplySaved sent it to the container, and that runs on login and profile switch, so a list edited in the panel did nothing at all until the next reload while the panel showed it filled in. Mine, from the commit that added the feature. The tell was there to be read: roles and players are the two halves of ONE narrowing -- helperUnitExcluded reads both -- and only one of them was in this function. A setting the panel stores but never pushes is the "lying control" this feature has been cleaned of three times already; it came back through a path nobody re-read. Array in, map out, empty as nil -- the same conversion PIH_ApplySaved does, and for the same reason: a present map means "these players and nobody else", so an empty one would silence the helper for someone who just removed their last name. AND THE READOUT COULD NOT SEE IT EITHER, which is why a missing push was invisible. /df debug pi now prints "players watched" READ OFF THE CONTAINER, not off the profile -- a field that repeated the setting would have agreed with the panel and hidden the fault, exactly as "roles excluded" beside it reads the container's own copy. ⚠ The second half of the report -- effects showing while Power Infusion was on cooldown -- is NOT addressed here and is not explained by this. The gate is global and this list is per-unit, so a missing list cannot hold the gate open. Not guessing at it: the readout's "gate intends" / "chokepoint says" pair and the "driven by" line answer it directly, and one of the states they name (a manual hold from /df debug pi on, which suspends the watcher) would produce exactly that symptom. (cherry picked from commit 02f3463e5214b82e2e8300f8e1fa4ed0e80ad2d8)
…e log Krathe's log answered it. 33 clean gate edges -- "DARK on cast" / "OPEN, cooldown cleared" alternating across 72-98 containers -- so the watcher, the gate and the broadcast were all working, while the placed icons carried on showing people during his cooldown. ☠ THE COUNT WAS LYING. SlotHandle:_applyHelperGate deferred to PLAYER_REGEN_ENABLED whenever InCombatLockdown() and returned TRUE, so SetHelperGate counted a QUEUED slot as a pushed one. In a raid you are in combat for the whole pull, so every edge on every placed slot queued and none of them landed during the fight -- while the log read as perfectly healthy. That is "the border DID go away but the PI icon did not", from the first report onwards: the border is frame-level and painted by DF, the icon is a container slot. ATTEMPT, THEN DEFER. The push is tried and the deferral is kept for an actual failure, so a refusal is MEASURED rather than assumed. The precedent is in the same walk: SetHelperGate's other half, applyGroupTuning, has always called native tuning setters on this edge with no combat guard at all, in combat, twice per Power Infusion cycle -- its own note says so. Two lanes of one broadcast cannot both be right about whether that is allowed, and the guard was the inconsistency. Pushed and deferred are now counted apart. AND THE DETECTOR GOES IN THE LOG, WHERE IT SHOULD HAVE GONE THE FIRST TIME. Krathe: "I've told you before I can't really run this debug commands in raid combat, you need to add what you need to the debug console." He had, and I put it behind /df debug pi twice anyway. One line per gate edge now carries pushed, deferred, the four slot counts, every condition in force (enabled, gateEnabled, combatOnly, inCombat, manual) and the size of the player list -- so a reload is the whole reporting procedure. Guarded by DF:DebugActive so the slot walk that builds it costs nothing with logging off. ☠ THE _ENV DIFF CAUGHT A BUG THE COMMENT WOULD NOT HAVE. pihGateEnabled is declared BELOW pihSet, so logging it compiled to a nil GLOBAL read -- the line would have reported "nil" forever while looking correct in the source. Hoisted to the forward-declaration block; the assignment stays at its own site. (cherry picked from commit f3ed12b0fb53117884f674184d6ce0ff16661880)
Krathe's log, after the combat fix: deferred=0 on every edge and "56 tot/56 dark"
on every DARK. So the slot lane is doing exactly what it should -- and the icons
are still on screen.
The one number that could not answer the next question was `pushed`. It counted
group HANDLES and placed SLOTS together, and the two lanes fail for entirely
different reasons: a handle is skipped when it is destroyed or its backend is
gone, a slot when it is parked or the push is refused. 72 pushed with icons still
showing, and no way to tell which 72.
Now reported apart:
· handlesSkipped -- ours, carrying the mark, and NOT reachable. Those render
whatever they last had and nothing in the broadcast can correct them, so they
are counted rather than passed over in silence (the walk used to skip them
with no record at all).
· groups N tot/N live/N dark, from GetHelperHandleStatus -- the handle lane's
equivalent of the slot line. The Cooldown Icons group IS a handle, so nothing
in GetHelperSlotStatus could ever see it, which is precisely the blind spot
in the screenshot: a row of three icons is what that group draws.
Diagnostic only; no rendering behaviour changes. What the next log will settle is
whether the group is being pushed and ignoring it, or is not in the walk at all.
(cherry picked from commit 8ec6c028e568d39ad440f107be0d435f92535919)
… map
Krathe's raid log, lane-split: on every DARK edge, 30 group handles and 30
placed slots, pushed=60, deferred=0, skipped=0, in combat -- and the helper
icons and borders carried on showing on his allowed players while Power
Infusion was on cooldown. The gate flipped, the broadcast reached every
container, the push succeeded, the filter did not blank the slot.
HELPER_GATE_DEAD_CF was { includeSpellIDs = { [1] = true } }: "match only spell
1", i.e. nothing. But includeSpellIDs is evaluated INSIDE
CanApplyIdentityCandidateFilters, and when the identity gate declines -- range,
visibility, a cinematic, every reason the latch work exists -- the include map
is skipped entirely and every helpful aura passes. This file documents that
fail-open at length (IDENTITY-GATE EXPOSURE), and its own classifier,
filterVulnerableToIdentityGate, has been returning TRUE for the dead filter all
along. A "dead" filter built on that lever is dead only while the gate happens
to be applied, which in a raid is intermittently -- and every report of this bug
was intermittent.
maxDuration = 0 is evaluated OUTSIDE that gate, in readable Lua
(DoesAuraPassCandidateFilters), and excludes every aura unconditionally. It is
SLOT_PARK_CF -- verified in game 2026-08-30 against a unit carrying eight live
buffs, rendering nothing -- and it is why parking a slot works when gating one
did not. The dead filter is that lever now.
A separate constant from SLOT_PARK_CF, deliberately: _cf() returns it by
identity and the value-tracked push compares against it, and a gated slot must
stay distinguishable from a parked one because the two are cleared by different
things.
One line, and it explains the whole history: the border (frame-level, painted by
DF, never through candidate filters) obeyed from the first report, and the
container-backed icons and groups obeyed only when the identity gate happened to
be applied at the moment of the push.
(cherry picked from commit d52f9dd1c22ceed97c5a332b065bb2ef2cd21cf9)
Krathe: "I might want to add my raid team to the list but turn off showing only for those players in a pug group without having to add/remove them all each time." The list was doing two jobs. Names meant narrowing and no names meant everyone, so the only way to stop narrowing for a night was to delete the names and type them back afterwards -- the same fault as the enable tick that used to delete records, a switch whose off position throws data away. playersOn separates them. Absent means on, so every existing profile loads exactly as it did: a profile with names was narrowing and still is, one without was not and still is not. Only an explicit off is stored, which is also why there is no defaults entry for a later migration to trip over. An empty list still means everyone even with the switch on. "Watch nobody" is not a state anyone asks for by emptying a box, and blanking the feature silently is the worse failure -- the same reason the engine has always pushed nil rather than an empty map. Both push sites make the decision the same way. The panel's live push and the engine's load push are two halves of one behaviour and have already drifted apart once, when the live half did not exist at all and an edited list did nothing until the next reload. The header count now reports how many the box is narrowing to rather than how many are saved, so a switched-off list shows none. A 5 beside a switch that is off would be the header lying about the one thing it exists to report; the names are one click away and the tick inside says why. (cherry picked from commit c638e9e2c6ffb3070081daa61ba6b22ff637632d)
…lds y Krathe, 2026-09-14: "I can't seem to tick 'only watch these players', it's not doing anything." With nil as the true arm, the and/or idiom collapses: `on and nil` is nil, so `or y` wins on both inputs. P.PIH_SetPlayersOn stored false whether the tick was being switched on or off, so the first click unticked it and no click could tick it again. P.PIH_SetIconShowsAura had the same shape and always pinned the Power Infusion icon, so "show the cooldown's own artwork" never took. Both are explicit ifs now. A sweep found the same idiom in six more places outside this feature; they are reported separately rather than folded in here. (cherry picked from commit 179082bfec498200334cf758022bf0a6a6c4f60f)
…s did
Sweep after the PI helper players tick (179082bf). Six more sites used the
idiom with nil as the true arm, and in every one the intended nil branch had
never once been taken:
Cards.lua the Other tab resolved card icons with the spec it is
meant to bypass
Groups.lua (x2) the Other pool previewed with the spec it must not use
BindingEditor macros showed the fallback line the comment says to hide
AuraContainer the assist verdict named every TRUSTED unit "cannot-assist"
in the idgate readout
ElementAppearance the alpha-host dump toned every line BAD, successes too
Factory `isBar and nil or defs` in the placed signature -- harmless
by its own note (both resolve identically on a bar), written
out so the expression says what it does
Each is now an explicit if, or the and/or with a non-nil true arm. No
global-name change in any file; line endings preserved (BindingEditor is LF
in the index and stays LF).
(cherry picked from commit 868e2e409b8b1708014d80b17f5dc8cd8b4841c0)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
66 commits on top of
main, all field-tested on Krathe's raid lane over the last week. Two things are deliberately not in here: the battle-res slot-park guard (that is #265 againststable, and will reachmainthrough the stable merge — if that lands first, this rebases with one small conflict in_applyHelperGate, which I'll resolve), and the local-krathetoc suffix.Where it lives now
The helper is a fourth pool tab inside the Aura Designer (Any Buff / Debuff / … / Power Infusion Helper), not a page beside it and not a band on the Any Buff tab. Two sub-tabs: Triggers (what makes it fire, for whom) and Effects (the designer's own Effects tab, scoped to the helper's records). Its records still live in the Any Buff pool because the pool decides the caster filter — that is plumbing the user never sees. The Any Buff tab no longer shows the helper's indicators, and the alpha.6 changelog line saying it "lives on the Any Buff tab" will want updating.
Triggers
on/totalcount on the header.f.includeson the helper's filter, andFilterRegistry:ResolveSelectionfolds includes one level. The helper's list never gains their spells; edit them in their own lists. (selection.noIncludesopts a consumer out.)GUI:CreateCompactRosterWidget, narrowed for the column), with an "Only watch these players" switch so a raid team stays written down through a pug night. Absent means on; an empty list is still everyone.Effects
?.The gate (the part that matters in a raid)
pihShouldShow(inCombat)takes the event-carried combat truth rather than racing the handler order;DF.playerInCombat, neverInCombatLockdown(), per the standing rule._applyHelperGateattempts the push and defers only on an actual refusal (it used to defer always, so in a raid — combat for the whole pull — no edge on a placed icon ever landed). It arms the processor after the push so a placed icon clears with the rest.{ maxDuration = 0 }— the same verified park lever asSLOT_PARK_CF, evaluated outside the identity gate. It was anincludeSpellIDsmap, which the identity gate skips when it declines, so the "dead" filter failed open exactly when it was needed; every intermittent "icon stayed on after I cast PI" report was this.SetSlotOwnerUnit/Handle:SetUnit), reads the live designer (auto-layout preset overlays honoured), and is a no-op on non-priests (records untouched, everything suppressed throughPIH_SetEnabled(false))./df debug pishows what each placed slot is carrying.Also in here
x and nil or yin both addons — nil never wins an and/or, so theorvalue always did. Two of them were the helper's own ticks; the other six are listed in the commit.Verification
Every file compiles (226 non-Libs Lua files), no unexplained global-name changes per commit, line endings and BOM state preserved throughout, and one cherry-pick conflict against main's Rows.lua (the retired in-row helper band) resolved to the removal.