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

12 KiB
Raw Blame History

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 pathnot game-class Types. Their content lives in the separate layouts 0x21000024/22/21.

LayoutImporter.Resolve already loads BaseLayoutId, finds BaseElement, recurses it, and Merges — 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 mountLayoutImporter.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:

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.

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:

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).