Records the merge + three fix-round-A commits (grid scaling/Profiles
cleanup/popup split, the App.csproj plugin-copy-target bug, the
StartVisible visibility fix + duplicate-title cleanup), the real
DAT-font measurement that superseded the 07:55 lead's row-pitch theory,
and the carried CopyTo-naming deviation for the next reviewer.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Live-verified two real defects the previous commit's design didn't catch
without a running client:
1. Both new popups (mosstank-advanced.xml, mosstank-loot-editor.xml) were
registered with StartVisible=false, on the theory that the markup's own
visible="{AdvancedOptionsVisible}"/"{LootEditorVisible}" binding would be
the sole gate. It is NOT: RetailUiRuntime.MountPlugins wraps every
plugin window's markup availability in a PluginWindowVisibilityController
that ANDs it with a SEPARATE "requested visible" axis seeded from
StartVisible, flipped back to true only by OnShown() — the shelf's
un-minimize click. A ShowInSidePanel=false window has no shelf entry to
click, so StartVisible=false left "requested visible" permanently false
regardless of the markup binding, and both popups never rendered no
matter how many times the checkbox was toggled (confirmed live: checkbox
showed checked/green, no window appeared). Fix: StartVisible=true, which
keeps "requested visible" true from construction (OnHidden only ever
latches it back to false on a genuine minimize, explicitly no-opping
when the hide was caused by availability itself going false — exactly
what closing via "Back" does), leaving the markup binding as the sole
effective gate, same as the main panel's own always-true
ShowInSidePanel=true default.
2. Once visible, both popups showed garbled overlapping text at their top
edge ("AdvancedOptionsced Options — complete VTank settings table",
"Loot profileRules(first match wins)"): each file's own in-content
descriptive label duplicated the window's OS-level title bar
(MarkupDocument.Build auto-renders <panel title="..."> at a fixed (8,4)
— the exact corner the in-content label used to own back when these
were in-panel groups with no title bar of their own). Fix: dropped both
redundant labels; the window's own title ("Advanced Options" caption
duplicated as "MossTank Advanced Options"/"MossTank Loot Editor") is now
the only thing there.
Also repositioned both popups' default (x,y) from the placeholder
(440,60) — which overlapped the main panel — to (253,405)/(660,405),
below the main panel, side by side, both fitting within a 720-tall client.
Screenshots recaptured end to end against a live local ACE
(127.0.0.1:9000, testaccount/+Acdream) with the rebuilt Release exe from
this worktree, using an isolated ACDREAM_CONFIG_DIR/ACDREAM_DATA_DIR (no
stale persisted window layout from earlier probe runs — RetailWindowLayoutPersistence
keys plugin windows by name with authoredGeometryRevision=0, so a
previously-opened window's persisted x/y wins over any new authored
default forever; using a scratch config directory sidesteps that rather
than editing the real %APPDATA%\acdream\settings.json). tab-options.png,
tab-profiles.png, tab-vitals.png, and tab-monsters.png confirm the fix
round A grid-scaling and Profiles-cleanup commit's fixes hold on a live
client (no overlap, no gold buttons, no stacked New buttons, no dead
window space); popup-advanced.png and popup-loot-editor.png confirm both
popups now open as genuinely separate windows with clean single titles and
working move-up/move-down DAT icons (no blank button slots).
MossTank suite still 654/654; App markup/plugin/menu filter still
237/237 (no test changes in this commit — the earlier commit's
SecondaryPopupPanelsFitTheirOwnBoundsAndEveryBindingResolves already
covers both files' structure and bindings, and neither the visibility
seeding fix nor the label removal changes what those pins check).
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>
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>
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>
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>
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>
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>
Item H (slice-1 fix round), six sub-parts:
1. Restored VtankMetaProfileSerializerTests.LoadsKnownTypedCondActRecord
and SignedHighBitLandblockIdRoundTripsExactly (the latter adapted from
the deleted RoundTripPreservesEveryVtankConditionActionAndEmbeddedNav's
LandblockEquals/LandcellEquals coverage of unchecked((int)0x8B370000u)),
both deleted as collateral damage of an unrelated file move in commit
0d10399e0. Neither calls the deleted VtankMetaProfileSerializer.Save
writer (demoted to import-only in 3ff9461ef) — both are pure reader
assertions.
2. MetafSerializer.SaveMeta now refuses (throws InvalidOperationException
naming the count) to silently drop a disabled MetaRule: real VTank/
metaf has zero concept of "disabled" (confirmed: metaf_monolithic.py
has no "enabled"/"disabled" occurrences anywhere), so
MetaRule.Enabled is a MossTank-only extension with no metaf-compatible
marker. A new SaveMeta(profile, dropDisabledRules: true) overload lets
a caller accept the loss explicitly.
MossTankMetaProfileStore.WriteLegacyExport (the .af convenience mirror
beside MossTank's own fully-fidelity JSON storage) deliberately does
NOT opt in — it leaves that mirror stale and logs a warning via its
existing try/catch rather than losing the rule. Recorded as gap 6 in
docs/research/vtank-kb/07-meta-and-expressions.md section 5.
3. New VtankProfilesDefault (src/AcDream.App/Plugins/): the graphical
host's default VtankProfiles root (<DataDirectory>/vtank), extracted
out of Program.cs's inline Path.Combine call into its own pure,
injectable-root function specifically so the "Path.Combine only, never
a hard-coded Windows path" guarantee is a real, failable Linux-path
unit test (VtankProfilesDefaultTests.ResolveIsBuiltWithPathCombineOnly)
rather than something only checkable by reading the source — the
pattern item F's VtankProfileDirectory rewrite removed when
Resolve/PortableDefault moved out of the plugin.
4. New BuffedDoubleRequirementDoesNotApplyBonusWhenBaseKeyIsAbsent: the
KeyExistsDouble gate (ComputedItemInfo.cs:234) already existed in
BuffedDouble, but only the int side
(BuffedIntRequirementDoesNotApplyBonusWhenBaseKeyIsAbsent) had a
pinning test.
5/6. DoubleSpellBonuses gained an explicit Change field (KB doc 05
section 2.2: "additive unless the static table's Change==1, in which
case multiplicative", ComputedItemInfo.cs:244), replacing
BuffedDouble's prior `(int)bonus.Bonus == 1` magnitude-based proxy —
that proxy only worked because every multiplicative bonus in the
current 19 rows happens to fall in [1.0, 2.0) and every additive one
happens to be under 1.0; it would have silently mis-branched on a
future row like an additive 1.5 or a multiplicative 2.0+. Every
existing row's Change value was derived mechanically from its old
proxy result (no behavior change for the current table), and
BuffedDoubleRequirementAppliesAdditiveBonusWhenBaseKeyExists/
AppliesMultiplicativeBonusWhenChangeIsSet pin both branches through
the real named field.
Full MossTank suite: 574 -> 581. App.Tests
(Plugin|LaunchOptions|RuntimeOptions filter): 135 -> 137. Core.Tests
(Plugin filter): 50/50 (no change, no Core-side edits this item).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Item F (slice-1 fix round). IPluginHost.VtankProfileDirectory handed the
plugin a raw string path and told it to fall back to its own
System.IO-based portable default when null — a plugin reading and
resolving filesystem paths itself, which is exactly the seam the rest of
IPluginHost.Storage deliberately avoids (Core.Plugins.ScopedPluginHost
scopes/validates every key; the plugin never sees a path).
- IPluginHost: VtankProfileDirectory (string?) deleted; new VtankProfiles
(IPluginStorage, defaults to NoOpPluginStorage) added — a second,
UNSCOPED storage instance (unlike Storage, which Core scopes per
plugin manifest id) rooted at a host-composed VTank-compatible
directory.
- ScopedPluginHost.VtankProfiles forwards _inner.VtankProfiles directly
(no scoping — it names one shared external location, not per-plugin
data). New PluginSessionTests.ScopedHostForwardsVtankProfilesUnscoped
proves the forwarded instance is the exact same object (Assert.Same),
not a wrapper.
- AppPluginHost/Program.cs: new vtankProfiles constructor parameter,
composed as FilePluginStorage(runtimeOptions.VtankProfileDirectoryOverride
?? Path.Combine(applicationPaths.DataDirectory, "vtank")).
- RuntimeOptions.VtankProfileDirectoryOverride: new init-only property
parsed from ACDREAM_VTANK_PROFILE_DIR (row added to
docs/launch-options.md, side-effects column states the redirect is the
only effect and documents the NullIfEmpty whitespace-not-special-cased
quirk it shares with every other path-override flag). New
RuntimeOptionsTests.VtankProfileDirectoryOverrideIsNullUnlessSet.
- FilePluginStorage.List(prefix): empty prefix now means "the storage
root itself" instead of throwing (Resolve() rejects empty/whitespace
keys, which is correct for every OTHER caller but wrong for "list
everything" — VtankProfileDirectory needs exactly that).
- Headless: HeadlessPluginHost gained the same VtankProfiles
property/constructor param, threaded through HeadlessPluginSession.Create
-> HeadlessSessionHost -> HeadlessProcessHost, composed from the new
HeadlessPathSet.VtankProfilesDirectory (<DataDirectory>/vtank, no
ACDREAM_VTANK_PROFILE_DIR-equivalent override — Headless path overrides
are HeadlessPathOverrides/CLI flags, not env vars). A small
AcDream.Headless.Plugins.FilePluginStorage duplicates the App
implementation byte-for-byte (Headless does not reference AcDream.App
and no shared "platform plugins" library exists yet to host one copy;
documented as a reasonable future consolidation, not required here).
- VtankProfileDirectory.cs rewritten: Resolve/PortableDefault deleted
outright (no more System.IO, no plugin-owned portable-default fallback);
ListSettingsProfiles/ListNavigationProfiles/ListMetaProfiles now take
IPluginStorage and enumerate through EnumerateFileNames, which calls
storage.List(string.Empty) and skips any key containing '/' (VTank's
profile directory is flat; a nested key from some other IPluginStorage
implementation is not a profile file). VtankProfileDirectoryTests
rewritten against an in-memory IPluginStorage fake instead of real
temp directories; new NestedPathKeysAreNotTreatedAsProfileFiles pins
that skip. The prior Resolve/PortableDefault-specific tests (Linux-path
guarantee, host-override-vs-portable-default) are superseded by
RuntimeOptionsTests.VtankProfileDirectoryOverrideIsNullUnlessSet plus
the RuntimeOptions.FromEnvironment Path.Combine-only composition in
Program.cs.
- docs/architecture/acdream-architecture.md: one sentence in the
Storage/List(prefix) paragraph naming VtankProfiles as the second,
unscoped storage.
No production caller of VtankProfileDirectory's listing methods exists
yet (A2's foundation is not wired into MossTankProfileStore/
MossTankMetaProfileStore/MossTankRouteProfileStore's own selection —
per that slice's own ledger note), so this is a contract + plumbing
change with no MossTank runtime behavior change.
MossTank suite: 562/562. Core.Tests (Plugin filter): 50/50. App.Tests
(Plugin|LaunchOptions|RuntimeOptions filter): 135/135. Headless.Tests:
173/174 (the one failure, HeadlessCredentialResolverTests.
LinuxRejectsGroupOrOtherCredentialPermissions, is a pre-existing
Linux-only lane gate that throws PlatformNotSupportedException on this
Windows host — unrelated to this change).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Item A (slice-1 fix round). VTank/metaf's Portal2/UseNPC nav nodes carry
TWO coordinate triples (metaf_monolithic.py:356-357,11482,11618 —
"FORMAT: ptl/tlk myx myy myz tgtx tgty tgtz tgtObjectClass tgtName"): the
outer header ("myxyz", retail's own dead-weight last-save player position
per docs/research/vtank-kb/06-navigation-and-nav.md section 1.2) and the
embedded d-record ("tgtxyz", the real target coordinate used to match a
live world object by name+class+proximity). The prior port's
RouteWaypoint had a single Position field, so both the .af reader
(MetafSerializer.ReadNavNode) and the binary .nav reader
(VtankNavRouteSerializer.ReadWaypoint, case 6/7) overwrote "myxyz" with
"tgtxyz" on load, and the .af writer echoed the same Position value for
BOTH triples on save — a real .af round trip of the same waypoint was
lossy, which is why aphus/augments/lockandkey/neftet were excluded from
the byte-identity proof.
- RouteWaypoint: new ReferencePosition field (Position stays "myxyz",
ReferencePosition is "tgtxyz"); included in Clone().
- MetafSerializer.ReadNavNode/RenderNavNode: ptl/tlk read/write both
triples distinctly. WriteBinaryNavBlob's embedded-route writer (the
MossTank runtime blob EmbedNav actions carry) fixed the same way — it
was echoing Position for the reference triple too.
- VtankNavRouteSerializer.ReadWaypoint case 6/7: keep the header triple in
Position, read the trailing triple into ReferencePosition instead of
overwriting Position.
- Navigation.TickUse: TryFindObject now searches near ReferencePosition
(the real target coordinate) instead of Position, preserving the
correct runtime search behavior now that Position no longer aliases it.
- MossTankPanel.AddSelectedObjectWaypoint: new Portal2/UseNPC waypoints
now set Position from the live snapshot (matching retail's own
"wherever the character stood") and ReferencePosition from the selected
object's live position (the real search anchor) — previously both were
set from the object's position.
- MossTankRouteProfileStore's WaypointDocument DTO carries the reference
triple too, so MossTank's own JSON-persisted routes round-trip it.
- MetafSerializerTests: un-excluded aphus/augments/lockandkey/neftet.af
from the byte-identity proof (they all embed a ptl/tlk node and now
round-trip correctly) and added example_sort_meta.af, which also
passes. bore_quest.af was NOT added despite the slice-1 contract's
ask: it is hand-edited the same way as the already-excluded
bore_enhanced.af (space instead of tab between "IF:"/"DO:" and the
following keyword, confirmed at bore_quest.af line 9 — metaf's own
Rule.ExportToMetAF always joins with a tab, metaf_monolithic.py:12371),
so it can never byte-match; documented alongside bore_enhanced's
existing exclusion note instead. New PtlNodeKeepsBothCoordinateTriplesDistinct
test pins the two-triple split directly (failed before this change:
Position held the second triple with nowhere to read the first triple
back from). VtankNavRouteSerializerTests updated to assert the split
instead of the old collapsed value.
- jmp direction: metaf's NJump class has no strafe-direction field at all
(metaf_monolithic.py:11708-11821, confirmed reading ImportFromMetAF/
ExportToMetAF end to end) — the .af format cannot represent
RouteWaypoint.JumpDirection, full stop. ReadNavNode no longer assigns
JumpDirection = Forward explicitly (the model's own default), and the
loss is now recorded as gap 9 in docs/research/vtank-kb/
06-navigation-and-nav.md section 6.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
docs/plugin-ui-markup.md's Columns section is rewritten to match every
production change from items 1-9:
- New "Width semantics" section: width="*", the last-column-always-auto
rule (and how it combines with an earlier explicit "*"), the
non-last-column Build-time throw, and the runtime overflow clamp.
- The <column> attribute grammar table gains column onclick
(type="text", optional, Action<int>) and points width at the new
section instead of the retired "last column ignores its own width"
one-liner.
- New "Short columns past their own row count" section: text/icon draw
nothing, check draws unchecked.
- Check-column glyph section updated for the shared UiCheckLamp
primitive and its horizontal centering.
- New "The PITCH convention for transcribing a VTank column table"
section: VVS's WPaddingOuter=3px/WPadding=7px/16px scrollbar reserve/
forced-13px-check-columns have no direct acdream equivalent, so
transcribing a real mainView.xml column table means declaring each
column's PITCH (fixedwidth+7, or 13+7 for a check column regardless
of its real fixedwidth) and reserving 16px on the last column.
- The worked example now uses rowheight="18" (VVS's own Padding*2+
ControlHeight pitch, already the widget's default) and PITCH-computed
widths (20 for a check column, 127 for a 120px text column) instead
of arbitrary numbers, with a text onclick and a width="*" icon
column added to demonstrate both new attributes in place.
- The bindings truth table gains column onclick (type="text") and both
column width throw/silent rows (non-last vs. the list's last column).
- Testing conventions paragraph updated to mention width semantics, the
row-bound click guard, and the two end-to-end tests.
Verification: dotnet build AcDream.slnx -c Release (green), dotnet test
tests/AcDream.App.Tests -c Release --filter
"FullyQualifiedName~Markup|FullyQualifiedName~UiMarkupList|FullyQualifiedName~PluginSidePanel"
(130/130), dotnet test tests/AcDream.Plugins.MossTank.Tests -c Release
--filter "FullyQualifiedName~Markup" (9/9) — closes the fix round.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>