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>
18 KiB
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—gmToolbarUIshortcut model + theHandleDropReleasedrag 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:
- 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 id0x00000002); the back-track sprite is on the meter element's own DirectState (e.g. health0x0600193E). FixBuildMeterto detect the 1-container case and read the back track from the element'sStateMedia[""], fill from the child. (Vitals meters0x2100006Chave 2 containers and work — use them as the contrast.) SelectedObjectController(analogue ofVitalsController— see the working bind pattern atVitalsController.cs:61-97): on selection-change,SetVisible(true)on0x100001A1(/A2for owned items), bindUiMeter.Fillto() => combat.GetHealthPercent(selGuid), bind the name text0x1000019FtoClientObjectTable.Get(selGuid)?.Name, set the0x100001A0overlay state; on deselectSetVisible(false).- Selection notification: there is no
SelectionChangedevent today —_selectedGuidis a rawuint?onGameWindow(~GameWindow.cs:844), written byPickAndStoreSelection(LMB) andSelectClosestCombatTarget(Tab/Q), cleared on despawn. Either add an event or poll-and-diff aFunc<uint?>(theTargetIndicatorPanelpattern). Brainstorm: event vs poll. - Health is ready:
CombatState.GetHealthPercent(guid)+CombatState.HealthChanged(CombatState.cs:92,45), wired fromUpdateHealth 0x01C0(GameEventWiring.cs:155). To force a fresh value on selection, retail sendsQueryHealth—SocialActions.BuildQueryHealth(0x01BF) already exists (SocialActions.cs:49). Brainstorm: send QueryHealth on select, or rely on server broadcasts for now? - Mana is NOT ready (the harder half): no remote-target mana anywhere (
CombatStateis health-only;LocalPlayerState.ManaPercentis self-only).QueryItemManaResponse (0x0264)is parsed (GameEvents.cs:416) but unregistered inGameEventWiring, and there is no outboundQueryItemManabuilder (its C→S opcode is unknown —0x0264is 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. - 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:
BeginDragalways passespayload: null(UiRoot.cs:188);DragPayloadhas a private setter (UiRoot.cs:73) → needs aSetDragPayload(object)escape hatch (or a source-payload callback).UiItemSlot.OnEventhandles onlyMouseDown→Clicked(UiItemSlot.cs:101-105) — no DragBegin/DragEnter/DragOver/DropReleased cases. (UiItemSlot.ItemId:19is the payload source.)UiField'sCatchDroppedItem/MouseOverTopare doc-comment only (UiField.cs:10-11) — the bodies belong onUiItemSlot, per the spine doc §5.6.- No
IItemListDragHandlerinterface exists; no drag ghost renderer; noInqDropIconInfohelper.
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 viaonShortcutsatGameEventWiring.cs:415). Port retailShortCutManager::shortCuts_[18](acclient.h:36492) as a small mutableShortcutStore(18 slots;Load/AddOrReplace(slot,guid)→displaced/Remove(slot)). - Wire builders exist with a naming bug.
InventoryActions.BuildAddShortcut(0x019C,InventoryActions.cs:99) — paramobjectTypeshould beobjectGuid; the trailing field is packedspellId(u16)|layer(u16)(0 for items). Byte layout is already correct for item-only callers; fix the names before wiring. Field order confirmed by ACEShortcut.cs:33, holtburgershortcuts.rs:37, retailShortCutDataacclient.h:36484.BuildRemoveShortcut(0x019D) is fine. - No
SendAddShortcut/SendRemoveShortcutonWorldSession— wrap the builders (pattern =SendChangeCombatMode:NextGameActionSequence()+Build*()+SendGameAction(),:1064). - Drop flow (retail
gmToolbarUI::HandleDropRelease :197971):InqDropIconInfoflags&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.ToolbarControllerimplementsIItemListDragHandler+ getsActions 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):
- 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);UiHosthas no open/close API (UiHost.cs:37). Needs at minimum anI-key toggle to open/close the inventory window. (Faithful Dragbar/Resizebar resize stays deferred — IA-12 whole-window-drag is fine.) UiItemListN-cell grid mode — currently single-cell (UiItemList.cs:12, only sizes_cells[0]);Flush/AddItemskeleton exists but no column-count/pitch/wrap (LIKELY 6 cols × 36px; confirm fromUIElement_ItemList::ItemList_AddItem).- Sub-window mount in
LayoutImporter—gmInventoryUI(0x21000023) nests paperdoll (0x21000024), backpack (0x21000022), 3D-items (0x21000021) as child elements whose class id has its ownBaseLayoutId. The importer only does TEMPLATE inheritance today (LayoutImporter.cs:196-228) — it has never instantiated a nestedgm*UIwindow. New capability. - Wire gaps (inventory deep-dive §4.3): builders
DropItem 0x001B,GetAndWieldItem 0x001A,NoLongerViewingContents 0x0195(all absent); parsersViewContents 0x0196,SetStackSize 0x0197,InventoryRemoveObject(all absent); fixParsePutObjInContainer(drops the 4thcontainerType,GameEvents.cs:352) +ParseInventoryServerSaveFailed(dropsweenieError,:377); registerViewContents/0x019A/0x0052/0x00A0inGameEventWiring. UiViewport(Type 0xD) for the paperdoll 3D doll — the single biggest new piece. No widget, no factory registration, no renderer. Needs anIUiViewportRendererCore→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).InventoryController+PaperDollController(thegm*UI::PostInitfind-by-id pattern): backpack burden Meter (SetLoadLevel→fill0x69), own-pack list + side-pack list, the element-id→EquipMaskmap 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):
- 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.
- Drag-drop spine completion (§B.1) — shared infra for B and C. Build once.
- Window manager (open/close) (§C.1) — enough to toggle the inventory window open.
- D.5.5 — inventory window (§C, grid + sub-window mount + wire gaps +
InventoryController). This gives the drag source. - D.5.3b — shortcut drag-to-add/reorder/remove (Stream B) — now that the spine + inventory source
ShortcutStore+ theBuildAddShortcutfix are in place. (Reorder-within-bar could land earlier with just steps 2 + the store.)
- 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 (0x0600193Ehealth) against the dump. - B:
DragPayloadshape (arecord ItemDragPayload(objId, srcContainer, srcSlot, flags)vs the slot itself)? Where does the drag ghost render (UiRoot.OnDraw vs UiItemSlot overlay)? IsUiItemListorUiItemSlotthe drop-target unit? Fire-and-forget vs optimistic-then-confirm for the shortcut wire? - C: Sub-window mount — recursive
Import()inLayoutImporter, or external stitch by the controller? Inventory grid column count (confirm 6 from decomp)? Does the paperdoll doll clone the playerWorldEntityor build a fresh ObjDesc-dressedAnimatedEntityState(player = camera, so there's no player-as-renderable today)?IUiViewportRenderertiming (post-world pass vs pre-pass)? Open the inventory byI-key only, or also the toolbar's inventory button?
7. ⚠ Corrections to the grounding research (verify against source)
_liveEntityInfoByGuidis GONE (retired in D.5.4 Task 10,a9d40ad). A research agent's notes reference it as the selected-object name source atGameWindow.cs:835/2559/12129— stale. Post-D.5.4 the name resolves viaClientObjectTable.Get(guid)?.Name, or theGameWindow.LiveName(guid)/DescribeLiveEntity(guid)helpers (which now read the table). Likewise "ClientObjectTabledoes 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.mdfirst, 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 (theAddShortcut 0x019C/RemoveShortcut 0x019Dwire + the drag-drop spine completion; item shortcuts already render+click); (C) start the paperdoll+inventory window (one window —gmInventoryUInests 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 viaClientObjectTable.Get(guid)/GetContents(containerGuid). Branchclaude/hopeful-maxwell-214a12(kept, unmerged).
MEMORY.md index line:
- Handoff: finish the bar + inventory/paperdoll window (2026-06-18) — 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.