merge: integrate main (D.2b paperdoll/inventory UI line) into the physics/collision branch

Brings the 134 D.2b UI commits onto the physics/collision development line so
main can fast-forward. Today's #149 (BSP-less static collision) + #150 (open
doors fully passable) + the full collision/streaming/dense-town-FPS arc meet
the paperdoll/inventory work.

# Conflicts:
#	docs/ISSUES.md
#	docs/architecture/retail-divergence-register.md
This commit is contained in:
Erik 2026-06-25 12:57:46 +02:00
commit 01594b4cfd
95 changed files with 17201 additions and 167 deletions

View file

@ -0,0 +1,200 @@
# Handoff — window manager → inventory window → paperdoll (the next D.2b core-panels arc)
**Date:** 2026-06-20
**From:** the D.5.3 session that shipped the drag-drop spine (B.1), toolbar shortcut reorder/remove +
wire (B.2), and toolbar collapse-to-one-row — all **merged to `main`** (merge commit `abbd97b`;
branch `claude/hopeful-maxwell-214a12`, tip `14443e5`). Full suite green (2752) at merge.
**Line numbers below WILL drift — grep the symbol, don't trust the line.**
---
## 0. What just shipped (the foundation the next work builds on)
This is now on `main` and visually confirmed:
- **Drag-drop spine (B.1)** — the shared widget-level drag machine. `UiRoot` holds the device-level
drag (BeginDrag/UpdateDragHover/FinishDrag, ghost snapshotted at begin, drop delivered only on a real
hit). `UiElement` has `GetDragPayload()`/`GetDragGhost()`/`IsDragSource` virtuals (UiRoot stays
item-agnostic). `UiItemSlot` is drag source + drop target + accept/reject overlay; `UiItemList` owns
a registered `IItemListDragHandler { OnDragLift; OnDragOver; HandleDropRelease }`. Payload =
`ItemDragPayload(ObjId, SourceKind, SourceSlot, SourceCell)`. Spec/plan:
`docs/superpowers/{specs,plans}/2026-06-20-d2b-drag-drop-spine*.md`.
- **Toolbar shortcut drag (B.2)** — retail **remove-on-lift / place-on-drop / no-restore**.
`ToolbarController : IItemListDragHandler` drives a `ShortcutStore` (`AcDream.Core.Items`, 18 slots)
+ the wire: `InventoryActions.BuildAddShortcut(seq,index,objectGuid,spellId,layer)` /
`BuildRemoveShortcut`, sent via `WorldSession.SendAddShortcut`/`SendRemoveShortcut`. Green-cross
accept sprite `0x060011FA`. Spec/plan: `docs/superpowers/{specs,plans}/2026-06-20-d2b-toolbar-shortcut-drag*.md`.
- **Toolbar collapse**`UiCollapsibleFrame` bottom-edge snap resize (1-row ↔ 2-row). Spec:
`docs/superpowers/specs/2026-06-20-d2b-toolbar-collapse-design.md`.
**The one B-stream piece still owed:** *drag FROM inventory onto the bar.* B.2 did the within-bar
reorder/remove (`SourceKind == ShortcutBar`). The fresh-from-inventory branch (retail `flags & 0xE == 0`
`CreateShortcutToItem`) needs the inventory window as a drag SOURCE — so it lands WITH Stream C
below, not before it.
---
## 1. Read first
- This doc.
- `docs/research/2026-06-16-ui-panels-synthesis.md` §4 — **the build plan** for the core panels
(build order, widget list, cross-panel wire table). Stream C follows it.
- `docs/research/2026-06-16-inventory-deep-dive.md``gmInventoryUI 0x21000023` nests paperdoll
(`0x21000024`) + backpack (`0x21000022`) + 3D-items (`0x21000021`); backpack burden Meter
(`SetLoadLevel`→fill `0x69`); full inventory wire catalog + acdream parse-status.
- `docs/research/2026-06-16-equipment-paperdoll-deep-dive.md` — the doll = `UIElement_Viewport`
(Type `0xD`) hosting a re-dressed player clone; ~25 equip slots; wield = `GetAndWieldItem 0x001A`
(builder MISSING); needs a Core→App `IUiViewportRenderer` seam.
- `docs/research/2026-06-18-d53-bar-finish-and-inventory-handoff.md` §C — the predecessor's
current-code readiness for the inventory window (most still accurate; deltas noted below).
- `claude-memory/project_d2b_retail_ui.md` (the toolkit, now incl. B.1/B.2/collapse) +
`claude-memory/project_object_item_model.md` (the D.5.4 `ClientObjectTable`).
**Mandatory workflow** (CLAUDE.md): grep `docs/research/named-retail/acclient_2013_pseudo_c.txt` by
`class::method` → cross-ref ACE/holtburger → pseudocode → port; conformance tests throughout. Each
sub-phase gets its own brainstorm → spec → plan → subagent-driven (the flow this arc has been using).
---
## 2. Scope — three sub-phases, in order
| # | Sub-phase | One-liner |
|---|---|---|
| **A** | **Window manager (open/close + `I`-key)** | The small shared infra to open/close a window + a keybind to toggle the inventory window. Prerequisite for everything below. |
| **B** | **Inventory window (Stream C / D.5.5)** | `gmInventoryUI` nesting paperdoll + backpack + 3D-items. The drag SOURCE that completes "drag-from-inventory." |
| **C** | **Paperdoll** (`UiViewport`) | The 3D dressed-doll viewport — the heaviest piece, its own Core→App seam. Last. |
**Out of scope:** the spell bar (a separate spell-casting feature, NOT the action-bar spell shortcuts);
the faithful dragbar/resizebar window resize (IA-12 whole-window-drag + the new bottom-edge collapse
are the accepted approximations).
---
## 3. Sub-phase A — window manager (open/close + `I`-key) — DO THIS FIRST
**Current state:** every retail-UI window is **always-on at a hardcoded position** — the vitals,
chat, and toolbar are added to `_uiHost.Root` unconditionally at startup under `ACDREAM_RETAIL_UI=1`
(grep `_uiHost.Root.AddChild` in `GameWindow.cs`; the mounts are in the `_options.RetailUi` block).
`UiHost` has **no open/close API** (`src/AcDream.App/UI/UiHost.cs` — it exposes `Root`, `Tick`,
`Draw`, `WireMouse/Keyboard` only). Input flows through `InputDispatcher` (Phase K) —
`keybinds.json` + `KeyBindings`; the `I` key needs an action + binding.
**Minimum deliverable:** open/close a top-level window + toggle the inventory window with `I`.
Concretely:
- A small open/close concept on `UiHost`/`UiRoot` (e.g. register a named window + Show/Hide/Toggle that
flips `Visible` and brings it to front via `ZOrder`). Today windows are always `Visible=true`; the
manager makes a window default-hidden and toggleable. (z-order/persist can be minimal — `Visible` +
a top-most `ZOrder` bump on open.)
- An `InputAction` (e.g. `ToggleInventory`) + the retail default `I` keybind (cross-check
`docs/research/named-retail/retail-default.keymap.txt`), wired through the existing
`InputDispatcher` (see `claude-memory/project_input_pipeline.md`), gated by `WantsKeyboard` so it
doesn't fire while a chat input is focused.
**Brainstorm questions (A):** Is the window manager a property on `UiRoot` (a registry of named
top-level windows + Show/Hide/Toggle), or a thin `UiHost` API? How is z-order handled on open (bring
to front)? Does `I` toggle (open↔close) or only open (Esc/close-button to close)? Persist open-state
across the session (in-memory) only — no disk persistence yet (that's the deferred Plan-2)?
Faithful retail note: retail windows are opened by the radar/menu buttons + hotkeys and managed by
keystone.dll (no decomp) — so this is a toolkit-defined manager (IA-12/IA-15 umbrella), kept minimal.
---
## 4. Sub-phase B — inventory window (Stream C / D.5.5) — follow the synthesis §4
**The design is already written — follow `2026-06-16-ui-panels-synthesis.md` §4.** This section is the
**current-code readiness** + what's missing. Don't re-derive the design.
**READY (and stronger now, post-B.1/B.2):**
- `UiItemSlot` + `UiItemList` + `IconComposer` (`src/AcDream.App/UI/`) — the shared item-cell spine,
now with the full drag-drop machine (B.1). An inventory grid is `UiItemList` cells + an
`IItemListDragHandler` on the controller.
- `DatWidgetFactory` registers `0x10000031 → UiItemList` (grep `0x10000031` in
`src/AcDream.App/UI/Layout/DatWidgetFactory.cs`).
- The data path: `ClientObjectTable.GetContents(containerGuid)` → ordered guids → `Get(guid)` → full
icon fields (`src/AcDream.Core/Items/ClientObjectTable.cs`). The object/container model shipped in
D.5.4 (`project_object_item_model.md`).
- The shortcut wire + `ShortcutStore` (B.2) — so **drag-from-inventory-onto-the-bar** is now a small
addition: the inventory cell is a drag SOURCE (its handler's `OnDragLift` does NOT remove — an
inventory→bar drag creates a shortcut REFERENCE; the item stays in the pack), and `ToolbarController.
HandleDropRelease` gains the `SourceKind == Inventory` branch (place a new shortcut at the target via
`SendAddShortcut`, no source-bump) — retail's `flags & 0xE == 0` `CreateShortcutToItem` path.
**MISSING (the build, in synthesis order — see the 2026-06-18 handoff §C.1-6 for the detailed list):**
1. **Window manager** — §3 above (do first).
2. **`UiItemList` N-cell grid mode** — currently single-cell (`UiItemList.cs`; `Flush`/`AddItem`
skeleton exists, no column-count/pitch/wrap). LIKELY ~6 cols; confirm from
`UIElement_ItemList::ItemList_AddItem`.
3. **Sub-window mount in `LayoutImporter`**`gmInventoryUI 0x21000023` nests `gm*UI` children with
their own `BaseLayoutId`; the importer only does TEMPLATE inheritance today
(`src/AcDream.App/UI/Layout/LayoutImporter.cs`) — instantiating a nested `gm*UI` window is new.
4. **Wire gaps** (inventory deep-dive §4.3): builders `DropItem 0x001B`, `GetAndWieldItem 0x001A`,
`NoLongerViewingContents 0x0195`; parsers `ViewContents 0x0196`, `SetStackSize 0x0197`,
`InventoryRemoveObject`; fix `ParsePutObjInContainer` (drops the 4th `containerType`) +
`ParseInventoryServerSaveFailed` (drops `weenieError`); register `ViewContents`/`0x019A`/`0x0052`/
`0x00A0` in `GameEventWiring`. (Grep these symbols — none landed this session.)
5. **`InventoryController`** (`gm*UI::PostInit` find-by-id pattern): backpack burden Meter
(`SetLoadLevel`→fill `0x69`), own-pack list + side-pack list, `ObjDescEvent 0xF625` → re-dress.
**Brainstorm questions (B):** Sub-window mount — recursive `Import()` in `LayoutImporter`, or an
external stitch by the controller? Grid column count (confirm 6 from decomp)? Open by `I`-key only, or
also a toolbar inventory button? The inventory cell's drag semantics (lift does NOT remove from the
pack — it's a copy-to-shortcut for the bar, but a real MOVE between packs DOES relocate via
`PutItemInContainer 0x0019`) — pin the SourceKind→action matrix (deep-dive §5.7 opcode table).
---
## 5. Sub-phase C — paperdoll (`UiViewport`, Type 0xD) — heaviest, last
**The single biggest new piece.** No widget, no factory registration, no renderer today. Needs an
`IUiViewportRenderer` **Core→App seam** (structure Rule 2) for a scissored single-entity GL pass — the
doll is the local player's ObjDesc-dressed entity in a fixed viewport. ~25 equip slots; the
element-id→`EquipMask` map; wield = `GetAndWieldItem 0x001A` (builder still MISSING). Brainstorm
separately (it's substantial). Follow `2026-06-16-equipment-paperdoll-deep-dive.md`.
**Brainstorm questions (C):** Does the doll clone the player `WorldEntity` or build a fresh
ObjDesc-dressed `AnimatedEntityState` (the player is the camera, so there's no player-as-renderable
today)? `IUiViewportRenderer` timing (post-world pass vs pre-pass)? Scissor infra (the toolkit has no
GL scissor yet — see the collapse spec's clipping discussion).
---
## 6. Build order + dependency graph
```
A. window manager (open/close + I-key) ← do first; unblocks B
B. inventory window (grid + sub-window mount + wire gaps + InventoryController)
│ └─ also completes B.2's drag-from-inventory (inventory cell = drag source;
│ ToolbarController.HandleDropRelease gains the SourceKind==Inventory branch)
C. paperdoll (UiViewport + IUiViewportRenderer seam + PaperDollController) ← heaviest, last
```
Critical-path note: A is small and unblocks B; the inventory window (B) is the big one and the drag
*source* that closes the B-stream; the paperdoll (C) is the heaviest and independent enough to come
last.
---
## 7. ⚠ State notes for the fresh session
- **Start from `main`** (merge `abbd97b`) — it has all of D.5.2 + D.5.4 + this session's D.5.3 B.1/B.2
+ the collapse, plus the indoor-lighting handoff (`f7f3e08`, issues `#142`/`#143` are LIGHTING, not
UI). Create a new worktree off `main`.
- **ISSUES:** `#144` is the (LOW, latent) empty-item-slot click note from B.1; `#141` is the toolbar
selected-object display (D.5.3a done; mana/stack deferred). `#142`/`#143` are the lighting issues.
- The drag spine's `IItemListDragHandler` + `ItemDragPayload` + `ShortcutStore` + the shortcut wire all
exist now — reuse them; don't rebuild.
- Don't re-port what WorldBuilder/the toolkit already gives (read `worldbuilder-inventory.md` first for
any dat/render work).
---
## 8. New-session prompt (paste into a fresh session)
> Continue acdream's D.2b retail-UI track. **Read `docs/research/2026-06-20-window-manager-inventory-handoff.md` first**, then the 2026-06-16 UI deep-dives + synthesis §4 it references. The drag-drop spine (B.1), toolbar shortcut reorder/remove + wire (B.2), and toolbar collapse all shipped + merged to `main` (`abbd97b`). Next, in order: **(A)** a minimal **window manager** — open/close + an `I`-key toggle for the inventory window (today every retail-UI window is always-on at a hardcoded position; `UiHost` has no open/close API); **(B)** the **inventory window** (`gmInventoryUI` nesting paperdoll/backpack/3D-items — `UiItemList` N-cell grid + sub-window mount in `LayoutImporter` + the inventory wire gaps + `InventoryController`; it's the drag SOURCE that completes B.2's drag-from-inventory via a `SourceKind==Inventory` branch in `ToolbarController.HandleDropRelease`); **(C)** the **paperdoll** `UiViewport` (Type 0xD) 3D doll, with a Core→App `IUiViewportRenderer` seam — heaviest, last. Spell bar DEFERRED. Use the full brainstorm → spec → plan → subagent-driven flow per sub-phase; mandatory grep-named→cross-ref→pseudocode→port for any wire format; conformance tests throughout. Reuse the shipped spine (`IItemListDragHandler`/`ItemDragPayload`/`ShortcutStore`/the AddShortcut/RemoveShortcut wire) — don't rebuild it. Resolve objects via `ClientObjectTable.Get(guid)` / `GetContents(containerGuid)`. Start a new worktree off `main`.
**MEMORY.md index line:**
- [Handoff: window manager → inventory → paperdoll (2026-06-20)](research/2026-06-20-window-manager-inventory-handoff.md) — next D.2b-UI arc after B.1/B.2/collapse (all merged to main `abbd97b`). 3 sub-phases: (A) window manager (open/close + I-key — UiHost has no open/close API today; windows are always-on); (B) inventory window (Stream C, synthesis §4 — UiItemList grid + sub-window mount + wire gaps DropItem 0x1B/GetAndWieldItem 0x1A/ViewContents 0x196 + InventoryController; the drag SOURCE that closes B.2's drag-from-inventory via a SourceKind==Inventory branch); (C) paperdoll UiViewport Type 0xD + Core→App IUiViewportRenderer seam (heaviest). Reuse the shipped drag spine + ShortcutStore + shortcut wire. Spell bar DEFERRED.

View file

@ -0,0 +1,167 @@
# Handoff — D.2b inventory: B-Controller shipped + visually confirmed; B-Wire next
**Date:** 2026-06-21 (later than the B-Grid handoff of the same date)
**From:** the session that shipped **B-Controller** (inventory population + burden meter +
captions) and root-caused/fixed the **backdrop wash-out** (a #145 continuation) at the visual gate.
**Predecessor:** `docs/research/2026-06-21-d2b-bgrid-shipped-bcontroller-next-handoff.md`
(window manager + B-Grid). This doc supersedes it for "what's next."
**Branch:** `claude/hopeful-maxwell-214a12`, tip **`c38f098`**. `main` is a clean
fast-forward ancestor — ff `main` + branch fresh, or continue this branch.
**Line numbers drift — grep the symbol.**
---
## 0. What shipped this session (all committed, build + tests green, VISUALLY CONFIRMED)
**B-Controller** — `InventoryController` (the `gm*UI::PostInit` analogue) binds the imported
`gmInventoryUI 0x21000023` tree by id and populates it from `ClientObjectTable`. F12 (with
`ACDREAM_RETAIL_UI=1`) now shows your live inventory on the dark backdrop: the "Contents of
Backpack" grid, the pack-selector strip, the vertical burden bar ("Burden 17%"), and the captions.
Commits (after the input handoff's `4e23a7b`):
- `5875ac8` `BurdenMath` encumbrance ports (Core)
- `fb050ae` `ClientObjectTable.SumCarriedBurden` (Core)
- `5e75d2a` `UiMeter` vertical fill (`Vertical`/`FillFromBottom`/`DrawVBar`)
- `89c640a` `InventoryController` bind + populate + burden + captions
- `383e8b7` follow-up (Populate uses `GetContents` only — dropped a subagent's index-fallback workaround)
- `03fbf44` GameWindow wiring (the `InventoryController.Bind` in the inventory-init block)
- `7bb4bd3` divergence rows AP-48..51 + ISSUES/roadmap
- `1ccf07b` Opus phase-boundary review fixes (burden % saturation + equipped-item filter)
- `417b137` **render fixes** (backdrop wash-out [#145 continuation] + captions) — the visual-gate fix
- `c38f098` docs (ISSUES/roadmap: visually confirmed + #145 continuation + overflow follow-up)
Spec: `docs/superpowers/specs/2026-06-21-d2b-inventory-controller-design.md`.
Plan: `docs/superpowers/plans/2026-06-21-d2b-inventory-controller.md`.
Tests: **App 532 / Core 1526 green.** `InventoryFrameImportProbe` is a real-dat smoke test
(gated on `ACDREAM_DAT_DIR`) that **locks the ZLevel fix** (asserts each mounted panel's
ZOrder > the backdrop's).
---
## 1. Read first
- This doc.
- `claude-memory/project_d2b_retail_ui.md` — the B-Controller SHIPPED entry + DO-NOT-RETRY (updated this session).
- `docs/superpowers/specs/2026-06-21-d2b-inventory-controller-design.md` — the B-Controller spec.
- `docs/research/2026-06-16-inventory-deep-dive.md` §4 — the inventory wire catalog (for **B-Wire**).
- `docs/research/2026-06-16-ui-panels-synthesis.md` §3.3 — the de-duped wire-gap TODO.
- `docs/ISSUES.md` — the **#145** entry (now closed via the continuation), the **D.2b-B SHIPPED** entry, and the new **contents-grid overflow** issue.
---
## 2. Key discoveries (durable, non-obvious — DO-NOT-RETRY)
1. **THE BIG ONE — a mounted sub-window slot must keep its OWN frame ZLevel, NOT inherit the
base layout root's.** The gm*UI sub-window roots (`0x100001C8` backpack, `0x100001C4`
3D-items) carry **ZLevel 1000**. `ElementReader.Merge`'s zero-wins-base rule (`ElementReader.cs:161`,
`ZLevel = derived.ZLevel != 0 ? derived.ZLevel : base_.ZLevel`) made the mounted slots
(own ZLevel 0) inherit that 1000, and the #145 ZOrder fold (`ReadOrder ZLevel·10000`)
turned 1000 into ZOrder ≈ 10,000,000 — *behind* the frame's Alphablend backdrop
(`0x100001D0`, ZLevel 100 → ≈ 1,000,000). The backdrop then **overpainted/washed out** the
panels' captions + burden meter + item cells. The **paperdoll** root happens to be ZLevel 0,
so it survived — which is why the B-Grid session believed #145 was "done." Fix
(`417b137`): `LayoutImporter.Resolve` sets `result.ZLevel = self.ZLevel` inside the
`ShouldMountBaseChildren` block. **Root-caused via a one-shot `TextRenderer` sprite-segment-order
dump** (the backdrop's segment was drawing AFTER the panel content) + a live ZLevel probe — when
"drawn but invisible," dump the actual paint order, don't keep theorizing z-math.
2. **Burden formula (ported into `Core.Items.BurdenMath`):**
`capacity = Str × (150 + clamp(aug×30, 0, 150))` (`EncumbranceSystem::EncumbranceCapacity`
`0x004fcc00`); `load = burden / capacity` (`EncumbranceSystem::Load` `0x004fcc40` — the
decompiler MANGLED the FP divide to `return burden`; cross-checked vs the existing `ComputeMax`
+ the r06 doc); `gmBackpackUI::SetLoadLevel` (`0x004a6ea0`) → fill = `clamp(load/3, 0, 1)`,
%text = `floor(load × 100)` **SATURATING at 300%** (computed from the CLAMPED fill, not
unclamped — caught in review). `CACQualities::InqLoad` `0x0058f130`. **acdream has no wire
`EncumbranceVal` yet** → client-side `SumCarriedBurden` fallback (divergence **AP-48**;
B-Wire pins the real wire value).
3. **`DatWidgetFactory.BuildMeter`'s single-image path is ALREADY correct for the burden meter**
(meter-own DirectState `0x0600121D` = back/track; its one Type-3 child `0x0600121C` = fill)
per `UIElement_Meter::DrawChildren` `0x0046fbd0` (the child = `m_pcChildImage = GetChildRecursive(this, 2)`).
The inventory deep-dive's "0x0600121C = back" label was backwards re: draw role. The only
meter change was a **vertical** fill path (`UiMeter.Vertical`/`FillFromBottom`/`DrawVBar`).
A near-empty fill at low load is CORRECT (Str 290 → 17% → ~3px of a 58px bar), not a bug.
4. **Captions: drive the HOST `UiText` directly.** The caption elements (`0x100001D7` "Burden",
`0x100001D8` %, `0x100001C5` "Contents of Backpack") resolve to `UiText`. A nested *child*
`UiText` does NOT paint — `AttachCaption` now sets the host UiText's `LinesProvider`/`Centered`/`DatFont`.
5. **Equipped items excluded** from the pack grid + selector (`CurrentlyEquippedLocation != None`)
— a mid-session self-wield routes them through `MoveItem(item, WielderGuid=player)` into
`GetContents(player)`; retail's gm3DItemsUI shows pack contents only.
---
## 3. Current visual state (F12, `ACDREAM_RETAIL_UI=1`)
Renders correctly: dark backdrop BEHIND; paperdoll equip slots (generic blue border); backpack
strip ("Burden" + % + vertical bar + side-bag cell + main-pack cell); 3D-items "Contents of
Backpack" grid populated (6-col). **Expected gaps (all are the follow-ups below):**
- Contents grid OVERFLOWS the 96px panel (no scroll) — "part hanging off the window."
- Paperdoll equip slots show a generic blue `UiDatElement` border, not per-slot silhouettes → Sub-phase C.
- The inventory window starts HIDDEN; F12 toggles it (the auto-open debug hack was reverted).
---
## 4. What's next (build order; each gets its own brainstorm → spec → plan → subagent flow)
**(a) B-Wire — RECOMMENDED NEXT.** Parse the wire `EncumbranceVal` (ACE `PropertyInt 5`) for the
player so the burden bar reads the server value (retires **AP-48**, the client-side sum). Plus the
other inventory wire gaps catalogued in `2026-06-16-inventory-deep-dive.md §4` + the de-duped TODO
in `2026-06-16-ui-panels-synthesis.md §3.3`: builders `DropItem 0x001B`, `GetAndWieldItem 0x001A`,
`NoLongerViewingContents 0x0195`; parsers `ViewContents 0x0196`, `SetStackSize`, `InventoryRemoveObject`;
fix the dropped 4th field on `0x0022` + the dropped error on `0x00A0`; register the unwired parsers;
extend `CreateObject` if any icon/stack fields are still discarded. **Mandatory grep-named →
cross-ref → pseudocode → port** for each wire format; ACE handlers + holtburger fixtures are the oracles.
**(b) Contents-grid scroll polish.** The 6-col grid shows ALL loose items, overflowing the 96px
panel. Wire the gutter `UiScrollbar` (`0x100001C7`) to the grid + clip the grid to the panel +
scroll the overflow. Filed as an OPEN issue in `docs/ISSUES.md`; was out-of-scope for B-Controller
(spec §10). Small, self-contained.
**(c) B-Drag.** Inventory cell as a drag SOURCE (reuse the shipped spine — `UiItemSlot` is already a
full drag source/target) + the `SourceKind == Inventory` branch in `ToolbarController.HandleDropRelease`.
**(d) Sub-phase C — the biggest piece.** Paperdoll: register `0x10000032` (`UiItemSlot`) so equip
slots draw per-slot silhouettes (fixes the generic-blue-slot art) + the `UiViewport` (Type `0xD`)
3D character doll, which needs a new Core→App `IUiViewportRenderer` seam (Code-Structure Rule 2).
Research: `docs/research/2026-06-16-equipment-paperdoll-deep-dive.md`.
(Then D.5.3-remaining: the toolbar mana meter + stack slider + spell shortcuts; vendor/trade/spellbook.)
---
## 5. Caveats / open items
- **Chat + toolbar #145 z-order eyeball (the input handoff's first ask):** analytically cleared
this session — chat's only non-zero-ZLevel element (`0x1000000E`, ZLevel 900) was already the
backmost top-level sibling, so the #145 fold can't change its order; the toolbar now honors
ZLevel (retail-faithful). No regression seen across ~10 launches. Not separately user-confirmed
in isolation, but low-risk — glance once if convenient.
- The `InventoryFrameImportProbe` real-dat test SKIPS in CI (no dat). It only asserts when
`ACDREAM_DAT_DIR` (or the default `~/Documents/Asheron's Call`) exists.
- `ClientObjectTable.GetContents(playerGuid)` returns `IReadOnlyList<uint>` (guids), NOT
`ClientObject`s — a prior research agent's claim was wrong; trust the source.
- The main-pack cell (`m_topContainer 0x100001C9`) uses a placeholder icon (the main pack ≡ the
player, no item IconId) — AP-51. Pin a backpack RenderSurface DID if it bothers you.
---
## 6. New-session prompt (paste into a fresh session)
> Continue acdream's D.2b retail-UI inventory arc. **Read
> `docs/research/2026-06-21-d2b-bcontroller-shipped-next-handoff.md` first.** B-Controller
> (inventory population + burden meter + captions) shipped + visually confirmed on
> `claude/hopeful-maxwell-214a12` (tip `c38f098`; `main` is a clean ff ancestor — ff + branch
> fresh, or continue). The backdrop wash-out (a #145 continuation: mounted sub-window slots were
> inheriting the base root's ZLevel 1000 → behind the backdrop) is fixed. **Next: B-Wire** — parse
> the player's wire `EncumbranceVal` (PropertyInt 5) to retire the client-side burden-sum fallback
> (AP-48), plus the inventory wire gaps in `2026-06-16-inventory-deep-dive.md §4` /
> `2026-06-16-ui-panels-synthesis.md §3.3`. Use the full brainstorm → spec → plan → subagent-driven
> flow; mandatory grep-named → cross-ref → pseudocode → port for every wire format; conformance
> tests throughout. After B-Wire: the contents-grid scroll polish (wire gutter `0x100001C7`),
> B-Drag (inventory drag SOURCE), then Sub-phase C (paperdoll `UiViewport` doll + `0x10000032`
> UiItemSlot per-slot art).
**MEMORY.md index line** (already added this session):
- [Project: D.2b retail UI engine](project_d2b_retail_ui.md) — … Inventory B-Controller SHIPPED 2026-06-21. DO-NOT-RETRY: a mounted sub-window slot must keep its OWN frame ZLevel — inheriting the base root's ZLevel 1000 sinks the panel behind the Alphablend backdrop (the #145-continuation wash-out).

View file

@ -0,0 +1,173 @@
# Handoff — D.2b inventory: A + B-Grid shipped, #145 fixed, B-Controller next
**Date:** 2026-06-21
**From:** the session that shipped the **window manager (Sub-phase A)**, **B-Grid** (inventory
sub-window mount + grid), and fixed **#145** (ZLevel z-order). All committed on branch
`claude/hopeful-maxwell-214a12`, tip `4904ff4`. Full suite green (2767).
**Predecessor handoff:** `docs/research/2026-06-20-window-manager-inventory-handoff.md` (the
A→B→C arc). This doc supersedes it for "what's next."
**Line numbers drift — grep the symbol.**
---
## 0. What shipped this session
- **Sub-phase A — window manager.** `UiRoot` named-window registry (`RegisterWindow` /
`ShowWindow` / `HideWindow` / `ToggleWindow` / `BringToFront`), raise-on-click, and F12 →
toggle via the **existing** `InputAction.ToggleInventoryPanel`. `UiHost` forwarders. Spec
`docs/superpowers/specs/2026-06-20-d2b-window-manager-design.md`; plan
`…/plans/2026-06-20-d2b-window-manager.md`. Commits `6409038`/`036db8b`/`882b4dd`/`e3152ad`.
Visually confirmed (F12 toggles, click raises, chat-focus gate).
- **Cross-check correction:** retail's inventory key is **F12** (`ToggleInventoryPanel`),
NOT `I` (`I` = the `Laugh` emote per `retail-default.keymap.txt:246`). The handoff guessed I.
- **Sub-phase B-Grid — inventory frame.** `UiItemList` N-cell **grid mode**
(`Columns`/`CellWidth`/`CellHeight`/`CellOffset`/`LayoutCells`; single-cell toolbar preserved)
+ the **sub-window mount** in `LayoutImporter.Resolve` + the `GameWindow` swap of the A
placeholder for `LayoutImporter.Import(0x21000023)`. Spec
`…/specs/2026-06-20-d2b-inventory-grid-mount-design.md`; plan
`…/plans/2026-06-20-d2b-inventory-grid-mount.md`. Commits `4fd4b09`/`85098f5`/`7d971a2`.
- **#145 — ZLevel z-order (DONE `45a5cc5`).** The importer mapped `ZOrder ← ReadOrder` only, so
the gmInventoryUI full-window backdrop (`0x100001D0`, ReadOrder 4) painted over the panels
(ReadOrder 1-3). Now `ZOrder = ReadOrder ZLevel·10000` (higher ZLevel = further back). See §3.
---
## 1. Read first
- This doc + the two B-Grid spec/plan files above.
- `docs/research/2026-06-16-ui-panels-synthesis.md` §4 (the core-panels build plan).
- `docs/research/2026-06-16-inventory-deep-dive.md` (gmInventoryUI nesting + wire catalog).
- `claude-memory/project_d2b_retail_ui.md` (toolkit crib — updated this session).
- `claude-memory/project_object_item_model.md` (the `ClientObjectTable` — B-Controller's data source).
---
## 2. Decomposition + status
Sub-phase B was decomposed into four steps (approved this session):
| Step | Status |
|---|---|
| **B-Grid** — UiItemList grid + sub-window mount | ✅ **SHIPPED** (mount works for all 3 panels; paperdoll equip-slot positions visually confirmed) |
| **B-Controller** — populate grids + burden meter + captions | ⬜ **NEXT** (see §5) |
| **B-Wire** — wire gaps (DropItem/ViewContents/…) | ⬜ pending |
| **B-Drag** — inventory cell as drag SOURCE (reuse the shipped spine) | ⬜ pending |
| **Sub-phase C** — paperdoll doll + per-slot equip art | ⬜ pending (the wrong-slot-graphics fix lives here) |
---
## 3. Key discoveries (durable, non-obvious)
1. **The nested panels are Type-0 inheritors, not game-class Types.** `gmInventoryUI 0x21000023`'s
three panels — paperdoll `0x100001CD`, backpack `0x100001CE`, 3D-items `0x100001CF` — are
**Type-0, media-less, childless leaves** that nest via `BaseElement`+`BaseLayoutId`
(BaseLayoutId → `0x21000024`/`0x21000022`/`0x21000021`). The research agent's "game-class Type
+ recursive Import" model was WRONG — confirmed by dumping `0x21000023`.
2. **The mount = inheritance carrying base children.** `ElementReader.Merge` dropped the base's
children. The mount (`LayoutImporter.ShouldMountBaseChildren` + the `Resolve` tail) attaches a
base's resolved subtree when the derived element is a pure container (no own children, no own
media) inheriting from a base WITH children. Inert for media-bearing inheritors (close
button/title) and childless style prototypes (vitals/chat/toolbar).
3. **ZLevel z-order: higher = further back.** `DatWidgetFactory` folds `ZLevel` into `ZOrder`
(`ReadOrder ZLevel·10000`). ZLevels observed: vitals all **0**; toolbar **0,1,2**; chat
**0,900**; inventory **0,10,50,100** (backdrop 100). Vitals unchanged; **chat + toolbar shifted
to their dat layering — needs a visual regression confirm (see §6).**
4. **The paperdoll equip slots need `0x10000032` (UiItemSlot) registration.** They resolve to type
`0x10000032`, which the factory does NOT register, so they fall back to a generic `UiDatElement`
sprite (the uniform blue border seen on screen) instead of per-slot silhouettes. This + the
dressed doll is **Sub-phase C**.
---
## 4. Current visual state (F12 with ACDREAM_RETAIL_UI=1)
The inventory window opens and renders the nested frame: outer chrome + title + the **paperdoll
equip slots correctly positioned** over the backdrop. **Gaps (all expected — next sub-steps):**
- Equip slots draw a generic border, not per-slot silhouettes → **C**.
- Backpack strip + 3D-items contents look empty (the panels mounted; their content is unpopulated)
**B-Controller**.
- Item cells empty everywhere (no items bound) → **B-Controller**.
---
## 5. B-Controller — the next step
**Goal:** make the inventory show your live contents — populate the backpack/3D-items grids from
`ClientObjectTable`, drive the burden meter, render the captions. The `gm*UI::PostInit` analogue.
**READY:**
- `ClientObjectTable.GetContents(containerGuid)` → ordered guids; `Get(guid)` → icon fields
(`src/AcDream.Core/Items/ClientObjectTable.cs`). The player's own pack is already in the table
from the CreateObject stream — **no new wire needed for the own-pack path** (B-Wire is for other
containers + live deltas).
- `UiItemList` grid mode (B-Grid) — set `Columns`/`CellWidth`/`CellHeight` (read the cell pitch
from the nested layout's item template) then `Flush()` + `AddItem()` per item.
- `IconComposer` for the cell icons; `UiItemSlot.SetItem(guid, iconTex)`.
- `ImportedLayout.FindElement(id)` to bind by id (the burden meter, the lists).
- The burden meter is element `0x100001D9` (Type 7 → `UiMeter`); drive fill via
`SetAttribute_Float`-equivalent (`UiMeter.Fill`); load ratio = burden/maxBurden.
**MISSING / to build:**
- `InventoryController`: find-by-id bind of the burden meter + the backpack/3D-items lists from
`invLayout` (the `ImportedLayout` built in `GameWindow`); subscribe to `ClientObjectTable`
`ObjectAdded/Moved/Removed`; rebuild cells.
- **Grid params:** dump `0x21000022` (backpack) + `0x21000021` (3D-items) to read the item-list
element sizes + cell template (geometry suggests backpack ~1 col, 3D-items ~6 cols of 36×36).
- **Type-0 text captions** ("Burden", "Contents of Backpack") don't render today — `UiDatElement`
doesn't draw text. Decide: promote those Type-0 elements to `UiText`, or a caption pass.
- Burden meter: verify `BuildMeter` produces a valid meter for the backpack meter's child shape
(it may differ from the vitals 3-slice shape).
**Brainstorm Qs (B-Controller):** Where does `InventoryController` get `invLayout` (expose the
`ImportedLayout` from `GameWindow`, or import inside the controller)? Cell pitch source (dump vs
hardcode 36)? Type-0 caption rendering (UiText promotion vs a caption helper)?
---
## 6. Open items / caveats
- **⚠ Confirm chat + toolbar didn't regress from the #145 ZLevel fix** (chat ZLevel 900, toolbar
1,2). The user looked but didn't explicitly confirm; first thing to eyeball next session.
- Type-0 text captions don't render (see §5).
- The B-Drag step reuses the **already-shipped** drag spine (`UiItemSlot` is a full drag source +
drop target; `IItemListDragHandler`/`ItemDragPayload`/`ShortcutStore`/the AddShortcut wire all
exist) — do NOT rebuild it. It needs the inventory cell as a SOURCE + the `SourceKind==Inventory`
branch in `ToolbarController.HandleDropRelease`.
---
## 7. Branch state
All work is on `claude/hopeful-maxwell-214a12` (tip `4904ff4`), which is `main` (`a391b86`) +
this session's A + B-Grid + #145 commits (a clean fast-forward chain — `main` is an ancestor).
Decide at the start of the next session: **fast-forward `main` to `4904ff4` and start a fresh
worktree off `main`** (matches the prior-arc pattern), or continue this branch.
---
## 8. New-session prompt (paste into a fresh session)
> Continue acdream's D.2b retail-UI inventory arc. **Read
> `docs/research/2026-06-21-d2b-bgrid-shipped-bcontroller-next-handoff.md` first.** Sub-phase A
> (window manager, F12) and B-Grid (inventory sub-window mount + UiItemList grid) shipped, and #145
> (ZLevel z-order — the backdrop now sits behind the panels) is fixed — all on
> `claude/hopeful-maxwell-214a12` (tip `4904ff4`; `main` is an ancestor, so ff `main` and branch
> fresh, or continue). F12 now shows the nested inventory frame with the paperdoll equip slots
> correctly positioned, but the backpack/3D-items panels are empty and the equip slots show a
> generic border. **Next: B-Controller** — bind the imported `gmInventoryUI` tree by id and
> populate the backpack/3D-items grids from `ClientObjectTable.GetContents(player)` + drive the
> burden meter (`0x100001D9`) + render the Type-0 captions. Use the full brainstorm → spec → plan →
> subagent-driven flow; mandatory grep-named→cross-ref→pseudocode→port for any wire format;
> conformance tests throughout. Reuse the shipped grid + window manager + drag spine — don't
> rebuild. **First, eyeball chat + toolbar for any z-order regression from the #145 ZLevel fix.**
> After B-Controller: B-Wire (wire gaps), B-Drag (inventory drag SOURCE), then Sub-phase C
> (paperdoll doll + per-slot equip art — the wrong-graphics fix; needs `0x10000032` UiItemSlot
> registration).
**MEMORY.md index line:**
- [Handoff: D.2b A+B-Grid shipped, #145 fixed, B-Controller next (2026-06-21)](research/2026-06-21-d2b-bgrid-shipped-bcontroller-next-handoff.md) — window manager (F12) + inventory sub-window mount (inheritance-children, NOT game-class Type) + UiItemList grid all shipped; #145 ZLevel z-order fixed (backdrop behind panels). Next B-Controller (populate grids + burden meter from ClientObjectTable). Paperdoll equip slots need 0x10000032; chat/toolbar ZLevel-regression unconfirmed.

View file

@ -0,0 +1,130 @@
# Handoff — D.2b inventory window finish (Stage 1) shipped + visually confirmed; slot-art + paperdoll next
**Date:** 2026-06-21 (after B-Wire)
**From:** the session that shipped **B-Wire** (inventory wire layer) **and Stage 1 of the
inventory window finish** (scroll + frame + vertical resize + 102-slot grid), both visually
confirmed at the live gate.
**Branch:** `claude/hopeful-maxwell-214a12`, tip **`1be7e65`** (+ this docs commit). `main` is a
clean ff ancestor — ff `main` + branch fresh, or continue this branch.
**Line numbers drift — grep the symbol.**
---
## 0. What shipped this session (all committed, build + full suite green, VISUALLY CONFIRMED)
Full suite green (`--no-build`, with the client running): **Core.Net 334 / App 543 / UI 425 /
Core 1530**, 0 failures.
### B-Wire — inventory wire layer (`b56087b``7c006d1`)
The burden bar now reads the server's wire `EncumbranceVal` (PropertyInt 5). Root cause was
**delivery, not binding**: login PD dropped the player's int table, live `0x02CD` was unparsed, and
`ObjectTableWiring` gated non-UiEffects ints out. Plus the full inventory wire pass (DropItem 0x001B
/ GetAndWieldItem 0x001A / NoLongerViewingContents 0x0195 builders; ViewContents 0x0196 /
SetStackSize 0x0197 / InventoryRemoveObject 0x0024 parsers; 0x0022 + 0x00A0 field fixes). Spec/plan
`docs/superpowers/{specs,plans}/2026-06-21-d2b-inventory-wire*.md`. Detail: the B-Wire SHIPPED entry
in `claude-memory/project_d2b_retail_ui.md`.
### Inventory window finish Stage 1 (`366af0c``1be7e65`)
Spec/plan `docs/superpowers/{specs,plans}/2026-06-21-d2b-inventory-window-finish*.md`.
- **Scroll**`UiItemList` clip+scroll via the shared `UiScrollable`; gutter scrollbar `0x100001C7`
bound like `ChatWindowController`; mouse-wheel.
- **Frame** — wrapped in the 8-piece bevel chrome (`UiNineSlicePanel`), like vitals/chat/toolbar.
- **Vertical resize** — bottom-edge only (horizontal blocked); the grid + sub-window + scrollbar +
backdrop stretch, paperdoll + side-bags pinned; scrollbar thumb reflects view/content.
- **102-slot grid** — contents grid pads empty frames to the main-pack capacity (default 102);
side-bag column pads to 7.
---
## 1. Read first
- This doc.
- `claude-memory/project_d2b_retail_ui.md` — the **Stage 1 SHIPPED** entry + DO-NOT-RETRY (updated this session).
- `docs/ISSUES.md` — the **OPEN** "Inventory + equipment slots show the wrong empty-slot background art" issue (the immediate next task) + the two **SHIPPED** entries.
- `docs/architecture/retail-divergence-register.md` — AP-52 (side-bag 7 fallback), AP-53 (main-pack 102 fallback), AP-54 (resize approximation), AP-51 (main-pack icon, still open).
- `docs/research/2026-06-16-equipment-paperdoll-deep-dive.md` — for Stage 2 (paperdoll).
- `.layout-dumps/uiitem-0x21000037.txt` — the UIItem cell template states (for the correct empty-slot sprite).
---
## 2. Key discoveries (durable, non-obvious — DO-NOT-RETRY)
1. **Grid cells must be `Anchors = None`.** `UiElement.DrawSelfAndChildren` runs `ApplyAnchor` on
every child AFTER `OnDraw`; with default `Left|Top` anchors it captured each cell's scroll-0
position and reset `Top` to it every frame, fighting `UiItemList.LayoutCells`' scroll offset →
the grid "escaped the window" when scrolled. `AddItem` now sets `cell.Anchors = None` so
`LayoutCells` is the sole authority. **The unit tests missed it by calling `LayoutCells` in
isolation, never the draw traversal** — the regression test now drives the `ApplyAnchor`
interaction. (Fix `14ea938`.)
2. **Scroll reuses `UiScrollable` + a whole-row logical clip (no GL scissor)** — mirrors
`UiText.cs:198` (`cell.Visible = top >= 0 && top + h <= Height`). The gutter scrollbar `0x100001C7`
is a factory Type-11 `UiScrollbar` inheriting base `0x2100003E`; bind its `Model` to `grid.Scroll`
+ the same scrollbar sprite ids the chat scrollbar uses.
3. **Vertical-resize cascade via anchors.** Frame: `Resizable=true; ResizeX=false;
ResizableEdges=Bottom`. The stretch elements (grid `0x100001C6`, its sub-window `0x100001CF`,
scrollbar `0x100001C7`, backdrop `0x100001D0`, + the content root) get `Anchors=Left|Top|Bottom`
(fixed width, height tracks parent); paperdoll `0x100001CD` + side-bag `0x100001CE` pinned
`Left|Top`. `ComputeAnchoredRect`: Top|Bottom ⇒ `h = parentH mB mT`.
4. **Capacity-padding pattern.** The contents grid + side-bag column pad empty `UiItemSlot`s up to
the pack capacity (player `ItemsCapacity` default 102; `ContainersCapacity` default 7) after the
loose-item loop in `InventoryController.Populate`. Padding changed several existing tests' counts
(intended — update, don't revert).
5. **DO-NOT auto-kill the client.** The user manages client lifecycle: launch with a **plain
`dotnet run --no-build`** (no `Get-Process … CloseMainWindow/Stop-Process` preamble). If a rebuild
is blocked by a running client (`MSB3021`/`MSB3027` "locked by AcDream.App"), **ASK the user to
close it** — do not kill it. (`feedback_dont_kill_clients_before_launch`.) This overrides the
CLAUDE.md graceful-close snippet.
---
## 3. Current visual state (F12, `ACDREAM_RETAIL_UI=1`)
Renders correctly + confirmed: 8-piece bevel frame; vertical bottom-edge resize (expand-only,
372..560 px); contents grid clips to its panel, scrolls (wheel + gutter scrollbar), shows the full
**102-slot** main pack (items + empty frames); side-bag column with bags + empty slots; vertical
burden bar; backdrop covers the window. **Known-wrong (next task):**
- **Empty-slot background art is wrong** — inventory + side-bag empty cells use the TOOLBAR empty
sprite (`UiItemSlot.EmptySprite = 0x060074CF`); paperdoll equip slots show a generic blue
`UiDatElement` border. (User-flagged.)
- **Main-pack cell has no backpack icon** (AP-51, placeholder `tex=0`).
---
## 4. What's next (build order)
**(a) FIX the empty-slot background art — RECOMMENDED NEXT (the OPEN ISSUE).** Inventory + side-bag
empty cells should use the retail pack-slot frame, not the toolbar's `0x060074CF`. Find the correct
sprite from the UIItem cell template `0x21000037` (`.layout-dumps/uiitem-0x21000037.txt`) empty
state; set it on the cells `InventoryController` creates (an `EmptySprite` override per slot kind, or
a controller-set default distinct from the toolbar's). The paperdoll equip slots' per-slot
silhouettes are Sub-phase C (below). Small, self-contained; brainstorm → spec → plan as usual.
**(b) Main-pack backpack icon (AP-51).** The main-pack cell (`m_topContainer 0x100001C9`) should show
the equipped-backpack art. Pin the backpack RenderSurface DID (or the equipped-pack `CreateObject`
icon path).
**(c) Sub-phase C — paperdoll.** The 3D character doll via a Core→App `IUiViewportRenderer` seam
(`UiViewport`, Type `0xD`) + the `0x10000032` `UiItemSlot` registration so equip slots draw per-slot
silhouettes. Research: `2026-06-16-equipment-paperdoll-deep-dive.md`. The heaviest piece.
**(d) Deferred polish:** container-switching (select a side pack → show its 24-slot contents via the
already-parsed `ViewContents 0x0196`); the side-bag column scrollbar (`0x100001CB`, inert — 7 fit);
shrink-below-default resize (currently expand-only); the selected-target bar + stack split slider
(per the wiki notes the user pasted). B-Drag (drag items between slots) also remains.
Each gets the full brainstorm → spec → plan → execute flow.
---
## 5. New-session prompt (paste into a fresh session)
> Continue acdream's D.2b retail-UI inventory arc. **Read
> `docs/research/2026-06-21-d2b-inventory-finish-handoff.md` first.** B-Wire + the inventory window
> finish Stage 1 (scroll, 8-piece frame, vertical resize, 102-slot grid) shipped + visually confirmed
> on `claude/hopeful-maxwell-214a12` (tip `1be7e65`; `main` is a clean ff ancestor). **Next: fix the
> wrong empty-slot background art** — inventory + side-bag empty cells use the toolbar empty sprite
> `0x060074CF`; they need the retail pack-slot frame from the UIItem cell template `0x21000037` (see
> `.layout-dumps/uiitem-0x21000037.txt`). Then the main-pack backpack icon (AP-51), then Sub-phase C
> (paperdoll `UiViewport` doll + `0x10000032` per-slot equip silhouettes). Use the full brainstorm →
> spec → plan → subagent/inline flow. **DO NOT auto-kill the running client** — launch with plain
> `dotnet run --no-build`; if a rebuild is locked by a running client, ask the user to close it.

View file

@ -0,0 +1,54 @@
# Handoff — D.2b inventory container-switching (next clean win)
**Date:** 2026-06-22 (banked at the end of the empty-slot-art session, per [[feedback_session_handoff]])
**Branch:** `claude/hopeful-maxwell-214a12`, tip `1f8dd7a` (+ this docs commit). `main` is a clean ff ancestor (68 commits behind).
**Status:** SCOPED, not started. Brainstorm explored the wire + controller; this captures the design sketch so a fresh session executes it with the live-wire round-trip getting proper attention. Run the full brainstorm → spec → plan → execute flow.
**Line numbers drift — grep the symbol.**
---
## Why this is the next pick
After the empty-slot-art arc (inventory contents/side-bag/main-pack empty cells now dat-resolve their art — see [[project_d2b_retail_ui]] + the empty-slot handoff), the side-bag column is **rendered but inert**: clicking a bag does nothing. Container-switching makes it functional AND closes the **AP-56** deferral (the selected-container triangle + green/yellow selection square live here). The user picked this over the paperdoll (Sub-phase C) because the paperdoll's empty equip slots are entangled with the heavy 3D doll viewport (retail empties show the doll), whereas container-switching is self-contained.
This is the **first inventory feature with live two-way wire interaction** (a `Use``ViewContents` round-trip), a step up from the read-only display work so far — hence executing it fresh rather than tired.
## Read first
- This doc.
- `claude-memory/project_d2b_retail_ui.md` — the D.2b SSOT + the empty-slot-art SHIPPED entry (the resolver + AP-55/AP-56).
- `claude-memory/reference_retail_inventory_paperdoll.md` — the player-facing spec: the "preferred pack" auto-fit rules + the 7-side-bag / 102-main-pack capacities.
- `docs/architecture/retail-divergence-register.md`**AP-56** (the selected-container indicator deferred — this phase retires it) + AP-53 (container-switch noted as deferred).
## What's already in place (the wire layer is DONE)
- **`InteractRequests.BuildUse` (`0x0036`)** — open/use a container by guid (`src/AcDream.Core.Net/Messages/InteractRequests.cs:38`). This is how retail opens a container.
- **`GameEvents.ParseViewContents` (`0x0196`)** — the server's full contents list `(ContainerGuid, [guid, containerType]×N)` (`src/AcDream.Core.Net/Messages/GameEvents.cs:372`). Handler at `GameEventWiring.cs:260`.
- **`WorldSession.SendNoLongerViewingContents` (`0x0195`)** — close a container view (`WorldSession.cs:1212`).
- **`ClientObjectTable.GetContents(containerId)`** — the per-container item index (`src/AcDream.Core/Items/ClientObjectTable.cs:313`).
- **The standing TODO** at `GameEventWiring.cs:256`: `ViewContents` currently does an **additive merge**; container-open must treat it as a **full REPLACE** (flush the container's prior membership first) so server-side removals don't linger. **This phase consumes that TODO.**
## Design sketch (to be confirmed in the brainstorm)
1. **Open-container state in `InventoryController`.** Add an `_openContainer` field (default = the player guid / main pack). `Populate` shows `GetContents(_openContainer)` instead of always the player. Repaint when the open container's membership changes (extend `Concerns`).
2. **Click a side-bag cell → open it.** The side-bag `UiItemSlot`s already carry the bag guid (bound in `Populate`). On click: set `_openContainer = bagGuid`; `SendUse(bagGuid)` (opens it server-side); when the previous container was a side bag, `SendNoLongerViewingContents(prevGuid)`. Click the main-pack cell (`0x100001C9`) → reset `_openContainer` to the player.
3. **`ViewContents 0x0196` → full REPLACE.** Flush the container's membership in `ClientObjectTable` before recording the entries (the TODO). Then `InventoryController` repaints the grid for `_openContainer`.
4. **Selected-container indicator (retires AP-56).** Draw the open-container triangle `0x06005D9C` + the green/yellow selection square on the selected side-bag cell — a new `UiItemSlot` "selected" overlay (model it on the existing `DragAccept` overlay in `UiItemSlot.OnDraw`; guard `id != 0` per [[feedback_ui_resolve_zero_magenta]]).
5. **Caption.** "Contents of Backpack" → the open container's name when a side bag is selected (the caption host is already driven by `InventoryController.AttachCaption`).
## Open questions for the brainstorm
- **The green/yellow selection-square sprite id** — the triangle is `0x06005D9C` (from the container prototype `0x1000033F` child `0x10000450`); the selection square's id is NOT yet found. Dump `0x21000037` / the gmBackpackUI layout (`0x21000022`) for the selected-container state media, or cdb a live retail client with a side bag open.
- **Are a side bag's contents pre-known, or do they arrive only on open?** Likely the latter (retail sends contents on open via `ViewContents`, not upfront) — so the `Use` round-trip is required, and `GetContents(bagGuid)` is empty until the reply lands. Confirm against a live capture (WireMCP on `127.0.0.1:9000`, or watch the `ViewContents` handler at the live gate).
- **Preferred-pack semantics** (from `reference_retail_inventory_paperdoll`): opening a pack makes it the "preferred" pack for auto-fit. Out of scope for the first cut (display-only switching), but note it.
- **Empty-container display** — a side bag with no contents shows an empty 24-slot grid (its `ItemsCapacity`, default 24 per AP-52/the wiki). Pad like the main pack does.
## Acceptance / scope
- First cut: click a side bag → its contents show in the grid; the selected bag shows the triangle + selection square; click the main pack → back. `ViewContents` is a full replace. Build + full suite green; **visual gate** (F12, the user clicks bags and confirms the grid switches + the selected indicator).
- Retires **AP-56** (selected-container indicator) in the same commit that draws it.
- Out of scope: preferred-pack auto-fit, drag-INTO a side bag (that's B-Drag), the side-bag column scrollbar (`0x100001CB`, inert — 7 fit).
## New-session prompt
> Continue acdream's D.2b retail-UI inventory arc. **Read `docs/research/2026-06-22-container-switching-handoff.md` first.** The empty-slot art shipped + was visually confirmed (`claude/hopeful-maxwell-214a12`, tip `1f8dd7a`). **Next: inventory container-switching** — click a side bag → show its contents in the grid (via `Use 0x0036``ViewContents 0x0196` full-replace), draw the selected-container triangle `0x06005D9C` + green/yellow selection square (retires AP-56), click the main pack to go back. The wire builders/parsers all exist (`BuildUse`, `ParseViewContents`, `SendNoLongerViewingContents`); consume the `GameEventWiring.cs:256` full-REPLACE TODO. Run the full brainstorm → spec → plan → subagent-driven → visual-gate flow. **DO NOT auto-kill the running client** — launch with plain `dotnet run`; if a rebuild is locked, ask the user to close it.

View file

@ -0,0 +1,91 @@
# Handoff — D.2b Sub-phase C: the paperdoll (equip slots + 3D doll)
**Date:** 2026-06-22 (banked at the end of the inventory drag-drop session, per [[feedback_session_handoff]])
**Branch:** `claude/hopeful-maxwell-214a12`, tip `82ab0e0`. `main` is a clean fast-forward ancestor.
**Status:** SCOPED, not started. The decomposition + corrections + reuse map below are the design sketch; a fresh session runs the full brainstorm → spec → plan → subagent-driven → visual-gate flow **per slice**.
**Line numbers drift — grep the symbol.**
> **⚠ SUPERSEDED / CORRECTED 2026-06-23 — Slice 1 SHIPPED (`a4c6852`).** This handoff's "⚠ Correction"
> section below ("empty equip slots are TRANSPARENT, no silhouettes") **was WRONG** and cost a visual-gate
> iteration. The user-axiom truth (his retail screenshots): the paperdoll has a **"Slots" toggle button
> (`0x100005BE` = `m_SlotCheckbox`)**. Button OFF (default) = the **live 3-D character** (the doll — can be
> naked) + the non-armor slots (weapon/shield/wand/jewelry/under-clothes); button ON = the doll **hides** and
> the **armor slots** become visible. **The "figure"/"silhouette" IS the doll (live render), NOT per-slot
> silhouette sprites — there are none.** So the doll `UiViewport` (Type 0xD, `0x100001D5`) + the Slots toggle
> = **Slice 2**. Slice 1 (equip-slot bind + wield/unwield + **visible empty-slot frames** + the `EquipMask`
> correction + the `PickupEvent` unwield fix) SHIPPED. **The corrected model + the full DO-NOT-RETRY list live
> in `claude-memory/project_d2b_retail_ui.md` (the Slice 1 entry) — read that, not the §"⚠ Correction" below.**
---
## Read first (in this order)
1. **This doc.**
2. **`docs/research/2026-06-16-equipment-paperdoll-deep-dive.md`** — the AUTHORITATIVE research. Full element-id → `EquipMask` table (§3a, ~25 slots), the viewport mechanism (§5), the wield wire (§4), the reuse analysis (§5c), and the open `UNVERIFIED` list (§7). **Do NOT re-derive what's already CONFIRMED there — cite it.**
3. **`claude-memory/project_d2b_retail_ui.md`** — the D.2b SSOT (the shipped log: container-switching, icons, capacity bars, drag-drop; the `UiItemSlot`/drag-spine/importer facts you'll reuse).
4. **`docs/architecture/retail-divergence-register.md`** — for the AP rows you'll add; and `docs/architecture/code-structure.md` Rule 2 (the Core→App seam constraint that governs the doll viewport).
## Why this is next
The inventory window is functionally complete in 2D (browse/switch bags, icons, capacity bars, drag-drop move). The **last placeholder** is the paperdoll: the ~25 equip slots render as generic blue `UiDatElement` borders, and there's no character doll. It's the standing roadmap NEXT and the most visible remaining gap when F12 is open.
## ⚠ Correction to carry in (the deep-dive settles this — don't repeat the old assumption)
The earlier brainstorm framed Slice 1 as "per-slot body-part **silhouettes**." **That is wrong.** Per the deep-dive §2a (decomp lines 99102, CONFIRMED): retail equip slots **default INVISIBLE and show only when occupied** — an **empty** slot is **transparent and the doll body shows through it**; an **occupied** slot shows the item icon. There are no per-slot silhouette sprites. acdream's current blue border is simply the wrong rendering of an empty slot.
Consequence: the doll is the visual backdrop the empty slots reveal. Slice 1 (slots) and Slice 2 (doll) are therefore **interdependent** for the empty-slot look — decide the empty-slot treatment for a doll-less Slice 1 in the brainstorm (transparent-to-panel vs a temporary placeholder).
## The decomposition — TWO slices (each its own spec → plan → implement)
### Slice 1 — equip slots (functional; the lighter, independently-valuable first piece)
Bind the ~25 equip slots to live equipped-item data + make them drag-drop equip/unequip targets. **No 3D doll yet.** Delivers: you SEE your equipped gear (icons in the right slots) and can drag gear onto a slot to wield it / off to unwield.
- **The slots already exist + are positioned** (the inventory frame mounts gmPaperDollUI at `0x100001CD`; `DatWidgetFactory` builds `0x10000031``UiItemList`; B-Grid confirmed the positions). The work is a new **`PaperdollController`** (the `gmPaperDollUI::PostInit` analogue) that:
- Maps each slot **element-id → `EquipMask`** via the deep-dive §3a table (port `GetLocationInfoFromElementID`, decomp 173620 — it's a static switch; transcribe the table, don't call the decomp at runtime).
- Populates each slot's icon from the item whose `ClientObject.CurrentlyEquippedLocation == that slot's EquipMask` (acdream ALREADY tracks `CurrentlyEquippedLocation` per item via `WieldObject 0x0023` + `CreateObject`; `ClientObjectTable`). So the equipped-state data is already there — no new parse needed for Slice 1 MVP.
- Makes each slot a `UiItemSlot` (the shipped shared widget) — single 32×32 cell, occupancy-gated icon, drag accept/reject overlay. Empty-slot rendering per the correction above.
- Registers the controller as the `IItemListDragHandler` for the equip slots (reuse the shipped drag-drop spine): `OnDragOver` accepts iff the dragged item's `ValidLocations` includes the slot's `EquipMask`; `HandleDropRelease` = **wield** (`GetAndWieldItem 0x001A`) optimistically + via wire; dragging a slot's item OUT to the grid = un-wield (`PutItemInContainer 0x0019`, the SHIPPED path).
- **THE WIRE GAP (build it):** `GetAndWieldItem` (`0x001A`, payload `uint itemGuid; (uint)EquipMask`) has **no builder/sender in acdream** (deep-dive §4a — only the unrelated UI font property `0x1A` exists). Add `InteractRequests.BuildGetAndWieldItem(seq, itemGuid, equipMask)` (mirror `BuildPickUp`) + `WorldSession.SendGetAndWieldItem(...)` (mirror `SendPutItemInContainer`). ACE: `GameActionGetAndWieldItem.Handle` (item + EquipMask). holtburger `commands.rs:808` confirms the shape.
- **The appearance reply** (`ObjDescEvent 0xF625`) is already PARSED (deep-dive §4a); it matters for the DOLL (Slice 2), not Slice 1's icons.
**Slice 1 scope cuts:** no doll, no part-selection highlight, no left/right-side disambiguation polish (the paired wrist/finger slots — handle via the SIDE column in §3a only if trivial), no Aetheria sigil slots (hidden by default).
### Slice 2 — the 3D doll viewport (the heavy piece: the UI↔3D bridge)
The `UIElement_Viewport` (Type `0xD`, element `0x100001D5` in gmPaperDollUI) hosting a re-dressed clone of the local player, drawn into the slot region behind the equip slots. **This is the single biggest new piece** — see deep-dive §5 (CONFIRMED viewport mechanism) + §5d (the design-open seam).
- **Reuse the existing char path** (deep-dive §5c, CONFIRMED): acdream already renders animated, equipped characters in-world via `EntitySpawnAdapter``AnimatedEntityState` (palette/part/hidden-part overrides from ObjDesc). The doll = "build a `WorldEntity` from the local player's Setup + current ObjDesc, feed it through that pipeline, draw it with a fixed camera + one distant light into a UI rect." Re-dress on `ObjDescEvent 0xF625` = rebuild the entity's overrides.
- **The new widget = `UiViewport`** (registers at dat Type `0xD`, leaf). It needs a **render-into-rect seam**: a Core `IUiViewportRenderer` interface implemented in App (Code-Structure Rule 2 — Core defines, App implements; never the reverse), invoked as a scissored single-entity 3D pass keyed to the widget's screen rect. The exact integration (after the world pass vs a UI overlay) is **DESIGN-OPEN — settle in the Slice 2 brainstorm** (deep-dive §7).
- Camera/light immediates, heading-toward-viewer (191.37°), idle animation, and the player-clone-vs-fresh-WorldEntity question are all in deep-dive §5d + §7 (some `UNVERIFIED` — decode the float immediates there).
## What's already in place (reuse, don't rebuild)
- **The mount:** `GameWindow.cs:~2208` pins `0x100001CD` (paperdoll) in the inventory frame; the importer pulls in the nested gmPaperDollUI (`0x21000024`) subtree (`InventoryFrameImportProbe.cs:25` = `PaperdollPanel`). The equip-slot elements are already imported + positioned.
- **`UiItemSlot`** (shared item widget) — occupancy-gated icon, `SpriteResolve`, drag payload/ghost, `DragAcceptSprite`/`DragRejectSprite`, `IsOpenContainer`/`Selected`/`CapacityFill` overlays. The equip slot is the single-cell case.
- **The drag-drop spine** (`IItemListDragHandler`, `UiRoot` BeginDrag/FinishDrag, `UiItemSlot` drag events) + the **optimistic-move machinery** (`ClientObjectTable.MoveItemOptimistic`/`ConfirmMove`/`RollbackMove` + the gapless-insert + the pending-count) — wield/unwield reuses this (wield = an equip-target drop handler; unwield = the shipped `PutItemInContainer` path).
- **`ClientObjectTable.CurrentlyEquippedLocation`** per item — the equipped-state source for Slice 1 (no new parse needed for MVP).
- **`IconComposer`** — item icons (incl. the `IsThePlayer` constant-backpack special; deep-dive notes the paperdoll icon path).
- **`EntitySpawnAdapter` / `AnimatedEntityState`** — the char render path Slice 2 reuses.
- **`ObjDescEvent 0xF625`** parse + `CreateObject.ReadModelData` (palette/part/texture/anim-part) — Slice 2's re-dress input.
## Open questions for the brainstorm(s)
- **Slice 1 empty-slot look without the doll** — transparent-to-panel vs a temporary placeholder (the doll is the real backdrop). The correction above means there's no silhouette sprite to use.
- **Equipped-icon source**`CurrentlyEquippedLocation` (have it) is enough for MVP; the deep-dive §3c/§7 notes the richer `InventoryPlacement` equipped list (PlayerDescription equipped section) is **not surfaced** by `PlayerDescriptionParser` (partial). Only extend the parser if MVP needs priority/coverage resolution (overlapping armor/clothing) — likely Slice-2-or-later.
- **`GetAndWieldItem` placement/auto-slot** — does the EquipMask come purely from the drop-target slot (like the doll's `GetLocationInfoFromElementID`), and how does auto-wield (drop on the doll body, not a specific slot) pick the slot? holtburger `resolve_and_clear_slots` (§4a) is the reference.
- **The whole of Slice 2's UI↔3D seam** (deep-dive §7) — the heaviest design call; its own brainstorm.
- **`0x100001E0 = MissileAmmo 0x800000`** is LIKELY (corrupted decomp immediate, §7) — confirm by dump if the ammo slot matters for MVP.
## Acceptance / scope per slice
- **Slice 1:** equip slots show your equipped gear's icons in the right slots; drag a wieldable item onto a slot → it wields (`GetAndWieldItem 0x001A`, optimistic + rollback); drag a slot's item to the pack → it unwields. Build + full suite green; **visual gate** (F12 → equipped gear shows in the doll slots, drag-to-equip works). Divergence rows for any approximation. Retires the "generic blue border" gap.
- **Slice 2:** the 3D doll renders the re-dressed player clone behind the slots; empty slots show the doll body through them; equipping updates the doll live (via `ObjDescEvent`). Its own spec/plan; the `IUiViewportRenderer` seam is the crux.
## New-session prompt
> Continue acdream's D.2b retail-UI inventory arc on branch `claude/hopeful-maxwell-214a12` (tip `82ab0e0`; `main` is a clean ff ancestor). **READ FIRST:** `docs/research/2026-06-22-paperdoll-handoff.md`, then its "Read first" list (esp. the deep-dive `docs/research/2026-06-16-equipment-paperdoll-deep-dive.md` and `claude-memory/project_d2b_retail_ui.md`).
>
> **NEXT: Sub-phase C — the paperdoll, Slice 1 (equip slots), first.** Bind the ~25 mounted equip slots (under `0x100001CD` / gmPaperDollUI `0x21000024`) to live equipped-item data and make them drag-drop wield/unwield targets — a new `PaperdollController` (the `gmPaperDollUI::PostInit` analogue) that maps element-id → `EquipMask` (deep-dive §3a table), populates each slot's icon from the item whose `CurrentlyEquippedLocation == that slot's EquipMask`, and registers as the equip-slots' `IItemListDragHandler`. **Build the wire gap:** `GetAndWieldItem 0x001A` (`itemGuid + EquipMask`) — `InteractRequests.BuildGetAndWieldItem` + `WorldSession.SendGetAndWieldItem` (mirror `BuildPickUp`/`SendPutItemInContainer`); wield = optimistic + rollback (reuse `MoveItemOptimistic`); unwield = the shipped `PutItemInContainer 0x0019` path. **Carry the correction:** retail empty equip slots are TRANSPARENT (the doll shows through), NOT silhouettes — decide the doll-less empty-slot look in the brainstorm. **Slice 2 (the 3D doll `UiViewport` Type 0xD + the Core→App `IUiViewportRenderer` seam) is a SEPARATE, heavier cycle — do not start it in Slice 1.**
>
> Run the full **brainstorm → spec → plan → subagent-driven → visual-gate** flow for Slice 1. **DO NOT auto-kill the running client** — launch with plain `dotnet run`; if a rebuild is locked, ask the user to close it. Reuse the shipped `UiItemSlot` + drag-drop spine + optimistic-move machinery; don't rebuild them.

View file

@ -0,0 +1,89 @@
# Handoff — D.2b Sub-phase C, Slice 2: the paperdoll 3-D doll + the "Slots" toggle
**Date:** 2026-06-23 (end of the Slice 1 session)
**Branch:** `claude/hopeful-maxwell-214a12`. **Slice 1 SHIPPED + merged**`main` is fast-forwarded to the branch tip (`main == branch` at `6897148`). Slice 2 continues on this branch.
**Status:** SCOPED, not started. Run the full **brainstorm → spec → plan → subagent-driven → visual-gate** flow.
**Line numbers drift — grep the symbol.**
---
## Read first (in this order)
1. **This doc.**
2. **`claude-memory/project_d2b_retail_ui.md`** — the D.2b SSOT. The **"SLICE 1 … SHIPPED 2026-06-23"** entry holds the CORRECTED paperdoll model + the full DO-NOT-RETRY. **It supersedes the visual/look claims in the older docs below.**
3. **`docs/research/2026-06-16-equipment-paperdoll-deep-dive.md`** — the AUTHORITATIVE element-id / wire / viewport research: §5 (the viewport mechanism, CONFIRMED), §5c (reuse analysis), §5d + §7 (camera/light immediates + the UNVERIFIED list). **⚠ Caveat:** its §2a "empty slot shows the doll body behind it" and the 2026-06-22 handoff's "transparent / per-slot silhouettes" framing were **WRONG about the LOOK** — see the corrected model below. The element ids, opcodes, and viewport plumbing in it are still good.
4. **`docs/architecture/code-structure.md` Rule 2** — Core defines interfaces, App implements; never the reverse. Governs the `IUiViewportRenderer` seam.
5. **`src/AcDream.App/UI/Layout/PaperdollController.cs`** — Slice 1's controller (the 21 slot bindings + wield/unwield drag handler you'll extend, not rewrite).
6. **`memory/reference_modern_rendering_pipeline.md`** + `EntitySpawnAdapter`/`AnimatedEntityState` — the in-world animated-character path the doll reuses.
## The CORRECTED paperdoll model (USER AXIOM — confirmed against his retail screenshots, 2026-06-23)
**There are NO per-slot silhouette sprites.** The "figure" in the paperdoll **IS the live 3-D character** (the doll — it can be naked if nothing is equipped). The panel has **two views toggled by the "Slots" button** (`0x100005BE` = `m_SlotCheckbox`, a `UIElement_Button`):
- **Button OFF (default) — "doll view":** show the **3-D character** + the **non-armor** slots (under-clothes / weapon / wand / shield / jewelry). The armor slots are hidden.
- **Button ON — "slot view":** the **character disappears** and the **armor slots become visible** (the grid).
It is a **TOGGLE (mutually exclusive: doll XOR armor-slots)**, NOT slots-overlaid-on-the-doll. Verify the exact show/hide split against the decomp (below) + the user's screenshots before coding — do not re-derive the look from the old deep-dive prose.
**What Slice 1 already did (don't redo):** bound all ~21 equip slots (`PaperdollController`, element-id→`EquipMask` map), wield (`GetAndWieldItem 0x001A`, optimistic + rollback) + unwield (the inventory-grid path + the `PickupEvent` two-table fix), and gave empty slots a **visible frame** (`emptySlotSprite = 0x06004D20`) so all positions are usable. Slice 1 shows **all** slots at once (no toggle, no doll). Slice 2 adds the doll + the toggle on top.
## Slice 2 — two pieces (each its own spec → plan → implement; the viewport is the crux)
### A. The "Slots" toggle (the lighter piece — do FIRST, it de-risks the layout)
Wire the `m_SlotCheckbox` button (`0x100005BE`) to switch between doll-view and slot-view:
- **Map each slot as ARMOR vs NON-ARMOR.** Retail's `RemakeCharacterInventory` (decomp `0x004a65c0`) builds `clothingPriorityMask` from the mask **`0x8007fff`** (line 176016) — clothing/jewelry/misc; the armor slots are the `*Armor` `EquipMask` bits (`ChestArmor 0x200 … LowerLegArmor 0x4000`). Confirm the exact partition from `GetLocationInfoFromElementID` (the §3a table) + the toggle handler.
- **READ THE TOGGLE HANDLER FIRST:** `gmPaperDollUI::ListenToElementMessage` (decomp `0x004a5c30`, ~line 175593) — the `idMessage == 1` branch (button click, ~line 175628+, NOT yet read this session) is the show/hide logic. The init does `SetAttribute_Bool(m_SlotCheckbox, 0xe, 0)` (line 175585) = start unchecked (doll view). Port the exact `SetVisible` toggling it does on the armor slots + the viewport.
- In acdream: `PaperdollController` keeps refs to the armor slots (toggle-hidden in doll view) + the viewport widget; the button's click flips their `Visible`. The non-armor slots stay visible in both views (verify).
- **Slice-1 caveat to revisit:** AP-66 (empty slots show a frame) — decide whether the frame stays in slot-view (likely yes) and what an empty NON-ARMOR slot shows in doll-view. The "frames flip to transparent / doll-through" idea in AP-66 was written under the overlay assumption; the TOGGLE model means there is no doll behind the armor slots in slot-view, so the frame likely **stays**. Re-word AP-66 once the toggle behavior is confirmed.
### B. The 3-D doll `UiViewport` (the heavy piece — the UI↔3D bridge, the real crux)
A `UiViewport` widget (registers at dat **Type `0xD`**, element **`0x100001D5`**) that renders a re-dressed clone of the local player into the widget's screen rect (a scissored single-entity 3-D pass), shown in doll-view.
- **The Core→App seam (Code-Structure Rule 2):** define `IUiViewportRenderer` in `AcDream.Core` (or the UI.Abstractions layer), implement in `AcDream.App` (it has GL + `EntitySpawnAdapter`). The widget's `OnDraw` only has a 2-D `UiRenderContext`; the 3-D pass needs a dedicated overlay hook the `UiHost`/`GameWindow` invokes for any `UiViewport` present (NOT inside `OnDraw`). **The exact integration point (after the world pass vs a UI overlay) is DESIGN-OPEN — settle it in the brainstorm** (deep-dive §5d/§7).
- **Reuse the existing char path (deep-dive §5c, CONFIRMED):** build a `WorldEntity` from the local player's Setup + current ObjDesc, feed it through `EntitySpawnAdapter`/`AnimatedEntityState` (palette/part/hidden-part overrides), draw it with a fixed camera + one distant light into the rect. Re-dress on `ObjDescEvent 0xF625` (already PARSED) = rebuild the entity's overrides — the C# analog of retail `RedressCreature` (decomp `0x004a5c90`?/line 173990 / 175535).
- **Camera/light/heading immediates** (retail `gmPaperDollUI::PostInit`, decomp ~175509-535; raw hex read this session, **decode + VERIFY**):
- `SetCamera(viewport, &dir, &pos)` with `dir ≈ (0x3df5c28f=0.12, 0xc019999a=-2.4, 0x3f6147ae=0.88)`, `pos = (0,0,0)`. (Arg order pos-vs-dir UNVERIFIED — `UIElement_Viewport::SetCamera``CreatureMode::SetCameraPosition/Direction`.)
- `SetLight(viewport, DISTANT_LIGHT, 2.0f, &dir)` with `dir ≈ (0x3e99999a=0.3, 0x3ff33333=1.9, <3rd component is a strncpy/lifter artifact — recover from Ghidra/re-decompile>)`.
- `set_heading(191.367905°)` so the doll faces the viewer; idle animation via `m_didAnimation` (`UpdateForRace` `0x004a…`/line 174129 swaps the idle DID per body-type via `DBObj::GetDIDByEnum`); `CreatureMode::UseSharpMode` (sharper mip bias).
- **Player-clone vs fresh WorldEntity (UNVERIFIED, deep-dive §7):** retail clones the player `CPhysicsObj` (`makeObject(GetPhysicsObject(player_id))`). acdream's local player is NOT a renderable `WorldEntity` (it's the camera), so LIKELY build a dedicated `WorldEntity` from the player's Setup + ObjDesc and host it in a private viewport scene. Confirm the player's Setup id + current ObjDesc are available client-side (PlayerDescription / CreateObject / the equipped ObjDescEvents).
- **Polish, NOT MVP:** part-selection lighting (`ApplyPartSelectionLighting` / `GetSelectionMaskFromObject` / `CreateClickMap`, lines 174034/174762/174636) — the "which armor piece is this?" highlight + the doll click-map; the Aetheria sigil slots (`0x10000595/96/97`, `SetVisible(0)` by default). Defer.
## What's already in place (reuse, don't rebuild)
- **`PaperdollController`** — the slot bindings + wield/unwield + the element-id→`EquipMask` map. Extend it with the armor/non-armor partition + the viewport ref + the toggle.
- **The mount + import** — the gmPaperDollUI subtree (`0x21000024`) is imported under `0x100001CD` in the inventory frame; the viewport element `0x100001D5` is in the tree (currently skipped — `DatWidgetFactory` Type 12→skip; Type `0xD` is NOT registered yet → register it to `UiViewport`).
- **`EntitySpawnAdapter` / `AnimatedEntityState` / `WorldEntity`** — the per-instance animated-character render path (palette/part/hidden-part overrides). The doll's model pipeline.
- **`ObjDescEvent 0xF625`** parse + `CreateObject.ReadModelData` (palette/sub-palette/texture/anim-part) — the re-dress input.
- **`ClientObjectTable` + the wield wire** (`GetAndWieldItem`, `WieldObject 0x0023` + `ConfirmMove`, `PickupEvent` two-table fix) — all shipped in Slice 1.
## Open questions / UNVERIFIED (settle in the brainstorm)
1. **Overlay vs replace** — confirmed REPLACE (toggle) per the user, but verify the exact `SetVisible` set the `ListenToElementMessage idMessage==1` handler toggles (armor slots + viewport; do the non-armor slots stay shown in slot-view?).
2. **The `IUiViewportRenderer` integration point** — after the world pass, or a dedicated UI-overlay 3-D pass? Scissor + viewport from the widget's screen rect. Code-Structure Rule 2 = Core interface, App impl.
3. **Camera/light immediates** — decode + verify the floats above; recover the corrupted 3rd light component from Ghidra.
4. **Player-clone vs fresh `WorldEntity`** — acdream has no player `WorldEntity`; build one from Setup+ObjDesc. Confirm the data is available client-side.
5. **AP-66 fate** — does the empty-slot frame stay in slot-view (likely) or change? Re-word the register row once the toggle behavior is confirmed.
6. **`0x100001E0 = MissileAmmo 0x800000`** still LIKELY (AP-62) — gate-verify the ammo slot.
## Decomp anchors (named-retail `acclient_2013_pseudo_c.txt`)
- `gmPaperDollUI::PostInit` (~175232; the slot/viewport/button init I read this session — `SetVisible(0)` per slot, `SetCamera`/`SetLight`/`UseSharpMode`/`RedressCreature` at 175509-535, `m_SlotCheckbox = 0x100005BE` at 175557).
- `gmPaperDollUI::ListenToElementMessage` (~175593; **read the `idMessage==1` branch ~175628+ for the toggle**).
- `gmPaperDollUI::RedressCreature` (173990 / 175535; clone + `set_heading(191.37°)` + `set_sequence_animation` + `DoObjDescChangesFromDefault`).
- `gmPaperDollUI::UpdateForRace` (174129; per-body-type camera + idle DID).
- `gmPaperDollUI::RemakeCharacterInventory` (175983) + `SetUIItemIntoLocation` (175713/175950; populate equipped items — Slice 1 does the equivalent).
- `UIElement_Viewport::Create/SetCamera/SetLight/PostInit` + `CreatureMode::Render` (deep-dive §5a/§5b, lines 119029/91665).
## Acceptance (Slice 2)
- The "Slots" button toggles: doll-view (3-D character + non-armor slots) ↔ slot-view (armor slots). Matches the user's two screenshots.
- The doll renders the re-dressed local player (correct race/gender/equipped gear; naked if nothing equipped), faces the viewer, idles; updates live on equip/unequip via `ObjDescEvent 0xF625`.
- Build + full suite green; **visual gate** (user compares to retail). Divergence rows for any approximation; the `IUiViewportRenderer` seam respects Code-Structure Rule 2.
## New-session prompt
> Continue acdream's D.2b retail-UI inventory arc on branch `claude/hopeful-maxwell-214a12` (Slice 1 shipped; `main` is ff-merged to the tip). **READ FIRST:** `docs/research/2026-06-23-paperdoll-slice2-handoff.md`, then its "Read first" list — especially the **Slice 1 entry in `claude-memory/project_d2b_retail_ui.md`** (the CORRECTED paperdoll model) and the deep-dive `docs/research/2026-06-16-equipment-paperdoll-deep-dive.md` §5.
>
> **NEXT: Sub-phase C — the paperdoll, Slice 2.** The corrected model (user axiom): the paperdoll "figure" IS the **live 3-D character** (not silhouettes); the **"Slots" button (`0x100005BE`)** TOGGLES between **doll-view** (3-D character + non-armor slots) and **slot-view** (armor slots) — mutually exclusive, NOT overlaid. Build (A) the **toggle** first — read `gmPaperDollUI::ListenToElementMessage`'s `idMessage==1` branch (~decomp 175628+) for the exact `SetVisible` show/hide of the armor slots + viewport; partition armor vs non-armor slots (armor = the `*Armor` EquipMask bits; clothing/jewelry/weapon = non-armor, cf. `clothingPriorityMask` `0x8007fff`). Then (B) the **3-D doll `UiViewport`** (register dat **Type `0xD`**, element `0x100001D5`): a Core `IUiViewportRenderer` seam (Code-Structure Rule 2 — Core defines, App implements) driving a scissored single-entity 3-D pass keyed to the widget rect; reuse `EntitySpawnAdapter`/`AnimatedEntityState` by building a `WorldEntity` from the local player's Setup + current ObjDesc, re-dressed on `ObjDescEvent 0xF625` (the C# analog of `RedressCreature`); fixed camera + one distant light + `set_heading(191.37°)` + idle anim (decode the `PostInit` immediates ~175509-535 + verify). **Extend the shipped `PaperdollController` — do NOT rewrite the slot bindings/wield/unwield.** The UI↔3-D integration point is DESIGN-OPEN — settle it in the brainstorm. Run the full **brainstorm → spec → plan → subagent-driven → visual-gate** flow. **DO NOT auto-kill the running client** — launch with plain `dotnet run`; if a rebuild is locked, ask the user to close it.