docs: handoff — finish the action bar (selected-object meter + shortcut drag) + start the inventory/paperdoll window

Next D.2b-UI work after D.5.4. 3 streams (spell bar deferred): selected-object
meter, shortcut drag/add/reorder/remove, inventory+paperdoll window. Current-code
anchors + dependency graph + build order + brainstorm questions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-18 21:29:04 +02:00
parent 6eb0fbde46
commit d400bc6105

View file

@ -0,0 +1,239 @@
# Handoff — finish the action bar + start the inventory/paperdoll window
**Date:** 2026-06-18
**From:** the D.5.4 object/item-model session (SHIPPED `b506f53..6eb0fbde`, 2672 tests green, visually
confirmed on Barris/Coldeve). The data model is now solid — every server object lives in
`ClientObjectTable`, resolvable by guid. This handoff frames the NEXT work on the D.2b retail-UI track.
**Branch:** `claude/hopeful-maxwell-214a12` (kept, unmerged — carries D.5.2 + D.5.4).
**Line numbers below are as of HEAD `6eb0fbde` and WILL drift — grep the symbol, don't trust the line.**
---
## 0. Scope (settled with the user)
Three work streams. **The spell bar is explicitly DEFERRED** (it is a separate feature — a dedicated
spell-casting bar — NOT the action-bar spell *shortcuts*; do not build spell-glyph rendering/casting here).
| Stream | What | Roadmap |
|---|---|---|
| **A. Selected-object meter** | The action bar's bottom strip: the player's currently-**selected** world object's Health/Mana meter + name (+ stack slider, deferred). Currently hidden. | D.5.3 (issue #140) |
| **B. Shortcut drag / add / reorder / remove** | Drag an item from the inventory window onto a hotbar slot; reorder slots; remove. The `AddShortcut`/`RemoveShortcut` wire. Item shortcuts already RENDER + click-to-use (D.5.1/D.5.4); this is the interactive management. | D.5.3 / D.5.5 |
| **C. Paperdoll + inventory window** | One combined window (`gmInventoryUI` nests paperdoll + backpack + 3D-items). It is the **drag SOURCE** that Stream B needs. | D.5.5 |
**Out of scope:** the spell bar; the stack-split UI (entry box `0x100001A3` + slider `0x100001A4`);
the faithful Dragbar/Resizebar window resize (the IA-12 whole-window-drag approximation stays for now).
**Dependency reality:** Stream B's drag-*from-inventory* needs Stream C (the inventory window) as the
drag source, and both B and C need the **drag-drop spine completed** (shared infra, §B.1). So this is
really 2-3 sub-phases — see the build order in §4. Each gets its own brainstorm → spec → plan.
---
## 1. Read first
- This doc.
- `docs/research/2026-06-16-ui-panels-synthesis.md`**the build plan** for the core panels (build order, widget list, cross-panel wire table). Stream C follows it.
- `docs/research/2026-06-16-ui-item-slot-icon-dragdrop-spine-deep-dive.md` — the drag-drop spine design (§5 pseudocode is the spec for Stream B's widget hooks).
- `docs/research/2026-06-16-inventory-deep-dive.md` + `docs/research/2026-06-16-equipment-paperdoll-deep-dive.md` — the two panels' LayoutDesc maps + wire catalog.
- `docs/research/2026-06-16-action-bar-toolbar-deep-dive.md``gmToolbarUI` shortcut model + the `HandleDropRelease` drag flags.
- `claude-memory/project_object_item_model.md` (D.5.4) + `claude-memory/project_d2b_retail_ui.md` (D.2b/D.5.1/D.5.2 toolkit).
**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.
The named-decomp anchors for each stream are inline below.
---
## 2. Stream A — selected-object meter (the smallest, mostly self-contained)
**Goal:** when the player selects a world object (LMB pick or Tab/Q combat-target), the action bar's
bottom strip shows that object's **Health meter** + **name**; **Mana meter** for owned items.
**Retail lifecycle** (the oracle): `gmToolbarUI::HandleSelectionChanged`
(`acclient_2013_pseudo_c.txt:198635`) — on selection it `SetVisible(1)`s the right meter and fires
`CM_Combat::Event_QueryHealth(guid)` (creatures/players) or `CM_Item::Event_QueryItemMana(guid)`
(owned items). The server replies `UpdateHealth (0x01C0)` / `UpdateItemMana`, and
`RecvNotice_UpdateObjectHealth` (`:196213`) / `RecvNotice_UpdateItemMana` (`:196188`) call
`SetAttribute_Float(meter, 0x69, pct)`**property `0x69` is the fill ratio**. `UIElement_Meter`'s
fill element is child id `2` (`UIElement_Meter::Initialize :123328`; `OnSetAttribute :123712`).
Mana is gated on `IsOwnedByPlayer` (`:198763`).
**LayoutDesc elements** (toolbar `0x21000016`, `.layout-dumps/toolbar-0x21000016.txt:621-811`):
container `0x1000019E`; name text `0x1000019F` (Type 0) + state overlay `0x100001A0`
(states `ObjectSelected 0x06001937` / `StackedItemSelected 0x06004CF4`); **health meter `0x100001A1`**
(Type 7); **mana meter `0x100001A2`** (Type 7); stack entry `0x100001A3`; stack slider `0x100001A4`
(Type 11). All currently in `ToolbarController` `HiddenIds` (~`ToolbarController.cs:41`),
`SetVisible(false)` at Bind (~`:100`).
**Work items:**
1. **Fix the meter render bug** (the launch-log `meter 0x100001A1/A2: 1 Type-3 slice container
(expected 2)` warning). `DatWidgetFactory.BuildMeter` (~`DatWidgetFactory.cs:135-154`) assumes 2
Type-3 slice containers (back + fill). The toolbar meters have **1** container (the fill, child
id `0x00000002`); the **back-track sprite is on the meter element's own DirectState**
(e.g. health `0x0600193E`). Fix `BuildMeter` to detect the 1-container case and read the back
track from the element's `StateMedia[""]`, fill from the child. (Vitals meters `0x2100006C` have 2
containers and work — use them as the contrast.)
2. **`SelectedObjectController`** (analogue of `VitalsController` — see the working bind pattern at
`VitalsController.cs:61-97`): on selection-change, `SetVisible(true)` on `0x100001A1`(/`A2` for owned
items), bind `UiMeter.Fill` to `() => combat.GetHealthPercent(selGuid)`, bind the name text
`0x1000019F` to `ClientObjectTable.Get(selGuid)?.Name`, set the `0x100001A0` overlay state; on
deselect `SetVisible(false)`.
3. **Selection notification:** there is no `SelectionChanged` event today — `_selectedGuid` is a raw
`uint?` on `GameWindow` (~`GameWindow.cs:844`), written by `PickAndStoreSelection` (LMB) and
`SelectClosestCombatTarget` (Tab/Q), cleared on despawn. Either add an event or poll-and-diff a
`Func<uint?>` (the `TargetIndicatorPanel` pattern). **Brainstorm: event vs poll.**
4. **Health is ready:** `CombatState.GetHealthPercent(guid)` + `CombatState.HealthChanged`
(`CombatState.cs:92,45`), wired from `UpdateHealth 0x01C0` (`GameEventWiring.cs:155`).
To force a fresh value on selection, retail sends `QueryHealth``SocialActions.BuildQueryHealth`
(0x01BF) already exists (`SocialActions.cs:49`). **Brainstorm: send QueryHealth on select, or rely
on server broadcasts for now?**
5. **Mana is NOT ready** (the harder half): no remote-target mana anywhere (`CombatState` is
health-only; `LocalPlayerState.ManaPercent` is self-only). `QueryItemManaResponse (0x0264)` is
*parsed* (`GameEvents.cs:416`) but **unregistered** in `GameEventWiring`, and there is **no
outbound `QueryItemMana` builder** (its C→S opcode is unknown — `0x0264` is the reply).
**Brainstorm/decide: defer mana entirely for D.5.3 (health-only, matching that mana is owned-item-only
anyway), or do the full mana path?** Recommend deferring mana → ship health-meter + name first.
6. **Stack slider/entry (`0x100001A3/A4`):** deferred (stack-split UI).
**Why A is mostly standalone:** it doesn't need the drag-drop spine, the window manager, or the
inventory window. It's the quickest win and finishes the bar's *display*. Good first chunk.
---
## 3. Stream B — shortcut drag / add / reorder / remove
**Item shortcuts already render + click-to-use** (D.5.1 + D.5.4). This stream is the interactive
management: drag an item from inventory onto a slot, reorder, remove.
### B.1 — the drag-drop spine (SHARED infra, also needed by Stream C)
`UiRoot` has the **complete** retail drag state machine, LIVE-wired to Silk.NET input:
`BeginDrag`/`UpdateDragHover`/`FinishDrag` firing `DragBegin 0x15`/`DragEnter 0x21`/`DragOver 0x1C`/
`DropReleased 0x3E` (`UiRoot.cs:450-496`), promoted on >3px move, bridged via `UiHost.WireMouse`
(`UiHost.cs:78-88`, called at `GameWindow.cs:1769`). **But:**
- `BeginDrag` always passes `payload: null` (`UiRoot.cs:188`); `DragPayload` has a private setter
(`UiRoot.cs:73`) → needs a `SetDragPayload(object)` escape hatch (or a source-payload callback).
- `UiItemSlot.OnEvent` handles only `MouseDown→Clicked` (`UiItemSlot.cs:101-105`) — **no
DragBegin/DragEnter/DragOver/DropReleased cases**. (`UiItemSlot.ItemId` `:19` is the payload source.)
- `UiField`'s `CatchDroppedItem`/`MouseOverTop` are **doc-comment only** (`UiField.cs:10-11`) — the
bodies belong on `UiItemSlot`, per the spine doc §5.6.
- No `IItemListDragHandler` interface exists; no drag ghost renderer; no `InqDropIconInfo` helper.
**Build (spine doc §5.7 is the spec):** (1) payload injection in `UiItemSlot` on DragBegin
(`{objId=ItemId, srcContainer, srcSlot}`); (2) a cursor-following **drag ghost** (the icon is already
in `UiItemSlot.IconTexture`); (3) drop-target hooks on `UiItemSlot` (DragEnter/Over→accept/reject
overlay `0x10000041`/`0x10000040`/`0x1000003f`; DropReleased→`HandleDropRelease`); (4)
`IItemListDragHandler { bool OnDragOver(...); void HandleDropRelease(...) }` that panels implement +
register on their `UiItemList`.
### B.2 — the shortcut model + wire
- **Mutable store missing.** Shortcuts are a **read-only** `IReadOnlyList<ShortcutEntry>`
(`GameWindow.Shortcuts ~:600`, set once from PlayerDescription via `onShortcuts` at
`GameEventWiring.cs:415`). Port retail `ShortCutManager::shortCuts_[18]` (`acclient.h:36492`) as a
small mutable `ShortcutStore` (18 slots; `Load`/`AddOrReplace(slot,guid)→displaced`/`Remove(slot)`).
- **Wire builders exist with a naming bug.** `InventoryActions.BuildAddShortcut` (0x019C,
`InventoryActions.cs:99`) — param `objectType` should be `objectGuid`; the trailing field is packed
`spellId(u16)|layer(u16)` (0 for items). Byte layout is already correct for item-only callers; **fix
the names before wiring.** Field order confirmed by ACE `Shortcut.cs:33`, holtburger
`shortcuts.rs:37`, retail `ShortCutData` `acclient.h:36484`. `BuildRemoveShortcut` (0x019D) is fine.
- **No `SendAddShortcut`/`SendRemoveShortcut` on `WorldSession`** — wrap the builders (pattern =
`SendChangeCombatMode`: `NextGameActionSequence()` + `Build*()` + `SendGameAction()`, `:1064`).
- **Drop flow** (retail `gmToolbarUI::HandleDropRelease :197971`): `InqDropIconInfo` flags
`&0xE==0` = fresh-from-inventory (place), `&4` = reorder. On drop: remove target if occupied (0x019D)
→ update store → add (0x019C) → `Populate()`. Reorder also puts the displaced item back in the source
slot. `ToolbarController` implements `IItemListDragHandler` + gets `Action`s for the two sends.
**Reorder-within-bar needs no inventory; drag-from-inventory needs Stream C.**
---
## 4. Stream C — paperdoll + inventory window (one window)
**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 (post-D.5.1/D.5.4):** `UiItemSlot` + `UiItemList` + `IconComposer` (`src/AcDream.App/UI/`),
`DatWidgetFactory` registers `0x10000031→UiItemList` (`:70`); the data path is
`ClientObjectTable.GetContents(containerGuid)` → ordered guids → `Get(guid)` → full icon fields
(`ClientObjectTable.cs:273,188`). The toolkit + data model are in place.
**MISSING (the build, in synthesis order):**
1. **Window manager** (deferred Plan-2): open/close/z-order/persist. Today every window is **always-on
at a hardcoded position** (`ACDREAM_RETAIL_UI=1`, `GameWindow.cs:1906`); `UiHost` has no
open/close API (`UiHost.cs:37`). Needs at minimum an **`I`-key toggle** to open/close the inventory
window. (Faithful Dragbar/Resizebar resize stays deferred — IA-12 whole-window-drag is fine.)
2. **`UiItemList` N-cell grid mode** — currently single-cell (`UiItemList.cs:12`, only sizes
`_cells[0]`); `Flush`/`AddItem` skeleton exists but no column-count/pitch/wrap (LIKELY 6 cols × 36px;
confirm from `UIElement_ItemList::ItemList_AddItem`).
3. **Sub-window mount in `LayoutImporter`**`gmInventoryUI` (`0x21000023`) nests paperdoll
(`0x21000024`), backpack (`0x21000022`), 3D-items (`0x21000021`) as child elements whose class id
has its own `BaseLayoutId`. The importer only does TEMPLATE inheritance today
(`LayoutImporter.cs:196-228`) — it has never instantiated a nested `gm*UI` window. New capability.
4. **Wire gaps** (inventory deep-dive §4.3): builders `DropItem 0x001B`, `GetAndWieldItem 0x001A`,
`NoLongerViewingContents 0x0195` (all absent); parsers `ViewContents 0x0196`, `SetStackSize 0x0197`,
`InventoryRemoveObject` (all absent); fix `ParsePutObjInContainer` (drops the 4th `containerType`,
`GameEvents.cs:352`) + `ParseInventoryServerSaveFailed` (drops `weenieError`, `:377`); register
`ViewContents`/`0x019A`/`0x0052`/`0x00A0` in `GameEventWiring`.
5. **`UiViewport` (Type 0xD)** for the paperdoll 3D doll — **the single biggest new piece.** No widget,
no factory registration, no renderer. Needs an `IUiViewportRenderer` **Core→App seam** (Rule 2) for a
scissored single-entity GL pass. The doll is the local player's ObjDesc-dressed entity in a fixed
viewport. **Heavy — brainstorm separately (see §5 open questions).**
6. **`InventoryController` + `PaperDollController`** (the `gm*UI::PostInit` find-by-id pattern):
backpack burden Meter (`SetLoadLevel`→fill `0x69`), own-pack list + side-pack list, the
element-id→`EquipMask` map for paperdoll slots, `ObjDescEvent 0xF625` → re-dress.
---
## 5. Recommended build order + the dependency graph
This spans **2-3 sub-phases**. Suggested sequence (each its own brainstorm → spec → plan):
1. **D.5.3a — selected-object meter** (Stream A). Standalone, quickest, finishes the bar's display.
No spine/window-manager dependency. Recommend health-meter + name first; defer mana.
2. **Drag-drop spine completion** (§B.1) — shared infra for B and C. Build once.
3. **Window manager (open/close)** (§C.1) — enough to toggle the inventory window open.
4. **D.5.5 — inventory window** (§C, grid + sub-window mount + wire gaps + `InventoryController`).
This gives the drag **source**.
5. **D.5.3b — shortcut drag-to-add/reorder/remove** (Stream B) — now that the spine + inventory source
+ `ShortcutStore` + the `BuildAddShortcut` fix are in place. (Reorder-within-bar could land earlier
with just steps 2 + the store.)
6. **Paperdoll** (`UiViewport` + `PaperDollController`, §C.5/6) — the 3D doll, the heaviest piece.
**Critical-path note:** the drag-drop spine (step 2) is the lynchpin — both shortcut drag and inventory
drag depend on it. Do it early and well (it has its own spine deep-dive as the spec).
---
## 6. Open questions for the brainstorm(s)
- **A:** SelectionChanged event vs poll-and-diff? Send `QueryHealth (0x01BF)` on select, or rely on
server broadcasts? Defer mana (health-only) for D.5.3 — confirm. The meter render-bug fix:
back-track from the element's own DirectState — verify the sprite ids (`0x0600193E` health) against the
dump.
- **B:** `DragPayload` shape (a `record ItemDragPayload(objId, srcContainer, srcSlot, flags)` vs the
slot itself)? Where does the drag ghost render (UiRoot.OnDraw vs UiItemSlot overlay)? Is `UiItemList`
or `UiItemSlot` the drop-target unit? Fire-and-forget vs optimistic-then-confirm for the shortcut wire?
- **C:** Sub-window mount — recursive `Import()` in `LayoutImporter`, or external stitch by the
controller? Inventory grid column count (confirm 6 from decomp)? Does the paperdoll doll clone the
player `WorldEntity` or build a fresh ObjDesc-dressed `AnimatedEntityState` (player = camera, so there's
no player-as-renderable today)? `IUiViewportRenderer` timing (post-world pass vs pre-pass)? Open the
inventory by `I`-key only, or also the toolbar's inventory button?
---
## 7. ⚠ Corrections to the grounding research (verify against source)
- **`_liveEntityInfoByGuid` is GONE** (retired in D.5.4 Task 10, `a9d40ad`). A research agent's notes
reference it as the selected-object name source at `GameWindow.cs:835/2559/12129` — **stale.**
Post-D.5.4 the name resolves via `ClientObjectTable.Get(guid)?.Name`, or the `GameWindow.LiveName(guid)`
/ `DescribeLiveEntity(guid)` helpers (which now read the table). Likewise "`ClientObjectTable` does not
exist yet" is wrong — it shipped in D.5.4. Trust the table, not the dict.
- **Line numbers throughout drift** (D.5.4 removed ~75 lines from `GameWindow`). Grep the symbol.
---
## 8. New-session prompt (paste into a fresh session)
> Continue acdream's D.2b retail-UI track. **Read `docs/research/2026-06-18-d53-bar-finish-and-inventory-handoff.md` first**, then the 2026-06-16 UI deep-dives it references. Three work streams (spell bar DEFERRED — it is a separate feature, not the action-bar spell shortcuts): **(A)** the action bar's selected-object meter (Health + name; mana deferred — issue #140); **(B)** shortcut drag/add/reorder/remove (the `AddShortcut 0x019C`/`RemoveShortcut 0x019D` wire + the drag-drop spine completion; item shortcuts already render+click); **(C)** start the paperdoll+inventory window (one window — `gmInventoryUI` nests paperdoll/backpack/3D-items). The drag-drop spine (UiRoot has the machine; UiItemSlot lacks the hooks) is shared infra for B and C — build it early. Suggested order: A (standalone quick win) → drag-drop spine → window manager (open/close) → inventory window → shortcut drag → paperdoll (UiViewport). Use the full brainstorm → spec → plan → subagent-driven flow per stream; mandatory grep-named→cross-ref→pseudocode→port for any wire format; conformance tests throughout. Data model is solid post-D.5.4: resolve every object via `ClientObjectTable.Get(guid)` / `GetContents(containerGuid)`. Branch `claude/hopeful-maxwell-214a12` (kept, unmerged).
**MEMORY.md index line:**
- [Handoff: finish the bar + inventory/paperdoll window (2026-06-18)](research/2026-06-18-d53-bar-finish-and-inventory-handoff.md) — next D.2b-UI work after D.5.4. 3 streams (spell bar DEFERRED): (A) selected-object meter (health+name, mana deferred; fix DatWidgetFactory 1-slice-container meter bug; SelectedObjectController like VitalsController), (B) shortcut drag/add/reorder/remove (UiRoot has the drag machine, UiItemSlot lacks hooks; mutable ShortcutStore missing; BuildAddShortcut naming bug), (C) inventory+paperdoll window (needs window-manager open/close + UiItemList grid mode + sub-window mount + wire gaps + UiViewport). Build order + per-stream anchors + brainstorm questions inside. ⚠ _liveEntityInfoByGuid is GONE (D.5.4) — name via ClientObjectTable.Get.