acdream/docs/superpowers/specs/2026-06-20-d2b-inventory-grid-mount-design.md
Erik aa60615912 docs(D.2b-B): B-Grid design spec — inventory sub-window mount + grid mode
First step of Sub-phase B (inventory window). Dumped LayoutDesc 0x21000023:
the three nested panels (paperdoll/backpack/3D-items) are Type-0 pure-
container leaves that nest via the EXISTING BaseElement+BaseLayoutId
inheritance path (BaseLayoutId → 0x21000024/22/21), NOT game-class Types as
the research agent claimed. ElementReader.Merge drops base children, so they
import empty today. Mount = a surgical ~4-line LayoutImporter.Resolve change:
a childless, media-less inheritor attaches its base's resolved subtree
(predicate ShouldMountBaseChildren, unit-testable; inert for media-bearing
inheritors + childless style prototypes, so vitals/chat/toolbar are
unaffected). Plus UiItemList N-cell grid mode (single-cell toolbar preserved).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 22:25:50 +02:00

235 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Design — D.2b Sub-phase B-Grid (inventory sub-window mount + UiItemList grid mode)
**Date:** 2026-06-20
**Phase:** D.2b retail-UI arc, Sub-phase **B** (inventory window), step **B-Grid** — the first of
four (B-Grid → B-Controller → B-Wire → B-Drag). Decomposition approved 2026-06-20.
**Status:** approved design shape, dat-grounded, pre-implementation.
**Predecessor:** Sub-phase A (window manager) shipped; F12 toggles a hidden placeholder window
registered as `WindowNames.Inventory`. B-Grid makes that window show the real nested frame.
---
## 1. Context & goal
B-Grid makes `LayoutImporter.Import(0x21000023)` (gmInventoryUI) produce the **full nested
inventory frame** and gives `UiItemList` an **N-cell grid** so item cells tile. After B-Grid,
F12 shows the real bordered inventory frame — outer chrome + a backpack strip + a 3D-items area
+ an (empty) paperdoll panel — replacing the placeholder. Cells are empty until B-Controller
populates them; paperdoll *content* is Sub-phase C.
**Goal is structural, not interactive:** the frame renders with the correct nested geometry and
a working grid widget. No wire, no controller, no drag (those are B-Wire / B-Controller / B-Drag).
---
## 2. The dat-grounded finding (why this design)
Dumping `LayoutDesc 0x21000023` (via `AcDream.Cli dump-vitals-layout`) overturned the research
agent's "game-class Type + recursive Import" model. The actual structure:
- Root `0x100001CC`, Type `0x10000023` (gmInventoryUI), 300×362. Children:
- `0x100001CD` paperdoll panel — **Type 0**, `BaseElement 0x100001D4`, **`BaseLayoutId 0x21000024`**, childless, **no own media**, 224×214 @ (0,23)
- `0x100001CE` backpack panel — **Type 0**, `BaseElement 0x100001C8`, **`BaseLayoutId 0x21000022`**, childless, **no own media**, 61×339 @ (239,23)
- `0x100001CF` 3D-items panel — **Type 0**, `BaseElement 0x100001C4`, **`BaseLayoutId 0x21000021`**, childless, **no own media**, 234×120 @ (0,237)
- `0x100001D2` close button — Type 0, inherits, **has own media** (Normal/Normal_pressed)
- `0x100001D3` title — Type 0, inherits, **has own media** (DirectState)
- `0x100001D0` backdrop, `0x100001D1` bottom rule — Type 3, `BaseElement 0`, own media
So the three panels are **Type-0 pure-container leaves that nest via the existing
`BaseElement`+`BaseLayoutId` inheritance path** — *not* game-class Types. Their content lives in
the separate layouts `0x21000024/22/21`.
`LayoutImporter.Resolve` already loads `BaseLayoutId`, finds `BaseElement`, recurses it, and
`Merge`s — **but** `ElementReader.Merge` (`ElementReader.cs:162`) sets
`Children = new List(derived.Children)`, dropping the base's children. So today the panels import
as **empty containers**. The fix is a surgical attach of the base's resolved children to the
panel — not a new Type-dispatch mechanism.
---
## 3. Scope
**In scope (B-Grid):**
- **Sub-window mount** — `LayoutImporter.Resolve` attaches a base element's resolved children to
a childless, media-less inheriting element (the panels), so `Import(0x21000023)` yields the
full nested tree.
- **`UiItemList` grid mode** — column count + cell pitch so `AddItem`/`OnDraw` tile cells in a
grid; single-cell (toolbar) behavior preserved.
- Unit tests; a regression guard that vitals/chat/toolbar import unchanged.
**Out of scope (deferred):**
- Populating cells from `ClientObjectTable`, the burden meter, find-by-id binding → **B-Controller**.
- Inventory wire gaps (`DropItem`/`ViewContents`/…) → **B-Wire**.
- Inventory cell as a drag source → **B-Drag**.
- Paperdoll *content* rendering (the `UiViewport` doll) → **Sub-phase C**. (B-Grid's mount will
pull in the paperdoll panel's equip-slot frames as ordinary elements, but the 3D doll viewport
is C.)
- `0x10000032` (UiItemSlot) factory registration — **not needed**: `UiItemList.ConsumesDatChildren`
is true, so the importer skips cell templates; cells are built procedurally.
- Cell pitch *values* + exact column counts per panel — read by **B-Controller** from the nested
layouts at bind time. B-Grid supplies the mechanism, not the numbers.
---
## 4. Component 1 — sub-window mount (`LayoutImporter.Resolve`)
Restructure `Resolve` to capture the base's children and attach them when the derived element is a
pure container that inherits from a base with content:
```csharp
private static ElementInfo Resolve(
DatCollection dats, ElementDesc d, HashSet<(uint, uint)> baseChain)
{
var self = ToInfo(d);
var result = self;
List<ElementInfo>? baseChildren = null;
if (d.BaseElement != 0 && d.BaseLayoutId != 0
&& baseChain.Add((d.BaseLayoutId, d.BaseElement)))
{
var baseLd = dats.Get<LayoutDesc>(d.BaseLayoutId);
var baseDesc = baseLd is null ? null : FindDesc(baseLd, d.BaseElement);
if (baseDesc is not null)
{
var baseInfo = Resolve(dats, baseDesc, baseChain);
result = ElementReader.Merge(baseInfo, self);
baseChildren = baseInfo.Children; // capture the base's resolved subtree
}
}
// Derived's own children (authoritative when present).
foreach (var kv in d.Children)
result.Children.Add(Resolve(dats, kv.Value, new HashSet<(uint, uint)>()));
// Sub-window mount: a PURE-CONTAINER leaf (no own children AND no own media) that inherits
// from a base WITH content attaches the base's subtree. This targets the gmInventoryUI
// panels (0x100001CD/CE/CF — Type-0, media-less, childless, BaseLayoutId → a gm*UI window)
// and is inert for: media-bearing inheritors (close button/title keep their own media,
// so self.StateMedia is non-empty → skipped), normal elements with their own children
// (result.Children already populated → skipped), and childless style-prototype inheritors
// (vitals/chat/toolbar text — the base prototype has no children → baseChildren empty).
if (LayoutImporter.ShouldMountBaseChildren(d.Children.Count, self.StateMedia.Count, baseChildren?.Count ?? 0))
result.Children.AddRange(baseChildren!);
return result;
}
/// <summary>True when a pure-container leaf should inherit its base's subtree (sub-window mount):
/// the derived element has no own children, no own state media, and the base resolved to ≥1 child.</summary>
internal static bool ShouldMountBaseChildren(int derivedChildCount, int derivedMediaCount, int baseChildCount)
=> derivedChildCount == 0 && derivedMediaCount == 0 && baseChildCount > 0;
```
The predicate is extracted as `ShouldMountBaseChildren` so it is **unit-testable without dats**.
**Coordinate correctness:** the panel inherits the base window-root's fields via `Merge` (so it
gets that window's size) and the base's children via the mount. The children's X/Y are relative to
the panel; `ScreenPosition` composes parent offsets, so a child at panel-local (x,y) with the panel
at (0,23) lands at the right screen pixel.
**Cycle safety:** `baseChain` already guards the inheritance recursion; the nested layouts are
distinct ids, so no cycle.
---
## 5. Component 2 — `UiItemList` grid mode
Add a column count + cell pitch; keep single-cell as the default.
```csharp
public int Columns { get; set; } = 1; // grid columns; 1 = single column
public float CellWidth { get; set; } // 0 = "fill the list" (single-cell legacy)
public float CellHeight { get; set; }
```
Layout rule (applied in `AddItem` and re-applied in `OnDraw` so a list resize reflows):
- **Fill mode** (`CellWidth <= 0`): the single cell fills the list — `cell[0]` at
`(0, 0, Width, Height)`. Unchanged toolbar behavior.
- **Grid mode** (`CellWidth > 0`): `cell[i]` at `(col·CellWidth, row·CellHeight, CellWidth,
CellHeight)` where `col = i % Columns`, `row = i / Columns`.
The toolbar constructor keeps adding its one cell with `CellWidth = 0` (defaults), so it stays in
fill mode. B-Controller sets `Columns` + `CellWidth/CellHeight` (read from the nested layout's
ItemList element + cell template) and `Flush()` + `AddItem()`s the grid cells.
The grid math is a pure helper for testing:
```csharp
internal static (float x, float y) CellOffset(int index, int columns, float cellW, float cellH)
{
int col = index % columns, row = index / columns;
return (col * cellW, row * cellH);
}
```
---
## 6. Testing
**Grid mode (pure, `tests/AcDream.App.Tests/UI/`):**
- `CellOffset(4, 3, 36, 36)` ⇒ `(36, 36)` (index 4 → col 1, row 1).
- Build a `UiItemList { Columns = 3, CellWidth = 36, CellHeight = 36 }`, `AddItem` 7 cells, assert
`GetItem(4)` is at `Left == 36 && Top == 36 && Width == 36`.
- Single-cell legacy: a default `UiItemList` (CellWidth 0) keeps its cell at the list size after
`OnDraw` — assert the existing toolbar behavior is unchanged.
**Mount predicate (pure):**
- `ShouldMountBaseChildren(0, 0, 5)` ⇒ true (panel: childless, media-less, base has 5 kids).
- `ShouldMountBaseChildren(0, 1, 5)` ⇒ false (close button: has own media).
- `ShouldMountBaseChildren(2, 0, 5)` ⇒ false (has own children).
- `ShouldMountBaseChildren(0, 0, 0)` ⇒ false (style prototype: base childless — vitals text).
**Regression guard (hard acceptance):** vitals (`0x2100006C`), chat (`0x21000006`), and toolbar
(`0x21000016`) must import + render unchanged. Their inherited bases are childless prototypes, so
the mount is inert for them — but this is verified, not assumed (the existing importer tests +
the visual check).
---
## 7. Divergence register
**No new row.** The mount makes `BaseElement`/`BaseLayoutId` inheritance carry the base's content
subtree, which is what retail must do for the gmInventoryUI panels to render at all (they are
childless in the dat — the content is only reachable through the base reference). This is
*completing* faithful inheritance, not diverging from it. IA-12 (toolkit-defined UI) already
covers the importer's reconstruction of keystone semantics. If a future retail-decomp pass shows
retail's child-inheritance differs in the children-present merge case (which no inventory element
exercises), that is a separate, later concern.
---
## 8. Acceptance criteria
- `dotnet build` green; `dotnet test` green (grid + mount-predicate tests added).
- `Import(0x21000023)` yields a tree where the three panels (`0x100001CD/CE/CF`) each have
children (the nested layout content), confirmed by a dat-backed assertion or the dump.
- Vitals/chat/toolbar import + render unchanged (regression guard).
- With `ACDREAM_RETAIL_UI=1`, F12 shows the real nested inventory frame (chrome + backpack strip +
3D-items area + empty paperdoll panel) instead of the blank placeholder.
- Visual verification by the user.
---
## 9. Open / confirmed-at-build items
- **Cell pitch + column counts** — B-Controller reads them from the nested layouts
(`0x21000022` backpack, `0x21000021` 3D-items) at bind time. Geometry from the outer dump
suggests the backpack strip is ~1 column and the 3D-items panel ~6 columns of 36×36, but the
exact values are B-Controller's to read, not B-Grid's to hardcode.
- The placeholder mount in `GameWindow` (Sub-phase A) is replaced here: instead of a bare
`UiNineSlicePanel`, the inventory window becomes `LayoutImporter.Import(0x21000023)`'s root,
registered under the same `WindowNames.Inventory`. (Mechanically a few lines in the RetailUi
block; the F12 wiring is untouched.)
---
## 10. References
- Dat dump: `AcDream.Cli dump-vitals-layout "<datdir>" 0x21000023` (the structure in §2).
- Format: `docs/research/2026-06-15-layoutdesc-format.md` (§8 Type table, §10/§12 inheritance).
- Code: `src/AcDream.App/UI/Layout/LayoutImporter.cs` (`Resolve`/`Merge` path),
`src/AcDream.App/UI/Layout/ElementReader.cs:162` (the children-drop), `UiItemList.cs`,
`UiItemSlot.cs` (the shipped drag spine — reused in B-Drag, untouched here).
- Handoff/decomposition: `docs/research/2026-06-20-window-manager-inventory-handoff.md` §4;
the Sub-phase B decomposition (B-Grid → B-Controller → B-Wire → B-Drag).