Commit graph

2707 commits

Author SHA1 Message Date
Erik
e9108277c4 feat: retail scrollbar chrome for markup <menu> overflow and <list> overflow
Owner live-client report 2026-09-07: "For scrollable dropdown or the meta
window we use the same assets as we do in for example chat or inventory
window."

<menu> markup wiring (MarkupDocument.cs): a plugin <menu> is now always
Scrollable (single-column, VTank HudCombo shape) instead of wrapping
overflow into more grid columns, with PopupScrollbarHideWhenDisabled=true
so the bar is entirely absent while the item count fits the "rows"
window. RetailScrollbarChrome.ApplyToMenuPopup wires the same chrome ids
the previous commit taught DrawScrollablePopupPlain to draw, for both
style="plain" and style="retail" markup menus.

<list> markup (UiMarkupList.cs / MarkupDocument.cs): a plugin <list>
(single-column or <column> multi-column) that overflows its own row
viewport now draws the retail scrollbar chrome at its right edge (VVS's
own placement, 16px wide) instead of being wheel-scroll-only with no
visible bar. The reserved 16px column only exists while rows actually
overflow, in both column-layout modes (ComputeColumnLayout receives the
already-shrunk width so the last/auto column absorbs the remainder
correctly); the bar is fully interactive (up/down arrows, track paging,
thumb drag) via a small UiScrollable projection kept in sync with the
list's own _topRow, which stays the single source of truth. Wheel
scrolling and a no-resolver hand-built list (draws nothing, no crash) are
unchanged.

Mutation shown to fail first: new
UiMarkupListScrollbarTests/MarkupDocumentTests cases were written against
pre-change UiMarkupList/MarkupDocument and failed (no scrollbar sprites
ever emitted since UiMarkupList had no SpriteResolve property at all, and
<menu> markup never set Scrollable) before the implementation landed;
after: SingleColumn_Overflowing_DrawsRetailScrollbarChromeAtRightEdge and
Columns_Overflowing_ReservesSixteenPixels_LastColumnShrinksAccordingly
pin sprite ids + exact reserved-width geometry,
*_ContentFits_DrawsNo(Scrollbar|ReservationLastColumnKeepsFullRemainder)
pin the no-overflow/no-bar case, *_UpArrowClick_ScrollsUpByOneRow and
ThumbDrag_MovesTopRowAndIsReadableByASubsequentClick pin interactivity via
a following row click resolving to the moved position (mirroring
MarkupListColumnsTests' own wheel-scroll pin), and the four new
MarkupDocumentTests menu cases pin Scrollable/PopupScrollbarHideWhenDisabled/
the six chrome-id properties plus an end-to-end open-popup draw for both
the overflowing (draws chrome) and non-overflowing (draws none) cases.
Every pre-existing MarkupListColumnsTests/MarkupDocumentTests case stays
green unchanged (none of their fixtures overflow their own viewport).

docs/plugin-ui-markup.md updated: the <menu> style paragraph and a new
<list> "Scrollbar" section describe the new chrome + auto-reservation, and
the PITCH-transcription guidance is corrected to say the 16px scrollbar
column is now automatic (no more manual fold-in/double-reservation advice).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 15:43:00 +02:00
Erik
b36b036475 docs(vt): slice 7 ledger — round B closed at 16/18, arch re-check verdict, round C dispatched, owner testing live
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 14:48:37 +02:00
Erik
e6e47dd98a docs(vt): post-campaign idea — readable export/diff for settings and loot, not a second storage format
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 11:51:14 +02:00
Erik
c62dc495a5 docs(vt): slice 7 ledger — both review verdicts, lead decisions, fix round B dispatched
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 11:13:00 +02:00
Erik
805d10e2b8 docs: file #490 (plugin-panel host StartVisible/ShowInSidePanel + layout revision) and #491 (buff lists not consumed by BuffPlan.Build)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 11:12:40 +02:00
Erik
327e0e93df docs(vt): slice 7 ledger — all nine tabs landed; lead's read of Route/Meta for fix round B; reviews dispatched
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 10:55:58 +02:00
Erik
fd5dfa49e0 docs(vt): document that plain <menu> style covers the open popup too
Follow-up to the UiMenu popup fix: the existing "menu style" paragraph in
plugin-ui-markup.md only described the closed-state button face swap from
the earlier S7 fix. Extend it to say the plain style now covers the whole
menu (closed AND open) — flat popup chrome matching <list>, a lighter hover
fill, no checkmark, and a plain scrollbar past the row cap — so a plugin
author reading the doc doesn't assume style="plain" only affects the
closed face.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 10:19:54 +02:00
Erik
fab134ae4f docs(vt): slice 7 ledger — owner: the open dropdown is still retail art; plain popup dispatched
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 09:55:44 +02:00
Erik
422ca75172 docs(vt): slice 7 ledger — fix round A landed, remaining tabs dispatched
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 09:55:03 +02:00
Erik
80dc762375 docs: headless console CLOSED — connected proof passed; #489 gains the diagnostics-interleave polish item
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 08:30:35 +02:00
Erik
39ed2b5f95 docs: file #489 — headless SpewBox growth without a console, plus console polish items
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 08:27:48 +02:00
Erik
8cb284d6f7 merge: headless console — interactive chat/command CLI for the bot host (review-closed)
Owner direction 2026-09-07. Reader thread → tick-drained queue, the same
ChatCommandRouter.Submit the chat box uses, event-stream renderer,
SpewBox pump, --console / ACDREAM_HEADLESS_CONSOLE (=0 disables).
Opus review APPROVE-WITH-FIXES, 12-item fix round, narrow re-check
MERGE-READY.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 08:25:22 +02:00
Erik
66160741f0 merge: plugin text and unknown-command refusals go to the chat window (owner-directed, AD-124)
Owner 2026-09-07: plugin output and 'Unknown command' land in the chat
scroll, not the SpewBox. Plugin text = Decal/VTank-faithful; the
unknown-command re-route is recorded as AD-124 (retail types it 0x1A).
Bad-argument refusals of real retail commands stay SpewBox-only (AP-183).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 08:24:02 +02:00
Erik
6b7b4bb213 fix #363/#367: route unknown-command refusals to chat, not SpewBox
Owner direction 2026-09-07 (verbatim): "Unknown commands like /vt or
stuff from plugins shall now go to the SpewBox. They should go to the
chatbox." Retail itself types ChatCommandRouter's "Unknown command"
refusals as ClientLocal (0x1A) -- the bit every ChatInterface window's
default filter excludes, so they only ever reached the transient
SpewBox overlay and left no transcript record.

Three call sites in ChatCommandRouter.Submit/EmitVerbHelp now call
IChatCommandFeedback.ShowSystemMessage (chat scroll, retail
Default/0x00) instead of ShowInterfaceText (SpewBox): the degenerate-
prefix "Unknown command: {verb}." guard, EmitVerbHelp's confirmed-
null-help branch, and EmitVerbHelp's unresolved-verb fallback. Every
OTHER 0x1A refusal in this file (AP-183 bad-argument refusals of REAL
retail commands -- lifestone, marketplace, channel list/on/off,
allegiance, house, the generic HandleFailureEvent(0x26) fallback,
DoStupidChannelHack, DoReply) is unchanged and still SpewBox-only --
the owner named only unknown commands and plugin text.

This is a deliberate deviation from retail's own 0x1A typing, recorded
as register row AD-124 (also covers the sibling plugin-text change in
a follow-up commit). docs/ISSUES.md #363/#367 get a one-line note
under each pointing at the re-route; their CLOSED status is untouched.

Mutation check: temporarily reverted all three ShowSystemMessage call
sites back to ShowInterfaceText and confirmed the 3 new/changed pinned
tests fail (Assert.Single() on an empty chat log) while the AP-183
boundary test (real command, bad args, still SpewBox) continues to
pass -- see ChatCommandRouterFeedbackRoutingTests.cs and the updated
ChatCommandRouterTests.cs/RetailCommandHelpTableTests.cs assertions.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 08:22:06 +02:00
Erik
47a1fe555c docs(vt): slice 7 ledger — Monsters landed; fix round A dispatched
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 08:20:10 +02:00
Erik
7381112394 docs(headless-console): record the 2026-09-07 Opus fix-round ledger entry
N4 + summary: /status and /quit are console-intercepted verbs (they
never reach ChatCommandRouter), unlike @status which is a real server
command and still passes through untouched. Records the full S1-S7/
N1-N5 fix-round outcome, final Headless (207/1/208) and App LaunchOptions
(4/4) suite counts, and the one-commit-per-item/mutation-shown-to-fail
discipline used throughout.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 08:15:35 +02:00
Erik
d8846d7a2d docs(vt): document <menu style> in plugin-ui-markup.md
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>
2026-09-07 08:15:32 +02:00
Erik
1637bae31d docs(vt): slice 7 ledger — owner's live look: overlaps and the gold dropdowns must go
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 07:56:49 +02:00
Erik
d1fe52368d docs(vt): slice 7 — live screenshots of S7.1/S7.2 (cropped) and the lead's read for the fix round
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 07:54:27 +02:00
Erik
55e454aedc fix(headless): S1 ACDREAM_HEADLESS_CONSOLE=0 disables even on a real terminal
HeadlessConsoleOptions.Resolve tested the environment variable against
the literal "1", so ACDREAM_HEADLESS_CONSOLE=0 silently fell through to
the terminal-shaped default (on when stdin is a real console) instead of
acting as an A/B off-switch. Now: once the variable is SET AT ALL, any
value other than "0" enables and "0" disables -- the same idiom
ACDREAM_RETAIL_CLOSE_DEGRADES / ACDREAM_RETAIL_UI already use. An unset
variable still falls through to the terminal default.

Registered the flag as the sixth entry in
LaunchOptionsDocumentationTests.DefaultOnBehaviorFlags and updated the
Conventions section of docs/launch-options.md plus the flag's own row
(side-effects column corrected to describe the real precedence).

ResolvePrefersFlagThenEnvironmentThenTerminalDefault's env="0"/terminal
=true case was shown to fail against the prior `== "1"` implementation
(expected false, old code returned true) before the fix landed; the
env="yes" case also failed on the same mutation (old code required the
literal "1", so "yes" fell through to terminal=false instead of enabling).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 07:51:41 +02:00
Erik
9ec145173e docs(vt): slice 7 ledger — ACE was up; the TCP probe was wrong (ACE is UDP)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 07:37:21 +02:00
Erik
8681a25e6c docs(vt): slice 7 ledger — S7.1/S7.2 landed, popup-height note, screenshots owed (ACE down)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 07:28:59 +02:00
Erik
97b1c9f6ce docs(headless-console): record implementation ledger and connected proof
Appends the plan's ledger with what shipped, the deliberate deviation
from the plan's illustrative bracket-prose example (Headless cannot
reference AcDream.UI.Abstractions, so the console's chat rendering is
its own terminal-shaped format using the same channel-name strings,
not a byte-for-byte port of ChatVM.FormatEntry), the chosen console
default and why, and the exact connected proof recipe for the owner to
run against a live ACE with +Acdream and MossTank (not run here per
the contract).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 06:57:05 +02:00
Erik
7bff9c649d feat(headless): typed --console option resolution + command-line flag
Adds the first piece of the interactive headless console (docs/plans/
2026-09-07-headless-console.md): HeadlessConsoleOptions.Resolve picks
--console, then ACDREAM_HEADLESS_CONSOLE=1, then a terminal-shaped
default, matching the project's typed-options-object convention rather
than a scattered env-var read. HeadlessCommandLine.Parse now accepts
--console as a bare flag (no value token) alongside the existing
paired options. Both new launch-options.md rows are added in this
commit per LaunchOptionsDocumentationTests' bidirectional rule.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 06:56:03 +02:00
Erik
fbdbbc7f90 docs(vt): slice 1c review D1 — the profile-dir override row and AD-122 say the first load moves flat .af files
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 06:54:00 +02:00
Erik
7cfe57827c merge(vt): slice 1c — metas/ and navs/ folders, no nav_ prefix (review-closed)
Owner decision 2026-09-07. Folder-scoped listing, one-time flat-file
migration, content sanity on load. Narrow Opus review: MERGE-READY.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 06:53:43 +02:00
Erik
615a401994 docs(vt): slice 7 visual gate script (draft, filled from KB 08)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 06:46:39 +02:00
Erik
812a533b83 feat(vtank): slice 1c step 1 — two-folder metas/navs layout, no nav_ marker
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>
2026-09-07 06:32:56 +02:00
Erik
a427d7db05 docs: plan the headless console (interactive chat/command CLI for the bot host)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 06:32:47 +02:00
Erik
43abc32edf docs(vt): owner direction — one plugin, two hosts; slice 2 gains headless autostart + a headless MossTank smoke test
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 06:29:46 +02:00
Erik
074762dd36 docs(vt): slice 7 plan — the nine tabs at VTank's geometry on the column markup; pulled forward after slice 1
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 06:23:28 +02:00
Erik
aafa66c821 docs(vt): owner decision — metas/ and navs/ subfolders, no nav_ prefix
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 06:20:16 +02:00
Erik
8080a99eef docs(vt): owner confirmed — binary .met/.nav readers stay for convert-on-import only
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 06:16:54 +02:00
Erik
55e0f12ae6 docs(vt): slice 1 closeout — Part A merged at f680bf234; owner items; next = slice 7
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 02:26:46 +02:00
Erik
b71bbe82f4 docs: file #488 — .utl expression block length prefix vs newline normalization (latent, from the slice-1 re-check)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 02:26:36 +02:00
Erik
f680bf234a merge(vt): slice 1 Part A — VTank .usd/.utl drop-in and metaf .af for metas and routes (review-closed)
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>
2026-09-07 02:25:28 +02:00
Erik
f74f7c3787 docs(vt): slice-1 ledger — Part A round 3 complete, final re-check dispatched
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 02:11:06 +02:00
Erik
f58e997b19 fix(vt): round 3 item 12 — cleanup pass (stale refs, silent swallows, comments)
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>
2026-09-07 02:09:22 +02:00
Erik
55010f92c7 docs(vt): round 3 item 11 — record the MyMonsters persistence gap
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>
2026-09-07 02:00:19 +02:00
Erik
43482c1380 feat(vt): round 3 item 9 — loot cutover to real .utl (consistency with the other three)
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>
2026-09-07 01:51:34 +02:00
Erik
d98bf4a139 docs(vt): round 3 item 7 — register MossTank's .cdf Nav/Meta format divergence
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>
2026-09-07 01:33:24 +02:00
Erik
f8c4149ca3 docs(vt): slice-1 ledger — Part A re-review verdicts, round 3 dispatched
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 00:56:23 +02:00
Erik
44fd5ce580 docs(vt): slice-1 ledger — Part A round 2 landed, re-reviews dispatched
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 00:38:14 +02:00
Erik
a6c2e3918e docs(vt): VT1 complete, VT2 FINAL — every catalog doc citation-verified and folded
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 23:53:41 +02:00
Erik
7ac43cee63 docs(vt): fold the citation-pass corrections into KB 01 (tString rows not in Advanced Options, template schema is 8 tables with an empty Settings table, PluginCore list-builder ranges, bk.a semantics, RechargeHandlerSet read-back, ~~ prefix resolved)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 23:53:41 +02:00
Erik
b9029082ae docs(vt): slice-1 ledger — Part A round 1b complete, round 2 dispatched
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 23:39:27 +02:00
Erik
a3b4a8e2e5 fix(vt): H test-restoration + disabled-rule refusal + double-side gate
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>
2026-09-06 23:20:20 +02:00
Erik
2040f2bcfb fix(vt): F host-composed VtankProfiles storage replaces raw path string
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>
2026-09-06 23:03:04 +02:00
Erik
0219c6e03d fix(vt): A ptl/tlk carry two coordinate triples, jmp direction loss recorded
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>
2026-09-06 22:13:48 +02:00
Erik
68627a873b docs(vt): slice-1 ledger — Part A fix round 1 (3/12) and round 1b dispatched
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 21:55:10 +02:00