Commit graph

4626 commits

Author SHA1 Message Date
Erik
466272ec55 fix(plugin-ui): Slice B review fixes — no magenta for bad DIDs, contract threshold, integral icon bindings, nearest did art, loud markup errors
Two Opus reviews of commit 8217a349e (Slice B: DAT icons in plugin
markup) found one BLOCKING defect and 14 SHOULD-FIX findings. All 15
fixed here in one commit per the review contract.

BLOCKING (finding 1): an unresolvable did painted a magenta square.
TextureCache.GetOrUploadRenderSurface's 1x1 magenta placeholder for a
missing RenderSurface is load-bearing for authored chrome, but
RetailMarkupIconResolver.ResolveDid only short-circuited did==0, so any
other unresolvable id fell through to that placeholder and got scaled
up by UiMarkupIcon/UiMarkupList/UiSimpleButton -- the classic
resolve(0)-style footgun (claude-memory/feedback_ui_resolve_zero_magenta.md),
just triggered by a missing id instead of a literal 0. Fixed by probing
Portal/HighRes existence via IDatReaderWriter.TryGet<RenderSurface>
BEFORE ever calling GetOrUploadRenderSurface -- that TryGet already
serializes concurrent DAT access internally (DatDatabaseWrapper's own
_databaseLock), the same synchronization IconComposer.TryDecode relies
on, so no additional lock was needed. RetailMarkupIconResolver now
takes IDatReaderWriter + TextureCache directly (RetailUiAssets gained a
TextureCache field, its one construction site in
InteractionRetainedUiComposition.cs updated) instead of the old
resolveSprite delegate, since it also needs the nearest-sampled upload
path for finding 6 below.

Finding 2 -- Smoke panel wiring bugs: its list fed iconkind="spell"
raw art DIDs (PluginSpellInfo.IconId) instead of spell ids, so
IMarkupIconResolver.ResolveSpell composited the wrong (or no) badge
every row. SmokeIconPanel.Binding.SpellIds now yields SpellId (the
printed text still shows IconId alongside). The bare-index demo and the
descriptor both moved from the unverified literal 7735 to 0x165 --
retail's real Melee Defense skill icon (SampleData.cs:64,
0x06000165) -- so the owner's visual gate proves real art, not a guess.
StartVisible flipped true, and a character with no self-buffs known
falls back to spell 1's real catalog entry (or an honest "no spells
known" row with icon 0 if even that fails) rather than fabricating art.

Finding 3 -- PluginIcons.Normalize's threshold was silently rewritten
from the contract's 0x01000000 to 0x06000000 during Slice B. Restored
to 0x01000000; the class/method XML docs now state the number directly
(no cref to the private const); the test table adds 0x02000000 (a value
that only distinguishes the two thresholds) and 0x01000000 itself
(passes through unchanged).

Finding 4 -- an unknown iconkind (e.g. "spel") only threw when a
resolver happened to be wired, because BuildIconSource/
BuildRowIconResolve validated inside their own null-icons early return.
A new ValidateIconKind helper runs UNCONDITIONALLY before that branch,
so a malformed iconkind is a Build-time author error on every host.

Finding 5 -- BindUintLiteralOrBinding required an exact uint property
type, rejecting the int-typed bindings Decal-facing code commonly uses
(MosswartMassacre's HudPictureBox.Image is int end to end). It now
matches BindUint's existing leniency: any property, converted via
Convert.ToUInt32 at read time. BindUintList likewise now accepts
IEnumerable<int> alongside IEnumerable<uint> (unchecked per-element
reinterpret -- icon ids never go negative in practice).

Finding 6 -- TextureCache._renderSurfaceGpuTextures was keyed by id
alone, so whichever caller asked for a given RenderSurface id FIRST won
the sampler for every later caller of the same id -- UiDatFont's glyph
atlases already request nearest:true while ResolveChrome's background
art requests nearest:false, so this was a real, reachable collision,
not hypothetical. Rekeyed to (id, nearest); RetailMarkupIconResolver.
ResolveDid now requests nearest:true (pixel-exact 32x32 icon art);
ResolveChrome is untouched (still nearest:false/linear). Audited every
other _renderSurfaceGpuTextures use site (TryGetValue/set/Dispose
iteration+Clear) plus the separate _nearestUiTextureSources/
_linearUiTwinHandles/_uploadMetadata dictionaries (all keyed by handle
or accounting name, unaffected) -- no other eviction/accounting path
assumed id-only keying.

Finding 7 -- column-reservation semantics, per the DECIDED shape:
MarkupDocument now sets button.IconSource / list.IconIdsSource +
IconResolve ONLY when a resolver (icons parameter) is actually wired --
previously button.IconSource was always assigned (even to an
always-empty func on an icons:null host); combined with this finding's
other half -- UiSimpleButton.OnDraw now reserves its icon column
whenever IconSource is non-null, regardless of a per-frame resolve miss,
so a bound id that goes briefly to 0 no longer slides the caption back
and forth -- would have permanently reserved a blank column on such a
host. UiMarkupList already reserved its column whenever IconIdsSource
was set; no draw-side change needed there.

Finding 8 -- added a with/without-icons comparison test for
UiMarkupList (mirroring the existing UiSimpleButton one): asserts the
row text quad's x is strictly greater with an icon column present, and
the icon quad itself has non-zero width.

Finding 9 -- <icon tooltip=""> (empty string) was still treated as
"has a tooltip" by a bare attribute-presence check, making the icon
swallow clicks with no visible tooltip ever appearing. Now uses
!string.IsNullOrWhiteSpace, matching ApplyCommon's own predicate for
every other element's tooltip.

Finding 10 -- PluginShelfButton.OnDraw drew nothing when a non-zero
descriptor icon id resolved to no texture (a bad Decal index, a DAT id
from a different install), rather than falling back to Initials the
way a zero id already did. Now decides once, on the first draw
(memoized, so Initials' string work never repeats every frame): a
failed resolve permanently switches Text to the initials fallback,
computed and assigned BEFORE base.OnDraw actually paints the caption.

Finding 11 -- MarkupDocument.AddElement's switch had no default arm, so
an unknown or miscased element name (<Icon>, <butotn>) silently
vanished from the built tree instead of failing loudly like every
other malformed-markup case. Added a default arm that throws
FormatException. Ran AcDream.Plugins.MossTank.Tests (337/337,
unchanged) and the full App markup suite to confirm no existing markup
relies on an unknown element.

Finding 12 -- PluginPanelDescriptor.IconSurfaceId's XML doc now states
that a bare Decal index is accepted and normalized, citing
PluginIcons.Normalize.

Finding 13 -- docs/plugin-ui-markup.md: replaced the blanket "wrong
type/missing property throws at Build" sentence with the per-attribute
truth table the review produced (which attributes are silent at
runtime vs. throw at Build, and each one's bound CLR/delegate type);
restated the icon-id boundary as 0x01000000; added the "do NOT add
0x06000000 to the four already-full IconId records" warning (citing
SkillBase._iconID / UIRegion::SetImageByDID @0x004f150e); documented
that 0x-prefixed hex is required (an unprefixed all-digit literal
parses as decimal); noted unknown element names now throw; called out
list colors (0xRRGGBB) vs. color=/background=/border= (#AARRGGBB) as
non-interchangeable grammars; documented the root <panel visible>
binding-only exception; corrected the shelf's collapse toggle glyphs
(</>, not the old doc's arrows) and the 28px collapsed-tab size; added
the IconId record-equality API-v1 note; and called out iconkind as
per-<list> (mixed id spaces need pre-normalized DIDs; the composited
spell badge has no did-space escape hatch) plus the existing
one-text-column LIMITATION being deferred to MossTank.

Coverage added for finding 14: a PluginSidePanelTests case proving the
shelf button normalizes a bare descriptor index before resolving, and a
reflection-based unit on AppAutomationSurface.ProjectWorldObject (its
public callers gate on IsAvailable, which needs a fully connected
session heavier than this mapping needs -- the plan's own documented
fallback) proving PluginWorldObject.IconId carries ClientObject.IconId
through unchanged; PluginInventoryItem.IconId uses the identical
one-line pattern inline in CaptureOwnedItems, reviewed by inspection.

Finding 15: recorded a "Review ledger" section in the plan doc with
both slices' commits, both review verdicts, and the two items
explicitly deferred to the MossTank plugin work (multi-column list,
root literal visible).

Verification: full solution builds green. Targeted filter
(Markup|PluginSidePanel|PluginIcons|AppAutomation|TextureCache|
UiDatFont) passes 131/131, including the two InstalledDat-lane tests
(RetailMarkupIconResolverInstalledDatTests,
AppAutomationSurfaceIconInstalledDatTests) actually resolving against
the real installed DAT, not skipping. AcDream.Plugins.MossTank.Tests
passes 337/337 unchanged. Full AcDream.App.Tests suite: 7351 passed /
97 skipped / 36 failed -- identical failure set/count to the
7334/97/36 baseline (the +17 passes are exactly the new/expanded
tests: 2 new PluginIconsTests.Normalize theory rows, 10 new
MarkupIconTests cases, 2 new PluginSidePanelTests cases, 1 new
AppAutomationSurfaceTests case, and the 2 new standalone test files).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 15:43:56 +02:00
Erik
2ebcc01640 fix(plugin-ui): size the shelf grip from the DAT font and make the collapsed tab findable
Owner report: "I dont see the < after I minimize the window" — after
collapsing the plugin shelf with the > toggle, the < (expand) glyph was not
visible.

Root cause, established with a real-DAT draw probe (font 0x40000000) before
changing anything: '<' and '>' share IDENTICAL glyph metrics (OffsetY=4,
Width=5, Height=7, VerticalOffsetBefore=5), so there is no per-glyph
asymmetry to explain "I see one but not the other." Against the pre-fix 12px
grip band, the toggle's FILL glyph plane measured fully INSIDE the band in
both states (local y=[3,10] of [0,12]) — the "16px line box overhangs a 12px
band" theory alone does not erase the glyph, so a bare clip fix would not
have addressed the report. What IS true: the border-inflated OUTLINE
(background/shadow) plane, drawn first per retail's UIElement_Text::DrawSelf,
spans y=[-1,14] before clipping and was cropped by the band's self-clip
(UiElement.ClipsChildren) to exactly [0,12] — a real but minor defect. The
actual explanation for the report is discoverability: the collapsed shelf
shrank to a bare 24x12 near-black sliver at the screen edge, several times
smaller than any other clickable affordance in the UI — easy to overlook even
though its pixels were, in fact, being drawn.

Fix (src/AcDream.App/UI/PluginSidePanel.cs):
- ExpandedGripBandHeight (new internal property) derives the EXPANDED
  grip/toggle band from the real font metrics — max(12, font.LineHeight + 2)
  — so neither the fill nor the border-inflated outline plane can clip for
  any font; the 12px constant remains only as the bitmap-font fallback.
  Threaded through the ctor's initial Height, OnTick's row-wrap height calc,
  LayoutChrome, and Reflow's entry-Top/expanded-Height math.
- The COLLAPSED tab is now ButtonExtent (28px) tall instead of the 12px grip
  band — the same size as an ordinary entry button — with the toggle glyph
  filling and centering in the taller band. This is the actual fix for the
  report: the collapsed affordance is now button-sized and findable, not a
  bug-for-bug-identical-but-larger clip fix.

Tests:
- tests/AcDream.App.Tests/UI/PluginSidePanelToggleGlyphClipTests.cs (new,
  Lane=InstalledDat): loads the real DAT font, builds a live shelf, and
  proves via TextRenderer.DebugSpriteSegmentVerts that the toggle's ink (fill
  + outline) is fully contained in its own clip band in BOTH the expanded
  ('>') and real-click-collapsed ('<') states, plus that the collapsed tab is
  button-sized. Verified failing against the pre-fix code (git stash of just
  this file) with concrete numbers: computed unclipped span [-1,14] does not
  fit inside the 12px band; collapsed height measured 12 (not button-sized).
  Passes after the fix.
- tests/AcDream.App.Tests/UI/PluginSidePanelTests.cs: added
  CollapseThenExpand_WhileStillDocked_ReturnsToTheIdenticalLeftAndTop (the
  dock/anchor invariant survives the collapsed-height change), and derived
  Drag_StartingOnShelfPadding_DoesNotMoveTheShelf's press-below-the-grip-band
  Y coordinate from the new ExpandedGripBandHeight accessor instead of a
  re-hard-coded literal.

Verification: dotnet build (App + tests) green. Filtered run
(PluginSidePanel|UiDatFont|Markup|UiRootInput) 131/131 passed, 0 skipped —
the InstalledDat lane tests actually ran (DAT dir resolved). Full
AcDream.App.Tests suite: 7334 passed / 97 skipped / 36 failed (baseline was
7331/97/36) — the 3 new tests are the only delta; the 36 failed test names
are byte-identical to the pre-existing set (cathedral collision installed-dat
gates, alpha-flush conformance, layout live-mount probes, Linux frame-pacing,
credential resolver — all unrelated to this change).

No retail-divergence register change needed: IA-27 already covers the
plugin shelf's non-retail collapse-toggle glyphs/behavior in general; this
is a bug fix within that already-declared deviation, not a new one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 15:16:03 +02:00
Erik
8217a349e0 feat(plugin-ui): Slice B — DAT icons in plugin markup (icon element, button/list icons, plugin icon ids)
Owner request: plugin panels (Decal/VirindiViewService-class, per the
MosswartMassacre reference usage) need to embed real DAT icons the way
FlagTrackerView.SafeSetListImage does — spell/skill art, raw portal
indices, and a window icon. This is Slice B of
docs/plans/2026-09-06-plugin-shelf-and-dat-icons.md (Slice A, the
movable/collapsible shelf, landed in 01b98ca30/4fada238e/718005b21).

What shipped:

- AcDream.Plugin.Abstractions.PluginIcons.Normalize: the one Decal-style
  bare-index -> 0x06xxxxxx RenderSurface DID grammar, applied at every
  icon SINK (descriptor IconSurfaceId in PluginShelfButton, and markup
  <icon did>/<button icon>/<list icons> did-kind ids) rather than on the
  plugin-facing records, which already carry real DIDs read straight
  from the client's tables.
- PluginSpellInfo.IconId / PluginSkillInfo.IconId /
  PluginInventoryItem.IconId / PluginWorldObject.IconId: additive init
  properties (default 0), filled in AppAutomationSurface from
  SpellMetadata.IconId (already projected from SpellBase.Icon by
  RetailSpellMetadataProjector — no gap there), a new BindSkillIcons
  parallel to BindSkillNames (GameWindow reads
  DatReaderWriter.Types.SkillBase.IconId — confirmed via reflection over
  the installed Chorizite.DatReaderWriter package, since its XML docs
  don't cover Pack/Unpack-generated public fields: Description, Name,
  IconId (uint), TrainedCost, SpecializedCost, Category, ChargenUse,
  MinLevel, Formula, UpperBound, LowerBound, LearnMod), and
  ClientObject.IconId in CaptureOwnedItems/ProjectWorldObject.
- IMarkupIconResolver (AcDream.App.UI): ResolveDid/ResolveSpell/
  ResolveItem. MarkupDocument.Build gains an optional parameter (null by
  default -> every icon sink resolves to nothing rather than throwing,
  so pre-Slice-B callers/tests are unaffected). RetailUiRuntime.
  MountPlugins builds ONE RetailMarkupIconResolver per pass from
  RetailUiAssets.ResolveSprite + RetailUiAssets.Icons (the shared
  IconComposer) + Toolbar.Objects (the SAME ClientObjectTable
  Magic/Toolbar bindings already borrow for their own icon resolution —
  no second object lookup introduced).
- New UiMarkupIcon widget (<icon x y w h did|spell|item tooltip>):
  exactly one source required (FormatException at Build otherwise,
  matching every other malformed-attribute rule), aspect-preserved,
  centered, click-through unless a tooltip makes it a real hit-test
  target.
- UiSimpleButton.IconSource and UiMarkupList.IconIdsSource/IconResolve:
  additive, default null/no-op, so every existing button/list caller
  (including the plugin shelf's own toggle/minimize buttons) is
  unaffected. Button icon draws flush left and shifts the caption's
  centering region right; list icons reserve a leading RowHeight-2
  column (Decal's IconColumn) and skip rows whose id is 0 or
  unresolvable.
- MarkupDocument centralizes the did/spell/item dispatch (including
  PluginIcons.Normalize for did) in two small helpers (BuildIconSource
  for <icon>/<button>, BuildRowIconResolve for <list>) so all three
  markup surfaces share one resolver call path.
- AcDream.Plugins.Smoke ships a RegisterPanelContent (in-memory KSML,
  no plugin-side .xml file) proof panel exercising every new surface:
  a bare-index <icon>, a literal-hex <icon>, a composited <icon
  spell=...>, a <button icon=...>, and a <list icons=... iconkind=
  spell> of the first five known self-buffs with their IconId printed
  alongside. Descriptor IconSurfaceId reuses the same bare index to
  prove the shelf button and the panel's own icon normalize identically.
- docs/plugin-ui-markup.md is the new SSOT for the full markup
  vocabulary + icon grammar + the Slice A shelf; linked from
  docs/README.md and docs/plans/2026-04-24-ui-framework.md.

Design decisions where the plan left room:
- Normalize runs inside the resolver dispatch (BuildIconSource/
  BuildRowIconResolve), not scattered at each markup call site, so
  every did-kind sink shares one choke point.
- did/spell/item all accept either a literal (decimal or 0x-hex) or a
  {Binding}, via one BindUintLiteralOrBinding helper, for symmetry —
  the plan only showed spell/item as bindings but didn't forbid a
  literal.
- <icon> requires exactly one source INCLUDING zero (not just two);
  an icon with no source is not a coherent element.
- The button/list icon draw math (icon column extent, padding) lives
  in the widgets themselves (UiSimpleButton/UiMarkupList), not in
  MarkupDocument, keeping the parser only responsible for wiring
  Func<(tex,w,h)> sources.

Tests: PluginIconsTests (Normalize table), MarkupIconTests (icon/button/
list resolver dispatch via a fake IMarkupIconResolver, plus draw-level
pins via the RecordingGpuDevice/TextRenderer apparatus already used by
UiAncestorClipTests/UiRenderContextDrawStringDatOutlineTests — "draws
nothing when unresolvable" and "button/list icon shifts the text"),
and AppAutomationSurfaceIconInstalledDatTests (Lane=InstalledDat: a
known spell's IconId matches the real installed SpellTable's own Icon
field exactly). Verified every new test fails to COMPILE without this
change (git-stashed the src/ changes, rebuilt the test project: CS0246
on IMarkupIconResolver) before restoring. Full App suite: 7331 passed /
97 skipped / 36 failed (identical pre-existing failure set/count to the
7306/97/36 baseline; the +25 passes are exactly the new tests).
AcDream.Plugins.MossTank.Tests (the main consumer of the touched
Plugin.Abstractions records) passes 337/337 unchanged, confirming
API-v1 binary/source compatibility. Full solution builds green.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 14:52:23 +02:00
Erik
718005b210 fix(plugin-ui): Slice A residuals — padding-drag pin, live dock detection, single clamper, chord text, register row
Opus re-review of 4fada238e (Slice A review-fix round) left seven residuals.
All seven addressed here, in one commit per the residual-round contract:

NEW-1 (test-coverage): no test pinned the Draggable:true->false change
itself. Added Drag_StartingOnShelfPadding_DoesNotMoveTheShelf, pressing the
shelf's own PADDING (left of the first button's Left=4, below the 12px grip)
and dragging 100px through the real UiRoot press/move/release path. Proven
to fail against the pre-4fada238e shelf: temporarily setting Draggable=true
in the ctor and re-running this test moved the shelf from Left=760 to 764
(clamped to parent.Width-Width) instead of leaving it in place — reverted
after confirming.

NEW-2 (correctness): RetailWindowLayoutPersistence.ClampAllToScreen clamps
generically to screen.Width-handle.Width, 4px (OuterPadding) off what the
shelf's own dock formula (parent.Width-Width-OuterPadding) would produce for
the same new parent size. A screen shrink that triggers that clamp used to
flip _userPositioned permanently even though nothing a user would call
"dragging" happened. PluginSidePanel.OnHandleMoved now re-derives "still
docked" at comparison time as a function of the CURRENT parent size: either
the live dock formula's own result, or what ClampAllToScreen's clamp would
produce from the PREVIOUS docked position — only a position matching
NEITHER flips _userPositioned. New test
ClampAllToScreen_AfterShrinkingTheRoot_DoesNotFlipAnchoring_ButARealDragStillDoes
shrinks the root, runs ClampAllToScreen, confirms the shelf still anchors
top-right through a later reflow and a collapse, then confirms a genuine
grip drag afterward still flips anchoring.

NEW-3 (decision, documented): KeepWindowReachable's per-tick clamp and
ClampAllToScreen's screen-resize sweep both touch plugin windows. Evidence
gathered and recorded as a doc comment on KeepWindowReachable:
RetailWindowManager.MoveTo (line 178) already short-circuits an unchanged
position before ever raising Moved; persistence's ScreenSize and Host.Root's
size derive from the same d.Window.Size and are reconciled every frame
(UiHost.Draw sets Root.Width/Height from screenSize); and because
RetainedGameplayUiFrame.Render ticks BEFORE it draws in the same frame,
ClampAllToScreen always resolves a screen-resize's clamp before
KeepWindowReachable ever observes the new size next tick, making
KeepWindowReachable's own pass a structural no-op for that case (never a
second write). KeepWindowReachable is NOT dropped, though: it is the only
reachability guarantee for (a) a plugin window whose geometry is mutated
directly rather than through MoveTo — pinned by the pre-existing
FullWidthPluginWindowStartsAndStaysReachableAtMinimumCanvas test, which
exercises exactly that with no screen resize at all — and (b) a host wired
with no RetailWindowLayoutPersistence at all. Decision: keep both; they do
not race.

NEW-4 (robustness): RetailWindowLayoutPersistence._attached is mutated
mid-session by WindowRegistered/WindowUnregistered (a callback invoked from
inside one of these loops — e.g. Apply -> Show()/Hide() -> a controller
unregistering another window — could otherwise mutate the list being
enumerated). All five bare `foreach (... in _attached)` loops
(RestoreAllCore, ClampAllToScreen, SaveAll, SaveNamed, RestoreNamed) now
snapshot with .ToArray(), matching Dispose's existing pattern.

NEW-5 (retail-faithfulness): the plugin-shelf hide message hard-coded
"Shift+Ctrl+F1" even though InputAction.TogglePluginManager is rebindable
through Configure Keyboard. RetailUiRuntime.PluginShelfHiddenMessage now
looks up the CURRENT binding via the live InputDispatcher and formats it
through Layout.RetailKeyNames.Describe — the exact formatter Configure
Keyboard's own row captions already use
(Layout/KeyboardConfigController.cs:284) — falling back to an honest
"bind it in Configure Keyboard" message when the action is unbound or no
dispatcher is wired.

NEW-6 (bookkeeping): added divergence-register row IA-27 for the plugin
shelf's repurposing of retail's plugin-manager chord (Shift+Ctrl+F1),
its ASCII </> toggle glyphs, its two acdream-authored system messages, and
its Draggable=false grip-only drag model — citing PluginSidePanel.cs and
the RetailUiRuntime.cs TogglePluginManager case. Header count bumped
23->24 active IA rows.

NEW-7 (correctness): the one-time dock wrote Left directly, so the
first-run docked position went unsaved until some later, unrelated event
happened to trigger a save. PluginSidePanel.OnTick now routes the one-time
dock through the retained-window handle's MoveTo when registered (falling
back to the direct field write when unregistered, as before) —
_dockLeft/_dockTop are set to the TARGET position BEFORE calling MoveTo so
the synchronous OnHandleMoved re-entry it triggers (NEW-2's logic) reads the
new dock position and does not flip _userPositioned. New test
FreshShelf_OneTick_SavesTheDockedPositionImmediately confirms the store has
the docked X/Y after exactly one tick and that a subsequent collapse still
preserves the right (not left) edge.

Verification: dotnet build src/AcDream.App (Release) green; targeted filter
(PluginSidePanel|RetailWindowLayout|Markup|UiRootInput|KeyboardConfig) 145
passed / 2 pre-existing Lane=Manual failures (unrelated, gated on
ACDREAM_PROBE_LIVE_MOUNT=1); full tests/AcDream.App.Tests suite: 7306
passed / 97 skipped / 36 failed — same 36 pre-existing failures by name
(installed-DAT/Cathedral/Linux/Lane=Manual probe tests), +3 passed vs the
7303 baseline (exactly the three new tests this round added).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 14:21:31 +02:00
Erik
4fada238e6 fix(plugin-ui): Slice A review fixes - real grip/toggle children, dialog z-order, persisted collapse/hide intent
Two independent Opus reviews of 01b98ca30 (Slice A - movable, collapsible
plugin shelf) found blocking design and behavior issues plus several
should-fix gaps. All addressed in this commit:

1. Grip/toggle are now REAL children instead of a drawn band + a
   cursor-position-dependent HandlesClick override + an OnEvent toggle
   hack: ShelfGripPanel (WindowMoveHandle=true) spans the top band minus
   the toggle width; a UiSimpleButton toggle sits beside it (HandlesClick
   already wins at UiRoot.OnMouseDown before the Draggable-window
   fallback). The shelf's own Draggable is now FALSE - verified against
   UiRoot.FindDragHandleWindow, which never reads a window's own
   Draggable flag at all (it walks for a WindowMoveHandle ancestor-or-self
   then climbs to the nearest child of UiRoot), so Draggable=true was
   never required for the grip to work and only armed the whole-window-
   drag fallback for clicks on the shelf's own padding - exactly the
   behavior the review asked NOT to have. The two Assert.Single(shelf.
   Children) test sites now filter by the (now internal) PluginShelfButton
   type instead of asserting child count.

2. Deleted the per-tick "always highest ZOrder" raise in OnTick. It fought
   RetailDialogFactory.Tick's own per-frame dialog re-raise, so a dialog
   opened while the shelf was visible could never end up on top of it.
   Registration's ordinary press-to-raise (a grip press calls
   BringToFront before the drag starts) remains.

3. Collapse and hide/show intent are now persisted through their own
   channel. RetainedWindowState gained a nullable RequestedVisible;
   RetailWindowHandle gained an internal StateChanged event that
   RetailWindowLayoutPersistence subscribes to (alongside Moved/Resized/
   Shown/Hidden) and that the shelf raises after a collapse toggle or
   Show/Hide. Capture() now persists state.RequestedVisible (the panel's
   own intent) instead of the derived IsVisible, so an availability hide
   (last plugin window unregistered) is never mistaken for a user hide.
   WindowNames.PluginShelf is now one of RetailUiRuntime's state-managed
   visibility windows, so Apply() restores the intent through
   RestoreWindowState directly rather than via Show()/Hide().

4. RetailWindowLayoutPersistence now subscribes to
   RetailWindowManager.WindowRegistered/WindowUnregistered so a window
   (a plugin window loaded after startup, or the shelf on any path that
   constructs persistence first) attaches even when it registers after
   persistence already exists.

5. Reflow()'s default height argument is now nullable and falls back to
   the last height OnTick actually measured (or unbounded if none yet),
   instead of always forcing a single-column layout - every call site
   OTHER than OnTick's own row-wrap (Add, unregister, Show/Hide, the
   collapse toggle, RestoreWindowState) used to collapse a wrapped
   multi-column layout to one column for a frame.

6. _userPositioned is now flipped only when the handle's position differs
   from the recorded dock placement, not on every RetailWindowHandle.
   Moved (which fires unconditionally on any completed window-drag
   release, including a zero-movement grip click, and on any
   ClampAllToScreen reachable-clamp).

7. New tests cover: a press+drag starting on an entry button does not
   move the shelf; the removed per-tick raise (a sibling with higher
   ZOrder keeps it after a tick); TogglePluginManager's hidden-and-
   collapsed -> shown-and-expanded / visible -> hidden transitions at the
   shelf API (no RetailUiRuntime construction harness exists in this test
   suite to exercise the action-routing switch itself - the "no plugin
   windows registered" message branch is therefore not covered here).

8. The hide branch of TogglePluginManager now displays "Plugin shelf
   hidden. Press Shift+Ctrl+F1 to show it again."; the show branch stays
   silent.

9. The collapse toggle now draws ASCII '<'/'>' instead of the DAT-font-
   dependent '«'/'»' glyphs (the only use of those code points in the App
   UI, silently dropped by UiDatFont when absent), and gets the same
   DatFont + bitmap fallback the shelf's entry buttons already have
   through UiSimpleButton. A new installed-DAT test pins that the default
   font actually carries both ASCII glyphs.

10. ResizeX/ResizeY are false on the shelf so a restored layout's saved
    dimensions can never stomp the derived Width/Height via ResizeTo.

11. WindowNames.PluginShelf replaces the "plugin-shelf" literal at every
    site (RetailUiRuntime, docs comments, tests).

12. The grip dims to half opacity while RetailWindowManager.IsLocked, the
    same visual cue every other retail window gets (the shelf's grip has
    no DatElementId, so RetailWindowLockPresentationController's authored-
    chrome dimming does not reach it on its own).

Every new test was verified to fail against the pre-fix source: reverting
src/AcDream.App/UI/PluginSidePanel.cs, IRetainedWindowStateController.cs,
RetailWindowHandle.cs, WindowNames.cs, and RetailUiRuntime.cs to their
01b98ca30 state makes the whole PluginSidePanelTests.cs file fail to even
compile (missing WindowNames.PluginShelf, the now-internal
PluginShelfButton type, and RetainedWindowState.RequestedVisible);
reverting RetailWindowLayoutPersistence.cs alone (fixed source elsewhere)
makes WindowRegisteredAfterConstruction_StillRoundTrips fail at runtime
with a null saved layout, confirming finding 4 in isolation.

Verified: dotnet build src/AcDream.App (Release) green; dotnet test
tests/AcDream.App.Tests (Release) targeted filter
(PluginSidePanel|RetailWindowLayout|Markup|UiRootInput) 105/105 green;
full suite 7303 passed / 97 skipped / 36 failed (identical failure set to
the pre-fix-round baseline - installed-DAT live-mount probes, Linux-only
pacing/credential tests, and alpha-flush COUNT-only conformance
divergences, none touching plugin UI; +9 tests, all passing, over the
prior 7294/97/36 baseline).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 13:51:25 +02:00
Erik
b41ccc87a0 docs(plugin-ui): plan the movable plugin shelf and DAT-icon markup slices
Owner pivot 2026-09-06: before MossTank feature work resumes, the plugin
shelf must be movable and easy to hide, and plugin markup must embed DAT
icons the way Decal/VirindiViewService plugins (MosswartMassacre) do.
This plan is the contract for Slice A (shelf) and Slice B (icons): verified
current-state facts, behavior, files, tests, review lenses, and scope.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 13:13:32 +02:00
Erik
01b98ca30c feat(plugin-ui): Slice A - movable, collapsible plugin shelf
Owner request (docs/plans/2026-09-06-plugin-shelf-and-dat-icons.md, Slice A):
the plugin shelf was pinned to the right screen edge every tick, had no drag
or hide affordance, and never persisted position. It is now a registered
retained window ("plugin-shelf") that gets drag, the UI lock, and
RetailWindowLayoutPersistence position/visibility/collapsed persistence for
free, the same way every other retained window does.

Design decisions where the plan left room:

- Grip + collapse toggle are drawn, not child elements. A real child for the
  toggle would have to live outside any WindowMoveHandle grip subtree (nesting
  it inside lets UiRoot's drag-handle promotion swallow the press before the
  button ever sees a click - `handleWindow is not null` outranks
  `HandlesClick` in UiRoot.OnMouseDown), and a grip element as a plain sibling
  changes UiElement.Children's shape, which the pre-existing single-button
  shelf tests assert directly (`Assert.Single(shelf.Children)`). Keeping the
  whole shelf Draggable=true and excluding just the toggle's pixel rect from
  an overridden HandlesClick (computed live from UiRoot.MouseX/MouseY, the
  only call site) gets grip-drags/buttons-and-toggle-don't without adding any
  child or touching the existing tests' shape assumptions.
- Availability (has plugin windows) vs the user's requested-visible intent
  are split the same way PluginWindowVisibilityController already splits it
  for individual plugin windows, but applied SYNCHRONOUSLY (not via
  VisibleSource) so Visible updates immediately after Add()/unregister with
  no dependency on a Tick ever running - required to keep the pre-Slice-A
  unregistered-shelf test (ShelfAndMinimizeButtonsHideAndRestoreWithoutUnregisteringWindow)
  green, since it never calls root.Tick().
- "The shelf was moved" (drag or a differing restored layout) is tracked via
  the shelf's own RetailWindowHandle.Moved event, captured through
  WindowManager.WindowRegistered the moment MountPlugins registers it - so
  unregistered/legacy use (the two other pre-existing tests) never sets this
  and behaves exactly as before.
- The one-time right-edge dock (no saved layout) fires on the first OnTick
  with a real parent width, replacing the old per-tick pin; Reflow's
  anchor math then preserves the top-right corner while still docked or the
  top-left corner once positioned, on any width change (entry add/remove,
  collapse, or a parent-height-driven column rewrap).

Caption finding: the Configure Keyboard row for InputAction.TogglePluginManager
resolves its label live from the installed DAT's action-map string table
(KeyboardConfigController.BuildActionRow, RetailActionMapRow.LabelHash) -
there is no "Plugin Manager" string literal anywhere in our code to rename to
"Plugin Shelf". The row keeps showing retail's own authored name; only the
acdream-side action semantics changed.

Tests added to PluginSidePanelTests.cs (all 7 fail to even compile against
the pre-Slice-A PluginSidePanel, verified by temporarily reverting the source
files and re-running): default right-edge dock; top-right corner preserved
across a Reflow-driven width change while docked; grip drag moves the shelf
and top-left survives the next reflow once positioned; drag refused under
UiLocked; collapse via the real toggle click round-trips through
CaptureWindowState/RestoreWindowState; Show/Hide toggle sequence and a hidden
shelf staying hidden when a new plugin window registers; a full
RetailWindowLayoutPersistence round trip of X/Y/Visible/Collapsed onto a
fresh shelf instance. All 3 pre-existing tests remain green unmodified.

Verified: dotnet build src/AcDream.App (Release) green; the full App test
suite passes 7294/97 skipped/36 pre-existing unrelated failures (identical
failure set confirmed present on HEAD before this change - installed-DAT
live-mount probes, Linux-only pacing/credential tests, and known alpha-flush
COUNT-only conformance divergences, none touching plugin UI).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 13:11:43 +02:00
Erik
0275b4ca5a fix #485: isolate console capture and align Gitea portable test lanes
All checks were successful
CI / linux-portable (push) Successful in 3m40s
CI / windows-gate (push) Successful in 6m16s
CI / release (push) Successful in 2m13s
2026-09-06 11:41:27 +02:00
Erik
3ebb120dd2 fix #483: restore far terrain and close OVERHAUL with #484 deferred
Some checks failed
CI / linux-portable (push) Successful in 3m36s
CI / windows-gate (push) Failing after 6m52s
CI / release (push) Has been skipped
2026-09-06 10:22:51 +02:00
Erik
e0ba3e7f2c docs(overhaul): bound far-terrain repair and owner-first test order 2026-09-06 09:21:02 +02:00
Erik
c775eea166 docs(overhaul): prepare owner G4 checklist and reconcile active status 2026-09-05 21:46:06 +02:00
Erik
64d7a3981a docs(overhaul): seal final visual controls and retain shutdown failure 2026-09-05 21:40:59 +02:00
Erik
89a9a7f2ac docs(overhaul): verify final lifecycle reconnect and R6 stability gates 2026-09-05 21:16:26 +02:00
Erik
cf1bc84033 docs(overhaul): record final C1a gates and live execution bounds 2026-09-05 20:56:33 +02:00
Erik
eea83ac884 test(overhaul): pin verified cathedral geometry and membership 2026-09-05 20:44:30 +02:00
Erik
00ea65a164 docs(overhaul): seal matching cathedral capture and mutation release 2026-09-05 20:33:19 +02:00
Erik
66825e0bb8 test(overhaul): integrate guarded cathedral geometry witness 2026-09-05 20:29:32 +02:00
Erik
1e645ee404 fix(physics): port ordered part-array building transit 2026-09-05 20:28:52 +02:00
Erik
4e4e8e8253 docs(overhaul): bound nonempty geometry witness integration
Prepare the original unpinned witness with an explicit cathedral-shell non-vacuity gate. Write-only while the separate production bridge owns test execution; preserve the scratch and its failed diagnostics.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
2026-09-05 20:17:35 +02:00
Erik
4c3aafcd07 docs(overhaul): resolve malformed building reciprocal guard
Use the existing InvalidDataException content-integrity boundary for a loaded destination's out-of-range positive reciprocal index. Preserve negative/unavailable skips; record the managed guard with AP-159 at implementation landing.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
2026-09-05 20:14:34 +02:00
Erik
f72d471a31 docs(overhaul): bound retail part-array building bridge repair
Port the proven missing box admission and immediate destination transit, preserving sphere callers and separate registered source/equality residuals. Keep the failed authored-input diagnostic and original golden untouched.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
2026-09-05 20:12:31 +02:00
Erik
846cdd94b3 docs(overhaul): establish cathedral ramp bridge rejection evidence
Preserve the three explicit diagnostic outcomes and all18 outgoing edges. Native box containment rejects the extra room; five later-part sphere-input alarms remain red and nondecisive for these edge results. Correct AD-117's disproven widening guarantee. No production geometry or golden change, FPS remains deferred, G4 unpassed.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
2026-09-05 20:11:17 +02:00
Erik
f9bb47ec63 feat(diagnostics): retain bounded local crash reports (#477)
Project the original Run failure with loaded build, cached GPU and cell context before unwind. Preserve crash status and rethrow; contain report failures and omit arbitrary messages, source paths and session data.

Focused 19/19, narrow production/privacy review and default Release 17044/0/0 pass. Preserve the wrong-package smoke failure and real emitted report; corrected recipe10 smoke exits gracefully with two provisional PNG passes. AMD initiating cause and G4 remain open; FPS deferred. No new retail behavior deviation.
2026-09-05 19:49:22 +02:00
Erik
9baef7c507 docs(overhaul): record extended stability and bound ramp diagnostic 2026-09-05 19:24:41 +02:00
Erik
051db2e3bb docs #477: record active extended cathedral reproduction 2026-09-05 19:00:31 +02:00
Erik
f4da814af6 fix #477: preserve fatal swapchain operation and result
Native-boundary injection: old code 9 pass / 6 expected Assert.Throws failures. Separate acquire and present sabotage each fail their 3 fatal cases; restored focused gate 82/82. One independent API/production review PASS. Lead locked Release 0W0E; literal-hermetic 17051/0/0, manifest32/32. No retirement, recovery, normal result policy or retail behavior changes. This is first-failure evidence preservation, not an AMD reset fix; extended reproduction and G4 remain open.
2026-09-05 18:57:08 +02:00
Erik
c8ec04e97d docs #477: bound first-failure reporting and extended reproduction 2026-09-05 18:49:22 +02:00
Erik
cd888a4932 fix #482: restore neighboring outdoor wall collision
Retain building and object collision for available neighboring landcells when the terrain polygon does not cover the sphere center. Verified against named and paired retail dispatch. Add prepared-flat regressions and installed cathedral repro with sabotage proof. Full Release gate 17036/0/0 and graphical wall blocking, overlap correction and escape pass provisionally. Owner accepts functional collision; exact retail settle coordinates are not claimed. Retire UN-10; keep AP159, AMD stability and final G4 open.
2026-09-05 18:42:31 +02:00
Erik
508c6014ec docs(overhaul): capture retail cathedral ramp membership 2026-09-05 18:01:10 +02:00
Erik
620540aba2 docs(overhaul): record cathedral GPU recurrence and isolated checks 2026-09-05 17:01:44 +02:00
Erik
f4bf5ffab8 Record C1a bridge scope finding without changing production or golden 2026-09-05 15:37:23 +02:00
Erik
b7cb6886c5 Record final lighting correctness lanes and unchanged remaining gates 2026-09-05 15:33:49 +02:00
Erik
b2bdb09ccb Align final test expectations with reviewed lighting repair 2026-09-05 15:29:17 +02:00
Erik
6349f0911e Record final lighting and portal gates; bound stale test expectations 2026-09-05 15:24:41 +02:00
Erik
0ae2666ef0 Finish bounded lighting-observer cleanup and record verification 2026-09-05 15:15:59 +02:00
Erik
2f192570a2 docs(overhaul): reconcile owner issue status and FPS deferral 2026-09-05 15:07:01 +02:00
Erik
1772c1a5a2 docs(overhaul): record repaired lighting gate and activate observer cleanup 2026-09-05 15:05:11 +02:00
Erik
a5debaca2b fix(overhaul): integrate reviewed room-light selection repair
Exact26 code/test/architecture/register blobs from621b41fa3; campaign ledger and lead verification included. Independent retail and production/lifetime/ABI reviews PASS. Lead69Core/176App/2actualshader pixels, viewer/clear/NaN negative controls fail as intended, exact restoration69PASS. AP68retired; AP16/35/85 residuals honest. Fresh campaign Release and graphical lighting proof still owed; temporary observer cleanup contract conditional. FPS deferred; no G4 or main merge.
2026-09-05 14:57:52 +02:00
Erik
e8efe1131f test(overhaul): prepare the bounded dungeon lighting controls 2026-09-05 14:18:56 +02:00
Erik
4bc208b574 docs(overhaul): contract the observed dungeon room-light repair 2026-09-05 14:14:24 +02:00
Erik
c42f66f167 docs(overhaul): flag obsolete Town Network light-flood fix guidance 2026-09-05 14:05:41 +02:00
Erik
491e8adefa docs(overhaul): confirm dungeon static-light starvation in live frame 2026-09-05 14:02:10 +02:00
Erik
59b0b6e72f test(overhaul): integrate reviewed consumed-light checkpoint observation 2026-09-05 13:56:49 +02:00
Erik
ad866b7f65 docs(overhaul): verify light insertion order against retail binary 2026-09-05 13:46:14 +02:00
Erik
6c3ffd79ec docs(overhaul): bound consumed-light observation for dark dungeons 2026-09-05 13:43:01 +02:00
Erik
7724b63116 docs(overhaul): record graphical passes and remaining owner findings 2026-09-05 13:41:25 +02:00
Erik
1e376c244b fix(overhaul): integrate reviewed resize capture sequencing 2026-09-05 13:26:06 +02:00
Erik
58185285d3 docs(overhaul): bound resize automation frame-boundary correction 2026-09-05 13:18:36 +02:00
Erik
f6285e0edb docs(overhaul): record held-item pass and failed first-resize gate 2026-09-05 13:17:00 +02:00
Erik
0ca10cf249 fix(overhaul): integrate completed-frame capture and host visibility 2026-09-05 13:09:55 +02:00