The previous commit split mosstank-advanced.xml/mosstank-loot-editor.xml
out of mosstank.xml into their own files (MossTankPlugin.cs now registers
three AddPanel calls). MossTankPlugin resolves each markup path relative to
its own assembly directory (plugins/AcDream.Plugins.MossTank/), but
AcDream.App.csproj's CopyMossTankPluginToBuildOutput/
CopyMossTankPluginToPublishOutput targets only ever copied mosstank.xml
into that folder — a hardcoded file list, not a glob, so the new files
silently landed in the App's own bin root instead of the plugin directory
(confirmed by inspecting the build output before running any screenshots:
plugins/AcDream.Plugins.MossTank/ had only mosstank.xml). Left as-is, the
two new panels would have thrown FileNotFoundException on load in a live
client — this would have shipped a broken build to the screenshot/visual
gate undetected by any existing test, since no test builds AcDream.App and
inspects its output layout. Both Copy targets' SourceFiles list now
include all three markup files.
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>
Brings cfa703065's owner-driven fix (plugin <menu> renders VTank's plain
flat combo by default; style="retail" opts back into the gold pushbutton
art) into the S7.1-S7.3 window/tabs work so the fix round below can build
on both. Resolved conflicts: the ledger keeps both worktrees' entries in
chronological order (07:55/08:05 owner-read entries appended after this
worktree's own S7.1/S7.2/S7.3 entries); plugin-ui-markup.md's <menu> row
lists both new attributes (scroll, style); MarkupDocumentTests.cs keeps
both new test blocks (slider min/max + menu scroll from this worktree,
menu style from latest-main-sync).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Records commit 57ced0aff (the VTank 23-column Monsters grid) in the
slice-7 ledger, including the Weapon/Offhand roster-cycle deviation and
the full-suite pass counts.
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>
UiMenu.RetailButtonArt (default true; every retail user unchanged);
MarkupDocument sets plain for plugin markup, style="retail" opts back.
Lead-reviewed diff; 10 new tests; goldens for the retail path.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Adds style to the <menu> attribute row and one sentence explaining why
plain is now the default (owner report: retail's gold pushbutton art
read as an out-of-place button next to a plugin's own dark list boxes)
and what style="retail" opts back into.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Wires the new UiMenu.RetailButtonArt switch (previous commit) into
plugin markup: <menu style="plain"> (also the default when the
attribute is absent) builds RetailButtonArt=false so a plugin's
dropdown gets the flat VTank-matching box; style="retail" opts a
panel back into the gold pushbutton face. Any other value throws
FormatException at Build naming the element, matching the existing
<icon iconkind> validation convention (ValidateIconKind).
Mutation check: temporarily stubbed ValidateMenuStyle to always return
true (as if the switch didn't exist) — 3 of the 4 new
MarkupDocumentTests.Menu_* tests failed exactly as expected
(Menu_NoStyleAttribute_DefaultsToPlain_RetailButtonArtFalse,
Menu_StylePlain_Explicit_RetailButtonArtFalse,
Menu_UnknownStyle_ThrowsFormatException_NamingTheElement); the
style="retail" test passed trivially either way, as expected for that
case. Restored before committing.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Owner live-client report 2026-09-07: "Those BIG gold/yellow buttons HAS
to go. That is not how vtank looks." VTank/Decal's HudCombo is a flat
dark box (background/border matching its own HudList) with a
left-aligned value and a small down-arrow — retail's gold pushbutton
art (the 3-slice LED-arrow face UiMenu.DrawButtonFace draws) is a
different widget family entirely.
Adds UiMenu.RetailButtonArt (default true, so every existing
non-markup UiMenu caller — chat's channel menu, vendor's category
dropdown, Config's option menus, the retail confirmation dialog, and
DatWidgetFactory's generic Type-6 element — keeps its byte-identical
retail face) plus DrawPlainClosedState/DrawPlainTriangle, which draw
the flat box entirely with UiRenderContext.DrawFill/DrawRectOutline (no
sprite or DAT quad at all) using colors mirroring UiMarkupList's own
chrome (background 0,0,0,0.92; border 0.46,0.37,0.16,1; text
0.91,0.87,0.76,1). Open/pressed only tints the border
(0.70,0.58,0.24,1) — never a sprite swap.
Mutation check: temporarily disabled the new `if (!RetailButtonArt)`
branch in OnDraw (reverting it to the pre-fix unconditional retail
path) — 3 of the 6 new UiMenuPlainStyleTests failed exactly as
expected (Plain_ClosedState_DrawsNoTexturedFaceQuad,
Plain_ClosedState_DrawsFillOutlineTextAndTriangle,
Plain_ClosedState_TriangleSitsRightAligned_TextSitsAtListPadding); the
3 retail-path/default-value tests kept passing since they don't
exercise the removed branch. Restored before committing.
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>
Campaign VT slice 7 needs to transcribe VTank's own Vitals sliders
(minimum=0/maximum=100) and its long Profiles/Route named-item combos
control-for-control (docs/research/vtank-kb/08-ui-views.md §3's two
markup gaps). acdream's <slider> was hardwired to a 0.0-1.0 scalar and
<menu> always wrapped overflow into extra columns instead of VVS's
single scrolling column.
<slider min max>: optional literal attributes declaring the range the
bound value/onchange speak in (e.g. min="0" max="100" for a percent),
while UiScrollbar itself keeps its existing 0.0-1.0 internal math
untouched — MarkupDocument rescales at the binding boundary. Omitting
both (every pre-existing <slider>) keeps the exact historical identity
range.
<menu scroll="true">: wires UiMenu.Scrollable plus the same
track/thumb/up/down chrome sprites ConfigOptionsPageController and
VendorUiController already apply to their own Scrollable menus,
previously only reachable from C#. Omitting scroll keeps the
historical column-wrapping default.
Both are additive — no existing <slider>/<menu> markup changes
behavior. New pins in MarkupDocumentTests.cs shown to fail against the
prior MarkupDocument.cs (5 failures: Build_SliderWithNoMinMax_*,
Build_SliderWithMinMax_*, Slider_MinMax_Draws*,
Build_MenuWithScrollAttribute_*, Menu_Scroll_Draws*) before this
change, all green after.
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>
CombatSettings.Rules lives only in MossTank's JSON side-car;
VtankSettingsProfileSerializer preserves the real .usd MyMonsters table
byte-for-byte but never parses it into MonsterRules or regenerates it from
them (VtankSettingsProfileSerializer.cs:24-30). MonsterRules.cs/
MonsterExpression.cs (the rule-grammar evaluator itself) is a faithful,
well-cited port with no material gap — only the real-file round trip is
missing.
Filed TS-86 (temporary stopgap; slice 3 ports the table) and added it as
gap item 6 in docs/research/vtank-kb/03-combat.md section 8 (previously
absent — the existing five gaps are about rule-grammar/priority fidelity,
not about whether the real table round-trips at all). Corrected the TS
section header's stale active-row count (was undercounting by one before
this row) to the actual count. Documentation-only; the parse is
deliberately NOT implemented this round, per the task's explicit scope.
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>
Filed AD-122: VtankProfileDirectory.WriteCharacterBinding writes a real VTank
.cdf's Nav/Meta lines (4-5) as MossTank's own .af (metaf text) names, not
VTank's native binary .nav/.met. When ACDREAM_VTANK_PROFILE_DIR points at a
real installed VirindiTank profile folder for direct interop, that .cdf
names files a real VTank instance cannot load — Settings (.usd) and Loot
(.utl) stay real/binary-compatible; only Nav/Meta went .af-only for slice 1.
Added a warning sentence to the ACDREAM_VTANK_PROFILE_DIR row in
docs/launch-options.md pointing at the register row. Documentation-only;
no code or test changes.
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>
MossTankProfileStore's SideCarDocument.InventoryEnableLooting duplicated
the real "EnableLooting" .usd Settings row, and the side-car's own Apply()
ran AFTER the .usd was applied in LoadCurrent — so a drop-in profile with
looting on loaded with it silently off, clobbered by whatever the side-car
happened to hold (default false for a profile with no side-car at all).
Deleted the duplicate field; settings.Inventory.Loot.Enabled is now owned
exclusively by the .usd row. Added a reflection test proving no SideCarDocument
field (typed or with its Combat/Buff/Vitals/Inventory group prefix stripped)
collides with a VtankOptionCatalog name, and a drop-in regression test:
a real .usd fixture with EnableLooting=True and NO side-car now loads with
panel.LootEnabled == true.
Mutation: reverted MossTankProfileStore.cs to HEAD (keeping only the new
tests) and ran both new tests — SideCarDocumentHasNoFieldNamedForARealVtankSetting
failed with `["InventoryEnableLooting"]`, DropInUsdWithLootingEnabledAndNoSideCarLoadsLootingEnabled
failed with Expected True/Actual False — confirming both tests exercise the bug
before the fix.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Campaign VT slice 1 Part A round 2 step 5: the Profiles tab's action set
(Create/Select/Save/Delete/name field/mine-only) was missing Delete
entirely — every profile family only ever had Create/Copy/Clear. Adds
MossTankProfileStore.Delete (removes the selected named profile's real
.usd file and its side-car, falls back to "By char"; refuses for "By
char" itself, which has nothing to delete — see ClearCurrent for that
case) and wires it through MossTankPanel.DeleteProfile to a new "Delete"
button in mosstank.xml, next to "Clear profile!".
Create/Select/the name field/the mine-only toggle already bind to the
directory-backed store from steps 1-4; this closes the one missing verb.
Mutation shown to fail: Delete short-circuited to always refuse made
DeleteProfileRemovesTheRealFileAndFallsBackToByCharacter fail (selection
stayed on the named file instead of falling back); restored, it passes
along with the By-char refusal companion test. The markup contract's
interactive-control count was updated for the new button (190 -> 191).
595 MossTank tests passing (was 593).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Adds the same migrate-and-delete / counterpart-already-exists coverage
for MossTankRouteProfileStore that Settings and Meta already had,
completing step 4's "pin with a test per store" requirement. Also
corrects an assumption in the route store's own waypoint model: metaf's
"pnt" node format is bare x/y/z (three doubles, MetafSerializer.cs:217)
with no cell id at all for any waypoint type, not just "jmp" — so the
migration test checks EastWest/NorthSouth instead of CellId.
Mutation shown to fail: MossTankRouteProfileStore.MigrateLegacyIfNeeded
short-circuited to a no-op made RouteStoreMigratesLegacyJsonProfileToAfAndDeletesTheJsonKey
fail (LoadCurrent returned false, nothing to load); restored, it passes
along with the companion counterpart-exists test.
593 MossTank tests passing (was 591).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
MossTankMetaProfileStore and MossTankRouteProfileStore now store their
profiles as real .af files through IPluginHost.VtankProfiles, named and
listed by VtankProfileDirectory's rules, instead of a hashed JSON
document plus a stale "exports/" mirror (WriteLegacyExport deleted from
both). Named Meta profiles are plain shared files (matching VTank's own
ac() picker, which has no per-character sub-profile carve-out); named
route profiles carry a "nav_" prefix (metaf's own observed convention
for a stand-alone nav .af, confirmed against the committed nav_*.af
fixtures) so a route and a Meta profile sharing a user-typed name never
collide in the shared VtankProfiles directory. Both stores' Selected/
AvailableNames strip the file extension (and the route store's "nav_"
prefix) for display, matching the bare names users type at /vt meta|nav.
The route store now persists only the fields metaf's NAV: grammar
actually carries (Mode, Waypoints, FollowTarget) — Enabled/Priority/
MinimumDistanceMeters/FollowAroundCorners/OpenDoors/Door* are real VTank
Settings-table rows already owned end-to-end by MossTankProfileStore's
.usd profile (round 2 step 1), matching real VTank's own split between
global nav prefs and the per-route file; ClearCurrent and LoadCurrent
were narrowed to match. LoadCurrent gained an ISpellCatalog parameter
(TryLoadNav's own requirement); both call sites now pass
host.Automation.Spells.
Both stores gained the same one-time legacy-JSON migration as the
Settings store: first load converts a not-yet-migrated JSON profile to
.af and deletes the JSON key, leaving an existing .af counterpart (and
its stale JSON) untouched. The Meta store's SaveCurrent/Create now
refuse (return false, set SaveNotice, leave the prior .af content in
place) rather than silently drop a disabled rule that metaf/.af cannot
represent — MossTankPanel's four rule-editing call sites were updated to
prefer that refusal notice over their own generic success message.
Mutations shown to fail: MossTankMetaProfileStore.MigrateLegacyIfNeeded
stubbed to a no-op made MetaStoreMigratesLegacyJsonProfileToAfAndDeletesTheJsonKey
fail (legacy key was not deleted); SaveCurrent's SaveMeta call switched to
dropDisabledRules:true made MetaStoreRefusesToSaveADisabledRuleAndKeepsThePriorAfContent
fail (the disabled rule was silently written). Both restored and green.
NavigationTests.RouteProfilesRoundTripEveryWaypointField (asserting the
pre-cutover JSON-carries-everything behavior) was split into
RouteProfilesRoundTripWaypointFieldsButLeaveSettingsOwnedFieldsAlone and
FollowModeRouteRoundTripsTheFollowTargetThroughAf, and now also
documents two pre-existing, already-recorded metaf representational
gaps discovered by routing this path through .af for the first time:
JumpDirection has no .af representation at all (MetafSerializer.cs:924)
and a "jmp" node carries no cell id (six bare fields, no hex component).
MossTankPanelTests' nav/meta export-path tests were updated from the
retired "exports/meta|nav/" mirror to the real file locations.
591 MossTank tests passing (was 588 after step 1's commit, +3 new tests
net of the two renamed/retired ones).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
MossTankProfileStore now stores the VTank-catalog Settings (Combat/Buffs/
Vitals/Inventory/Navigation) as a real .usd database read/written through
VtankSettingsProfileSerializer, listed via VtankProfileDirectory's real
naming rules (auto "By char" file, "--Name_Server_" sub-profiles, .cdf
per-character binding) instead of a hashed JSON document. Only state with
no VTank setting name (item/consumable lists, the monster-rule table, and
a handful of MossTank-only knobs) still lives in a small JSON side-car
keyed by the real .usd file name.
A one-time migration converts a not-yet-migrated legacy JSON profile into
its .usd + side-car pair on first load and deletes the JSON key, leaving
an existing .usd counterpart (and its stale JSON) untouched. `opt setinall`
now patches every known .usd file's Settings row directly, keeping each
file's side-car DynamicSettings mirror in sync so MossTankPanel's existing
option-override replay doesn't clobber the freshly patched value on the
next load.
VtankProfileDirectory gained WriteCharacterBinding (the .cdf writer
counterpart to the existing reader). VtankSettingsProfileSerializer.Apply
is now internal so the store can seed live settings directly from a
parsed database without a text round-trip.
Mutation shown to fail: MigrateLegacyIfNeeded stubbed to a no-op made
FirstLoadMigratesLegacyJsonMacroProfileToUsdAndDeletesTheJsonKey fail
(legacy key was not deleted); restored, it passes along with the
ExistingUsdCounterpartLeavesLegacyJsonUntouchedAndUnread companion test.
585 -> 587 MossTank tests passing (net +2 after adjusting three existing
tests to the new, more retail-faithful defaults/identity: a brand-new
profile now seeds from VTank's own shipped defaultsettings.usd rather
than a MossTank-guessed CLR default, and Selected/MacroProfileNames now
surface the real VTank file name instead of a bare invented name).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>