Commit graph

4796 commits

Author SHA1 Message Date
Erik
e7ed603e6e fix(vtank): slice 7 fix round B item 7 — Consumables left-list click removes, not selects
Retail ground truth (PluginCore.cs:7683-7700): a click on the Consumables
tab's left list removes that row directly — there is no select-then-press-
Remove step for that list, unlike our previous SelectConsumableRow which
only updated the selection index. SelectConsumableRow now sets the index
AND immediately calls RemoveSelectedConsumableCore, matching the "click
removes" convention the right-hand Excluded Scarab Types list already
uses; the "Remove" button (RemoveSelectedConsumable) stays as a second
path, same as Items/Buffs/Route. Retitled the list's tooltip to match
("Click a row to remove it.").

New test: ConsumablesLeftListRowClickRemovesTheRowDirectly. Mutation
check: commenting out the RemoveSelectedConsumableCore() call turned it
red ("The collection contained 2 items" instead of Assert.Single); restoring
it turns it green.

tests/AcDream.Plugins.MossTank.Tests: 666/666 (was 665/665, +1).
tests/AcDream.App.Tests --filter Markup|Plugin|UiMenu|Slider: 276/3 skipped/279 (unchanged).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 12:11:56 +02:00
Erik
ce5652f524 fix(vtank): slice 7 fix round B item 6 — separate and centre the Monsters two-letter headers
Owner: "WC FC Cp DC Cs" run together. Measured each against the real
installed DAT font 0x40000000 (AcDream.Cli's temporary measure-text-temp
verb, same pen-advance formula as items 4/6): WC=19px, FC=14px, Cp=14px,
DC=17px, Cs=13px against a 20px column pitch. UiLabel draws raw/unclipped/
left-aligned text with no built-in centering, so all five previously drew
flush against their column's left edge with no visual separation from the
DAT text metrics.

Fix: each header gets its own x nudge (columnStart + round((20-width)/2))
and its declared w shrunk to its real measured width, centering it over
its own check column. The shrunk w also keeps AssertWithinParent/the new
sibling-overlap pin honest — a couple of these (nudged DC/Cs) would
otherwise have crossed into the Name column's x=280 start at the old w=20.

tests/AcDream.Plugins.MossTank.Tests: 665/665 (unchanged — pure geometry,
no behavior change; MonstersGridHasVtanksTwentyThreeColumnsInOrderWithRetailHeaderTooltips
keys off text content, not position, so it's unaffected).
tests/AcDream.App.Tests --filter Markup|Plugin|UiMenu|Slider: 276/3 skipped/279 (unchanged).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 12:07:52 +02:00
Erik
06c9b95222 feat(vtank): slice 7 fix round B item 5 — Meta tab down to VTank's real 5 controls, rule editor as a popup
VTank's own Meta tab (docs/research/vtank-kb/08-ui-views.md §1 "Tab: Meta")
is only 5 controls: the lstMetaRules grid, cmdMetaCreate ("Create"), a bold
"Current State:" caption, and a SETTABLE cmbMetaCurrentState choice — no
profile toolbar, no "Enable Meta" checkbox, no inline editor at all.

- Removed from the Meta tab entirely: the profile menu/name-draft field/
  New/CopyTo/Clear/Delete row (Profiles already carries the Meta combo/
  CopyTo/Delete) and the "Enable Meta" toggle (already on Options) — same
  accepted-regression shape as fix round A's Macro/Nav CopyTo-name-field
  loss (no Profiles-tab equivalent exists for these, and VTank's own tab
  has none either).
- Moved the inline State/Condition/Action editor (fields, condition/action
  menus, numeric steppers, Apply/Remove/MoveUp/MoveDown) into a new popup,
  mosstank-metaeditor.xml, registered the same StartVisible=true/
  ShowInSidePanel=false way as the buff picker. Opened by a grid text-cell
  click (SelectMetaRule, still populating the draft via SelectMetaRuleCore)
  or the tab's own "Create" button (CreateMetaRule — new, distinct from the
  still-existing AddMetaRule the tests call directly); Apply and Cancel
  (HideMetaEditor) both close it. DeleteMetaRuleAt/MoveMetaRuleUpAt/
  MoveMetaRuleDownAt call SelectMetaRuleCore directly and do NOT open the
  popup.
- "Add" -> "Create": CreateMetaRule adds a default rule (delegating to the
  existing AddMetaRuleCore) and opens the editor so it isn't left silently
  default-valued.
- Added the settable current-state menu: MetaCurrentStateNames/
  SelectedMetaCurrentState/SetMetaCurrentState expose MetaEngine's own
  already-public States/Transition(string) — choosing a state here forces
  the live engine into it, matching VTank's own manual override.
- The grid returns to VTank's full-width 856x116 proportion (848x116 here)
  now nothing else shares the tab.
- Bold text isn't representable in the plain retail UI font (0x40000000 has
  no bold face); "Current State:" uses the same bright caption color other
  tab headers use instead — documented in mosstank.xml's own comment, not
  silently dropped.

New tests: MetaEditorPopupOpensOnCellClickOrCreateAndClosesOnApplyOrCancel,
MetaCurrentStateMenuForcesTheLiveEngineIntoTheChosenState. Both mutation-
checked: commenting out SelectMetaRule's `_metaEditorVisible = true` turned
the first red ("Expected: True, Actual: False"); commenting out
SetMetaCurrentState's `_meta.Transition(value)` turned the second red
("Expected: Hunt, Actual: Default"). Restoring both turns them green.

EveryInteractiveControlDeclaresARealHandlerBinding's pinned control count:
186 -> 166 (-7 controls removed for good, -14 moved into the new popup file
this scan doesn't cover, +1 the new current-state menu).
SecondaryPopupPanelsFitTheirOwnBoundsAndEveryBindingResolves gained
mosstank-metaeditor.xml (630x236... — 630x160, corrected below).

tests/AcDream.Plugins.MossTank.Tests: 665/665 (was 663/663, +2 new tests).
tests/AcDream.App.Tests --filter Markup|Plugin|UiMenu|Slider: 276/3 skipped/279 (unchanged).

Screenshot with at least two rules (item 17) is owed with the round's other
live captures.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 12:05:27 +02:00
Erik
897e262eda refactor(vtank): slice 7 fix round B item 13 — glob mosstank*.xml everywhere it's named
Reordered ahead of items 5-12 on purpose: item 5 adds a fourth popup
(mosstank-metaeditor.xml) and would otherwise need the same three-file
manual edit this item retires. Every place a mosstank-*.xml file used to be
named one at a time now globs mosstank*.xml instead, so adding a new popup
panel needs exactly one new file, not four matching edits:

- AcDream.Plugins.MossTank.csproj: <None Update="mosstank*.xml"> replaces
  four separate <None Update> entries.
- AcDream.Plugins.MossTank.Tests.csproj: one globbed <None Include> with
  Link="%(Filename)%(Extension)" replaces four separate Include/Link pairs.
- AcDream.App.csproj: a new _MossTankPluginMarkup item
  (Include="…/mosstank*.xml") replaces the literal four-file semicolon list
  in both CopyMossTankPluginToBuildOutput and …ToPublishOutput's Copy
  SourceFiles.
- MossTankMarkupContractTests: AuthoredControlsInTheSameContainerNeverOverlapASibling
  (item 2's new pin) and NoButtonAnywhereUsesTheUnrenderableArrowGlyphs now
  iterate Directory.GetFiles(AppContext.BaseDirectory, "mosstank*.xml")
  instead of a hardcoded 4-file array/Theory. Left
  SecondaryPopupPanelsFitTheirOwnBoundsAndEveryBindingResolves alone — it
  pairs each file with its OWN expected w/h, which a glob can't supply.
- LinuxPlatformBoundaryTests.ShippedPluginCopiesUseResolvedTargetPathsForBuildAndPublish
  now asserts on the glob pattern instead of the literal mosstank.xml
  substring the old Copy SourceFiles list contained.

A missed file in any of these four places used to silently drop a panel at
mount (fix round A's own #missing-popup-files near-miss) instead of failing
the build; the glob makes that failure mode structurally impossible.

tests/AcDream.Plugins.MossTank.Tests: 662/662 (665 -> 662: the 4-case
Theory collapsed into 1 Fact with an internal loop — same coverage, 3 fewer
reported xunit tests).
tests/AcDream.App.Tests --filter Markup|Plugin|UiMenu|Slider: 276/3 skipped/279 (unchanged).
Verified the glob actually copies all four files: `ls
src/AcDream.App/bin/Release/net10.0/plugins/AcDream.Plugins.MossTank/*.xml`
lists all four post-build.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 11:53:01 +02:00
Erik
7f95c771db fix(vtank): slice 7 fix round B item 4 — widen Profiles' CopyTo column, align the four Delete buttons
Measured "CopyTo" against the real installed DAT font 0x40000000 (a
temporary AcDream.Cli measure-text-temp verb using the same pen-advance
formula UiDatFont.GlyphAdvance uses, HorizontalOffsetBefore+Width+
HorizontalOffsetAfter, then removed): 42px wide against a declared 40px
button. UiSimpleButton centers its caption without clipping, so the extra
2px split evenly past both edges — a real, if small, overflow on three
rows (Macro/Nav/Meta all use "CopyTo"; Loot uses "New" instead).

Fix: widen the CopyTo/New column from 40 to 50px (296->306 x, +10 gap
preserved) and column-shift every following control on all four rows by
the same +10 to preserve every original gap. While shifting, also aligned
the four "Delete" buttons (DeleteProfile/DeleteRouteProfile/
DeleteLootProfile/DeleteMetaProfile) into one shared column at x=566 —
they previously sat at four different x's (546/456/346/346) because each
row has a different control count in front of it; the shorter rows now
have a wider gap before Delete instead of four misaligned columns.

tests/AcDream.Plugins.MossTank.Tests: 665/665 (unchanged — no new
behavior, pure geometry; the existing fit/overlap pins from items 2/3
still cover this file and stay green).
tests/AcDream.App.Tests --filter Markup|Plugin|UiMenu|Slider: 276/3 skipped/279 (unchanged).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 11:44:18 +02:00
Erik
687f420d02 fix(vtank): slice 7 fix round B item 3 — bottom-band clipping on notice labels
AssertWithinParent (MossTankMarkupContractTests) already checks a child's
y+h against its parent's declared height, but only when h>0 — most notice/
status labels have no explicit h attribute (Height defaults to 0 in
MarkupDocument.F), so the check silently never fired for them even though
they visually clip the tab's 194px content band.

Fix: give the two labels that actually overflow an explicit h="16" and
raise them so 194-16=178 is their max y (item 3's "a full 16px line fits"
rule) — Consumables' ProfileNotice (184 -> 178; also trimmed the "Add
Selected" button above it from h=25 to h=24 so the two touch at y=178
without overlapping) and Meta's MetaNotice (already at the correct y=178,
just needed the explicit w/h so the existing pin actually applies to it).
Buffs' Coverage label already landed at y=178/h=16 in item 2's Buffs
redesign. The Loot Editor popup's own bottom row (LootRangeText/-/+) was
already explicit and already fit (282+16=298<=300) — no change needed.

Mutation check: reverting ProfileNotice to y=184 turned
AuthoredShellFitsTheMinimumCanvasAndEverySizedChildFitsItsParent red
("<label> crosses the bottom edge of <group>"); restoring y=178 turns it
green again.

tests/AcDream.Plugins.MossTank.Tests: 665/665 (unchanged count — no new
pins added, the existing one now actually fires).
tests/AcDream.App.Tests --filter Markup|Plugin|UiMenu|Slider: 276/3 skipped/279 (unchanged).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 11:36:27 +02:00
Erik
0c2c3b572d fix(vtank): slice 7 fix round B item 2 — restore VTank's Buffs silhouette, add sibling-overlap pin
Owner's silhouette rule: VTank's own controls are never shrunk/moved/
reordered to make room for MossTank extras. The Buffs tab's Difficulty "+"
button (338,108,26,20) overlapped the Extra Buff Spells "Add..." button
(336,102,120,16) because both VTank lists had been narrowed to 246/256x84
to coexist with MossTank's toggle/difficulty/rebuff/Buff-cast-now block in
the same two columns.

Fix: restore both lists to VTank's real 320x116 at the far left (x=4) and
far right (x=524) with their own 120-wide "Add..." row below each list.
Move the MossTank-only extras block (7 toggles, Difficulty +/-, Rebuff +/-,
the Buff button+status, Coverage) into the empty 184px middle strip between
the two lists that VTank's own layout never uses. Coverage moves to y=178
(194-16) per the upcoming bottom-fit rule.

Also fixed: a second real overlap the new pin found on the Options tab —
"Pet Min. Monsters:" (614,48,125,16) overlapped its own field
(734,48,40,16) by 5px; narrowed the label to the same 120px column width
every other label/field pair in that block uses.

Added AuthoredControlsInTheSameContainerNeverOverlapASibling (4 file cases)
plus a direct predicate pin (AssertNoSiblingOverlap_CatchesARealOverlap...)
proving the helper catches a real overlap, ignores touching edges, and
ignores two <group> tab-pages sharing one rectangle (mutually exclusive via
their own visible="{...}" binding) — the existing AssertWithinParent only
ever checked a child against its OWN parent's bounds, never against a
sibling.

Mutation check: setting DifficultyUp's button to (472,112) — the same rect
as DifficultyDown — turned AuthoredControlsInTheSameContainerNeverOverlapASibling
red against the real mosstank.xml ("<button> text='-' ... overlaps sibling
<button> text='+' ..."); reverting turns it green.

tests/AcDream.Plugins.MossTank.Tests: 665/665 (was 660/660, +5: the new
theory's 4 file cases + 1 predicate fact).
tests/AcDream.App.Tests --filter Markup|Plugin|UiMenu|Slider: 276/3 skipped/279 (unchanged).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 11:33:08 +02:00
Erik
040d5f3d81 test(vtank): slice 7 fix round B item 1 — retire the red plain-menu-scroll pin
Menu_Scroll_DrawsAScrollbarWhenTheMarkupItemCountOverflowsTheVisibleRows was
RED at HEAD: <menu> now defaults to the plain style (RetailButtonArt=false,
the "BIG gold/yellow buttons" owner fix), so a markup menu with no style
attribute draws DrawScrollablePopupPlain's flat DrawFill thumb, never the
retail sprite path (DrawPopupScrollbar) the test asserted on.

Fix: give the existing test style="retail" so it keeps proving the retail
sprite path draws ScrollThumbSprite. Add a plain sibling
(Menu_Scroll_DrawsAPlainFlatThumbFillWhenTheMarkupItemCountOverflowsTheVisibleRows)
that proves the DEFAULT (no style attribute) path draws the flat thumb fill:
an untextured (texture=0) quad sized ScrollbarWidth-2 wide, tinted
PlainBorderColor. Untextured DrawFill calls all batch into one texture=0
render segment, so the assertion scans per-quad (6 verts x 8 floats) inside
each segment rather than treating a whole segment as one quad.

Mutation check: commenting out DrawPopupScrollbarPlain's thumb DrawFill call
turned the new test red ("expected a plain flat thumb fill... among the
drawn segments"); restoring the call turns it green again.

tests/AcDream.Plugins.MossTank.Tests: 660/660 (unchanged).
tests/AcDream.App.Tests --filter Markup|Plugin|UiMenu|Slider: 276 passed / 3
skipped / 279 total (was 274 passed / 1 failed / 3 skipped / 278 total).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 11:24:12 +02:00
Erik
dbdde0783d merge(vt): plain <menu> popup from the campaign branch into the slice-7 panel work (ledger union)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 10:54:25 +02:00
Erik
41fc1d88d1 docs(vt): slice 7 ledger — S7.4-S7.6 landed, fresh live screenshots
Recaptured Items/Consumables/Buffs/Route/Meta screenshots (stale since
before this sub-slice) plus the new buff picker popup against a live
local ACE, isolated ACDREAM_CONFIG_DIR/ACDREAM_DATA_DIR (fresh authored
window positions, no stale-persisted-layout override), and an
ACDREAM_UI_PROBE_SCRIPT route through the five changed tabs. All six
confirm plain controls, no overlapping captions, correct grid rendering,
and real DAT move-icon art. Records the three S7.4-S7.6 commit SHAs and
their deviations in the ledger.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 10:51:55 +02:00
Erik
cc323f6a59 feat(vtank): slice 7 S7.6 — Meta tab's real 6-column rules grid
VTank's own lstMetaRules grid (delete/move-up/move-down icon-ish cells +
State/Condition/Action text cells, docs/research/vtank-kb/08-ui-views.md
§1 "Tab: Meta"; PluginCore.cs:2218-2247) replaces the padded
single-column rules list: any State/Condition/Action cell click opens
the rule editor below (reusing the existing SelectMetaRule), the
up/down cells reorder using the same 0x060028FC/FD DAT icons the
Monsters and Route grids already established, and the delete cell is a
plain text "X" — no retail DAT delete-glyph id is confirmed anywhere in
this codebase (unlike the already-established move icons), and "X" is
ASCII the default retail font renders without the unrenderable-glyph
risk NoButtonAnywhereUsesTheUnrenderableArrowGlyphs guards against.

The new pin (contract control count 180->186) and the new panel test
were shown to fail against a targeted mutation before being confirmed
green. MossTank suite 659 -> 660; App markup/plugin filter holds 192/192.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 10:37:52 +02:00
Erik
6118062a7a feat(vtank): slice 7 S7.5 — Route tab's real waypoint grid
VTank's own clWP/clWPc 2-column waypoint grid (text + a 1-based position
counter standing in for VTank's own count column) replaces the padded
single-column list, with any-cell-click delete matching
PluginCore.cs:3576-3599. Also: the recall menu gets scroll="true" for
grammar parity (still 4 real recall kinds — see the deviation note
below); the nav-type menu now shows VTank's own "Follow" caption for the
Target mode via a pure display remap (SelectedRouteMode/SelectRouteMode
translate the string, RouteMode.Target itself is unchanged, zero
behavior change); and a third nav image button (icon 0x060011F7, "Select
Nearest Point") is added.

Deviations, documented at their own binding site:
- VTank's cmbRecallType lists 27 named retail recalls; MossTank's
  RouteRecallKind models 4 (Lifestone/Marketplace/Primary/Secondary
  Portal). Expanding to 27 needs real per-recall spell-id data, which is
  casting-algorithm behavior out of this UI-parity slice's scope.
- VTank's btnNavResetPoint reassigns the LIVE navigation cursor mid-route
  (a mutable index MossTank's navigation controller never exposes to a
  plugin); SelectNearestRouteWaypoint instead moves the tab's own EDIT
  selection to the closest waypoint by horizontal distance — real,
  testable UI-only behavior that stays out of live-navigation territory.

The new pin (contract control count 177->180) and the new panel test
were shown to fail against a targeted mutation before being confirmed
green. MossTank suite 658 -> 659; App markup/plugin filter holds 192/192.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 10:33:49 +02:00
Erik
f5409530f2 feat(vtank): slice 7 S7.4 — Items/Consumables/Buffs grids and picker
VTank's real Items (2-col name/hands), Consumables (adds the right-hand
"Excluded Scarab Types" icon+text grid), and Buffs (Extra Buff Spells /
Blacklisted Buff Families lists + a shared SelfBuffChoiceView-style
picker popup) tabs are transcribed from docs/research/vtank-kb/
08-ui-views.md §1, replacing the old single-column-list adaptations with
real per-cell grids and VTank's own click-to-delete/cycle semantics
(PluginCore.cs:8529-8562 Items, :7683-7776 Consumables, :7323-7355 Buffs).

Deviations, documented at their own binding site:
- Items' Hands column has no backing wieldable-handedness data anywhere
  in the plugin surface, so handedness is session-local UI state only
  (not persisted across profile save/load) — same "deliberate adaptation"
  shape as S7.3's weapon-roster substitution for Monsters' Weapon/Offhand.
- Consumables' "Add Selected" accepts any selected owned item rather than
  requiring VTank's own SpellComponent object-class check (no classifier
  surface exists for plugins) — the added token is still the item's real
  Name, which SpellComponentPolicy already matches against.
- Buffs' ExtraBuffSpellNames/BlacklistedBuffFamilyNames (BuffPlan.cs) add
  storage + UI only; wiring them into BuffPlan.Build's cast selection is
  real casting-algorithm behavior, out of this UI-parity slice's scope
  (tracked in the slice 7 plan ledger as a real, accepted gap for a
  future Campaign VT behavior slice).

Every new/changed pin (contract control count 167->177, the new
mosstank-buffpicker.xml popup pin, the three new MossTankPanelTests
interaction tests) was shown to fail against a targeted mutation before
being confirmed green. MossTank suite 654 -> 658; App markup/plugin
filter holds 192/192.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 10:26:13 +02:00
Erik
22e6ae88a1 merge(vt): plain <menu> popup — dark rows, selected fill, plain scrollbar, no checkmark (owner: the open dropdown was still retail art)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 10:21:11 +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
5bdf2fa395 fix(vt): plain-style menu popup — no retail gradient/checkmark/gold scrollbar
Owner live-client report 2026-09-07 ("Drop down menus look horrible, there
is also a checkmark on the text there"): the prior S7 fix only replaced the
CLOSED-state button face when RetailButtonArt=false. The OPEN popup still
drew retail's sprite art unconditionally — the tan/orange gradient panel
(PopupBgSprite), the per-row highlight sprite (which bakes a checkbox/
checkmark glyph into its leftmost ~17px, per TextIndent's doc comment), and
the ornate gold scrollbar chrome — regardless of the menu's style.

UiMenu.OnDrawOverlay now branches on RetailButtonArt before ever reading
SpriteResolve: plain mode draws through two new methods (DrawGridPopupPlain,
DrawScrollablePopupPlain) that use only DrawFill/DrawRectOutline — flat
background + 1px border, the current entry filled like a list selection
(PlainSelectedColor, same value as UiMarkupList.SelectedColor), a new
hover fill (PlainHoverColor) for the row under the pointer, and left-aligned
text at PlainPadding. No checkmark is possible by construction since plain
mode never resolves or draws any sprite. Hover tracking needed a small new
mechanism: UiMenu.ReceivesHoverMouseMove now returns true while a plain
popup is open, so UiRoot's hover dispatch keeps delivering MouseMove to
_hoveredPopupIndex (reset on every open/close transition and on
HoverLeave). Scrollbar overflow (DrawPopupScrollbarPlain) draws a 1px-
bordered track and a flat thumb, both in PlainBorderColor, sharing the
exact UiScrollbar.ThumbRect geometry the hit-test math already uses — no
DAT track/thumb/arrow-button art. Hit-testing (OnHitTest/OnEvent's
MouseDown pick logic) is untouched; the retail sprite branch is now a
separate, unmodified path proven byte-identical by a new golden test.

Mutation proof: reverting UiMenu.cs alone (keeping the new tests) fails the
build outright — the six new tests reference PlainSelectedColor/
PlainHoverColor, which only exist after this change (CS1061 with the old
class). Filters run: AcDream.App.Tests Markup|UiMenu|Menu|Scrollbar
(242 passed, 3 pre-existing unrelated Lane=Manual live-DAT-probe failures
that require ACDREAM_PROBE_LIVE_MOUNT=1 and predate this change) and
AcDream.Plugins.MossTank.Tests Markup (9/9 passed).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 10:19:44 +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
66b070def4 docs(vt): slice 7 ledger — fix round A landed, four commits recorded
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>
2026-09-07 09:50:54 +02:00
Erik
78b42a519e fix(vtank): popup panels actually show; drop duplicate popup titles; fresh screenshots
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>
2026-09-07 09:49:32 +02:00
Erik
e414b2f56e fix(app): copy MossTank's two new popup panels into the plugin's output dir
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>
2026-09-07 09:13:33 +02:00
Erik
565a33d789 feat(vtank): slice 7 fix round A — grid scaling, Profiles cleanup, popups as own panels
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>
2026-09-07 09:09:41 +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
074a1561b6 test(headless): the console's unknown-verb pin follows AD-124 — chat scroll, not SpewBox
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 08:27:39 +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
045cd0a195 merge(vt): plain <menu> style from latest-main-sync into slice7 panel work
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>
2026-09-07 08:23:29 +02:00
Erik
2b65217d29 fix #363: route plugin system text to the chat window, not SpewBox
Owner direction 2026-09-07 (same instruction as the sibling unknown-
command fix, previous commit): plugin-originated text must land in
the chat window instead of retail's ClientLocal (0x1A) SpewBox-only
channel. AppAutomationSurface.PostSystemMessage -- the production
implementation of IPluginChat.PostSystemMessage that MossTank/VTank-
style plugins call -- now passes RetailLogTextType.Default instead of
ClientLocal to RuntimeCommunicationState.AddText, so the text reaches
the chat transcript via Chat.OnSystemMessage instead of the SpewBox.
This matches Decal's own AddChatText behavior for plugin output.

IPluginChat.PostSystemMessage's doc comment is updated to describe
the new destination instead of the old one. Register row AD-124
(previous commit) already covers this site alongside the sibling
unknown-command change.

Mutation check: temporarily reverted PostSystemMessage's AddText call
back to ClientLocal and confirmed the new
AppAutomationSurfaceTests.PostSystemMessage_RoutesToChatLog_NeverSpewBox
test fails (Assert.Single() on an empty chat log) before restoring the
fix. Also adds ChatVMTests.RecentLines_ShowsPluginSystemMessage_TaggedDefault
pinning that a ChatVM bound to the same ChatLog surfaces the line.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 08:22:22 +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
c3b4f78624 docs(vt): slice 7 ledger — S7.3 Monsters grid landed
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>
2026-09-07 08:18:37 +02:00
Erik
57ced0aff1 feat(vtank): slice 7 S7.3 — Monsters tab as VTank's own 23-column grid
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>
2026-09-07 08:18:14 +02:00
Erik
cfa7030659 merge(vt): plain combo style for plugin <menu> (owner: the gold dropdowns go)
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>
2026-09-07 08:17:08 +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
19c831211b feat(vtank): slice 7 fix — <menu style> selects plain vs retail art
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>
2026-09-07 08:15:23 +02:00
Erik
cc11e077a4 feat(vtank): slice 7 fix — UiMenu plain closed state, gold art opt-in
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>
2026-09-07 08:15:07 +02:00
Erik
e3639a4c18 fix(headless): N5 dim only lifecycle/command/portal lines
HeadlessConsoleRenderer dimmed every line uniformly, so chat and
interface text (player-visible content) read the same washed-out weight
as scheduling/session-status noise like "entered world" or "command
rejected: ...". Only lifecycle, command, and portal lines are scheduling
noise; chat and interface text now print at the terminal's default
weight.

ChatAndInterfaceTextPrintAtDefaultWeightNeverDimmed was shown to fail
against the prior dim-everything WriteLine (mutation: dim parameter not
yet threaded through, every call still unconditionally wrapped in the
ANSI dim/reset pair) -- the chat line and interface text both carried
the dim escape sequence.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 08:13:24 +02:00
Erik
70e86c180e test(headless): S3 end-to-end HeadlessProcessHost console proof
Existing coverage exercises HeadlessSessionHost.SubmitConsoleLine
directly (bypassing the background reader thread and the scheduler) or
HeadlessConsoleController in isolation (a fake submit callback, no real
session). Add one test that drives the actual production wiring: a
plain StringReader feeds "hello" and "/quit" through the real
HeadlessProcessHost constructor (background reader thread -> per-tick
ConsolePump -> ChatCommandRouter.Submit -> the wire), asserting the
outbound Talk action reaches the fixture AND that /quit ends
RunAsync with HeadlessExitCode.Success -- the same graceful path an
external cancellation takes.

Mutation: removed the session.ConsolePump assignment in
HeadlessProcessHost's constructor (never wiring the drain+pump
delegate). The test failed with a TimeoutException -- the queued
console lines were never drained, so /quit's cancellation never fired
and RunAsync ran until the test's own 10s WaitAsync bound.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 08:10:52 +02:00
Erik
314cb5f4a5 fix(headless): S7 report why a multi-session process never attaches the console
A configured process with 2+ sessions and --console silently skipped
console attachment (the plan's "single-session only for the first cut"),
indistinguishable from --console simply having worked. Report it
explicitly through the same HeadlessDiagnosticWriter.Message stream
every other structured event already uses.

TwoSessionsWithConsoleFlagReportsSingleSessionOnly was shown to fail
against the silent-skip branch (mutation: the else-branch body removed)
-- the diagnostics stream carried only the ordinary lifecycle/resource
JSON lines, with no "single-session only" message anywhere.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 08:07:41 +02:00
Erik
2ff1e1228c fix(headless): S6 thread standardOutputIsTerminal instead of reading Console inside HeadlessProcessHost
HeadlessProcessHost read System.Console.IsOutputRedirected directly to
pick the console renderer's color mode, which only Program.cs (the
executable's own entry point) should ever touch -- the same reasoning
that already put the stdin probe there. Resolve
standardOutputIsTerminal next to the existing !Console.IsInputRedirected
probe in Program.cs and thread it through HeadlessEntryPoint.Run into
HeadlessProcessHost's constructor as a plain parameter.

StandardOutputIsTerminalParameterControlsColorNotTheRealConsole was
shown to fail with the parameter still unused (useColor still reading
the real Console.IsOutputRedirected, which the test host always
redirects): the standardOutputIsTerminal:true case expected dimmed
lifecycle output but got none, since the real console read forced
useColor=false regardless of what the test passed in.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 08:05:37 +02:00
Erik
3328b2f2b3 refactor(headless): S5 poll SpewBox on the console pump instead of decorating one call
HeadlessConsoleChatFeedback wrapped RuntimeChatCommandFeedback per
SubmitConsoleLine call, so it only ever saw interface text produced by
the console's OWN typed line -- a server-driven refusal or a plugin's
own Log/interface-text write (RuntimeCommunicationState.AddText's
ClientLocal branch, called from anywhere else) never reached the
console at all, because that branch enqueues into SpewBoxState and
never touches ChatLog/RuntimeChatDelta.

Delete the decorator. HeadlessConsoleSpewBoxPump instead polls the SAME
SpewBoxState the graphical overlay's SpewBoxController.Tick already
reads, diffing against the previous visible snapshot so it prints only
newly-appeared entries. HeadlessProcessHost's ConsolePump now runs the
input drain and the SpewBox pump together each tick.
HeadlessSessionHost.SubmitConsoleLine drops its onInterfaceText
parameter -- it is just ChatCommandRouter.Submit against a plain
RuntimeChatCommandFeedback now, same as LoginCommandSequence.

N1: also corrected this method's own doc comment, which described
dispatch as "plugin verb registry first, then retail client/server
slash commands" -- the real ChatCommandRouter.Submit order is retail's
catalog, local /help, plugin verbs, the channel-tag fallback, an
explicit server command, then chat.

PumpPrintsInterfaceTextNotOriginatingFromTheConsole was shown to fail
against a no-op Pump() (mutation) -- the enqueued plugin-shaped line
never printed. UnknownVerbProducesTheSameInterfaceTextTheChatBoxShows
was reworked to assert against the real SpewBoxState directly instead
of the deleted decorator's callback.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 08:01:24 +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
f0b7a136b2 fix(headless): S4 report UnknownCommand/Dropped and never let submit escape DrainDue
HeadlessConsoleController.Handle called _submit(rawLine) bare: an
UnknownCommand/Dropped outcome printed nothing (the operator had no way
to tell their line did nothing), and any exception from the submit
callback would propagate out of DrainDue into the scheduler's
per-session quarantine catch, faulting the whole session over one
console typo. Wrap the submit in try/catch and report both cases with a
visible line, mirroring LoginCommandSequence.DrainDue's own reporting
for login-line failures.

N1: also corrected this class's own <remarks> doc comment, which
described the dispatch order as "plugin-verb registry -> client/server
slash commands" -- the real ChatCommandRouter.Submit order is retail's
client-command catalog, then local /help, then plugin verbs, then the
unregistered-channel-tag fallback, then an explicit server command, then
chat.

UnknownOrDroppedOutcomePrintsAVisibleLine and
SubmitFailurePrintsALineAndNeverEscapesDrainDue were shown to fail
against the prior bare `_submit(rawLine);` call: the outcome tests found
nothing printed, and the failure test caught the InvalidOperationException
escaping DrainDue itself rather than being reported as a line.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 07:56:34 +02:00
Erik
cb076c6558 test(headless): S2 make the reader-thread pin falsifiable
The existing ordering test proves lines drain in order on the calling
thread but only argues "never the reader thread" structurally (the
reader loop has no dispatch code to run). Add ThreadIdRecordingTextReader,
which records the actual managed thread id ReadLine() ran on, and assert
from inside the controller's submit callback that the executing thread is
neither that reader thread nor any other thread -- only the DrainDue
caller's.

Mutation: routed DrainDue's Handle(line) call through a dedicated
new Thread(...).Start()/Join() instead of calling it inline. The new
test failed (drainCallerThreadId != observedSubmitThreadId, off by one
full OS thread) before reverting the mutation.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 07:54:28 +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
c01ae15825 fix(headless): N3 reject --console for validate mode instead of ignoring it
validate never starts a session, so a silently-ignored --console read as
"it worked" to an operator who typo'd their command. Reject with a clear
message instead.

New test ValidateModeRejectsTheConsoleFlag was shown to fail first
(mutation: the guard absent — HeadlessCommandLine.Parse returned normally
for `validate --config bot.json --console` instead of throwing).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 07:50:05 +02:00
Erik
215442dc1d refactor(headless): N2 rename HeadlessCommandLine.Console to ConsoleEnabled
"Console" read as if it might mean System.Console; ConsoleEnabled says
what the flag actually gates. Pure rename, no behavior change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 07:48:42 +02:00