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>
This commit is contained in:
parent
e3152ade9a
commit
aa60615912
1 changed files with 235 additions and 0 deletions
|
|
@ -0,0 +1,235 @@
|
|||
# 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).
|
||||
Loading…
Add table
Add a link
Reference in a new issue