Records this round's two owner-reported fixes (the fabricated Buffs-tab
middle strip removed; Advanced Options tooltips removed) against the
already-closed slice 7 campaign, with commit references and the
MossTank/App test-count deltas.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Owner's third live look (2026-09-07): "Also still have tooltips in the
advanced options, remove please." VTank's own AdvancedOptionsView has
no tooltip mechanism at all (docs/research/vtank-kb/08-ui-views.md
§1) — its own description readout (AdvancedOptionDescription, the
label under the field) is the retail in-popup info channel, not a
hover tooltip. Removes the three tooltip= attributes
mosstank-advanced.xml had grown (the option list, the category
checklist, the value field); nothing else in the file set one.
New test AdvancedOptionsPopupHasNoElementWithATooltip builds the real
mosstank-advanced.xml against a real MossTankPanel and walks the
WHOLE resolved UiElement tree checking RuntimeTooltipTextSource is
null everywhere, rather than re-checking the three known offenders —
MarkupDocument.Build only ever sets that field when the source element
declared a real tooltip= attribute, so a null source function means no
tooltip was ever bound. Shown to fail first against a targeted
mutation (a tooltip= temporarily reintroduced on the option list) —
failed with "UiMarkupList carries a tooltip" — then reverted and
restored green.
App markup/plugin filter: 261 -> 262 (one new Fact).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Owner's third live look (2026-09-07): "Screenshot shows options under
the buffing tab circled in red. Those are made up, remove them from
here." VTank's real Buffs tab (docs/research/vtank-kb/08-ui-views.md
§1 "Tab: Buffs") is exactly two labels + two 320x116 lists + two
"Add..." buttons — nothing in the middle strip. The MossTank-only
extras block that used to live there (7 category toggles, the
difficulty-margin/rebuff-under spinners, a second Force/Stop buffing
button + status, and the "N attributes, N trained skills, N buff
lines" coverage counts) is removed outright from mosstank.xml.
The underlying settings are not lost: BuffSettings' seven category
bools, SkillExcessOverDifficulty, and RebuffWhenUnderSeconds stay in
the model and its JSON side-car, and the latter two are real,
already-editable VtankOptionCatalog entries
(SpellDiffExcessThreshold-Buff / RebuffTimeRemainingSeconds) reachable
from Advanced Options. Force Buff / Cancel Force Buff stay on the
Options tab, exactly where VTank has them.
Orphaned XML-only bindings deleted from MossTankPanel.cs: the seven
*Enabled/Toggle* pairs (TrainedOnlyEnabled/ToggleTrainedOnly,
AttributesEnabled/ToggleAttributes, ProtectionsEnabled/
ToggleProtections, AurasEnabled/ToggleAuras, BanesEnabled/ToggleBanes,
RegenerationEnabled/ToggleRegeneration, OtherEnabled/ToggleOther),
DifficultyText/DifficultyUp/DifficultyDown, RebuffText/RebuffUp/
RebuffDown, BuffButtonText, and the Buff action property (StartOrStop
itself stays — still reachable from the `/mt buff` slash command,
MossTankCommands.cs). BuffStatus and Coverage stay: both are still
exercised directly by MossTankPanelTests independent of any control
reading them, and neither is listed among the settings the owner's
report says must stay in the model, since neither backs a real
BuffSettings field — they're the "force-buff status" and "coverage"
strings the report never asked to keep alive as UI, only as state.
Test changes: MossTankMarkupContractTests' old
BuffsExtrasStripIsCenteredAtThePanelsFloorAndCoverageSitsUnderTheButtonRow
(which asserted the removed strip's geometry) is replaced with
BuffsTabIsVtankOnlySixControls, asserting the Buffs group is exactly
two lists + two "Add..." buttons + two labels, with no toggle, no
slider, and none of the deleted bindings anywhere in it. Shown to fail
first against a targeted mutation (a stray toggle re-added to the
group) before the fix was restored. MossTankMarkupBuildOverRealFilesTests'
BuffsExtrasStripStaysCenteredAndNonOverlappingAtTheFloor (which proved
the same now-deleted strip stayed centered at the panel's floor) is
deleted outright; the two remaining lists' own non-overlap/
within-bounds behavior stays covered generically by
ResolvedMainPanelHasNoOverlapOrOutOfBoundsChildAtThisSize.
MossTank suite: 726 -> 726 (one Fact replaced, same count). App
markup/plugin filter: 262 -> 261 (one Fact removed, none added here).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Campaign VT slice 7: window at VTank's proportions (resizable, anchored),
Options/Profiles/Vitals/Monsters/Items/Consumables/Buffs/Route/Meta
transcribed from mainView.xml with VTank's per-cell behaviors, Advanced
Options in VTank's click model, the buff picker and meta-editor popups,
VTank's 27 recalls wired to routes, plain dropdowns/sliders with retail
scrollbar chrome. Two Opus lenses + rounds A–F + owner live looks; final
combined re-check MERGE-READY.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
# Conflicts:
# docs/plans/2026-09-07-campaign-vt-slice7-tabs.md
Three small resemblance-re-check nits.
Buffs: the extras strip (toggles/Difficulty/Rebuff/Buff button, no VTank
counterpart) stayed at its pre-Round-D x=332 while Round D item 4's own
+128 right-anchor shift moved the Blacklisted Buff Families list from
x=524 to x=652 — an 8px left margin against a 138px right margin.
Discovered a hard geometric ceiling while fixing this: Extra Buff
Spells never moves (anchor="left top bottom", pinned x=4..324) but
Blacklisted Buff Families tracks the growing right edge, sitting at
x=524 at the panel's own tested FLOOR (856x236) and x=652 at the
984-wide enlarged default — a 128px swing. A single fixed-position
strip can only be centered against ONE of those widths; centering
against the 984 default (tried x=397 first) put the strip's right edge
55px inside where the list sits at the floor — a real overlap the
existing ResolvedMainPanelHasNoOverlapOrOutOfBoundsChildAtThisSize pin
(856x236 case) caught immediately. x=333 is the widest safe position
(9px margins against the floor's 324..524 gap) — 1px from where fix
round B item 2 already had it. The residual 984-default asymmetry
(~9/137, barely changed from ~8/138) is an accepted, unavoidable-
without-a-redesign limitation, the same "dead margin, polish later"
trade already accepted for six other tabs at this size. Coverage moved
from y=213 (anchored to the group's bottom edge, a real 55px empty
hole once the panel grew past its original 194-tall size) to sit
directly under the Buff button row (y=176, no anchor).
Loot Editor: dropped the leftover standalone "Back" button — the
window's own OS-level title bar already closes it, matching every
other plugin popup (Advanced Options included) since round D.
CloseLootEditor stays real, still called by ToggleLootEditorVisible.
Route: the waypoint grid's count column (declared width="30") was the
grid's LAST column, so docs/plugin-ui-markup.md's own "Width semantics"
rule made that declared width never validated/used — it silently
absorbed whatever remainder was left (33px normally, but only 17px once
the scrollbar's 16px reservation ate into the list's own 370px width,
since 337+30 already exceeded 370-16=354). A trailing filler column
alone wasn't sufficient — proved by a targeted mutation below — since
even non-last, a truly fixed 30px column still gets clamped by the
same 354px ceiling. The real text column narrows 337->324
(324+30=354, exactly matching the scrollbar-reduced width) so the
count column gets its full 30px whether or not the scrollbar shows.
Every new/changed pin shown to fail against a targeted mutation first:
LootEditorHasNoLeftoverBackButton failed with the button re-inserted,
then passed once removed. RouteShapedGrid_CountColumnStaysThirtyPxWhen
TheListScrolls (a synthetic list matching Route's real 370/324/30/*
shape, 9 rows forcing the scrollbar, reflecting into UiMarkupList's own
_cachedLayout) failed both against the original 2-column 337/30 shape
(17px, reproducing the reported squeeze) AND against filler-only-no-
narrowing (also 17px, proving the filler alone isn't the fix), then
passed at exactly 30px once both changes landed together.
BuffsExtrasStripIsCenteredAtThePanelsFloorAndCoverageSitsUnderTheButtonRow
and its App-side resolved-tree counterpart
BuffsExtrasStripStaysCenteredAndNonOverlappingAtTheFloor pin the
floor-centered position and the Coverage gap.
MossTank suite 725 -> 726 (one new pin); App markup/plugin filter
260 -> 262 (two new pins).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Resemblance re-check: "Monsters' move-down arrow drifted ~67 px from
move-up because the last (auto) column absorbed the new width — add a
trailing filler column." docs/plugin-ui-markup.md's own "Width
semantics" rule makes the LAST <column> in a <list> ALWAYS treated as
auto, regardless of its own declared width — MonsterMoveDownIcons was
the grid's 23rd and, until this commit, LAST column, so it silently
absorbed every pixel of window growth instead of staying at its
declared 23px pitch. The gap was already wrong (89.5px) at the
authored 984-wide default, not just after a resize.
Adds a 24th, trailing <column type="text"> with empty items
(MonsterFillerColumn) so MoveDownIcons is no longer last and keeps its
own real width; the filler absorbs the remainder instead — the same
"fixed icon columns + trailing auto filler" shape lstMetaRules' three
16px icon columns + two auto text columns already prove out
(MarkupListColumnsTests.EndToEnd_MetaShapedSixColumnList_...). The
filler's onclick (MonsterFillerClick) is a real, bound no-op —
MossTankMarkupContractTests.EveryInteractiveControlDeclaresARealHandlerBinding
requires every <column> to declare SOME handler, so "no binding at all"
(the other option this item's own spec offered) is not actually legal
in this codebase; clicking blank filler space does nothing, but the
binding itself is real.
Mutation shown to fail first: the new
MonstersMoveUpAndMoveDownIconsStayAdjacentAtEveryWidth theory (984 and
1100, MarkupDocument.Build over the real mosstank.xml with a real
IMarkupIconResolver stub) failed at BOTH widths against the unmodified
markup — 89.5px apart at 984, 147.5px at 1100 — then passed (fixed
~23px gap at both) once the filler column was added.
MonstersGridHasVtanksTwentyThreeColumnsInOrderWithRetailHeaderTooltips'
23->24 column-count/type update is the other required change; VTank's
own 23-column shape is unchanged, only a MossTank-only 24th filler is
new.
MossTank suite 724/724 (one test's expectations updated, no net count
change); App markup/plugin filter 258 -> 260 (the new theory's two
InlineData cases).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Resemblance re-check: "the recall combo's full spell names overflow the
120 px box on most of the 27 entries — VTank's cmbRecallType uses terse
captions ('Primary', 'LS Sending', 'PtOIA', 'Fac. Hub'...): show those,
keep the full name in the waypoint and .af." Round E's D-2 had widened
the combo 120->300 to fit RecallDisplayName's full spell names; this
reverts the widening and shows VTank's own cmbRecallType captions
instead, restoring the box to VTank's real 120 width.
Added RouteWaypoint.RecallShortCaption(RouteRecallKind) — VTank's 27
terse cmbRecallType strings transcribed from docs/research/vtank-kb/
08-ui-views.md §1 (this worktree has no refs/vtank/ checkout to read
mainView.xml directly; the KB doc is its own faithful transcription of
that exact file, duplicate quirk included). The UI file's own caption
list carries a genuine VTank authoring duplicate — "Fac. Hub" then
"FacHub" again four slots later — that pushes every later caption one
position past the 26 real castable recalls it captions; removing that
duplicate restores an exact 1:1, in-order match onto RouteRecallKind's
26 real spells (confirmed by meaning, not just position — "Candeth" for
Return to the Keep is Candeth Keep, the literal destination; the three
"Soc. *" captions are the Society stronghold recalls). Marketplace was
never a cmbRecallType option (VTank's one slash-only recall) and keeps
its own literal caption since this combo still lists it.
RouteRecallNames/SelectedRouteRecall/SelectRouteRecall now read/parse
RecallShortCaption instead of RecallDisplayName — this is the ONLY
thing that changed. AddRouteRecallCore/RecallLabel still write the full
name (RecallDisplayName) onto the waypoint, the Route grid row, and the
.af file, completely untouched by this commit.
27 captions used, in RouteRecallKind order: Primary, Secondary, LS,
LS Sending, Portal, Aphus, Sanctuary, Caul, GW, Aerlinthe, Mt. Lethe,
Ulgrim's, Bur, PtOIA, Graveyard, Colosseum, Fac. Hub, Gear K. Camp,
Neftet, Candeth, Rynthid, VR Rocks, VR Tree, Soc. CH, Soc. RB, Soc. EW,
Marketplace.
Mutation shown to fail first: RouteRecallComboShowsVtanksTerseCaptionsButTheWaypointKeepsTheFullName
replaces RouteRecallComboShowsVtankNamesNotEnumIdentifiers with the new
expected terse-caption list. Verified by temporarily reverting
RouteRecallNames/SelectedRouteRecall back to RecallDisplayName (a
targeted mutation, then reverted) — the test failed with "Expected:
[Primary, Secondary, LS, ...] Actual: [Primary Portal Recall, Secondary
Portal Recall, Lifestone Recall, ...]" — then passed once
RecallShortCaption was restored.
MossTank suite 724/724 (one test replaced, net count unchanged); App
markup/plugin filter 258/258 (unaffected — no App-side markup change).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Owner's second live look: "On items tab, we show some options there
right of the items we add. That should not be there. Remove that."
VTank's real Items tab is exactly 6 controls (docs/research/vtank-kb/
08-ui-views.md §1 "Tab: Items" — 2 labels, the 2-column
clWeaponName/clHandedness list, Add, Add (no buffs)); mosstank.xml's
Items group also carried a right-of-list block with no VTank
counterpart: the Weapon/Offhand readout (MonsterEquipmentText), the
Refill Worn Mana toggle + slider + its two status labels
(RefillWornManaText/ItemManaRechargeStatus), and the ProfileNotice
hint label. All five are deleted from the markup.
The pre-existing "Remove" button stays — it traces back to the
plugin's very first automation PoC commit (`4e6e9bc9d`, long before
Campaign VT slice 7 existed), the accepted "slice-1 Delete" carried
forward per the round's own scope.
RefillWornMana/RefillWornMana-Item-ManaPercent are real
VtankOptionCatalog entries (Bool/Int, not tString) — nothing was
deleted from the plugin, they remain real settings, still editable in
the Advanced Options popup or via `/vt opt set`; only this second,
redundant Items-tab surface for them is gone. ItemManaRechargeStatus
was a pure runtime status readout with no setting behind it and no
VTank equivalent, so it has no replacement — matching VTank's own tab,
which shows no such status either.
Mutation shown to fail first: the new
ItemsTabIsVtankOnlyPlusTheAcceptedSliceOneRemoveButton test asserted
the Items group has no toggle/slider and no label bound to any of the
four removed properties, against the UNCHANGED markup — failed on the
Refill Worn Mana toggle still being present — then passed once the
block was deleted from mosstank.xml.
MossTank suite 723 -> 724 (one new pin); App markup/plugin filter
258/258 (unaffected — no App-side markup change).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Owner's second live look: "Buff_Profile banes and prots got duplicated.
Only the one with ALL works as intended." BuffProfile-Prots/-Banes are
tString rows (VtankOptionCatalog.DeclaredType == String); VTank's own
Advanced Options list has NO case for tString at all (KB
01-settings-and-profiles.md, db.cs:132-166 for the list switch and
:201-203 for the click handler — both "Not exposed"), so the hyphen
rows never belonged in the list next to their real tEnum twins
(BuffProfile_Prots/_Banes) — that mismatched pair is the "duplicate"
the owner saw. FilteredAdvancedOptionNames now excludes every tString
row (also catches BlacklistedSpellComps, the catalog's third tString
entry); the enum rows stay listed and clickable exactly as before.
tString settings are unaffected otherwise — still real, still settable
via `/vt opt set`, only no longer rendered in this list.
Mutation shown to fail first: the new
AdvancedOptionListHidesTStringSettingsButKeepsTheirEnumCounterparts
test asserted BuffProfile-Prots/-Banes/BlacklistedSpellComps absent and
the two enum names present against the UNCHANGED filter — failed with
"BuffProfile-Prots" found in the collection — then passed once
FilteredAdvancedOptionNames gained the DeclaredType != String guard.
MossTank suite 722 -> 723 (one new pin); App markup/plugin filter
258/258 (unaffected — no App-side markup change).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Owner's second live look (2026-09-07): "The whole bottom window of
advanced options should not be there." VTank's own AdvancedOptionsView
is a bare list + category list + edit field + description (KB
08-ui-views.md §1) — no notice line, no extra controls. Deletes the
"Editing X." notice label and the entire "MossTank Extras" section fix
round B item 8/9 added below it (Checkpoint/Jump/Remove/Set Follow
Target + status label/Follow Corners/Open Doors/Nav Priority/
Follow-Nav min distance stepper), shrinking the popup back to VTank's
392x300 (resizable, min = that; was 392x450).
Every control removed had a real path already, so nothing is actually
unreachable:
- Checkpoint/Jump: `/mt addnavcheckpoint` / `/mt addnavjump` (already
documented in the /vt help text).
- Remove: click any row of the Route tab's own waypoint grid
(DeleteRouteWaypointAt) — RemoveRouteWaypointCore and
DeleteRouteWaypointAtCore are the identical removal logic, so the
button duplicated a control that already exists.
- Set Follow Target: re-select "Follow" in the Route tab's own
nav-mode menu — SelectRouteMode already calls CaptureFollowTarget
every time RouteMode.Target is (re-)selected.
- Follow Corners/Open Doors/Nav Priority/Follow-Nav min distance:
real VtankOptionCatalog entries (FollowAroundCorners, OpenDoors,
NavPriorityBoost, NavCloseStopRange) already listed and editable in
THIS SAME popup's own option list/field above, or via
`/vt opt set <Name> <value>`.
No C# state or command was deleted — MossTankPanel's properties/actions
(RemoveRouteWaypoint, SetFollowTarget, AddRouteCheckpoint, AddRouteJump,
NavigationSettings.FollowAroundCorners/OpenDoors/Priority/
MinimumDistanceMeters) are untouched; only the redundant second UI
surface for them is gone.
Mutation shown to fail first: MossTankMarkupContractTests'
ExpectedPopupBounds and MossTankMarkupBuildOverRealFilesTests'
WideningTheRealAdvancedOptionsPopupGrowsTheOptionListWithoutOverlappingItsSibling
were changed to expect 392x300 before the markup shrink — both failed
with "Expected: 300 Actual: 450" against the still-450-tall file, then
passed once mosstank-advanced.xml's own h/minh dropped to 300.
MossTank suite 722/722 (no count change, no control removed here had a
dedicated test); App markup/plugin filter 258/258.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Records round E (the architecture re-check of round D): all six items
fixed, commit SHAs, test-count deltas per item, and the two real test-
methodology bugs plus one test-harness false positive found while
building D-6's resolved-geometry pin.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Every real-file re-layout pin so far inspects ONE named element after a
resize (the Monsters list, the Advanced Options option/category
lists). Adds a whole-tree pin at both ends of the main panel's declared
resize range — the minw/minh floor (856x236) and one enlarged size
(1100x320) past the 984x271 default — for each of the nine tabs in
turn: builds the real mosstank.xml through MarkupDocument.Build, draws
once at the authored default to capture anchor baselines, resizes, and
asserts no sibling overlap and no child crossing its parent's edge.
Adapts MossTankMarkupContractTests' AssertNoSiblingOverlap/
AssertWithinParent algorithm (rectangle intersection, ignore
touching/zero-size boxes, two bare <group> tab pages never overlap-
check each other) from the authored XElement tree to the resolved
UiElement tree (real Left/Top/Width/Height post-anchor), since a resize
only exists at that level.
Two real bugs surfaced and fixed in the test itself while building this:
(1) reusing one built tree across all nine tabs let an earlier tab's
resize leave the shared root already at the target size by the time a
later tab's own descendants took their "first" (baseline) anchor
capture — fixed by building a fresh tree per tab; (2) walking into an
INVISIBLE tab group's descendants compared their stale, never-
anchored-for-this-size geometry against the group's own (correctly
resized) bounds — fixed by skipping recursion past an invisible node,
since it never got a real anchor pass this round. A genuine false
positive from the test harness itself was also found and excluded:
UiLabel.OnDraw (the CT-GF1 fix) always overwrites Width/Height to the
current measured text extent, and this stub renderer has no real
DatFont, so it falls through to a crude "text.Length * 7f" placeholder
far wider than the authored box or the real DAT font would ever
measure — labels are excluded from the size checks (position still
checked via recursion) for the same reason D-3's own comment cites this
constraint.
Mutation shown to fail first: removing the Monsters list's
anchor="left right top bottom" attribute (temporary edit, reverted —
net diff on mosstank.xml is zero) reproduced "UiMarkupList @
(0,16,976,151) crosses the right edge of UiPanel (w=848)" at the 856
floor, exactly the failure this pin exists to catch.
App markup/plugin filter 244 -> 246; MossTank suite holds 722/722
(App-side test only).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
UiMarkupList.OnDraw/DrawColumns re-ran their "keep the selected row
visible" clamp on EVERY draw call, not only when SelectedIndexSource
actually changed. A real bound list (MossTank's Advanced Options,
selected="{SelectedAdvancedOptionIndex}") holds a stable selected index
while the user operates the scrollbar, so the very next frame's OnDraw
saw that (unchanged) row now outside the just-scrolled view and yanked
_topRow straight back to it — undoing the arrow click or thumb drag
before it was ever visible. This is exactly the "sometimes works"
symptom: a scroll that happens to keep the selected row in view
survives; one that moves it out of view gets reverted on the next
frame. Gate the clamp on an observed change in the selected index
(int.MinValue sentinel so the first frame still reveals it), so a
stable selection no longer fights user-driven scrolling while a
genuinely new selection is still auto-revealed once.
New tests build a real nested Panel > Group > List tree at non-zero
offsets and drive it exclusively through UiRoot's public mouse entry
points, ruling out a coordinate-frame bug in the hit-test/dispatch
chain as well as proving the fix for both single-column and
<column>-mode lists (arrow click and thumb drag).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
RefreshAdvancedOptions' own doc comment lists every real mutation site
that must call it (category toggle, edit/apply, selection change,
profile load) but ShowAdvancedOptions itself was missing — opening the
popup only called LoadAdvancedOptionDraft. A setting changed while the
popup was closed (/vt opt set, one of the Options tab's own direct
checkboxes, or a profile load) left the cached AdvancedOptionNames/
AdvancedOptionValueColumn on whatever they held from construction or
the popup's last open, until some in-popup interaction happened to
refresh them.
Mutation shown to fail first: reverting the source fix (git checkout,
patch saved and reapplied) reproduced the stale column — toggling
CombatEnabled then opening the popup still showed the pre-toggle value.
MossTank suite 721 -> 722; App markup/plugin filter holds 244/244.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
mosstank.xml's own top-of-file comment already claimed the Advanced
Options popup "gets its own resizable=\"true\" + anchored lists" — the
popup file itself still carried the dead resize="none" attribute
(MarkupDocument.cs: "resize" only ever means anything once
resizable="true" already arms the master switch), so the claim was
false and the popup was permanently fixed-size.
resizable="true" now floors minw/minh at the pre-fix authored size
(392x450 — never shrink below the tested layout, matching the main
panel's own rule). lOptionList grows WIDTH ONLY: the value-edit field,
description, and notice all sit directly below it with zero vertical
slack and none of them anchor to shift down in lockstep, so any height
growth here would walk the list straight into the field below — the
same "grow only in the direction with nothing in the way" principle the
main panel already applies to Items/Consumables/Buffs/Route's lists.
lFilterList (the category checklist beside it) tracks the growing right
edge via its own anchor="right" so the widening option list never walks
into it, matching the main panel's own right-pinned-sibling pattern.
AdvancedOptionDescription is declared anchor="left top right" for
markup consistency, but documented as a real no-op: UiLabel.OnDraw
(UiPanel.cs) always overwrites Width/Height to the current measured
text extent on every draw, right after the anchor pass runs in the same
frame, so nothing a label's anchor computes is ever visible — a
genuinely working vertical anchor was deliberately avoided since the
notice label directly below isn't anchored either and a large resize
would walk description into it; fully anchoring the rest of the
vertical stack is a larger redesign left for a future round.
Mutation shown to fail first: reverting the markup (git checkout, patch
saved and reapplied) failed at Assert.True(built.Resizable) — the popup
was still fixed-size.
App markup/plugin filter 243 -> 244; MossTank suite holds 721/721
(this item touches App-side markup only).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
RouteRecallNames/SelectedRouteRecall used Enum.GetNames/ToString(),
showing bare C# identifiers ("PrimaryPortalRecall",
"ParadoxTouchedOlthoiInfestedAreaRecall", ...) in the Route tab's
recall combo instead of VTank's real recall names. Both now route
through RouteWaypoint.RecallDisplayName (the same table
AddRouteRecallCore/RecallLabel already use), matching how
RouteInsertModeNames/RouteModeNames already show dedicated display
captions rather than raw enum text. SelectRouteRecall now parses that
same display string back (Enum.TryParse against the enum identifier no
longer matches what the combo sends).
mosstank.xml's cmbRecallType widened 120 -> 300: VTank's own combo is
also 120 wide, but only because its captions are hand-abbreviated
("LS", "Mt. Lethe", "PtOIA"); RecallDisplayName deliberately keeps
metaf's full names (round D item 3), so the box needs real room for
the longest one, "Paradox-touched Olthoi Infested Area Recall".
Nothing else shares that row past x=474 and the group floors at ~848
wide even at the panel's minw, so 300 has ample clearance.
Mutation shown to fail first: reverting MossTankPanel.cs's fix (git
checkout, patch saved and reapplied) reproduced the bare-identifier
list and the "PrimaryPortalRecall" default selection.
MossTank suite 720 -> 721; App markup/plugin filter holds 243/243
(no overlap from the widened menu).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The pre-cutover RouteRecallKind was {Lifestone=0, Marketplace=1,
PrimaryPortal=2, SecondaryPortal=3} — completely different kinds at the
SAME ordinals as today's much larger round-D enum (PrimaryPortalRecall=0,
SecondaryPortalRecall=1, LifestoneRecall=2, LifestoneSending=3, ...).
LegacyWaypointDocument.Recall was still typed RouteRecallKind, so
System.Text.Json deserialized a legacy JSON route's bare "Recall"
integer straight into the new enum — silently remapping every one of
the four old ordinals to the wrong new recall. Enum.IsDefined never
caught it (0..3 are all valid members of the new enum too, just for
different spells).
The field is now a bare int, translated through an explicit
MapLegacyRecall(legacyOrdinal) table in ToWaypoint() that also derives
RecallSpellId/RecallSpellName from the mapped kind (the pre-cutover
schema predates both fields entirely).
Mutation shown to fail first: reverting the source fix (git checkout,
patch saved and reapplied) reproduced the bug for all four legacy
ordinals — ordinal 2 (old PrimaryPortal) even landed on LifestoneRecall
instead of PrimaryPortalRecall, confirming the "still IsDefined, still
wrong" failure mode. The new theory asserts against the migrated
route's own written .af "rcl" line (RecallDisplayName) rather than the
value re-parsed back through LoadCurrent, since the .af format's
own name-only round-trip is a separate, pre-existing limitation
(no spell id, and "Marketplace Recall" cannot resolve through any
catalog because it isn't a real spell) unrelated to this fix.
MossTank suite 716 -> 720; App markup/plugin filter holds 243/243.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
#490 part 2 (plugin layout persistence has no revision bump) is fixed at
05f22d46f (RetailWindowManager.ComputeAuthoredGeometryRevision +
MigrateAuthoredGeometry's inequality comparison + MountPlugins wiring).
Part 1 (StartVisible=false + ShowInSidePanel=false is permanently
unshowable) remains open -- not touched by this work.
Also adds one sentence to docs/plugin-ui-markup.md's "Resizable panels and
anchors" section: changing a panel's authored w/h/minw/minh/resizable in a
later plugin update resets every user's stored size to the new default
exactly once, automatically -- no plugin-author call site to remember.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
MountPlugins registered every plugin window with authoredGeometryRevision
hard-coded to 0 (RetailUiRuntime.cs), so RetailWindowLayoutPersistence's
MigrateAuthoredGeometry -- gated on "saved revision >= authored revision"
-- never migrated a plugin window's saved size: 0 >= 0 forever. MossTank's
panel went 856x236 -> 984x271 and every user with a stored layout stayed
stuck at 856x236 with no way to see the new default.
Two changes:
1. MountPlugins now passes RetailWindowManager.ComputeAuthoredGeometryRevision
(added previous commit) as the plugin window's authoredGeometryRevision,
derived from the panel's own authored width/height/minw/minh/resizable.
2. MigrateAuthoredGeometry now compares revisions for INEQUALITY
(saved.Revision == authored.Revision) instead of ordering
(saved.Revision >= authored.Revision). A hash is not an incrementing
counter -- two different authored sizes can hash in either order -- so
"the authored size changed" has to mean "the value differs", not "the
value went up". Built-in windows' hand-picked incrementing literals
(chat: authoredGeometryRevision = 1) are unaffected: no existing saved
revision is ever equal to a later, different literal either way.
Mutation shown to fail first: the two new PluginMarkupPanel_AuthoredSizeChanged_*
tests in RetailWindowLayoutPersistenceTests.cs reproduce the exact bug with
concrete literals (856x236/400/150/true -> 984x271/... and
200x100/100/80/true -> 220x110/...) chosen so ComputeAuthoredGeometryRevision's
OLD hash is >= the NEW hash for each pair -- confirmed via a throwaway probe
before writing the assertions, so the pre-fix run fails deterministically
rather than by chance of hash ordering. Both failed before this commit
(size stayed at the old authored extent) and pass after
(PluginMarkupPanel_AuthoredSizeUnchanged_KeepsUserResizedSize, unaffected
either way, is a regression-safety companion). Full RetailWindow/Markup/
PluginSidePanel filter: 249 passed (was 246), 0 failed, 0 skipped.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Slice-7 resemblance re-check found UiMarkupList's column-less items= mode
and its <column> mode disagreed on whether a selected row gets a
persistent SelectedColor fill, so a plugin's Buffs lists highlighted a
row while the Monsters/Meta grids looked different for the same widget.
Real VVS lists (VTank's HudList) draw no such fill at all.
UiMarkupList.SelectionBandEnabled (default false) now gates the fill in
both OnDraw's legacy branch and DrawColumns; MarkupDocument parses
<list selectionband="true"> (same literal-bool convention as
openupward/clearonsubmit) to opt a single list back in. selected/onchange
semantics and scroll-into-view of the selected row are unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Part of #490 part 2: plugin panels register with authoredGeometryRevision
hard-coded to 0, so a plugin author who ships a new authored panel size
(MossTank: 856x236 -> 984x271) has no way to signal the change short of
adding a manual revision-bump call site, and every user's stored layout
keeps the old size forever. Built-in retail-imported windows solve this
with an explicit incrementing int literal at each Register call; a plugin
author does not maintain that call site by hand.
RetailWindowManager.ComputeAuthoredGeometryRevision derives the revision
from the authored geometry tuple itself (width, height, minw, minh,
resizable) via a fixed FNV-1a-style combine over the values' raw IEEE-754
bit patterns -- deliberately not System.HashCode, whose per-process reseed
would make the "same" authored geometry hash differently on every launch.
The sign bit is masked off so the result is never negative (Register's own
Math.Max(0, revision) would otherwise silently fold distinct negative
hashes onto the same "unversioned" 0 bucket used by legacy saves).
Mutation shown to fail first: without this method,
RetailWindowManagerTests.ComputeAuthoredGeometryRevision_* (7 new tests)
fails to compile (CS0117, method does not exist). No wiring yet -- this
commit only adds the pure, inert helper; MountPlugins still passes no
revision. That lands next along with the comparison-semantics fix it
depends on.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
An .af "rcl" node whose spell name failed to resolve against the
character's known spells (MetafSerializer.ReadNavNode's "rcl" case)
leaves RecallSpellId at 0 and Recall at its default ordinal
(RouteRecallKind.PrimaryPortalRecall — the enum field is never touched
on that path). SubmitRecall's old Recall-based fallback then silently
cast Primary Portal Recall (spell 48) for a waypoint that was never
authored as one. TickRecall now refuses the cast, names the unresolved
spell in the status notice, and skips the waypoint (CompleteAction) —
the same "refuse and continue" shape TickUse already uses for an
unavailable object. SubmitRecall's dead fallback branch (every genuine
path — AddRouteRecallCore, the binary .nav loader, the D-1 legacy-JSON
migration — writes RecallSpellId directly) is removed.
Mutation shown to fail first:
RecallWaypointWithUnresolvedSpellNameRefusesAndSkipsWithoutCasting
against the pre-fix code cast spell 48 (magic.CastSpellIds == [48]).
MossTank suite 715 -> 716; App markup/plugin filter holds 243/243.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Records the merge of claude/latest-main-sync-497549 plus round D's
four fix items (Advanced Options category names, VTank's click model,
the full 26-recall table, resizable/enlarged main panel) on the panel
worktree, with commit SHAs, test-count deltas, and the carried
deviations (#491, the RouteRecallKind-ordinal migration risk).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Owner live report 2026-09-07: "The size of the entire window needs to
be enlarged for default and should also be resizeable." mosstank.xml's
main panel is now resizable="true" with minw/minh floored at the
PRE-round-D authored size (856x236) and a new default authored size
~15% larger in both directions (984x271) — every one of the nine tab
groups grows by the identical 128px-wide/35px-tall delta (848x194 ->
976x229), preserving the original 0px group-to-panel margin design
exactly.
Every tab <group> gets anchor="left top right bottom" so a future
manual drag-resize keeps growing it. The two single-list tabs with
nothing beside their list (Monsters, Meta) give their list the SAME
full stretch, widened AND heightened by the same 128x35 delta at
author time — anchors only react to CHANGE from their first-draw
baseline, so bumping only the outer group while leaving the list's own
geometry untouched would leave it visually unchanged at the new
default (the growth invisibly reserved as margin until a further
manual resize). Every other tab's list(s) grow in HEIGHT only (+35),
pinned to whichever horizontal edge doesn't have a sibling list/button
column in the way (Items/Consumables/Buffs/Route all have one) — a
right-pinned sibling (Consumables' Excluded Scarab list, Buffs'
Blacklisted Buff Families list) is repositioned +128 at author time so
it already sits flush against the enlarged default's right edge.
Trailing "Add" rows/status labels below a taller list shift down by
the same +35 and get anchor="bottom" to keep tracking. Options/
Profiles/Vitals need no internal changes — only their own group grows.
Deviations, both documented in mosstank.xml's own Round D item 4
comment: Monsters' extra 128px of list width lands in its LAST column
(the MoveDown icon, which always absorbs the remainder per the markup
grammar) — a wider icon cell, not a redesigned grid; seven of nine
tabs' groups grow wider than their content uses, leaving harmless
empty space on the right at the enlarged default rather than
redesigning nine tabs' pixel geometry in one pass (a deliberate,
minimal-risk choice — nothing moved INTO another control's space, only
into previously-empty margin, so every existing AssertNoSiblingOverlap/
AssertWithinParent guarantee still holds).
Tests added:
- MossTankMarkupContractTests.AuthoredShellFitsTheMinimumCanvasAnd...
updated to the new 984x271 default.
- PanelIsResizableFlooredAtThePreRoundDAuthoredSize (resizable="true",
minw/minh = 856/236).
- EveryStretchingListDeclaresARealAnchor (contract pin: every list in
Monsters/Items/Consumables/Buffs/Route/Meta declares a real anchor
attribute, not the silent left-top default).
- MossTankMarkupBuildOverRealFilesTests.
WideningTheRealMainPanelWidensTheRealMonstersList (App.Tests): builds
the REAL mosstank.xml against a real MossTankPanel, widens the built
root by 100px, and confirms the Monsters list's own Width grows in
turn — the same mechanism MarkupResizableAnchorTests already proves
against synthetic markup, now proven against the shipped file.
Mutation shown to fail: temporarily removing the Monsters list's own
anchor attribute failed the new re-layout test (976 -> 976, no growth);
restored and confirmed green. A build-copy gotcha surfaced while
writing these tests: dotnet build's default incremental copy did NOT
refresh the test projects' deployed mosstank.xml after editing the
source file (PreserveNewest apparently didn't see it as newer under
normal incremental evaluation) — dotnet build --no-incremental was
needed to get a fresh copy into bin/ before the size/anchor tests
would actually exercise the new markup instead of a stale cached copy.
Verified: dotnet build AcDream.slnx -c Release (clean, --no-incremental)
green; MossTank suite 715/715 (713 -> 715, two new markup contract
pins); App markup/plugin filter 243/243 (242 -> 243, one new re-layout
test).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Owner live report 2026-09-07: "Under Route, Add recall is missing a
lot of recalls that VTank has and they do not work in routes yet."
RouteRecallKind's old 4-entry model (Lifestone/Marketplace/
PrimaryPortal/SecondaryPortal) is replaced with VTank's real
cmbRecallType table: 26 castable recall spells from metaf's own
NRecall table (metaf_monolithic.py:10981-11008) plus Marketplace
Recall, VTank's one entry with no spell (issued as /marketplace).
Deviation (no refs/vtank/ checkout in this worktree to confirm VTank's
real combo layout directly): the old Lifestone member, which issued
"/lifestone" as a slash command, is DROPPED rather than kept alongside
the new spell-based LifestoneRecall (1635) — VTank's real combo has
ONE "Lifestone Recall" entry and it is a real castable spell, so
keeping both would show two menu rows reading "Lifestone Recall" with
different behavior. Marketplace is the one member kept as a slash
command per the explicit "keep ours" instruction. PrimaryPortal/
SecondaryPortal are renamed to PrimaryPortalRecall/SecondaryPortalRecall
and now use their real spell ids (48, 2647) instead of the old runtime
KnownSelfBuffs name lookup. This reshuffles the enum's underlying
ordinals; the one place that mattered (MossTankRouteProfileStore's
legacy pre-cutover JSON migration DTO, which stores Recall as a raw
int) already guards with Enum.IsDefined and is documented as a
migration-only path for not-yet-migrated files — its fallback default
moved from the deleted Lifestone to PrimaryPortalRecall.
Changes:
- RouteRecallKind: 26 named members in VTank's own combo order (values
are plain sequential indices, not spell ids, so Enum.GetNames/
GetValues — which sort by underlying VALUE — reproduce that order)
plus Marketplace appended last.
- RouteWaypoint.RecallDisplayName: VTank's exact label text per kind.
- RouteWaypoint.SpellIdForRecall (new): the real spell id per kind
(Marketplace = 0, the existing "no spell" sentinel).
- NavigationController.SubmitRecall: unchanged fast path (RecallSpellId
!= 0 -> cast) now covers every recall added through the UI; the
fallback for a waypoint with no recorded id resolves through
SpellIdForRecall instead of the old runtime spell-name lookup, and
Marketplace still falls through to "/marketplace".
- MossTankPanel.AddRouteRecallCore: populates RecallSpellId AND
RecallSpellName on the new waypoint (matching what a metaf import or
the binary .nav loader already produces), so a waypoint added from
the Route tab's own combo executes identically to one round-tripped
through a real route file.
- mosstank.xml: the recall <menu> comment updated; rows 4->7 now that
scrolling through 27 entries is real, not grammar-only.
Tests added (NavigationTests.cs, MetafSerializerTests.cs):
- RouteRecallKindListsVTanksTwentySixRecallsInOrderPlusMarketplaceLast
(the 26-entry order via Enum.GetNames).
- RecallNameAndSpellIdTablesAgree (27-case Theory: name<->id both ways;
RouteRecallKind is internal so the Theory parameter is the public int
ordinal, cast back inside the method — a public method cannot expose
an internal-typed parameter, CS0051).
- RecallWaypointWithNonZeroSpellIdCastsThatSpell /
RecallWaypointForMarketplaceSubmitsTheSlashCommandNotACast (execution,
via a new FakeMagic tracking fake — FakeAutomation.Magic is now
settable instead of always NoOpAutomationSurface).
- RecallNodeRoundTripsByNameAndResolvesTheRealSpellIdFromTheCatalog
(three recalls through SaveNav/TryLoadNav with a new FakeSpellCatalog
that actually knows the spells, proving both the name AND the
resolved id survive the .af round trip).
Mutations shown to fail, then reverted: swapping the first two enum
members failed the order test; reducing SubmitRecall to `return false`
failed both execution tests (no cast recorded, "/marketplace" not
submitted).
Verified: dotnet build AcDream.slnx -c Release green; MossTank suite
713/713 (682 -> 713, 31 new tests); App markup/plugin filter 242/242
(unchanged).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Owner live report 2026-09-07, screenshots 2/3 (relayed VTank click
model — this worktree has no refs/vtank/ checkout to read
db.cs:118-165 directly): clicking a VALUE cell used to always just
select the row, no matter the setting's type. VTank's own model
dispatches by declared type: a tBool value flips in place, a tEnum
value cycles to the next label, and only int/double/single/string
select the row and load the current value into the edit field below
for typing. Apply/Back buttons are gone entirely (retail's own
AdvancedOptionsView has neither; Enter on the field already applies
via its existing onsubmit) — the popup closes from its own title bar
or the Options tab's toggle, both already independent of this content.
Changes:
- VtankDefaultSettingsDatabase.SettingEnumValues parses VTank's own
SettingsEnumInfo table (3 columns, 33 rows: Setting/Value/EnumValue)
from the embedded .usd — the real per-setting enum code->label table
(UseArcs: 1=No, 2=At Range, 3=Yes), not a hand-typed guess.
- MossTankPanel.DisplayAdvancedOptionValue shows the enum LABEL for
Enum-typed settings in the value column instead of the raw stored
integer.
- MossTankPanel.ClickAdvancedOptionValue dispatches to
FlipAdvancedOptionBool / CycleAdvancedOptionEnum (wraps past the
last entry) / SelectAdvancedOption by VtankOptionCatalog.DeclaredType.
- mosstank-advanced.xml: the value column's onclick is now
{ClickAdvancedOptionValue}; Apply/Back buttons removed; the notice
label and the whole "MossTank Extras" section below it moved up 26px
to reclaim the space; panel height 476->450.
- Removed the now-unreferenced ApplyAdvancedOption wrapper property
(ApplyAdvancedOptionCore is still used by the field's own onsubmit).
Mutations shown to fail: (1) reducing ClickAdvancedOptionValue to a
bare SelectAdvancedOption(index) call failed both the bool-flip test
(expected False, got True — the click never flipped it) and the
enum-cycle test (expected "At Range", got "No" — the click never
advanced); the numeric-select test correctly stayed green since
select-only is still its own expected behavior. Restored and
confirmed green.
Verified: dotnet build AcDream.slnx -c Release green; MossTank suite
682/682 (679 -> 682, three new interaction tests); App markup/plugin
filter 242/242 (unchanged, mosstank-advanced.xml's new footprint
392x450 updated in ExpectedPopupBounds).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Owner live report 2026-09-07: "Some Hex numbers with green button to
the right. What is that?" — the Advanced Options popup's category
filter checklist showed raw bitmask hex ("0x1", "0x2", ...) instead of
VTank's real category names (Misc, Recharge, MeleeCombat, SpellCombat,
Ranges, Navigation, Buffing, Crafting, Looting).
This worktree has no refs/vtank/ checkout, so VTank's own name/enum
string literal for each bit isn't directly readable. Derived the
mapping instead from real data already in the embedded .usd: for each
category name, docs/research/vtank-kb/01-settings-and-profiles.md §2
was searched for a setting whose Category column names EXACTLY that
one category (no `|` combination) — e.g. row 45 RandomHelperBuffs is
pure "Misc", row 18 AttackDistance is pure "Ranges" — then that
setting's own recorded bitmask was read back from
VtankDefaultSettingsDatabase.SettingCategoryBitmasks (never a typed-in
hex literal), so the mapping tracks the shipped database instead of
silently drifting from it. All 9 of VtankOptionCatalog.CategoryBits
resolve to a real name this way (VtankOptionCatalog.
CategoryNamesByBit), in VTank's own ascending-bit order.
Mutation shown to fail: reverting AdvancedOptionCategoryNames to the
old `$"0x{bit:X}"` projection failed the new
AdvancedOptionCategoryNamesShowRealNamesNotHexBitmasks test with
["0x1", "0x2", ...] instead of ["Misc", "Recharge", ...]; restored and
confirmed green.
Verified: dotnet build AcDream.slnx -c Release green; MossTank suite
679/679 (678 -> 679); App markup/plugin filter 242/242 (unchanged).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Merges claude/latest-main-sync-497549 into the slice-7 panel worktree
so round D can build on both: b71a8ea37 (retail scrollbar chrome on
plain <menu> popups and overflowing <list>s — chat/inventory sprite
ids, always-scrollable single-column menu) and 2e63391cc (<panel
resizable minw minh> + anchor="left top right bottom" markup).
Conflicts resolved keeping both intents:
- Ledger (docs/plans/2026-09-07-campaign-vt-slice7-tabs.md): unioned
both branches' entries into one chronological timeline instead of
picking a side.
- docs/plugin-ui-markup.md: kept both attribute additions per element
(slider min/max/style, menu scroll/style) AND anchor on every row.
- src/AcDream.App/UI/UiMarkupList.cs: kept this branch's fix round B
item 10 (VVS HudList grids have no row-selection highlight) over the
sync branch's older SelectedColor band draw in the <column> grid
path — the legacy single-column list path is unaffected either way.
- src/AcDream.App/UI/MarkupDocument.cs: the auto-merge left two
`Scrollable =` initializers on the same <menu> object (CS1912).
Kept the sync branch's `Scrollable = true` (VTank's HudCombo is
always a single scrolling column, never a wrapping grid) and
dropped this branch's `Scrollable = B(el, "scroll", false)` opt-in,
since the owner-driven always-scrollable design supersedes the
S7.2 opt-in one. Updated MarkupDocumentTests.cs to match: removed
Build_MenuWithNoScrollAttribute_KeepsScrollableFalse (asserted the
now-false opt-in default) and
Menu_Scroll_DrawsAPlainFlatThumbFillWhenTheMarkupItemCountOverflowsTheVisibleRows
(asserted a flat DrawFill thumb; the scrollbar is sprite-chrome for
every menu style now) — both fully superseded by
Menu_Markup_IsAlwaysScrollable_WithRetailScrollbarChromeWired and
UiMenuPlainStyleTests.Plain_OpenPopup_ScrollableOverflow_
DrawsRetailScrollbarChrome_RowsStayPlain.
Verified: dotnet build AcDream.slnx -c Release green; MossTank suite
678/678; App markup/plugin filter 242/242 (241 before this commit's
test-file trim, +1 net from the merge's own new tests, 0 red).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
EveryInteractiveControlDeclaresARealHandlerBinding asserted an exact
Assert.Equal(156, controls.Length), backed by ~60 lines of running
commentary recording every markup edit that ever bumped the number. That
count was a pure change detector: it carried no signal the test's own
per-control handler/enabled loop couldn't already catch on its own, and
it forced an edit to THIS test file every time an unrelated tab gained
or lost a single control.
Replaced the exact count with Assert.NotEmpty(controls) — it still
guards the selector itself (a broken interactive-element-name filter
that matched nothing would otherwise pass the loop vacuously) — and kept
the real assertion (the per-control handler/enabled loop) unchanged.
Moved the removed count's full history into this same commit's ledger
entry in docs/plans/2026-09-07-campaign-vt-slice7-tabs.md, per item
F13's own instruction.
Mutation named: temporarily broke the controls selector (appended
`&& false` to the interactive-element filter) and confirmed
Assert.NotEmpty fails ("Collection was empty") before restoring it.
MossTank suite holds at 678 (no test added or removed, one assertion
replaced). Full solution build green; App markup/plugin filter 203/203.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Adds the "Resizable panels and anchors" section to plugin-ui-markup.md:
the resizable/minw/minh attribute table for <panel>, the anchor grammar
for every element that now supports it, the AnchorEdges semantics
(left top default, left right / top bottom stretch, right/bottom pin-
and-move), group-relative child anchoring, and one worked example
(a resizable panel with a group and list that both stretch on drag).
Also updates the Elements table, the "every registered window" intro
paragraph, the ApplyCommon common-attributes paragraph, and the Testing
conventions section to reference the new grammar and its test coverage.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Owner live-client report 2026-09-07: "For scrollable dropdown or the meta
window we use the same assets as we do in for example chat or inventory
window."
<menu> markup wiring (MarkupDocument.cs): a plugin <menu> is now always
Scrollable (single-column, VTank HudCombo shape) instead of wrapping
overflow into more grid columns, with PopupScrollbarHideWhenDisabled=true
so the bar is entirely absent while the item count fits the "rows"
window. RetailScrollbarChrome.ApplyToMenuPopup wires the same chrome ids
the previous commit taught DrawScrollablePopupPlain to draw, for both
style="plain" and style="retail" markup menus.
<list> markup (UiMarkupList.cs / MarkupDocument.cs): a plugin <list>
(single-column or <column> multi-column) that overflows its own row
viewport now draws the retail scrollbar chrome at its right edge (VVS's
own placement, 16px wide) instead of being wheel-scroll-only with no
visible bar. The reserved 16px column only exists while rows actually
overflow, in both column-layout modes (ComputeColumnLayout receives the
already-shrunk width so the last/auto column absorbs the remainder
correctly); the bar is fully interactive (up/down arrows, track paging,
thumb drag) via a small UiScrollable projection kept in sync with the
list's own _topRow, which stays the single source of truth. Wheel
scrolling and a no-resolver hand-built list (draws nothing, no crash) are
unchanged.
Mutation shown to fail first: new
UiMarkupListScrollbarTests/MarkupDocumentTests cases were written against
pre-change UiMarkupList/MarkupDocument and failed (no scrollbar sprites
ever emitted since UiMarkupList had no SpriteResolve property at all, and
<menu> markup never set Scrollable) before the implementation landed;
after: SingleColumn_Overflowing_DrawsRetailScrollbarChromeAtRightEdge and
Columns_Overflowing_ReservesSixteenPixels_LastColumnShrinksAccordingly
pin sprite ids + exact reserved-width geometry,
*_ContentFits_DrawsNo(Scrollbar|ReservationLastColumnKeepsFullRemainder)
pin the no-overflow/no-bar case, *_UpArrowClick_ScrollsUpByOneRow and
ThumbDrag_MovesTopRowAndIsReadableByASubsequentClick pin interactivity via
a following row click resolving to the moved position (mirroring
MarkupListColumnsTests' own wheel-scroll pin), and the four new
MarkupDocumentTests menu cases pin Scrollable/PopupScrollbarHideWhenDisabled/
the six chrome-id properties plus an end-to-end open-popup draw for both
the overflowing (draws chrome) and non-overflowing (draws none) cases.
Every pre-existing MarkupListColumnsTests/MarkupDocumentTests case stays
green unchanged (none of their fixtures overflow their own viewport).
docs/plugin-ui-markup.md updated: the <menu> style paragraph and a new
<list> "Scrollbar" section describe the new chrome + auto-reservation, and
the PITCH-transcription guidance is corrected to say the 16px scrollbar
column is now automatic (no more manual fold-in/double-reservation advice).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Owner live-client report 2026-09-07: "For scrollable dropdown or the meta
window we use the same assets as we do in for example chat or inventory
window." The plain-style <menu> popup's scrollable-overflow scrollbar
(DrawScrollablePopupPlain / DrawPopupScrollbarPlain in UiMenu.cs) drew a
home-made flat 1px track + flat thumb instead of the gold track + up/down
arrow buttons + thumb the chat SpewBox and inventory UiItemList already
use through RetailScrollbarChrome. The owner only ever objected to the
retail ROW art (checkmark glyph, gradient panel) — the bar itself was
never in scope for the plain-row fix, so this change touches only the
scrollbar draw call and leaves the plain row rendering untouched.
DrawScrollablePopupPlain now calls the existing DrawPopupScrollbar helper
(the same procedural sprite-chrome draw VendorUiController/
ConfigOptionsPageController already use) whenever a SpriteResolve is
wired, falling back to the old flat DrawPopupScrollbarPlain only for a
hand-built UiMenu with no resolver at all. New
RetailScrollbarChrome.ApplyToMenuPopup(UiMenu) wires the same vertical
skin ids (Track/Up/Down/ThumbTop/Mid/Bot Normal) the chat/inventory
scrollbar uses onto a menu's own ScrollTrackSprite/etc properties.
Mutation shown to fail first: UiMenuPlainStyleTests's
Plain_OpenPopup_ScrollableOverflow_DrawsPlainTrackAndFlatThumb_NoDatArt
and Plain_ScrollablePopup_ContentFits_DrawsTrackWithNoThumb asserted
resolveCalls==0 and an all-fill scrollbar — both failed (6 resolve calls,
6 sprite quads instead of 0) against the new DrawPopupScrollbar call
before being rewritten to
Plain_OpenPopup_ScrollableOverflow_DrawsRetailScrollbarChrome_RowsStayPlain
and Plain_ScrollablePopup_ContentFits_DrawsNoScrollbarAtAll, which pin the
new sprite-chrome behavior (6 resolved ids on overflow: track, up, down,
thumb top/mid/bottom; 3 on content-fits: track+up+down, no thumb; 0 on a
menu built with no resolver) while re-asserting the rows are still plain
fills with zero retail row-sprite quads. Retail's own
RetailButtonArt=true popup path (DrawGridPopup/DrawScrollablePopup) is
untouched — its regression golden
(Retail_OpenPopup_DrawIsByteForByteUnchanged_RegressionGolden) still
passes byte-for-byte.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>