diff --git a/docs/research/2026-06-18-d53-bar-finish-and-inventory-handoff.md b/docs/research/2026-06-18-d53-bar-finish-and-inventory-handoff.md new file mode 100644 index 00000000..7695a1b0 --- /dev/null +++ b/docs/research/2026-06-18-d53-bar-finish-and-inventory-handoff.md @@ -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` (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` + (`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.