docs/plugin-ui-markup.md's Columns section is rewritten to match every
production change from items 1-9:
- New "Width semantics" section: width="*", the last-column-always-auto
rule (and how it combines with an earlier explicit "*"), the
non-last-column Build-time throw, and the runtime overflow clamp.
- The <column> attribute grammar table gains column onclick
(type="text", optional, Action<int>) and points width at the new
section instead of the retired "last column ignores its own width"
one-liner.
- New "Short columns past their own row count" section: text/icon draw
nothing, check draws unchecked.
- Check-column glyph section updated for the shared UiCheckLamp
primitive and its horizontal centering.
- New "The PITCH convention for transcribing a VTank column table"
section: VVS's WPaddingOuter=3px/WPadding=7px/16px scrollbar reserve/
forced-13px-check-columns have no direct acdream equivalent, so
transcribing a real mainView.xml column table means declaring each
column's PITCH (fixedwidth+7, or 13+7 for a check column regardless
of its real fixedwidth) and reserving 16px on the last column.
- The worked example now uses rowheight="18" (VVS's own Padding*2+
ControlHeight pitch, already the widget's default) and PITCH-computed
widths (20 for a check column, 127 for a 120px text column) instead
of arbitrary numbers, with a text onclick and a width="*" icon
column added to demonstrate both new attributes in place.
- The bindings truth table gains column onclick (type="text") and both
column width throw/silent rows (non-last vs. the list's last column).
- Testing conventions paragraph updated to mention width semantics, the
row-bound click guard, and the two end-to-end tests.
Verification: dotnet build AcDream.slnx -c Release (green), dotnet test
tests/AcDream.App.Tests -c Release --filter
"FullyQualifiedName~Markup|FullyQualifiedName~UiMarkupList|FullyQualifiedName~PluginSidePanel"
(130/130), dotnet test tests/AcDream.Plugins.MossTank.Tests -c Release
--filter "FullyQualifiedName~Markup" (9/9) — closes the fix round.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Two tests built through the full MarkupDocument.Build parse -> bind ->
draw (recording renderer) -> click pipeline, transcribing the two real
VTank list shapes named in the fix round brief
(refs/vtank/uTank2.ViewXML.mainView.xml):
- EndToEnd_MetaShapedSixColumnList: lstMetaRules's shape (3 icon
columns, a 150px text column, then two width="*" text columns, the
second with an onclick) at a list width (703) chosen so the two auto
columns' 505px leftover does NOT divide evenly (252/253) — proving
the last one absorbs the rounding slack. Asserts each column kind
draws inside its own x-range and a click in each of the six columns
reaches its own bound callback with the right row (icon columns
fire their own onclick; the plain text column still selects; the
onclick-bearing trailing text column fires that instead).
- EndToEnd_MonstersShapedTwentyThreeColumnList: lstMonsters's shape
(14 check columns, 7 text columns each given an onclick per fix item
1, 2 icon columns) built from real fixedwidth values with NO "*"
anywhere, at a list width (724) equal to the exact sum of every
column's declared width — the "no auto column, last absorbs the
remainder as today" case. Asserts the column-kind counts, each drawn
kind's x-range, and one representative click per kind reaches its own
callback (other same-kind columns share a dummy callback, proving the
bound-per-column dispatch, not just "some column of that kind fired").
Both use iconkind="item" for their icon columns (not the default
"did") so the fake resolver's echoed id is the literal texture drawn,
sidestepping PluginIcons.Normalize's did-namespace OR — an incidental
discovery while writing these (the first draft asserted on raw ids and
failed against the normalized 0x06000000-tagged textures).
Verified meaningful two ways: both failed first against the pre-fix
raw-id assumption (texture ids came back normalized), and — a stronger
check specific to these integration tests, since the underlying
mechanics were already covered by earlier fix-round commits — a
temporary mutation of ColumnLayout's auto-column share computation
(forcing share=0) made EndToEnd_MetaShapedSixColumnList fail exactly as
expected before being reverted.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The contract test suite's per-element binding-shape check treated
EVERY element's onclick as a plain Action, and its interactive-element
set (used both for "declares a real handler" and "has an accessible
tooltip") had no entry for <column> at all — both would misvalidate a
future mosstank.xml <column> the way Campaign VT slice 1 Part B and
this fix round's item 1 actually shipped it (onchange/onclick as
Action<int>, the row index).
Extracted the per-element dispatch out of
EveryInteractiveBindingMatchesTheRetainedUiDelegateShape into
AssertElementBindingsMatchRetainedUiDelegateShape, which now
special-cases "column": both onchange and onclick must resolve to
Action<int>, never plain Action. The interactive-element name list is
now the single shared InteractiveElementNames array (was duplicated
inline in EveryInteractiveControlDeclaresARealHandlerBinding and
TextlessAndAbbreviatedControlsHaveAccessibleRetailTooltips), with
"column" added — zero behavior change against mosstank.xml today (it
has no <column> elements yet), confirmed by the existing 190-control
count assertion staying green.
New tests: InteractiveElementNames_IncludesColumn pins the addition
directly; Column_OnchangeAndOnclick_MustBeActionOfInt builds a
synthetic <column> (not loaded from mosstank.xml, which has none) and
proves both a correctly-typed Action<int> onchange passes and a
column onclick bound to a plain Action (the shape every OTHER
element's onclick uses) is rejected — confirmed by temporarily
removing the "column" special case and rerunning: the plain-Action
onclick then passed silently ("No exception was thrown", the
generic Action check being satisfied by a real Action property),
proving the test only passes because the fix's column-specific
Action<int> enforcement is present.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
New internal static UiCheckLamp (LampSize=11f, the four checked/
unchecked colors, Draw) is the ONE definition of the five-band lamp
glyph, promoted out of UiMarkupToggle's private/internal fields —
UiMarkupToggle.OnDraw and UiMarkupList.DrawCheckCell both call
UiCheckLamp.Draw now instead of each carrying (or one exposing to the
other) its own copy of the same five DrawFill calls and four colors.
DrawCheckCell also centers the lamp horizontally in its cell instead of
drawing it flush left at cellX+1 — matching DrawIconCell, which already
centers its sprite. A check column declared wider than the glyph itself
(routine under fix item 11's PITCH-based authoring convention) no longer
strands the glyph in the cell's left edge.
UiMarkupListColumn's settable members are now internal init (Kind/Width
lost their `required` modifier — C# forbids `required` pairing with a
setter less visible than the containing public type, CS9032 — every
factory already sets both unconditionally, so this is a compiler-level
demotion, not a behavior change) — the type is constructible only
through its Text/Check/Icon factories from any external assembly (a
plugin) with no InternalsVisibleTo grant, so it can never assemble an
inconsistent instance via object-initializer syntax.
Replaced the inert draw-offset assertion in
Columns_CheckThenIcon_EachCellDrawsInsideItsOwnColumnBounds (check
column FIRST, so its own-cell assertion held trivially even with
completely broken column offsets) with
Columns_IconThenCheck_EachCellDrawsInsideItsOwnColumnBounds — check
column now SECOND, so the assertion can only pass if the glyph actually
moved into its own [20,100) cell.
New test: a single 50px-wide check column's glyph lands near the cell's
midpoint (~22.5) rather than the old flush-left x=4 — shown to fail
first before the centering change.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
DrawCheckCell drew nothing for a row past its own column's bound data —
inconsistent with docs/plugin-ui-markup.md's own description of VVS's
HudList, which materializes every cell in a row regardless of which
columns actually have data for it. Text/icon cells rightly stay
no-draw past their own count (no sensible default string or icon), but
a check cell always has one: unchecked.
DrawCheckCell now computes isChecked defensively (false when the flags
array is null or too short) instead of returning early, so a short
check column draws the unchecked lamp for every row past its own data
just like every other row.
New test: a 3-row list (driven by a longer text column) with a check
column bound to only 1 row now shows the unchecked lamp for rows 1 and
2 — shown to fail first (0 unchecked lamp quads, since DrawCheckCell
returned early past index 0).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
DrawColumns allocated four fresh IReadOnlyList<T>?[] sidecar arrays and
ColumnLayout allocated a fresh (float,float)[] EVERY draw call; worse,
OnEventColumns re-invoked every column's source Func a SECOND time (via
UiMarkupListColumn.RowCount()) just to recompute the same row count
DrawColumns had already materialized, and called ColumnLayout a second
time to recompute the same layout — on every single click/scroll event.
Columns is now a property with a custom setter that (re)sizes seven
instance-field caches to Columns.Count exactly once per assignment:
_cachedTextRows/_cachedColorRows/_cachedCheckRows/_cachedIconRows (the
per-column sidecar arrays), _cachedLayout (the column x/width array),
and _scratchIsAuto/_scratchFixedWidth (ComputeColumnLayout's own working
arrays, previously freshly allocated on every layout computation too).
DrawColumns writes into these caches instead of local arrays and records
_cachedRowCount; ComputeColumnLayout (the renamed, now-instance
ColumnLayout) writes into _cachedLayout in place instead of returning a
new array. OnEventColumns reads _cachedRowCount/_cachedLayout/the
per-column cached row arrays instead of re-invoking anything — this also
completes fix item 3's row-bound guard without a second RowCount() call.
UiMarkupListColumn.RowCount() stays as public API (still asserted by an
existing test) but is no longer called from UiMarkupList internally,
which is the "single row-count definition" the fix round asked for.
This does make OnEvent depend on at least one prior Draw call for
correct row/layout data (mirrors real frame order: draw every frame,
then handle input) — the four hit-test click tests that previously
fired OnEvent with no preceding Draw now draw once first, matching what
Scroll_OffsetIsRespectedBySubsequentHitTests already did.
New test: each column source Func increments its own call counter; one
Draw call invokes each exactly once, and five rounds of subsequent
click events invoke none of them again — shown to fail first (16 calls
instead of 1, from OnEventColumns's old RowCount()-per-column-per-event
re-invocation) before the caching change.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
BuildListColumn's per-attribute binder calls used the generic "column
items"/"column values"/"column onchange"/"column onclick" context
strings from the initial slice — indistinguishable when a list has
several columns of the same type. Route every column-attribute binder
call (text items/colors/onclick, check values/onchange, icon
values/onclick) through the ColumnContext helper item 2 introduced, so
every throw message reads column[N] type="..." attr. ValidateIconKind
gains an optional context parameter (default "iconkind" for the
existing non-column call sites — <icon>, <button icon>, <list icons>,
none of which changed message-wise beyond wording) so the column
iconkind check can identify its own column too.
Updated all twelve column-throw tests in MarkupListColumnsTests to
assert Assert.Contains on the distinguishing phrase: the ten
attribute-specific ones now check column[0] type="..." attr; the
unknown-<column type> and combined-with-legacy-attribute tests keep
(and, for the type one, add to) their existing distinguishing
assertions. Confirmed each attribute-specific assertion is meaningful
by first running the malformed-colors case against the pre-fix
"column colors must be..." message (mismatch), then landing the
production change alongside the rest.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Matches UiButton.cs:896-906 / UiElement.cs:683-720's own clip
discipline: every per-cell PushClip in DrawColumns now has its matching
PopClip in a finally, so a cell draw that throws (e.g. a plugin's icon
resolver misbehaving) doesn't leak that PushClip onto the context's
clip stack. Without this, the leaked entry combines badly with
UiElement.DrawSelfAndChildren's own outer clip pop on the way out
(it pops the wrong stack entry), permanently corrupting the shared
UiRenderContext's clip state for every draw that follows in the frame.
Added UiRenderContext.ClipStackDepth (internal, InternalsVisibleTo
AcDream.App.Tests) purely to make this provable from a test — the
number of PushClip calls not yet matched by PopClip.
New test: a column whose icon resolver throws mid-draw still leaves
the clip stack at its pre-draw depth after the exception propagates —
shown to fail first (leaked to depth 1 instead of 0, reproducing
exactly the "outer pop consumes the wrong stack entry" mechanism
described above) before the try/finally was added.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A short column (fewer bound rows than the list's own max-across-columns
row count) already draws nothing past its own row — DrawCheckCell/
DrawIconCell/DrawTextCell all no-op once index >= their own array's
Count. OnEventColumns's click routing didn't share that bound: a click
landing on a row the OVERALL list considers valid (row < the longest
column) but past a SHORTER column's own data would still invoke that
column's callback with a row index it never bound anything for.
Add the same per-column bound (index < columns[c].RowCount()) to the
check-column, icon-column, and text-column-with-onclick click paths —
text-without-onclick's plain SelectionChanged fallback is unaffected,
since row selection is a list-level concept already bounded by the
outer rowCount check.
New tests: a click past a short check/icon/onclick-bearing-text
column's own row count fires nothing (each shown to fail first — the
callback fired with the out-of-bound row index before this guard).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
VVS's HudList shares remaining width equally among every 0-width column
(docs/research/vtank-kb/08-ui-views.md's HudList row, Decal3XMLParser.cs
:409-418) after fixed-width columns are subtracted — the initial slice
only let the LAST column auto-size. Port the real rule:
- A non-last <column width> that is missing, unparseable, or non-positive
throws FormatException at Build naming the column index and declared
type (column[N] type="..." width) — UNLESS it is width="*".
- width="*" (any column, including the last) marks it auto: every auto
column shares the list's remaining width equally, with the LAST column
in the list (always implicitly auto, regardless of its own width or
IsAutoWidth) absorbing the integer-division rounding slack. With no
"*" anywhere this reduces to exactly the original "last column gets
100% of the remainder" contract — zero behavior change for every
existing column-less/single-"*" list.
- A declared width overflowing the list's total width is clamped to
whatever room is left, walked left to right; every column after the
overflow point gets 0 width (and never draws — DrawColumns already
skips a <=0-width cell).
UiMarkupListColumn gains IsAutoWidth (internal to the width contract,
set via each factory's new optional isAutoWidth parameter); ColumnLayout
in UiMarkupList is rewritten around an explicit auto/fixed classification
pass instead of the old "only index n-1 is special" one-liner.
MarkupDocument's new ParseColumnWidth owns the Build-time validation,
shared through the same ColumnContext helper item 1 introduced.
New tests: non-last missing/unparseable/non-positive width throws
(each naming column[N] type="..." width), width="*" parses without
throwing on both a non-last and the last column, two auto columns
share the remainder with the last absorbing the odd pixel, and an
overflowing declared width clamps with later columns drawing nothing
— each shown to fail first (compile error for the missing isAutoWidth
parameter, then a wrong split before the ColumnLayout rewrite).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
None of VTank's eight HudList instances relies on row selection
(PluginCore.cs Monsters/Items/Meta/Route/Buffs/Consumables cell
handlers) — every real text cell is an action target. Add an optional
onclick="{Action<int>}" to <column type="text">: when present, a click
fires it with the row index INSTEAD of selecting; when absent, the
original select-the-row behavior is unchanged. BuildListColumn now
takes the column's own index (used for its onclick throw message and
threaded through for the rest of the fix round's column-context
messages); the unknown-<column type> throw also picked up the index
as a side effect of that signature change.
New tests (MarkupListColumnsTests): the onclick attribute binds and
falls back correctly when absent, a malformed onclick throws at Build,
and a click actually reaches the bound callback instead of firing
SelectionChanged — each shown to fail first (missing TextClicked
member / no 4-arg Text() overload before the production change).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
VVS HudList parity (docs/research/vtank-kb/08-ui-views.md 2-3): <list>
accepts <column type="text|check|icon" width=... [iconkind] [colors]/>
children, each binding its own per-row source parallel to the row count.
Row count is the longest bound column; a click in a text column selects
(the list's own selected/onchange, unchanged), a click in a check/icon
column fires that column's own required onchange/onclick with the row
index and does NOT change selection. Last column always absorbs the
remaining width, recomputed live off the list's own Width rather than
baked in at Build. Per-cell horizontal clipping via UiRenderContext's
existing PushClip/PopClip. The check-column glyph reuses <toggle>'s own
five-band lamp primitive (UiMarkupToggle.DrawLamp/colors promoted from
private to internal) so it looks like every other checkbox. Unknown
column type, a missing required column binding, <column> combined with
the legacy items/icons/colors list attributes, or any non-<column> child
of <list> all throw FormatException at Build. A column-less <list> is
byte-for-byte the original single-text-column widget (new if/Columns
branch in OnDraw/OnEvent; the legacy branch's code is untouched).
New UiMarkupListColumn model (src/AcDream.App/UI/UiMarkupListColumn.cs)
carries each column's kind/width/bindings; MarkupDocument's `list` case
now builds either the legacy single-column fields or a Columns list,
never both. MarkupListColumnsTests (27 new tests) cover parse/binding
validation, draw-level column-offset/clipping/check-glyph/icon pins
against the recording renderer, hit-test routing, and a backward-
compatibility proof (a column-less list built through MarkupDocument
produces a byte-identical draw record to a hand-built UiMarkupList with
equivalent fields). Every new assertion was verified to fail first via
targeted temporary mutations (hit-test isolation, clip removal, last-
column-absorbs-remainder, required onchange/onclick, row-count = max),
each reverted after confirming failure.
docs/plugin-ui-markup.md gets a full Columns section (attribute grammar,
a Monsters-tab-style example, the no-header-row idiom, backward
compatibility) and the old single-text-column LIMITATION note is
retired; the bindings truth table gains the six new column-attribute
rows.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
05: recognized-vs-unrecognized rule payloads, 25 materials, only 3-arg
SpellInfo entries are live, Read/User1..5 unreachable from any loot plugin,
hv.a.e is a dead field, id.a.j = shareLoot, the Change-vs-Bonus branch
selector mis-port in the evaluator.
09: obtest location, the broken params dedup overload, the real ah call
sites, internal relay ctor, the next-waypoint handler at 3621-3639,
TryHandle is host-only, Classic Looter registry narrowed, UtilityBelt's
Harmony bypass route resolved.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Owner amendment 2026-09-06: MossTank implements the human-readable metaf
.af format for metas and nav routes instead of VTank's binary .met/.nav;
the reference converter lives in the owner's metas repo. Docs 06 §1/07 §1
stay as the binary record; their semantics sections remain the oracle.
Doc 07 spot-checked by the lead: ExpressionEvaluator.cs:787-790 (';'
returns the first operand), hn.cs:41-80 (the pass loop), bw.cs:25 (the
'> 5' six-view cap), d6.cs:7-8 (Button/Layout only).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
VTClassic's .utl blocks, all requirement types, EarlyMatch/NeedsID
identify-avoidance, ComputedItemInfo, salvage-combine extra block; VTank's
host-side corpse selection (rare-first, fo.cs:436-446), approach/open/loot
rules, the ownership-denial chat regexes (fo.cs:71-73), timeouts. MossTank
gap ranked. Lead spot-check of those three claims.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
x.cs reader/writer (uTank2 NAV 1.2, route types, ten waypoint types incl.
the live-position quirk of five of them), ca.cs cycle driver, fd.cs steering
and creep band, bi.cs jump (2000 ms cap), b7.cs door/lockpick, priority
interactions, 240 m/unit confirmed at four sites, two real routes decoded.
MossTank gap ranked. Lead spot-check: header vs a real file, jump cap,
conversion sites.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Buff plan and per-tick re-evaluation, the profile-item enchant pipeline
(PluginCore.cs:8327-8445), helper heals/random helper, the nine recharge
thresholds and rule order, kits/potions/food, dispel and worn-item mana.
MossTank gap ranked. Lead spot-check: fk.b() dead check, ba.cs 100-draw
loop, MySpell.HasScarabsInInventory.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
~48 documented subcommands plus 15 parser-only debug verbs (no /vt pause),
the d5/ah chat sinks, the three-tier export model (public static PC,
permission-gated relay, LootPluginBase SPI) annotated against
MosswartMassacre's real usage, and the interop gaps in Plugin.Abstractions.
Lead spot-check: PC field, eExternalsPermissionLevel, start/stop parser.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
All 137 settings with type/default/category/UI control/consumer citation,
the self-describing .usd table grammar (parsed from the decompile and
verified against defaultsettings.usd), profile selection and /vt opt.
MossTank gap: no .usd reader/writer, BuffProfileDocument drops ten fields,
RechargeHandlerSet opaque; setting-name coverage is already 137/137.
Lead spot-check: f3 accessors, the four reader classes, catalog count.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
02: the single-winner priority list (24 rule classes, 45 instantiations),
the 293 ms heartbeat + event poke, the force-combat-mode gate and its
stuck-state recovery, IdlePeace in full, MossTank gap. Two draft errors
corrected by the lead against the source: GoToPeaceModeToUseKits exists
(a5.cs:121, defaultsettings.usd:931) and the fallback-wand list is
Items-page insertion order (eq.cs:83-94, PluginCore.cs:8422-8434).
03: target acquisition/selection, monster rules, weapon/damage/ammo, attack
execution, debuffs, pets, MossTank gap. Spot-checked: the hardcoded
debuff-kind order (hi.cs:123-168) and quality-before-UseArcs (hi.cs:509-535).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
156 controls across nine tabs plus the three secondary views, each with type,
geometry and bound setting where determinable; VVS control semantics our
markup must offer (multi-column lists with text/check/icon columns, combo,
notebook); window icon and StoredViewInfo facts; MossTank gap. Three cited
claims spot-checked against refs/ by the lead.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>