acdream/docs/superpowers/specs/2026-06-16-d2b-toolbar-phase1-design.md
Erik 44fabd350e docs(D.5.1): toolbar phase-1 implementation plan (+ spec wiring-delta note)
12-task TDD plan: register D.5.1 -> CreateObject IconId capture -> ItemRepository.EnrichItem -> spawn-event icon wiring -> persist shortcuts -> IconComposer (CPU composite) -> UiItemSlot -> UiItemList + factory branch -> ToolbarController -> GameWindow mount -> visual gate -> bookkeeping. Concrete call sites pinned (WorldSession.cs:701 EntitySpawned, GameEventWiring.WireAll, GameWindow Items@598, BuildUse 0x0036). Synced the spec's CreateObject section with the wider-than-expected wiring found during planning.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 21:27:49 +02:00

19 KiB
Raw Permalink Blame History

D.5.1 — Toolbar (action bar) — Phase 1 design

Date: 2026-06-16 Status: design approved (brainstorm), spec under review → writing-plans next Phase: D.5.1 — first sub-phase of D.5 "Core panels" (D.2b retail-look track). NEW sub-phase; roadmap registration is plan step 0 (roadmap discipline rule 4). Builds on: the shipped D.2b widget toolkit (b7f7e2b89626cd) — generic Type-registered widgets built by DatWidgetFactory, assembled by LayoutImporter, bound by thin gm*UI::PostInit-style controllers. See claude-memory/project_d2b_retail_ui.md.

Research evidence base (the anchors live here — this spec cites, does not re-derive):


1. Goal

Ship the action bar (gmToolbarUI) as the first data-driven game panel (vitals and chat were HUD). 18 shortcut slots built from LayoutDesc 0x21000016 via the existing LayoutImporter, populated from the persisted PlayerDescription shortcut block, each pinned item rendering its real composited icon, with click-to-use. Gated ACDREAM_RETAIL_UI=1, whole-window-drag.

The point of doing the toolbar first is that it is the thinnest end-to-end slice that exercises the entire shared item spine — the UiItemSlot widget, the icon composite pipeline, the UiItemList widget, a find-by-id controller, and the CreateObject icon extension — on the simplest of the three panels (no nested sub-windows, no 3D viewport, no multi-column grid). Everything built here is reused verbatim by the inventory and paperdoll phases.

2. Scope

In scope (Phase 1):

  • UiItemSlot widget (port of UIElement_UIItem, class 0x10000032) — empty-slot + icon render.
  • UiItemList widget (port of UIElement_ItemList, class 0x10000031) — single-cell instances.
  • Icon composite pipeline (faithful CPU pre-composite — Approach A, §4.3).
  • CreateObject.TryParse extension to capture IconId onto ItemInstance.
  • ToolbarController — find-by-id bind, populate-from-shortcuts, deferred re-bind, click-to-use.
  • Toolbar window mounted under ACDREAM_RETAIL_UI=1, whole-window-drag.

Out of scope (later D.5 sub-phases):

  • Drag/reorder within the bar; drag-to-add from inventory (needs inventory as a drag source).
  • The AddShortCut/RemoveShortCut mutate wire (0x019C/0x019D) — builders already exist; wiring them is deferred to the drag phase.
  • The hidden selected-object Health/Mana meters (0x100001A1/A2) + the stack-split slider (0x100001A4) — stay SetVisible(0), matching gmToolbarUI::PostInit.
  • Spell shortcuts (ItemList_InsertSpellShortcut, CM_Magic path).
  • Faithful window manager (Dragbar/Resizebar drag-resize) — uses the accepted IA-12 whole-window-drag approximation.
  • Inventory and paperdoll panels.

3. Retail anchors (the load-bearing facts, verified)

All confirmed against the named decomp during the research phase and re-verified for this spec. Lines are acclient_2013_pseudo_c.txt.

  • Window: gmToolbarUI element class 0x10000007LayoutDesc 0x21000016 (300×122). gmToolbarUI::Register (decomp 196897), GetUIElementType0x10000007 (196707).
  • 18 slots, two rows of 9: element ids 0x100001A7-AF (top) + 0x100006B7-BF (bottom), wired in gmToolbarUI::InitShortcutArray (decomp 197051); each is a DynamicCast(0x10000031) = UIElement_ItemList, pushed into m_shortcutSlots in slot-index order.
  • Slot content: each slot list holds one UIElement_UIItem (item-cell, class 0x10000032). The cell's bound weenie guid is UIElement_UIItem::itemID (offset +0x5FC), read in UIItem_Update (decomp 230230: uint32_t itemID = this->itemID; … GetWeenieObject(itemID)).
  • Persisted model: ShortCutManager::shortCuts_[18] (acclient.h:36492); the struct is ShortCutData { int index_; uint objectID_; uint spellID_; } (acclient.h:36484). Delivered at login in the PlayerDescription SHORTCUT block (CharacterOptionDataFlag.SHORTCUT 0x1). acdream already parses it → PlayerDescriptionParser.cs:345-356Parsed.Shortcuts (ShortcutEntry{Index, ObjectGuid, SpellId, Layer}).
  • Populate at login: gmToolbarUI::UpdateFromPlayerDesc (decomp 198838) — FlushShortcuts then for i in 0..0x12 read shortCuts_[i]->objectID_ and AddShortcut(this, objId, i, send=0).
  • Deferred bind: UIElement_UIItem::SetDelayedShortcutNum / AddShortcut (decomp 196867) re-binds a slot whose weenie hasn't loaded yet once CreateObject for that guid arrives.
  • Activation (click-to-use): gmToolbarUI::UseShortcut (decomp 196395) → ItemHolder::UseObject (decomp 402923, 0.2s throttle m_timeLastUsed + 0.2) → ordinary use-item dispatch (NOT a shortcut-specific wire message). acdream's use-item path = InteractRequests.BuildUse (0x0036).
  • Icon composite: UIElement_UIItem::UIItem_SetIcon (230143) → ACCWeenieObject::GetIconData (408224) → IconData::RenderIcons (407524). Five layers, bottom→top: item-type default underlay DBObj::GetByEnum(0x10000004, lsb(itemType)+1); custom underlay _iconUnderlayID; base _iconID; custom overlay _iconOverlayID + SurfaceWindow::ReplaceColor tint; effect overlay DBObj::GetByEnum(0x10000005, lsb(effects)+1). Every layer is DBObj type 0xc = RenderSurface, id range 0x06000000-0x07FFFFFF — decoded DIRECTLY via TextureCache.GetOrUploadRenderSurface (the D.2b RenderSurface-vs-Surface gotcha: feeding a 0x06 id to GetOrUpload returns 1×1 magenta). Icon is NOT appraise-gated (no appraise branch in the icon path; appraise gates UpdateTooltip only).
  • acdream gap: CreateObject.TryParse currently DISCARDS IconId (CreateObject.cs:516: _ = ReadPackedDwordOfKnownType(..., IconTypePrefix)). ItemInstance already has the IconId/IconUnderlayId/IconOverlayId/StackSize/ContainerId fields.

4. Architecture & components

Five new/extended units, each with one purpose and a defined interface. The pattern mirrors the shipped vitals/chat re-drive exactly: dat LayoutDescLayoutImporterDatWidgetFactory builds widgets generically → a thin controller binds by id.

4.1 UiItemSlot (new behavioral widget) — port of UIElement_UIItem (0x10000032)

  • Location: src/AcDream.App/UI/UiItemSlot.cs.
  • Registration: DatWidgetFactory dispatches it on the resolved element class id 0x10000032. NOTE: the shipped factory keys off the small numeric Types (10x12); the item-slot/item-list are UIElement subclasses identified by a high class id, so the plan must add a class-id dispatch branch (the class id is already surfaced — ElementReader.Merge resolves it through the BaseElement chain, and UIElement_UIItem derives from UIElement_Field/Type 3, so do NOT register numeric Type 3 — that stays chrome UiDatElement, per the shipped toolkit's deliberate Type-3 rule). Behavioral leaf — overrides ConsumesDatChildren => true so the importer does NOT build its dat sub-elements (it reproduces them procedurally).
  • State: uint ItemId (the bound weenie guid, retail +0x5FC). Phase 1 needs only this. Quantity / selection / drag-accept / ghost / open-container overlay states are structurally reserved (documented as later-phase hooks) but inert.
  • Render: if ItemId == 0 → draw the empty-slot sprite (the dat state ItemSlot_Empty0x060074CF, read from the element's states like every other UiDatElement sprite). Else → draw the composited icon (§4.3) into the 32×32 cell. Phase 1 draws no quantity text / no overlays.
  • Depends on: the icon pipeline (§4.3), UiRenderContext.DrawSprite.

4.2 UiItemList (new behavioral widget) — port of UIElement_ItemList (0x10000031)

  • Location: src/AcDream.App/UI/UiItemList.cs.
  • Registration: DatWidgetFactory keyed off class id 0x10000031. Behavioral leaf (ConsumesDatChildren => true) — manages its UiItemSlot children procedurally.
  • Phase-1 API subset: AddItem(UiItemSlot) / Flush() / GetNumUIItems() / GetItem(int). The toolbar uses 18 single-cell instances (one UiItemSlot each), so the N-cell grid layout (column wrap, cell pitch) is NOT needed yet — deferred to the inventory phase. A single-cell list just hosts at most one slot.
  • Depends on: UiItemSlot.

4.3 Icon pipeline (Approach A — faithful CPU pre-composite)

  • Location: src/AcDream.App/UI/IconComposer.cs (App layer — it touches GL texture upload). Pure-decode helpers may live alongside TextureCache.
  • Behaviour: port IconData::RenderIcons (407524). For a given item's icon ids, build a single 32×32 BGRA composite on the CPU by alpha-compositing the layers bottom→top (§3 list), apply the ReplaceColor palette tint to the custom-overlay layer, then upload the result once as a GL texture and cache it keyed by the icon-id tuple (so identical items share one composite). The slot draws one sprite.
  • Layer decode: each layer id is a 0x06 RenderSurface decoded DIRECTLY (Portal/HighRes TryGet<RenderSurface>SurfaceDecoder.DecodeRenderSurface(palette:null)), the same path TextureCache.GetOrUploadRenderSurface already uses — but composited on the CPU rather than drawn as separate sprites.
  • Enum-mapper layers: the type-default underlay (GetByEnum(0x10000004, …)) and effect overlay (GetByEnum(0x10000005, …)) require reading the two DBObj enum-mapper tables. These are bounded lookups (index → RenderSurface id); port them as part of this unit. If a mapper proves more involved than the research suggests, the base + custom underlay/overlay layers still composite correctly and the enum layers can land as a tight follow-up within the phase (documented, not silently dropped).
  • Why pre-composite, not stacked draws: the custom-overlay ReplaceColor tint is a per-pixel palette operation, not a simple alpha-blend — it cannot be reproduced by a tinted DrawSprite. CPU compositing is therefore the faithful path, and it's the shared spine for all three panels, so it's built correctly once.
  • Depends on: DatCollection (RenderSurface decode), GL texture upload.

4.4 CreateObject icon extension + ItemInstance

  • Location: src/AcDream.Core.Net/Messages/CreateObject.cs, src/AcDream.Core/Items/ItemInstance.cs.
  • Change: in CreateObject.TryParse, capture the IconId (currently discarded at CreateObject.cs:516) — and the underlay/overlay/effect ids if present in the same block — onto the parsed object so ItemRepository stores them on ItemInstance (fields already exist).
  • Planning delta (see the plan): fact-gathering found this is wider than "just capture IconId." acdream has NO CreateObjectItemRepository wiring at all (the repo is populated only from PlayerDescription with stub ItemInstances), and Parsed.Shortcuts is parsed then discarded in GameEventWiring. So the plan adds three small wiring pieces: capture IconId (Task 1), enrich the repo from the WorldSession.EntitySpawned event (Tasks 23, ItemRepository.EnrichItem), and persist the shortcut list (Task 4). The icon source is CONFIRMED to be CreateObject for contained pack items (ACE WorldObject_Networking.cs:79 writes IconId unconditionally).
  • Step 0 verification: confirm against ACE source (WorldObject.SerializeCreateObject / the weenie property serialization) that a contained pack item's CreateObject actually carries IconId (synthesis risk #3 — LIKELY, not yet byte-traced). Reading ACE is sufficient; no live capture needed. If ACE only sends IconId for world-visible objects and relies on PlayerDescription for pack items, fall back to the PD inventory block as the icon source — this is a branch the plan must resolve before the icon pipeline is wired.

4.5 ToolbarController (new) — the gmToolbarUI::PostInit analogue

  • Location: src/AcDream.App/UI/ToolbarController.cs (alongside VitalsController, ChatWindowController).
  • Bind: Bind(LayoutDesc 0x21000016, …) — find the 18 slot UiItemLists by id (0x100001A7-AF + 0x100006B7-BF) into an ordered _slots[18]. Force the 2 meters (0x100001A1/A2) + slider (0x100001A4) hidden (matches gmToolbarUI::PostInit).
  • Populate (port UpdateFromPlayerDesc): on the PlayerDescription arriving, Flush all slots, then for each Parsed.Shortcuts entry resolve ObjectGuidItemRepository item → set _slots[Index]'s cell ItemId. The cell renders the composited icon from the item's IconId.
  • Deferred re-bind (port SetDelayedShortcutNum): if a shortcut's guid is not yet in ItemRepository, record it pending; when ItemRepository raises item-added for that guid, bind the waiting slot. (Reuse ItemRepository's existing item-change events.)
  • Click-to-use (port UseShortcut): a slot click → controller → existing InteractRequests.BuildUse (0x0036) for the cell's ItemId, gated by the 0.2s use-throttle (ItemHolder::UseObject). No special shortcut wire.
  • Depends on: PlayerDescriptionParser.Parsed.Shortcuts, ItemRepository, the slot widgets, the command/interact send path.

4.6 Wiring & gating

  • The toolbar window is built by LayoutImporter from 0x21000016 and mounted in UiRoot under ACDREAM_RETAIL_UI=1, like vitals/chat. Always-on this phase. Root is Anchors=None
    • Draggable (whole-window-drag, IA-12 approximation) — NOT Resizable (faithful resize is the deferred window manager).
  • GameWindow wiring follows the existing vitals/chat drain pattern (one controller constructed + bound; per-panel try/catch fault isolation already exists).

5. Data flow (login → visible toolbar)

  1. Login → PlayerDescription arrives → PlayerDescriptionParser fills Parsed.Shortcuts.
  2. In parallel, the player's pack items arrive as CreateObject messages → ItemRepository stores ItemInstances including IconId (the §4.4 extension).
  3. ToolbarController (bound to the imported 0x21000016 window) runs its populate pass: for each shortcut, resolve guid → item → set slot ItemId. Missing items → pending, re-bound on item-added.
  4. Each filled UiItemSlot asks IconComposer for the composited 32×32 texture (cached by icon-id tuple) and draws it; empty slots draw 0x060074CF.
  5. Click a filled slot → use-item (0x0036) with throttle.

6. Testing strategy

Conformance tests in the layer matching each unit; dat-free fixtures where possible (mirror the vitals 0x2100006C golden-fixture approach).

  • CreateObject IconId (tests/AcDream.Core.Net.Tests): a golden CreateObject byte buffer parses with the expected IconId (and the previously-discarded fields).
  • IconComposer (tests/AcDream.App.Tests): layer ORDER + presence given a synthetic icon-id tuple (assert the composite requests layers bottom→top in the RenderIcons order; assert the cache returns the same texture for the same tuple). The ReplaceColor tint math gets a small unit test against a known palette index.
  • UiItemSlot (tests/AcDream.App.Tests): ItemId==0 selects the empty sprite; ItemId!=0 requests the composite. ConsumesDatChildren==true.
  • UiItemList: AddItem/Flush/GetNumUIItems/GetItem over single-cell instances.
  • ToolbarController: find-by-id binds 18 slots from a fixture tree; shortcut→item resolution sets the right slot; an item arriving late triggers the deferred re-bind; a slot click emits a use-item for the bound guid with the throttle respected. Meters/slider hidden.
  • Build + full suite green before the visual gate.

7. Acceptance criteria

  • dotnet build + dotnet test green.
  • Visual (the user's gate): launch, log in +Acdream → an 18-slot action bar renders with the correct dat chrome + empty-slot sprites; any persisted shortcuts show their real composited item icons; clicking a pinned item uses it (observable server-side / in-world). Whole-window drag works.
  • Every AC-specific algorithm cites its named-decomp anchor in a comment (per the phase checklist).
  • Divergence rows added (§8); D.5.1 registered in the roadmap; memory updated if a durable lesson emerges.

8. Divergence register + roadmap (bookkeeping)

  • Whole-window-drag instead of faithful Dragbar-driven drag — already covered by the existing IA-12 row (reuse, no new row).
  • Icon enum-mapper layers: if the type-default-underlay / effect-overlay layers land as a follow-up rather than in the first commit, add a register row noting the temporarily-absent layers (and delete it when they land). The base + custom underlay/overlay layers are faithful from the first commit.
  • Roadmap: register D.5.1 — Toolbar under D.5 "Core panels" as plan step 0 (avoids the retroactive-registration deviation that the D.2b importer hit at roadmap line 428).

9. Open items carried from research (resolve in the plan, before the dependent step)

  • Step 0 — CreateObject IconId for contained items (synthesis risk #3): read ACE source to confirm pack-item CreateObject carries IconId; if not, use the PD inventory block. Gates §4.3/§4.4.
  • Use-item opcode (synthesis risk #4): ItemHolder::UseObject dispatch is confirmed; the precise 0x0035 vs 0x0036 branch was not traced to the send. acdream has both in InteractRequests; the toolbar uses single-item use (0x0036). Reconcile when wiring §4.5.
  • The empty-slot baseline is itself a valid visual verification even if +Acdream has no persisted shortcuts; pinning real items to verify icons may require the inventory phase (drag-to-add) or a server-side pre-pin.

10. Component boundary summary (isolation check)

Unit One purpose Interface Depends on
UiItemSlot render one item-in-a-slot ItemId setter; standard UiElement draw/hit IconComposer, render context
UiItemList hold N item slots AddItem/Flush/GetNumUIItems/GetItem UiItemSlot
IconComposer icon-id tuple → composited 32×32 texture GetIcon(iconIds) → texture (cached) DatCollection, GL upload
CreateObject/ItemInstance carry IconId from wire to model existing parse + fields
ToolbarController bind + populate + use Bind(layout, deps) shortcuts, ItemRepository, slots, send path

Each can be understood and tested without reading the others' internals; the controller is the only unit that knows about wire + model, keeping the widgets pure-presentation.