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>
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>
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>
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>
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>
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>
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>
SecondaryPopupPanelsFitTheirOwnBoundsAndEveryBindingResolves was a
[Theory] over a hand-maintained [InlineData] file list — every popup
added or renamed since (buffpicker, metaeditor) already needed a manual
row here, and a forgotten one would leave a new popup completely
uncovered with no test failure to flag it.
Converted to a single [Fact] that discovers popup files with the exact
same Directory.GetFiles(AppContext.BaseDirectory, "mosstank*.xml") glob
AuthoredControlsInTheSameContainerNeverOverlapASibling and
NoButtonAnywhereUsesTheUnrenderableArrowGlyphs already use (excluding
mosstank.xml itself, the main panel covered by its own tests). Expected
width/height per file now lives in an ExpectedPopupBounds dictionary; a
discovered file with no entry fails loudly, naming the file, instead of
silently going unchecked.
Mutation named: temporarily commented out the mosstank-buffpicker.xml
entry and confirmed the test now fails with the new "has no expected
width/height entry" message (rather than the old behavior, where an
unlisted file was simply never discovered) before restoring the entry.
MossTank suite 681 -> 678 (the four [InlineData] cases collapse into one
[Fact] with the same coverage; net -3 test count, not a coverage loss).
Full solution build green; App markup/plugin filter 203/203.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ItemHandsColumn ran a fresh .Select(...).ToArray() over _itemRows, PLUS a
per-row BaseItemName suffix-strip to recover the undecorated name, on
every single retained-UI draw. Folded per item F12: RefreshItemEditors
now captures the raw SortedCombatItemNames() result once as
_itemBaseNames — the SAME array used both to build the decorated
_itemRows (append " [no buffs]" where it applies) and to compute
_itemHandsColumn directly, so the " [no buffs]" suffix has exactly one
definition (added going forward) instead of two (added in RefreshItemEditors,
parsed back off in the old ItemHandsColumn getter).
CycleItemHandsAtCore (the grid's own "click cycles handedness" cell
action) mutates _itemHandedness but did not call RefreshItemEditors —
unlike the Monsters grid's mutators, which all already refresh after
mutating. Added the call so the cached column stays correct; also
switched it to read the cached _itemBaseNames instead of a second
SortedCombatItemNames() call.
Mutation named: temporarily restored the old live-recomputing
ItemHandsColumn getter (with a local copy of the suffix-stripping helper)
and confirmed the new
ItemHandsColumnDoesNotReallocateOnEveryReadAndNoBuffSuffixNeverLeaks pin
fails (Assert.Same throws — different array instances per read) before
restoring the fix. The same test also proves the no-buffs case still
resolves handedness correctly through the folded base-name array.
MossTank suite 680 -> 681 (one new pin). Full solution build green; App
markup/plugin filter 203/203.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
AssertWithinParent only flagged a child crossing a group's bottom edge
when that child declared its own h attribute — Number(child, "h") reads
0 for an absent attribute, so an unsized <label>/<field>/<toggle>/
<button> positioned right against a group's bottom edge silently passed
even though the retained-UI runtime still gives it a real default row
height at draw time. Added EffectiveHeight: falls back to each widget
kind's own implicit default when h is absent (label/field 16, toggle 20,
button 16 as a defensive floor since real buttons in this markup range
16-25px and always declare h explicitly); <list>/<menu> keep the old
"0 when absent" behavior since they have no implicit default at all.
Mutation named: temporarily reverted the check back to Number(child, "h")
and confirmed the new AssertWithinParent_CatchesAnUnsizedLabelNearTheBottomEdge
pin fails (no exception thrown for a synthetic <label y="190"> with no h
inside a 194-tall group, which should clip 12px past the bottom once the
16px default is counted) before restoring the fix. Audited every real
mosstank*.xml file for label/toggle/field/button elements missing h
(grep for each without ` h="`) — none exist, so this stricter check
introduces no new failures against the shipped markup.
MossTank suite 679 -> 680 (one new pin). Full solution build green; App
markup/plugin filter 203/203.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
EnsureDefaultMonsterRule() is a _combatSettings.Rules mutator (it appends
a DEFAULT row when the list is empty), but the /mt refresh command
handler never called RefreshMonsterEditor() afterward, unlike every other
Rules mutator in the plugin (AddMonsterRuleCore, DeleteMonsterRuleAtCore,
MoveMonsterRuleAtCore, UpdateMonsterActionsAt, the constructor, and
ResetProfileConsumers's own EnsureDefaultMonsterRule call).
Audited every _combatSettings.Rules mutation site in the plugin (grep for
"_combatSettings.Rules" across all of src/AcDream.Plugins.MossTank):
AddMonsterRuleCore, DeleteMonsterRuleAtCore, MoveMonsterRuleAtCore, and
UpdateMonsterActionsAt already call RefreshMonsterEditor right after
mutating; the constructor and ResetProfileConsumers already pair their
own EnsureDefaultMonsterRule call with one. The /mt refresh handler was
the only gap.
Mutation named: since Rules can never actually be observed empty through
the panel's own public surface (delete refuses removing DEFAULT, and
every profile-load path already re-adds it via ResetProfileConsumers
before this handler could see it), the new pin reaches the private
CombatSettings instance via reflection to clear Rules directly, and also
pokes the cached _monsterNameColumn field to a "STALE" sentinel first —
otherwise EnsureDefaultMonsterRule() re-adding "DEFAULT" would coincide
with the grid's already-cached construction-time value and the test
would pass even with the fix missing. Confirmed it fails (shows "STALE"
instead of "DEFAULT") with the RefreshMonsterEditor() call removed,
before restoring the fix.
MossTank suite 678 -> 679 (one new pin). Full solution build green; App
markup/plugin filter 203/203.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
AdvancedOptionCategoryEnabled returned _advancedOptionCategoryEnabled (the
panel's own mutable bool[]) directly. Any caller holding the returned
IReadOnlyList<bool> could cast it back to bool[] and flip category flags
without going through ToggleAdvancedOptionCategoryAt — bypassing the
clamp/cache-refresh that action performs. The property now returns a
ReadOnlyCollection<bool> view built once over the same backing array
(built once since the array's own reference never changes, only its
elements, so no re-allocation is needed on every read): a cast back to
bool[] now throws InvalidCastException instead of handing out a mutable
reference.
Mutation named: reverted the property to return the raw array and
confirmed the new
AdvancedOptionCategoryEnabledIsNotTheMutableBackingArray pin fails (no
exception thrown) before restoring the fix.
MossTank suite 677 -> 678 (one new pin). Full solution build green; App
markup/plugin filter 203/203.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
AdvancedOptionNames, AdvancedOptionValueColumn, AdvancedOptionName, and
AdvancedOptionDescription each re-ran FilteredAdvancedOptionNames() (a
Where over the full 163-entry VtankOptionCatalog) on EVERY retained-UI
draw of the popup, and the value column additionally ran
GetMetaOption(name).ToDisplayString() per row (Trim()/ToLowerInvariant()
allocations) every frame. All four are now materialized once into fields
by a new RefreshAdvancedOptions(), called from every real mutation point:
category toggle, edit/apply, selection change, and profile
load/create/clear/delete (via ResetProfileConsumers), plus construction.
Same pattern fix round B item 12 used for the Monsters/Meta/Route grids.
AdvancedOptionValueColumnMirrorsTheLiveSettingValue exercised a settings
change made OUTSIDE the popup's own mutators (ToggleCombatEnabled, a
main Options-tab checkbox) and expected the value column to reflect it
on the very next read — a real behavior this caching model intentionally
narrows (mirrored settings now catch up at the next real popup mutator,
not on every read). Updated the test to re-select the row afterward,
exercising the "selection change" refresh point, and documented why.
Mutation named: reverted the two property getters to their old live-
recomputing form and confirmed the new
AdvancedOptionsPopupBindingsDoNotReallocateOnEveryRead pin fails
(Assert.Same throws — different array instances per read) before
restoring the fix.
MossTank suite 676 -> 677 (one new pin). Full solution build green.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Three small, unrelated fixes bundled per the fix round's own item list
(the fourth, cropping the three popup screenshots to the panel, belongs to
the live-capture pass, item 17):
- SortedCombatItemNames(): one shared helper for the ordinal-sorted
registered weapon roster, replacing five separate identical
`_combatSettings.CombatItemNames.OrderBy(...).ToArray()` call sites
(RefreshItemEditors, DeleteItemRowAtCore, CycleItemHandsAtCore,
RemoveSelectedItemCore, CycleMonsterEquipmentAt).
- SelectTab already cleared LootEditorVisible/AdvancedOptionsVisible when
the user switched tabs (fix round A). The buff picker (S7.4) and the
Meta rule editor (item 5) are the same "own popup, own window" shape and
were missing from that guard — switching away from Buffs or Meta while
either popup was open left it orphaned, open over whatever tab the user
switched to. Both flags now clear in SelectTab too.
- <slider min max>: max<=min used to silently fall back to a range of 1
(the old `range == 0f` check, covering only max==min) or produce a
slider whose drag direction is inverted from its declared range
(max<min, never caught at all). Both are now a build-time
FormatException, the same rule every other <slider>/<menu>
attribute-format check in this file already follows.
New tests: SwitchingTabsClosesTheBuffPickerAndMetaEditorPopups,
Build_SliderWithMaxLessThanOrEqualToMin_Throws (both max<min and
max==min cases). Mutation checks: removing the two SelectTab clears
turned the first red ("Expected: False, Actual: True"); reverting the
slider validation to the old range==0f fallback turned both Theory cases
of the second red ("No exception was thrown"). Both restored to green.
tests/AcDream.Plugins.MossTank.Tests: 676/676 (was 675/675, +1).
tests/AcDream.App.Tests --filter Markup|Plugin|UiMenu|Slider: 285/3 skipped/288 (was 283/3/286, +2: the new Theory's 2 cases).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ExtraBuffSpellNames/BlacklistedBuffFamilyNames (Buffs tab, Campaign VT
S7.4) were never captured by SideCarDocument (the active JSON persistence
alongside the real VTank .usd) — a restart or profile switch silently
dropped them. BuffPlan.Build still does not READ either set when choosing
what to cast — that remains a real, separately tracked wiring gap (see
BuffSettings' own doc comments); this only fixes the storage/display
honesty the owner asked for.
- SideCarDocument gained BuffExtraSpellNames/BuffBlacklistedFamilyNames
(string[]), captured via the same Sorted() helper CombatItemNames/
ConsumableNames use, and applied via the same Replace() clear-then-
repopulate helper — a profile switch can't carry a stale entry over from
whichever profile was loaded before.
- PickBuffAtCore and DeleteExtraBuffAt/DeleteBlacklistedBuffFamilyAt now
call SaveProfile(), matching every other Add/Delete mutator in this file
— without this the newly-wired capture/apply would only fire
opportunistically on some unrelated save.
- Reworded both tab tooltips ("Named spell exemplars added beyond the
school-driven picks" / "Named buff families never cast, even if
otherwise wanted") to state plainly that the lists are stored and shown
but not yet used for casting, instead of implying they already affect
cast selection.
- (False start, reverted: an earlier pass edited LegacyBuffProfileDocument,
which the file's own comment marks migration-only dead code that nothing
else writes any more — the real fix belongs in SideCarDocument, the
active format SaveCurrent/LoadCurrent actually round-trip.)
New test: ExtraBuffAndBlacklistedFamilyNamesPersistAcrossSessions (add via
the picker in one panel, confirm both survive in a second panel sharing
the same storage). Mutation check: commenting out the two Replace() calls
in SideCarDocument.Apply turned it red ("Expected: [Spell 1], Actual: []");
restoring them turns it green.
tests/AcDream.Plugins.MossTank.Tests: 675/675 (was 674/674, +1).
tests/AcDream.App.Tests --filter Markup|Plugin|UiMenu|Slider: 278/3 skipped/281 (unchanged).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Every retained-UI draw re-evaluates every markup-bound column property.
Several of these did real per-row work on EVERY frame instead of returning
a cached field:
- ExcludedComponentIcons (Consumables) called ResolveComponentIcon per row,
which itself calls IAutomation.Items.CaptureOwnedItems() — a live
inventory snapshot — per row per frame.
- All 23 Monsters-grid columns (14 flag columns via MonsterFlagColumn, 7
text columns, 2 move-icon columns) re-ran a LINQ .Select(...).ToArray()
over _combatSettings.Rules every frame; Weapon/Offhand additionally
called ItemDisplayName (another CaptureOwnedItems() scan) per row.
- Meta's MetaStateColumn/MetaConditionColumn/MetaActionColumn/
MetaDeleteColumn/MetaMoveUpIcons/MetaMoveDownIcons and Route's
RouteWaypointCountColumn allocated a fresh array with LINQ every frame.
Fixed by materializing each into a field, computed once at the point of
actual mutation:
- ExcludedComponentIcons: computed in RefreshItemEditors (already the sole
owner of _excludedComponentRows).
- RouteWaypointCountColumn: computed in RefreshRouteEditor (already called
at 13 real mutation points, including profile load).
- Meta's 6 columns: computed in RefreshMetaEditor (already called at every
real mutation point, including profile load).
- Monsters' 23 columns: new RefreshMonsterEditor, wired into the 4 actual
Rules-mutating methods (AddMonsterRuleCore, DeleteMonsterRuleAtCore,
MoveMonsterRuleAtCore, UpdateMonsterActionsAt — the last already covers
every flag toggle and value-cycle action) plus panel construction and
ResetProfileConsumers (profile load/switch). The now-unused
MonsterFlagColumn helper is removed.
New tests (FakeAutomation gained a CaptureOwnedItemsCallCount counter):
ExcludedComponentIconsDoesNotScanLiveInventoryOnEveryRead,
MonsterGridColumnsDoNotScanLiveInventoryOrAllocateOnEveryRead (also asserts
Assert.Same across reads), MetaAndRouteGridColumnsDoNotReallocateOnEveryRead.
All three mutation-checked by temporarily reverting to the old per-read
computation: each turned red (call-count mismatch or Assert.Same failure)
against the reverted code; restoring the cached-field getters turns each
green.
tests/AcDream.Plugins.MossTank.Tests: 674/674 (was 671/671, +3 new tests).
tests/AcDream.App.Tests --filter Markup|Plugin|UiMenu|Slider: 278/3 skipped/281 (unchanged — App-layer surface untouched by this item).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
VTank's own AdvancedOptionsView (docs/research/vtank-kb/08-ui-views.md §1's
secondary-view table): lOptionList is clOpt(180)+clVal(62) two columns, not
a name-only list; lFilterList (268,4,120,180) is a category checklist
(check+text columns) filtering it; txtInfo (4,188,384,80) is a description
readout.
- lOptionList is now a real 2-column <list><column> grid: name (clOpt,
PITCH 187) + live value (clVal, PITCH 69, AdvancedOptionValueColumn via
the existing GetMetaOption/.ToDisplayString() pipeline).
- lFilterList genuinely filters lOptionList: VtankDefaultSettingsDatabase
now parses VTank's own SettingsCategories table (136 rows straight from
the embedded .usd, 2 columns: Setting name + Categories bitmask) into
VtankOptionCatalog.CategoryBits (9 distinct bits, 0x01-0x100). A setting
with no recorded bitmask always shows regardless of filter state. This
worktree has no refs/vtank/ checkout, so VTank's own category NAME
strings for those 9 bits aren't available anywhere in this repo — the
checklist honestly labels each by its raw bit value ("0x04" etc.)
instead of a guessed name.
- AdvancedOptionDescription surfaces VTank's own real Settings.Description
column (93 of 137 rows non-empty, genuine retail help text — e.g.
DoHelp's own wording about fellowship healing) prefixed with the option
name, filling the exact 384x80 readout and replacing the old bare
name-only label.
- SelectAdvancedOption/AdvancedOptionName now resolve through the FILTERED
list (not the raw 137-row catalog) so selection stays correct as the
filter narrows/widens it.
- Apply/Back move beside each other below the two lists to make room;
popup height grows 392 -> 476 for the taller lists + description block,
pushing item 8's "MossTank Extras" section further down (unchanged
internally).
New tests: AdvancedOptionCategoryFilterHidesNonMatchingSettings (unchecking
every category but EnableLooting's own 0x100 hides EnableNav but keeps
EnableLooting; re-checking restores the full list),
AdvancedOptionDescriptionSurfacesRealRetailHelpText,
AdvancedOptionValueColumnMirrorsTheLiveSettingValue. All three mutation-
checked: disabling FilteredAdvancedOptionNames's filter predicate, the
description's name-prefix, and the value column's GetMetaOption call each
turned their test red; restoring each turns it green.
tests/AcDream.Plugins.MossTank.Tests: 671/671 (was 668/668, +3).
tests/AcDream.App.Tests --filter Markup|Plugin|UiMenu|Slider: 276/3 skipped/279 (unchanged).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Transcribed VTank's own Route tab almost verbatim (docs/research/
vtank-kb/08-ui-views.md §1 "Tab: Route", 20 controls, +4px left margin for
our own convention): the waypoint list, a bottom row (nav-type menu +
insert-mode menu + three nav image buttons), and a right-hand 2-across
button grid in VTank's own row order (Add/Open Vendor; Add Portal-NPC/Add
NPC Talk; Add Recall+its recall-type menu; Add Pause+field+"seconds"
label; Add Chat+field).
- cmbNavInsertMode is now a real 3-option <menu> (RouteInsertMode.AddToEnd/
InsertAbove/InsertBelow, new enum in Navigation.cs) instead of the old
2-state ToggleRouteAddPosition button — InsertAbove/InsertBelow are now
genuinely distinct (before vs. after the selected waypoint), not both
collapsed into "not append".
- The three nav image buttons (advance/regress/nearest-point, DAT ids
0x060028FD/0x060028FC/0x060011F7) move from the waypoint-actions row to
the bottom row beside the nav-type menu, matching VTank's own layout.
- "Use NPC" is recaptioned "Add NPC Talk" (VTank's own cmdNavUseNPC text),
same AddRouteUseSelected binding.
- The pause duration is now a real editable field
(RoutePauseSecondsFieldText, default "5", parsed/clamped 0-3600 by
SetRoutePauseSecondsText) plus a "seconds" label, matching VTank's own
txtPauseWaypointTime/Label52 — replaces the RoutePauseDown/RoutePauseUp
stepper (removed).
- MossTank-only controls with no VTank Route-tab counterpart — Checkpoint,
Jump, Remove, Set Follow Target + its status label, Follow Corners, Open
Doors, Nav Priority (a real second copy of Options' own "Boost Nav.
Priority"), and the Follow/Nav Min Distance +/- stepper (a real
duplicate of Options' own editable field) — move to
mosstank-advanced.xml's new "MossTank Extras" section (popup height
300 -> 392 to fit them without touching the retail editor above).
New tests: RouteInsertModeControlsWhereANewWaypointLands (AddToEnd/
InsertAbove/InsertBelow each produce the correct insertion index),
RoutePauseSecondsFieldParsesAndClampsInput. Both mutation-checked:
hardcoding AddRouteWaypoint's insertion to always-append turned the first
red ("Point: (0N, 0E)" instead of containing "99"); removing the Math.Clamp
in SetRoutePauseSecondsText turned the second red ("99999" instead of the
clamped "3600"). Both restored to green.
EveryInteractiveControlDeclaresARealHandlerBinding: 166 -> 156 (mosstank.xml
Route tab 28 -> 18 controls; the 10 relocated/removed controls are either
gone or moved into mosstank-advanced.xml, a separate file this scan
doesn't cover).
SecondaryPopupPanelsFitTheirOwnBoundsAndEveryBindingResolves: mosstank-
advanced.xml's expected height 300 -> 392.
tests/AcDream.Plugins.MossTank.Tests: 668/668 (was 666/666, +2 new tests).
tests/AcDream.App.Tests --filter Markup|Plugin|UiMenu|Slider: 276/3 skipped/279 (unchanged).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Retail ground truth (PluginCore.cs:7683-7700): a click on the Consumables
tab's left list removes that row directly — there is no select-then-press-
Remove step for that list, unlike our previous SelectConsumableRow which
only updated the selection index. SelectConsumableRow now sets the index
AND immediately calls RemoveSelectedConsumableCore, matching the "click
removes" convention the right-hand Excluded Scarab Types list already
uses; the "Remove" button (RemoveSelectedConsumable) stays as a second
path, same as Items/Buffs/Route. Retitled the list's tooltip to match
("Click a row to remove it.").
New test: ConsumablesLeftListRowClickRemovesTheRowDirectly. Mutation
check: commenting out the RemoveSelectedConsumableCore() call turned it
red ("The collection contained 2 items" instead of Assert.Single); restoring
it turns it green.
tests/AcDream.Plugins.MossTank.Tests: 666/666 (was 665/665, +1).
tests/AcDream.App.Tests --filter Markup|Plugin|UiMenu|Slider: 276/3 skipped/279 (unchanged).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
VTank's own Meta tab (docs/research/vtank-kb/08-ui-views.md §1 "Tab: Meta")
is only 5 controls: the lstMetaRules grid, cmdMetaCreate ("Create"), a bold
"Current State:" caption, and a SETTABLE cmbMetaCurrentState choice — no
profile toolbar, no "Enable Meta" checkbox, no inline editor at all.
- Removed from the Meta tab entirely: the profile menu/name-draft field/
New/CopyTo/Clear/Delete row (Profiles already carries the Meta combo/
CopyTo/Delete) and the "Enable Meta" toggle (already on Options) — same
accepted-regression shape as fix round A's Macro/Nav CopyTo-name-field
loss (no Profiles-tab equivalent exists for these, and VTank's own tab
has none either).
- Moved the inline State/Condition/Action editor (fields, condition/action
menus, numeric steppers, Apply/Remove/MoveUp/MoveDown) into a new popup,
mosstank-metaeditor.xml, registered the same StartVisible=true/
ShowInSidePanel=false way as the buff picker. Opened by a grid text-cell
click (SelectMetaRule, still populating the draft via SelectMetaRuleCore)
or the tab's own "Create" button (CreateMetaRule — new, distinct from the
still-existing AddMetaRule the tests call directly); Apply and Cancel
(HideMetaEditor) both close it. DeleteMetaRuleAt/MoveMetaRuleUpAt/
MoveMetaRuleDownAt call SelectMetaRuleCore directly and do NOT open the
popup.
- "Add" -> "Create": CreateMetaRule adds a default rule (delegating to the
existing AddMetaRuleCore) and opens the editor so it isn't left silently
default-valued.
- Added the settable current-state menu: MetaCurrentStateNames/
SelectedMetaCurrentState/SetMetaCurrentState expose MetaEngine's own
already-public States/Transition(string) — choosing a state here forces
the live engine into it, matching VTank's own manual override.
- The grid returns to VTank's full-width 856x116 proportion (848x116 here)
now nothing else shares the tab.
- Bold text isn't representable in the plain retail UI font (0x40000000 has
no bold face); "Current State:" uses the same bright caption color other
tab headers use instead — documented in mosstank.xml's own comment, not
silently dropped.
New tests: MetaEditorPopupOpensOnCellClickOrCreateAndClosesOnApplyOrCancel,
MetaCurrentStateMenuForcesTheLiveEngineIntoTheChosenState. Both mutation-
checked: commenting out SelectMetaRule's `_metaEditorVisible = true` turned
the first red ("Expected: True, Actual: False"); commenting out
SetMetaCurrentState's `_meta.Transition(value)` turned the second red
("Expected: Hunt, Actual: Default"). Restoring both turns them green.
EveryInteractiveControlDeclaresARealHandlerBinding's pinned control count:
186 -> 166 (-7 controls removed for good, -14 moved into the new popup file
this scan doesn't cover, +1 the new current-state menu).
SecondaryPopupPanelsFitTheirOwnBoundsAndEveryBindingResolves gained
mosstank-metaeditor.xml (630x236... — 630x160, corrected below).
tests/AcDream.Plugins.MossTank.Tests: 665/665 (was 663/663, +2 new tests).
tests/AcDream.App.Tests --filter Markup|Plugin|UiMenu|Slider: 276/3 skipped/279 (unchanged).
Screenshot with at least two rules (item 17) is owed with the round's other
live captures.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Reordered ahead of items 5-12 on purpose: item 5 adds a fourth popup
(mosstank-metaeditor.xml) and would otherwise need the same three-file
manual edit this item retires. Every place a mosstank-*.xml file used to be
named one at a time now globs mosstank*.xml instead, so adding a new popup
panel needs exactly one new file, not four matching edits:
- AcDream.Plugins.MossTank.csproj: <None Update="mosstank*.xml"> replaces
four separate <None Update> entries.
- AcDream.Plugins.MossTank.Tests.csproj: one globbed <None Include> with
Link="%(Filename)%(Extension)" replaces four separate Include/Link pairs.
- AcDream.App.csproj: a new _MossTankPluginMarkup item
(Include="…/mosstank*.xml") replaces the literal four-file semicolon list
in both CopyMossTankPluginToBuildOutput and …ToPublishOutput's Copy
SourceFiles.
- MossTankMarkupContractTests: AuthoredControlsInTheSameContainerNeverOverlapASibling
(item 2's new pin) and NoButtonAnywhereUsesTheUnrenderableArrowGlyphs now
iterate Directory.GetFiles(AppContext.BaseDirectory, "mosstank*.xml")
instead of a hardcoded 4-file array/Theory. Left
SecondaryPopupPanelsFitTheirOwnBoundsAndEveryBindingResolves alone — it
pairs each file with its OWN expected w/h, which a glob can't supply.
- LinuxPlatformBoundaryTests.ShippedPluginCopiesUseResolvedTargetPathsForBuildAndPublish
now asserts on the glob pattern instead of the literal mosstank.xml
substring the old Copy SourceFiles list contained.
A missed file in any of these four places used to silently drop a panel at
mount (fix round A's own #missing-popup-files near-miss) instead of failing
the build; the glob makes that failure mode structurally impossible.
tests/AcDream.Plugins.MossTank.Tests: 662/662 (665 -> 662: the 4-case
Theory collapsed into 1 Fact with an internal loop — same coverage, 3 fewer
reported xunit tests).
tests/AcDream.App.Tests --filter Markup|Plugin|UiMenu|Slider: 276/3 skipped/279 (unchanged).
Verified the glob actually copies all four files: `ls
src/AcDream.App/bin/Release/net10.0/plugins/AcDream.Plugins.MossTank/*.xml`
lists all four post-build.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Owner's silhouette rule: VTank's own controls are never shrunk/moved/
reordered to make room for MossTank extras. The Buffs tab's Difficulty "+"
button (338,108,26,20) overlapped the Extra Buff Spells "Add..." button
(336,102,120,16) because both VTank lists had been narrowed to 246/256x84
to coexist with MossTank's toggle/difficulty/rebuff/Buff-cast-now block in
the same two columns.
Fix: restore both lists to VTank's real 320x116 at the far left (x=4) and
far right (x=524) with their own 120-wide "Add..." row below each list.
Move the MossTank-only extras block (7 toggles, Difficulty +/-, Rebuff +/-,
the Buff button+status, Coverage) into the empty 184px middle strip between
the two lists that VTank's own layout never uses. Coverage moves to y=178
(194-16) per the upcoming bottom-fit rule.
Also fixed: a second real overlap the new pin found on the Options tab —
"Pet Min. Monsters:" (614,48,125,16) overlapped its own field
(734,48,40,16) by 5px; narrowed the label to the same 120px column width
every other label/field pair in that block uses.
Added AuthoredControlsInTheSameContainerNeverOverlapASibling (4 file cases)
plus a direct predicate pin (AssertNoSiblingOverlap_CatchesARealOverlap...)
proving the helper catches a real overlap, ignores touching edges, and
ignores two <group> tab-pages sharing one rectangle (mutually exclusive via
their own visible="{...}" binding) — the existing AssertWithinParent only
ever checked a child against its OWN parent's bounds, never against a
sibling.
Mutation check: setting DifficultyUp's button to (472,112) — the same rect
as DifficultyDown — turned AuthoredControlsInTheSameContainerNeverOverlapASibling
red against the real mosstank.xml ("<button> text='-' ... overlaps sibling
<button> text='+' ..."); reverting turns it green.
tests/AcDream.Plugins.MossTank.Tests: 665/665 (was 660/660, +5: the new
theory's 4 file cases + 1 predicate fact).
tests/AcDream.App.Tests --filter Markup|Plugin|UiMenu|Slider: 276/3 skipped/279 (unchanged).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
VTank's own lstMetaRules grid (delete/move-up/move-down icon-ish cells +
State/Condition/Action text cells, docs/research/vtank-kb/08-ui-views.md
§1 "Tab: Meta"; PluginCore.cs:2218-2247) replaces the padded
single-column rules list: any State/Condition/Action cell click opens
the rule editor below (reusing the existing SelectMetaRule), the
up/down cells reorder using the same 0x060028FC/FD DAT icons the
Monsters and Route grids already established, and the delete cell is a
plain text "X" — no retail DAT delete-glyph id is confirmed anywhere in
this codebase (unlike the already-established move icons), and "X" is
ASCII the default retail font renders without the unrenderable-glyph
risk NoButtonAnywhereUsesTheUnrenderableArrowGlyphs guards against.
The new pin (contract control count 180->186) and the new panel test
were shown to fail against a targeted mutation before being confirmed
green. MossTank suite 659 -> 660; App markup/plugin filter holds 192/192.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
VTank's own clWP/clWPc 2-column waypoint grid (text + a 1-based position
counter standing in for VTank's own count column) replaces the padded
single-column list, with any-cell-click delete matching
PluginCore.cs:3576-3599. Also: the recall menu gets scroll="true" for
grammar parity (still 4 real recall kinds — see the deviation note
below); the nav-type menu now shows VTank's own "Follow" caption for the
Target mode via a pure display remap (SelectedRouteMode/SelectRouteMode
translate the string, RouteMode.Target itself is unchanged, zero
behavior change); and a third nav image button (icon 0x060011F7, "Select
Nearest Point") is added.
Deviations, documented at their own binding site:
- VTank's cmbRecallType lists 27 named retail recalls; MossTank's
RouteRecallKind models 4 (Lifestone/Marketplace/Primary/Secondary
Portal). Expanding to 27 needs real per-recall spell-id data, which is
casting-algorithm behavior out of this UI-parity slice's scope.
- VTank's btnNavResetPoint reassigns the LIVE navigation cursor mid-route
(a mutable index MossTank's navigation controller never exposes to a
plugin); SelectNearestRouteWaypoint instead moves the tab's own EDIT
selection to the closest waypoint by horizontal distance — real,
testable UI-only behavior that stays out of live-navigation territory.
The new pin (contract control count 177->180) and the new panel test
were shown to fail against a targeted mutation before being confirmed
green. MossTank suite 658 -> 659; App markup/plugin filter holds 192/192.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
VTank's real Items (2-col name/hands), Consumables (adds the right-hand
"Excluded Scarab Types" icon+text grid), and Buffs (Extra Buff Spells /
Blacklisted Buff Families lists + a shared SelfBuffChoiceView-style
picker popup) tabs are transcribed from docs/research/vtank-kb/
08-ui-views.md §1, replacing the old single-column-list adaptations with
real per-cell grids and VTank's own click-to-delete/cycle semantics
(PluginCore.cs:8529-8562 Items, :7683-7776 Consumables, :7323-7355 Buffs).
Deviations, documented at their own binding site:
- Items' Hands column has no backing wieldable-handedness data anywhere
in the plugin surface, so handedness is session-local UI state only
(not persisted across profile save/load) — same "deliberate adaptation"
shape as S7.3's weapon-roster substitution for Monsters' Weapon/Offhand.
- Consumables' "Add Selected" accepts any selected owned item rather than
requiring VTank's own SpellComponent object-class check (no classifier
surface exists for plugins) — the added token is still the item's real
Name, which SpellComponentPolicy already matches against.
- Buffs' ExtraBuffSpellNames/BlacklistedBuffFamilyNames (BuffPlan.cs) add
storage + UI only; wiring them into BuffPlan.Build's cast selection is
real casting-algorithm behavior, out of this UI-parity slice's scope
(tracked in the slice 7 plan ledger as a real, accepted gap for a
future Campaign VT behavior slice).
Every new/changed pin (contract control count 167->177, the new
mosstank-buffpicker.xml popup pin, the three new MossTankPanelTests
interaction tests) was shown to fail against a targeted mutation before
being confirmed green. MossTank suite 654 -> 658; App markup/plugin
filter holds 192/192.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Owner's live-client report (2026-09-07 08:05): "The Options tab labels
overlap, fix that" and "Those BIG gold/yellow buttons HAS to go" (the
gold-button half already landed upstream at cfa703065, merged into this
branch first). This commit is the geometry half of that fix round plus
the lead's 07:55 read (docs/plans/2026-09-07-campaign-vt-slice7-tabs.md).
Grid scaling: measured acdream's actual retail DAT font (0x40000000) via
AcDream.Cli's dump-font-atlas against a real installed DAT rather than
guessing — MaxCharHeight=16 matches VVS's own assumed row height exactly,
so no vertical scale was needed, but several captions render wider than
VVS's narrow font assumed ("Follow/Nav Min Distance:" measures 134px real
width vs an 80px-wide retail label column). <label>/<toggle> never clip or
use their declared "w" (UiLabel.OnDraw/UiMarkupToggle.OnDraw draw raw,
unclipped text), so the fix is a plain rightward SHIFT of the columns after
the overflowing captions rather than a width change: Options' columns 2-5
move +62px (90/220/386/552/611 -> 152/282/448/614/673, with the rightmost
Advanced Options/Run Macro/status sub-block also shrinking its declared
width 233->175 to stay inside the group's 848 budget), Profiles' columns
2-5 move +16px (140/280/330/440/530 -> 156/296/346/456/546). Both are pure
translations, so every original inter-column gap is preserved exactly.
Vitals needed no shift (its worst caption already clears its slider by 3px)
but got a small +4px safety margin anyway since it was the tightest
already-OK case measured.
Profiles leftovers deleted (owner + lead, "legacy controls"): the three
stacked ProfileNameDraft/RouteProfileNameDraft/MetaProfileNameDraft
field+"New"-button rows (three 20-tall fields at zero vertical gap that
rendered as one solid black bar behind three stacked "New" buttons — the
owner's screenshot), the acdream-only "Loot engine" classifier row
(no VTank equivalent), and the ProfileLifecycleNotice label (the raw
macro-profile recovery path the owner saw rendered). Deviation: Macro/Nav
CopyTo lose their only in-UI target-name entry point (Meta's naming
already exists on the Meta tab itself, so only Macro/Nav are affected) —
an accepted trade for matching VTank's own Profiles table, which has no
name-draft control at all (VVS's own combo is presumably editable
in-place; acdream's <menu> is not). LootStatus is kept (general feedback,
not itself a reported leftover) but repositioned into the freed space.
Window: Advanced Options and Loot Editor move out of mosstank.xml into
their own top-level plugin panels (mosstank-advanced.xml,
mosstank-loot-editor.xml, MossTankPlugin.cs registers a second and third
AddPanel with ShowInSidePanel=false since VTank's own popups have no
shelf entry). No new IUiRegistry show/hide API was needed: a root <panel>
already supports a data-bound visible="{Prop}" attribute
(MarkupDocument.Build re-evaluates VisibleSource every frame, proven by
the main panel's own visible="{WindowAvailable}"), so each popup's own
visible="{AdvancedOptionsVisible}"/"{LootEditorVisible}" is the SAME flag
the Options/Profiles checkbox flips and the popup's own "Back" button
clears — closing the popup unchecks the box for free. The main window
returns to VTank's 210-ish proportion (236 = 42px header/tab-strip +
194px per-tab content, both unchanged since S7.1) with no dead space
below the tabs. The eight tab-content *Visible getters (MossTankPanel.cs)
drop their `&& !_lootEditorVisible`/`&& !_advancedOptionsVisible` guards,
which only existed because the popups used to share screen space with the
tab content in the same window.
Trailing blank button slots (Route waypoint reorder, Meta rule reorder,
Loot Editor rule reorder): the retail default UI font (0x40000000) has no
glyph for U+2191/U+2193, so the "↑"/"↓" text buttons rendered as blank
slots (owner's screenshot). Replaced with the same DAT move-up/move-down
icons (0x060028FC/0x060028FD) the Monsters grid's own reorder columns
already use.
No remaining <menu style="retail"> in any of the three files (verified).
Pins updated (shown to fail against the old numbers first): the markup
contract's interactive-control count (202 -> 167, the two popups leaving
the file plus the Profiles deletions) and the authored-shell height pin
(350 -> 236). Added SecondaryPopupPanelsFitTheirOwnBoundsAndEveryBindingResolves
(parses both new files, checks their declared 392x300/268x300 footprint,
AssertWithinParent, and every markup binding resolves) and
NoButtonAnywhereUsesTheUnrenderableArrowGlyphs (no <button text="↑"|"↓">
survives anywhere) as new coverage for this fix round.
MossTank suite 651 -> 654 (the two new SecondaryPopupPanels theory cases
+ NoButtonAnywhereUsesTheUnrenderableArrowGlyphs); App markup/plugin/menu
filter stays 237/237.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Campaign VT slice 7 (docs/plans/2026-09-07-campaign-vt-slice7-tabs.md),
S7.3 (Monsters). Owner's bar: looks basically the same as VTank and
behaves the same from the player's seat.
The pre-S7.3 Monsters tab was a select-then-edit adaptation (one
padded-text list + a separate expression/priority/damage-menu/equip
editor row + 14 standalone toggles) forced by acdream's earlier
single-column-only <list>. Campaign VT slice 1 Part B already closed
that gap (the <list><column> markup extension, docs/plugin-ui-markup.md
"Columns"), so this slice replaces the whole tab body with VTank's own
one 23-column HudList grid (docs/research/vtank-kb/08-ui-views.md §1
"Tab: Monsters"): 14 check columns (F B G I Y V A R S WC FC Cp DC Cs,
each 20px = VVS's forced 13px check width + WPadding per the markup
doc's PITCH convention), 7 text columns (Name 127/P 27/Dmg type 63/
Ex. Vuln 63/Weapon 87/Offhand 87/PetDmg 63, each pitch = fixedwidth+7),
2 icon columns (move up/down, DAT ids 0x060028FC/0x060028FD), 15
header <label>s carrying VTank's own decompiled tooltip text
(PluginCore.cs:1693-1707 — corrects "Broadside of a Tumerok" to the
decompile's actual "Broadside of a Barn"), and the txtNewMonster field
+ Add + Add Sel row at VTank's exact geometry (8,136,388,16 /
404,136,50,16 / 462,136,50,16). Selection is unused, matching VTank —
every real cell is its own click target.
Per-cell behavior ported from PluginCore.cs:7885-8105's `b(object,int,
int)` handler: Name click deletes the row (guarded on the rule's own
IsDefault rather than literal row 0, so the invariant holds even if a
caller reorders DEFAULT); P cycles -1->0->1->2->3->4->-1; Dmg type
cycles every eDamageElement 0..13; Ex. Vuln and PetDmg reproduce the
decompile's own non-contiguous jumps (6->8->98 and 6->101->8->98
respectively — eDamageElement.cs); move up/down reorder but never
displace DEFAULT (a deliberate symmetric guard — VTank's own case 21
keeps MoveUp off row 0 but case 22 has no matching MoveDown guard,
which reads as an authoring oversight, not intended asymmetric
behavior, so a literal port was not preserved here). Case 6's "A"
column write is inverted in VTank's own obfuscated field, but that
inversion is an artifact of VTank's internal storage, not the checkbox
the player sees — our Attack flag is already declared un-inverted, so
no inversion is needed for parity.
Deviation: cases 18/19's Weapon/Offhand cycle VTank's own runtime list
of owned weapon-TYPE ids via an opaque item-kind classifier the
decompile alone doesn't resolve. MossTank has no "weapon type"
abstraction — Weapon/Offhand are always concrete owned items — so
these columns instead cycle the same registered weapon roster the
Items tab already maintains (ordinal-sorted for a deterministic
order), preserving the click-to-cycle-through-<AUTO> contract without
inventing a type abstraction outside this slice's scope.
MossTankPanel: the whole select-then-edit surface (SelectedMonsterRule
selection, RefreshMonsterEditor's cached row formatting, Apply/Remove/
priority-stepper/damage-menu/equip-from-selection actions,
HasMonsterFlag/ToggleMonsterFlag) is retired in favor of per-row
column-array properties (14 flag columns, 7 text columns, 2 icon
columns) and per-row mutation actions, all still writing through the
unchanged CombatSettings.Rules/MonsterRule/MonsterRuleActions model.
EnsureDefaultMonsterRule replaces RefreshMonsterEditor's DEFAULT
invariant at the two remaining call sites (ctor init, profile reload,
and the /vt refresh command). MonsterEquipmentText is kept as a
read-only DEFAULT-rule readout for the (out-of-scope) Items tab label
that already depended on it.
Tests (every pin below shown to fail against a targeted mutation,
reverted after confirming the failure — mutations: wrong priority-wrap
boundary, dropped DEFAULT-delete guard, dropped DEFAULT-displacement
guard on move, spliced Harm into the Ex.Vuln cycle, flag toggle
targeting row 0 unconditionally, a wrong header tooltip string; the
xml column-count contract's 205->202 pin was independently shown to
fail against the pre-edit test file and the new markup):
MossTankMarkupContractTests gained
MonstersGridHasVtanksTwentyThreeColumnsInOrderWithRetailHeaderTooltips
(column type/order + the 15 header tooltips) and updated the
205->202 interactive-control count plus the tooltip-check's <column>
skip (columns have no text/tooltip grammar). MossTankPanelTests
replaced the three old select-then-edit tests with
MonstersGridMutationsPersistAcrossSessions,
ToggleMonsterFlagAtWritesOnlyTheTargetedRow,
CycleMonsterPriorityAtWrapsExactlyNegativeOneThroughFour,
MonsterDamageColumnsCycleInTheExactRetailOrder (all three cycles),
DeleteMonsterRuleAtRemovesNonDefaultRowsButNeverDefault,
MoveMonsterRuleAtReordersButNeverDisplacesDefault,
AddMonsterRuleUsesTheDraftTextAndAddSelectedMonsterUsesTheWorldTarget,
and MonsterWeaponColumnCyclesTheRegisteredRosterAndPersistsByName
AcrossSessions, and updated MacroWieldsCasterEntersMagicBuffsThen
WieldsWeaponFightsThenIdlePeace for the new weapon-cycle API.
Full suite green: 651/651 MossTank tests (645 + 6 net), 109/109 App
markup-filtered tests.
Deviation not requiring a divergence-register row: this campaign ports
VTank plugin UI behavior, not retail AC client behavior, so
docs/architecture/retail-divergence-register.md's scope (acdream vs.
retail) does not apply here.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Campaign VT slice 7 (docs/plans/2026-09-07-campaign-vt-slice7-tabs.md),
S7.1 (Window) and S7.2 (Options/Profiles/Vitals). Owner's bar: looks
basically the same as VTank, judged side by side.
S7.1: mosstank.xml's content area is now 856 wide (VTank's own
mainView.xml, docs/research/vtank-kb/08-ui-views.md §0), tab order/
widths unchanged (already assessed as reading close to VTank's strip).
Panel height grows to 350 (not VTank's 210) only because the Advanced
Options and Loot Editor pages move to VTank's own POPUP geometry
(392x300 / 268x300, KB §1's secondary-view tables) as separate in-panel
groups (KB §5's documented design choice: acdream keeps these in one
window rather than VTank's separate OS-level popup windows) — the nine
normal tab bodies keep their existing 194-tall content (== VTank's 210
minus its own 16px HudTabView strip) and simply don't fill the extra
room the popups need. cShowAdvanced/cShowLootEditor are now VTank's own
Checkboxes (open AND close via the same control) instead of separate
Show/Hide push buttons — MossTankPanel gained ToggleAdvancedOptionsVisible/
ToggleLootEditorVisible thin wrappers over the existing Show/Hide
actions, and CombatMacroRunning (VTank's cOn "Run Macro" is a Checkbox
with a static caption, not a dynamic-caption push button).
S7.2: Options, Profiles, and Vitals are transcribed control-for-control
from KB §1's exact L,T,W,H and captions. Vitals' nine sliders now use
the new <slider min="0" max="100"> to bind the percent directly
(NormalHealthPercent/SetNormalHealthPercent etc. — thin wrappers over
the existing 0.0-1.0 fields, never independent state). Options gained
direct checkboxes for four settings that already existed but were only
reachable through the generic Advanced Options key-value editor
(AutoStackEnabled was already exposed; FastCastBuffsEnabled/
DontShootAtWallsEnabled/DebuffFallbackEnabled are new thin wrappers over
_buffSettings.FastCastBuffs/_combatSettings.UseProjectileAwareness/
_combatSettings.AllowDebuffFallback). Profiles gained a real Meta-profile
combo/CopyTo row (previously a stub label) and lost the Loot Priority
Boost toggle as a duplicate of the Options-tab control of the same name;
VTank's own bMetaClearViews ("Del. Meta Windows") has no acdream
equivalent — MossTank has no floating meta-debug windows — so that slot
is repurposed for our own per-type Delete buttons (slice 1) instead,
keeping VTank's row rhythm undisturbed. The Loot Editor popup's
LootEditorNotice label is dropped to fit VTank's 268x300 footprint
without clipping (a status label, not an action — the one deliberate
trim, noted in the group's own comment).
Contract test updates (both shown to fail against the prior mosstank.xml:
856/350 vs 800/244, 205 vs 194 controls): AuthoredShellFitsTheMinimum
CanvasAndEverySizedChildFitsItsParent's window-size pin, and
EveryInteractiveControlDeclaresARealHandlerBinding's control count.
New MossTankPanelTests pins: FastCastProjectileAwarenessAndDebuffFallback
ToggleTheirVtankDefaultsAndPersist, ToggleAdvancedOptionsAndLootEditor
VisibilityFlipBothWays, CombatMacroRunningReflectsTheSameStateAsCombat
ButtonText, VitalsPercentWrappersReadAndWriteTheSameFieldAsTheZeroToOnePair.
Full suite green: 645/645 MossTank tests, 183/183 (3 pre-existing skips)
App markup/menu/slider tests. AssertWithinParent (every nested group/
control fits its parent) and every other contract invariant (tab order,
binding shapes, tooltips) pass unchanged.
Deviation: S7.3-S7.6 (Monsters/Items/Consumables/Buffs/Route/Meta tab
bodies) are explicitly out of this sub-slice's scope per the plan and
are untouched beyond the geometry-only group-width widen (784->848,
content unchanged) needed so they aren't clipped by the wider window.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Slice 1c review D1 / slice 7 item 7: TryLoadNav tolerated (and silently
skipped) STATE:/IF:/DO: rule sections so a route load out of a full
Meta .af would still succeed against whichever NAV: block came first.
That is exactly the failure mode content-sanity elsewhere in this file
already guards against (a misplaced file in the wrong folder silently
"succeeding" against the wrong document) — a file with a STATE: block
is a Meta profile, possibly one with an embedded nav of its own, and
belongs in metas/, not navs/.
TryLoadNav now throws (refuses) the instant it sees a STATE: line,
with a message naming the metas/ folder, instead of walking past it
via the now-deleted SkipState helper.
SynthesizedGetOptFollowAndJumpFixtureRoundTrips previously used a
STATE:+embedded-NAV: fixture to assert TryLoadNav's OLD skip-and-load
behavior; that assertion is now inverted to assert refusal (shown to
fail against the prior MetafSerializer.cs: True vs expected False).
The "flw" nav-node parsing coverage that assertion also carried is
preserved in a new dedicated NAV:-only fixture,
FollowNavNodeParsesAsANavOnlyDocument.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A route (.af) file placed in metas/ used to silently "succeed" as an empty
MetaProfile: TryLoadMeta's STATE:/NAV: loop never adds a Rule for a NAV:-only
file, so the mistake was invisible. TryLoadMeta now tracks whether it saw
any STATE: block; a file with at least one NAV: block and zero STATE:
blocks throws (same FormatException path every other malformed-content
error already uses), naming the navs/ folder the file actually belongs in.
The opposite direction was already structurally caught by TryLoadNav's
existing "no NAV: block found" throw when a file has zero NAV: blocks (a
Meta profile with no embedded route, placed in navs/) — only the message
text is improved to name the metas/ folder. Both notices flow unchanged
through the existing MossTankProfileRecovery.Preserve/RecoveryNotice path
in MossTankMetaProfileStore.LoadCurrent/MossTankRouteProfileStore.LoadCurrent,
so no store-side code changes were needed for the wiring itself.
Mutation demonstrated: `git stash push -- src/AcDream.Plugins.MossTank/MetafSerializer.cs`
(reverting only the production fix, keeping every new test) reproduced 8
failures — the 5-fixture EveryNavOnlyAfFixtureIsRefusedByTryLoadMeta theory
(every real nav_*.af fixture parsed as a "successful" empty MetaProfile),
MetaOnlyContentIsRefusedByTryLoadNavWithMetasFolderNotice (message lacked
"metas/"), and the two store-level tests
MetaStoreRefusesToLoadANavOnlyFileWithNoticeNamingNavsFolder/
RouteStoreRefusesToLoadAMetaOnlyFileWithNoticeNamingMetasFolder — confirmed
by running the suite with the stash applied, then `git stash pop` to
restore the fix. All 640 tests (632 + 8 new) pass after.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Existing installs may still have real .af files sitting flat at the
VtankProfiles root from before step 1's two-folder cutover. Each store now
runs a one-time (per-instance, guarded) sweep on first LoadCurrent():
MossTankRouteProfileStore moves every flat nav_*.af/--nav_*.af file into
navs/ with the marker stripped (VtankProfileDirectory.StripLegacyNavMarker);
MossTankMetaProfileStore moves every OTHER flat .af file into metas/
unmarked. The partition predicate (IsLegacyFlatRouteFileName) is shared in
VtankProfileDirectory so both stores agree on which file belongs to which
sweep and neither touches the other's share. Both sweeps run before the
older legacy-JSON migrations so those see .af content already at its new
folder-qualified path. Collision rule: when the real destination already
exists, the flat file is left in place untouched (never overwritten) and
the collision is logged with both paths.
Mutation demonstrated: the 7 new tests (MetaStoreMigratesFlatAfFileIntoMetasFolder,
MetaStoreLeavesFlatFileInPlaceWhenMetasDestinationAlreadyExists,
RouteStoreMigratesFlatNavMarkedFileIntoNavsFolderWithMarkerStripped,
RouteStoreMigratesFlatHiddenAutoRouteFileWithMarkerStripped,
RouteStoreLeavesFlatFileInPlaceWhenNavsDestinationAlreadyExists) were run
against the store/VtankProfileDirectory code from the prior commit (no
migration sweep) and failed 4/7 (the other 3 pass vacuously since they only
assert the ABSENCE of cross-contamination, which trivially holds without
any sweep at all) before the MigrateFlatFilesTo*FolderIfNeeded methods were
added. All 632 tests (625 + 7 new) pass after.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Owner decision 2026-09-07: Meta and Nav profiles both use metaf .af and are
told apart by living in two dedicated VtankProfiles subfolders (metas/,
navs/) instead of the flat-directory nav_/--nav_ marker scheme from slice 1
Part A, which was only ever how the owner happened to name files in their
own metas repo. VtankProfileDirectory.ListMetaProfiles/ListNavigationProfiles
now enumerate metas/ and navs/ respectively via a new folder-scoped
EnumerateFolderFileNames helper; the NavMarker constant and the marker
overload of AutoCharacterFileName are deleted. MossTankMetaProfileStore and
MossTankRouteProfileStore build every real storage key with their folder
prefix (CurrentFileName, Select, Create, TryImportLegacy, the legacy-roster
sweep) and strip it back off for display (StripAf/Strip). The .cdf's Nav/Meta
lines (4-5) now carry the folder-relative key ("metas/Name.af",
"navs/Name.af"); AD-122 and the ACDREAM_VTANK_PROFILE_DIR launch-option row
are updated to describe this.
Mutation demonstrated: reverting VtankProfileDirectory.cs,
MossTankMetaProfileStore.cs, and MossTankRouteProfileStore.cs to HEAD~ (the
flat nav_-marker layout) while keeping the updated tests reproduces 8 test
failures (KeyNotFoundException / Assert.True(false) against the new
"metas/…"/"navs/…" keys the tests now expect, e.g.
MetaSaveAcceptsAnAfSuffixedNameWithoutDoublingIt,
MetaAndRouteProfilesWithTheSameNameDoNotCollide,
NavCommandsImportLegacyAndExportAf, MetaCommandsImportLegacyAndExportAf,
MetaRosterSweepConvertsEveryNamedLegacyProfileOnce,
MetaStoreLeavesLegacyJsonUntouchedWhenAfCounterpartExists,
MetaStoreRefusesToSaveADisabledRuleAndKeepsThePriorAfContent,
NavSaveAcceptsAnAfSuffixedNameWithoutDoublingIt) — confirmed by running the
suite immediately after the production-code edit, before the test-file
updates landed. All 625 tests pass after both sides of the change.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Campaign VT slice 1 Part A: the .usd document model + 137-setting
serializer with declared type tags and exact compare, metaf .af reader/
writer for metas and nav routes with real byte identity against the
owner's fixtures, .utl gate fixes, the VtankProfiles host storage
(ACDREAM_VTANK_PROFILE_DIR), and the cutover of all four profile stores
to real VTank files with one-time JSON migration. Two Opus lenses, three
fix rounds, two narrow re-reviews, final re-check: MERGE-READY.
Contract-doc ledger conflict resolved by keeping the campaign branch.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Five small fixes bundled per the round's cleanup item:
- VtankNavRouteSerializer.cs's doc comment cited a "WriteBinaryNavBlob"
method that no longer exists anywhere in the codebase (MetaEngine's
embedded-navigation contract moved to the typed MetaAction.EmbeddedRoute
NavigationSettings, saved/loaded through MetafSerializer.SaveNav/
TryLoadNav, back at round 2 step B) — corrected to name the real
mechanism.
- MossTankCommands.cs:274's comment referenced an "exports/nav/" mirror
directory that stopped existing when route profiles cut over to writing
their real .af file directly (round 2 steps 2-3) — corrected.
- docs/research/vtank-kb/07-meta-and-expressions.md section 5.2 row 6
described the pre-cutover "MossTankMetaProfileStore.WriteLegacyExport
convenience mirror" design; .af is now the SOLE authoritative Meta
store, so a disabled rule's save refusal now blocks the profile itself
— the row now says a disabled rule makes the profile file genuinely
unsaveable, not that a mirror goes stale.
- The two bare `catch (FormatException) { }` blocks that silently dropped
a corrupt monster-rule expression (one in SideCarDocument.Apply, reached
from a corrupt side-car; one in LegacyCombatProfileDocument.Apply,
reached during legacy-JSON migration) now log a warning via the host's
IPluginLogger, threaded through as an optional parameter from every call
site.
- VtankDatabase.Render()'s table-sort doc comment now states explicitly
that StringComparer.Ordinal matching .NET Framework's SortedDictionary
default order is confirmed only for the plain-ASCII table names VTank
ships (AntiExtraBuffSpells, MyMonsters, Settings, …), not as a general
claim for any string — comment only, no behavior change.
Added CorruptSideCarMonsterRuleIsLoggedNotSilentlySwallowed (FakeLogger
now captures Warn() calls via a new FakeHost.Logger property) pinning the
swallow-to-log fix.
Mutation: reverted MossTankProfileStore.cs to HEAD (keeping only the new
test) and ran it — failed with an empty Warnings collection, confirming
the silent-swallow bug before the fix.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Round 2 step 5 gave the settings Profiles tab a Delete action; Meta, Route,
and Loot never got the same verb. Added MossTankMetaProfileStore.Delete,
MossTankRouteProfileStore.Delete, and MossTankLootProfileStore.Delete
(same contract as Settings: remove the selected named profile's real file,
fall back to By char; refuse for By char itself, which has nothing to
delete — see each store's ClearCurrent for that case), wired through
MossTankPanel.DeleteMetaProfile/DeleteRouteProfile/DeleteLootProfile to
three new "Delete" buttons in mosstank.xml (Route tab row, the Meta tab's
button row, and the Loot rule editor's button row).
MossTankMarkupContractTests' interactive-control count moves 191 -> 194
for the three new buttons.
Mutation: reverted all four .cs files and mosstank.xml to HEAD (keeping
only the new/changed tests) — the test project failed to even COMPILE
(DeleteRouteProfile/DeleteMetaProfile/DeleteLootProfile do not exist on
MossTankPanel), confirming the six new behavioral tests
(DeleteRouteProfile/DeleteMetaProfile/DeleteLootProfile, each with a
successful-delete and a refuse-by-char case) and the markup-count update
all depend on this commit's code.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
MossTankLootProfileStore now reads/writes real .utl files through
VtankLootProfileSerializer.TryRead/Write in the VtankProfiles storage,
matching the Settings/.usd, Route/.af, and Meta/.af cutovers already
landed: directory-backed listing (new VtankProfileDirectory.ListLootProfiles,
*.utl, "--" hidden rule), .cdf participation (LootFileName, already present
in VtankCharacterBinding but never populated by this store), and a one-time
JSON migration (SweepLegacyRosterIfNeeded) that converts BOTH this
character's own "By char" document and every other named profile the
pre-cutover roster still lists — unlike Meta/Route (whose rosters were
already abandoned pre-round-2), loot's roster was still the LIVE mechanism
right up to this commit, so there is no separate "selected vs the rest"
split the way Settings/Meta/Route each have. WriteLegacyExport is deleted;
exports/ has no remaining loot writer.
Found and fixed a real representational-loss bug the cutover would
otherwise have introduced: VtankLootProfileSerializer.ExportRequirements
replaces an empty VtankRequirements list with a "safely disabled"
VTClassic placeholder requirement — correct when .utl was only ever a
courtesy export mirror alongside the authoritative JSON store, but
silently destructive once .utl becomes the SOLE store, since every
MossTank-authored rule's Expression text would be permanently discarded on
its first save/reload cycle. Added a MossTank-owned length-delimited
"MossTankRuleExpressions" block (using the serializer's own existing
UnknownBlocks round-trip contract — a real VTClassic reader just ignores it
as an unrecognized block, the same as any other extension) that restores
each affected rule's exact Expression text on load.
Filed AD-123: MossTank's own ByCharacter auto-.utl-file convention for loot
(kept for consistency with the other three stores) versus retail's real
loot picker, which seeds only [None] and has no per-character auto file at
all (docs/research/vtank-kb/01-settings-and-profiles.md section 3).
Updated four pre-existing tests for the new storage shape/behavior:
ProfileGiveControllerTests' FakeHost now wires VtankProfiles (the loot
store no longer uses Storage at all); LootProfilesAreIndependentNamedDocuments
and LootCommandsImportAndExportExactVtclassicUtlFiles now assert against
the real file/no-exports-mirror shape instead of the deleted hashed-JSON/
exports-mirror one. Added LootRosterSweepConvertsByCharacterAndEveryNamedLegacyProfileOnce
pinning the migration + its idempotence.
Mutation: reverted MossTankLootProfileStore.cs/VtankProfileDirectory.cs to
HEAD (keeping only the new/changed tests) and ran the three
cutover-dependent tests — all three failed (no real .utl file, no
ListLootProfiles, exports/ mirror still expected) — confirming they
exercise the bug/gap before the fix. LootingUsesVtankDefaultsAndPersistsTheOrderedRuleEditor
(pre-existing, unchanged) independently caught the representational-loss
bug during development before the MossTankRuleExpressions block was added.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The already-committed nav_briennecarlus.af/nav_empyrean.af/nav_lockandkeyjaw.af
fixtures (checkpoint/recall/portal2 node coverage, clean metaf headers)
were exercised by the model-equality proofs (parse, parse-write-parse,
binary-import-matches-af) but never by the REAL byte-identical writer-output
theory, which only ever covered nav_ab.af. Converted
WriterOutputMatchesMetafCanonicalEmissionNavOnly into a Theory over all
four nav-only fixtures.
Added two more coverage gaps this round's audit named:
- StringCellStripsEmbeddedNewlineAtConstructionNotJustOnWrite pins
VtankCell.String's gy.cs:84 newline strip (construction-time, not just
at WriteTo).
- RealAstFixturesRoundTripByteIdentical extends the existing schema-only
owner-{a,b,c}.ast coverage to a real parse -> Render() byte-identity
round trip, the same proof-4 rigor MetafSerializerTests already applies
to .af fixtures.
No writer bug surfaced in the extended nav theory or the .ast round trip —
both already passed; these are coverage-closing additions, not fixes.
Verified each new test has teeth via a temporary synthetic mutation rather
than a real revert (there is no production fix to revert here): the
string-cell test was checked against a temporarily-unstripped VtankCell.String
(failed, restored), and the .ast round-trip was checked against a
temporarily LF-only VtankWriter.AppendLine (all three .ast cases failed,
the four nav cases were unaffected since MetafSerializer's .af writer is a
separate code path from VtankUsdDocument's "y" grammar writer — restored).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
bk.a (refs/vtank/decompiled/bk.cs:6-35) appends a brand-new Settings row
([0]=name, [1]=value, every other column a void gy() cell) when a .usd
file's Settings table has no row for that name at all, and reports
"Done saving setting X to all profiles. (Changed N profiles)" with N =
every .usd file scanned. MossTankProfileStore.SetOptionInAll instead
silently skipped any file missing the row, and MossTankPanel's message
("Set option X in N profile(s) = value") matched neither retail text nor
count semantics.
Fixed SetOptionInAll to append (VtankCell { Tag = "0" } for every column,
then overwrite [nameColumn]/[valueColumn]) and to return the total scanned
file count instead of a per-row "changed" tally; changed the setinall
success message to retail's exact text.
Also added real type validation to /vt opt set|setinall: TryParseOptionValue
now takes the catalog's declared VtankSettingValueType and fails with
retail's exact "Option set: Invalid value specified. Proper type of X is
Y." text (refs/vtank/decompiled/uTank2/PluginCore.cs:5501,5508,5612) when
the typed value doesn't parse as that type — previously any non-empty
string silently succeeded regardless of the option's real type. The
Advanced Options editor keeps the original lax free-form parse (no
catalog type to check against there).
Mutation: reverted MossTankProfileStore.cs/MossTankCommands.cs to HEAD
(keeping only the new tests) and ran all four new/changed tests — all
four failed (old "Set option..." message text, no append, no type
validation) — confirming they exercise the bug before the fix.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Retail's real 2000 ms jump-charge ceiling (refs/vtank/decompiled/bi.cs:502-505,
bi.a) is enforced at the moment a jump STARTS charging, not by the storage
format — metaf's own NJump (metaf_monolithic.py:11708-11820) and VTank's
.nav both round-trip the authored value unclamped. The prior slice-1 port
misread this as a LOAD-time clamp: MetafSerializer's .af "jmp" parser and
the legacy-JSON route migration both clamped JumpChargeMilliseconds on
read, silently rewriting an authored 5000 ms waypoint down to 2000 ms even
when the route is never executed.
Removed both load-time clamps; NavigationController.TickJump now clamps
the EFFECTIVE charge duration (Math.Clamp(..., 0, 2000)) only at the one
place retail actually enforces it — the charge-hold comparison during
execution — leaving the stored/authored value untouched.
Renamed MetafSerializerTests.JumpNodeClampsChargeMillisecondsTo2000 to
JumpNodeLoadPreservesAuthoredChargeMillisecondsAboveRetailCeiling (now
asserts the 5000 ms value survives the .af load) and added a save+load
round-trip test and a Navigation execution test asserting the jump
releases at ~2000 ms of in-game charging despite a 5000 ms authored value.
Mutation: reverted MetafSerializer.cs/MossTankRouteProfileStore.cs/
Navigation.cs to HEAD (keeping only the new/changed tests) and ran the
three new/renamed tests — all three failed (load clamped to 2000,
execution never released before 5000 ms) — confirming they exercise the
bug before the fix.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Item 3: MossTankRouteProfileStore's per-character auto route file was named
nav_--Name_Server.af (NavPrefix + AutoCharacterFileName) — the "nav_" marker
came BEFORE the "--" hidden prefix, so the whole filename does not start
with "--" and defeats every StartsWith("--") hidden-file check in
VtankProfileDirectory, leaking another character's private route binding
into both the nav and meta pickers. Chose hidden-prefix-first naming
(--nav_Name_Server.af, matching VTank's own "--" convention with the nav_
kind marker second) via a new AutoCharacterFileName(name, server, ext,
marker) overload; applied only to the per-character auto file — named
routes keep their existing nav_Name.af (shared/visible) shape.
Item 4: ListNavigationProfiles and ListMetaProfiles shared the same flat
.af directory with no marker check at all, so each picker returned the
other's files too (a Meta profile appeared in the nav picker and vice
versa). ListNavigationProfiles now requires the nav_ marker;
ListMetaProfiles now excludes it.
Mutation: reverted VtankProfileDirectory.cs and MossTankRouteProfileStore.cs
to HEAD (keeping only the new/changed tests) — the build failed outright
(VtankProfileDirectory has no NavMarker/marker-overload for the new tests to
call), and the two pre-existing tests this round updated
(ListNavigationProfilesFiltersBothReservedPrefixes,
RouteStoreLeavesLegacyJsonUntouchedWhenAfCounterpartExists) independently
failed at runtime against their OLD un-marked/mis-ordered fixtures once
this round's marker/ordering requirement was pinned, confirming both are
exercising real, fixed behavior.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The pre-cutover roster documents (profiles/{index,meta/index,route/index}.json)
carried EVERY named profile a character had, but each store's cutover only
ever converted the ONE currently-selected profile (MigrateLegacyIfNeeded) —
every other named profile was silently orphaned: never converted to the new
.usd/.af format, never listed again. Worse, MossTankProfileStore actively
reused "profiles/index.json" for a MineOnly-only shape, so the very next
SetMineOnly() save would have permanently discarded the roster.
Added a one-time SweepLegacyRosterIfNeeded() to all three stores
(settings/meta/route), guarded by a per-instance flag so repeat LoadCurrent
calls are a no-op:
- Settings: entries owned by the currently-bound character convert to their
real per-(character,server) sub-profile .usd + side-car; entries owned by
a different character are written back to the roster for that character's
own future session. MineOnly now lives at a NEW dedicated key
(profiles/macro/preferences.json) so the old roster key is never again
flattened/overwritten.
- Meta/route: named profiles were never owner-scoped (one shared, globally-
hashed key per name), so every roster entry converts unconditionally.
Mutation: reverted all three store .cs files to HEAD (keeping only the new
tests) and ran SettingsRosterSweepConvertsEveryNamedLegacyProfileOnce,
MetaRosterSweepConvertsEveryNamedLegacyProfileOnce, and
RouteRosterSweepConvertsEveryNamedLegacyProfileOnce — all three failed
(missing converted .usd/.af files) — confirming the tests exercise the bug
before the fix.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>