acdream/docs/research/2026-06-16-action-bar-toolbar-deep-dive.md
Erik a5c5126e8d docs(D.5): action bar / inventory / paperdoll research drop
Five report-only deep-dives + synthesis for the next D.2b UI panels, built on the shipped widget toolkit. Confirms LayoutDesc ids (toolbar 0x21000016, inventory 0x21000023, backpack 0x21000022, paperdoll 0x21000024, 3ditems 0x21000021), the shared item-slot/item-list spine (UIElement_UIItem 0x10000032 / UIElement_ItemList 0x10000031), the 5-layer icon composite (IconData::RenderIcons @407524), the cross-panel wire catalog with acdream parse-status, and the dependency-ordered build plan.

Produced via a multi-agent research workflow; the spine agent died on a transient API error and was re-run as a focused follow-up with its decomp anchors verified against source.

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

27 KiB
Raw Blame History

Action bar / quick slots (gmToolbarUI) — retail-faithful deep dive

Date: 2026-06-16 Panel: action bar / shortcut bar — retail class gmToolbarUI, element class 0x10000007, LayoutDesc 0x21000016 (root element 300×122). Scope: handoff §3 Q1 (LayoutDesc/element map) + Q4 (shortcut slot model) + Q5 (wire + persistence) + Q6 (drag-drop / reorder). Report-only; no code written this phase. Builds on: the D.2b importer/widget toolkit (src/AcDream.App/UI/ + …/UI/Layout/). The "spine" item-slot/icon doc referenced in the handoff prompt does NOT exist in this worktree (searched **/*spine*, **/*item-slot*, the named path — all NOT FOUND), so the UIElement_UIItem / UIElement_ItemList facts below are derived here directly from the decomp; a later synthesis should reconcile with the spine doc if it lands.

1. Summary + confidence legend

The retail toolbar is one gmToolbarUI window that contains 18 single-cell item slots (two rows of 9: top 0x100001A7..AF, bottom 0x100006B7..BF), each slot a UIElement_ItemList (element class 0x10000031) holding at most one UIElement_UIItem (class 0x10000032). A slot stores nothing but the item it currently holds; the persistent model is ShortCutManager::shortCuts_[18] (an array of ShortCutData{ index_, objectID_, spellID_ }) living on the CPlayerModule. Shortcuts are server-persisted as a character option — they arrive in the big PlayerDescription login message (the CharacterOptionDataFlag::SHORTCUT block, already parsed by acdream) and are mutated live by two C2S game actions: AddShortCut 0x019C and RemoveShortCut 0x019D (both already have outbound builders in acdream). Activation of a slot does not use a "use-shortcut" wire message — it routes through the ordinary use-item path (ItemHolder::UseObject), so it reuses acdream's existing B.4 interaction pipeline. Drag-from-inventory and drag-to-reorder are handled by gmToolbarUI being an ItemListDragHandler (multiple inheritance) whose HandleDropRelease resolves the target slot and calls CreateShortcutToItem / AddShortcut / GetFirstEmptyShortcutToTheRightOf.

The 2 Meters + 1 Scrollbar in the layout dump are NOT bar paging or extra vitals: they are the selected-object Health & Mana meters (0x100001A1/0x100001A2) and the stack-size split slider (0x100001A4), all inside the m_pSelObjectField sub-panel and hidden by default (SetVisible(0) in PostInit) — they appear only when you select an object / split a stack.

Confidence legendCONFIRMED = quoted from named decomp or a reference file I opened; LIKELY = inferred from confirmed facts (source named); UNVERIFIED = educated guess, flagged.

2. LayoutDesc / element map (Q1) — CONFIRMED against .layout-dumps/toolbar-0x21000016.txt

LayoutDesc 0x21000016 (Id 553648150). The dump's Width=800 Height=600 is the LayoutDesc canvas; the root element 0x10000191 (ElementId 268435857, Type 0x10000463 = the registered gmToolbarUI class type) is 300×122 — that 300×122 matches the handoff's stated size and is the real window. The root's Type value 268435463 = 0x10000007… correction: dump shows Type = 268435463 which is 0x10000007 (the gmToolbarUI class id) — i.e. the root element registers as the panel class itself, exactly like gmToolbarUI::GetUIElementType returns 0x10000007 (decomp line 196707: return 0x10000007;). CONFIRMED.

2a. The 18 shortcut slots — element→slot-index map

gmToolbarUI::InitShortcutArray (decomp line 197051) wires the slots by walking GetChildRecursive(this, <id>) in order and DynamicCast(0x10000031) (= UIElement_ItemList), registering each with the drag handler and pushing into m_shortcutSlots. The push order is the slot index. The 18 ids extracted from the function body (decomp 197054197560):

Slot # Element id Row Dump X,Y (W×H) Hotkey msg (use / select)
0 0x100001A7 top 6,58 (32×32) 0x10000042 / 0x1000004E
1 0x100001A8 top 38,58 0x10000043 / 0x1000004F
2 0x100001A9 top 70,58 0x10000044 / 0x10000050
3 0x100001AA top 102,58 0x10000045 / 0x10000051
4 0x100001AB top 134,58 0x10000046 / 0x10000052
5 0x100001AC top 166,58 0x10000047 / 0x10000053
6 0x100001AD top 198,58 0x10000048 / 0x10000054
7 0x100001AE top 230,58 0x10000049 / 0x10000055
8 0x100001AF top 262,58 0x1000004A / 0x10000056
9 0x100006B7 bottom 6,90 0x1000004B / 0x10000057
10 0x100006B8 bottom 38,90 0x1000004C / 0x10000058
11 0x100006B9 bottom 70,90 0x1000004D / 0x10000059
12 0x100006BA bottom 102,90 0x10000132 / 0x10000138
13 0x100006BB bottom 134,90 0x10000133 / 0x10000139
14 0x100006BC bottom 166,90 0x10000134 / 0x1000013A
15 0x100006BD bottom 198,90 0x10000135 / 0x1000013B
16 0x100006BE bottom 230,90 0x10000136 / 0x1000013C
17 0x100006BF bottom 262,90 0x10000137 / 0x1000013D

CONFIRMED — slot ids from InitShortcutArray; X/Y from the dump; hotkey msg ids from gmToolbarUI::ListenToGlobalMessage (decomp 197564). The hotkey routing:

  • 0x10000042..0x1000004DUseShortcut(this, msg-0x10000042, 1) → slots 011, use (arg3=1). (decomp 197576197591)
  • 0x1000004E..0x10000059UseShortcut(this, msg-0x1000004E, 0) → slots 011, select (arg3=0). (decomp 197592197606)
  • 0x10000132..0x10000137UseShortcut(this, 0xC..0x11, 1) → slots 1217, use. (decomp 197616197645)
  • 0x10000138..0x1000013DUseShortcut(this, 0xC..0x11, 0) → slots 1217, select. (decomp 197646197674)

The slot count 18 is independently confirmed by the header struct ShortCutManager::shortCuts_[18] (acclient.h line 3649236494: struct __cppobj ShortCutManager : PackObj { ShortCutData *shortCuts_[18]; };) and the login-restore loop for (i=0; i<0x12; i++) in UpdateFromPlayerDesc (decomp 198879). ACE's comment corroborates the UX: "there are two rows. The top row is 1-9, the bottom row has no hotkeys" (Player_Character.cs:250).

Slot template: each slot's ElementDesc has BaseElement=0x100001B2 / BaseLayoutId=553648150 (the dump's last element, 0x100001B2, ElementId 268435890, which itself inherits BaseElement=268436281 BaseLayoutId=553648189). 0x100001B2 is the slot prototype (W=32 H=32) — i.e. the 18 slot elements are clones of one UIElement_ItemList prototype. LIKELY (from the dump's BaseElement chain; the resolved Type would surface 0x10000031 via ElementReader.Merge, exactly as the toolkit memory describes for Type-0 inheritance).

2b. The selected-object sub-panel + the "extra" widgets (resolves the prompt's "2 Meters / 1 Scrollbar = ?")

From gmToolbarUI::PostInit (decomp 198119) — all GetChildRecursive + DynamicCast:

Element id Field DynamicCast Type Dump location Purpose
0x1000019D m_pUseObjectButton (button) 55,27 (23×31) the Use button (sprite 0x06001129, Ghosted 0x0600120E)
0x100001A5 m_pExamineObjectButton (button) 218,27 (22×31) the Examine/Appraise button (sprite 0x06001127)
0x1000019E m_pSelObjectField (Type 3 container) 78,27 (140×31) the selected-object info sub-panel (dump 0x1000019E)
0x1000019F m_pSelObjectName DynamicCast(0xC) Text child of A field selected object's name
0x100001A1 m_pSelObjectHealthMeter DynamicCast(7) Meter child Meter #1 = target Health bar
0x100001A2 m_pSelObjectManaMeter DynamicCast(7) Meter child Meter #2 = target Mana bar
0x100001A3 m_pStackSizeEntryBox DynamicCast(0xC) Text child the stack-split number entry (gets NumberInputFilter)
0x100001A4 m_pStackSizeSlider DynamicCast(0xB) Scrollbar 50,13 (90×14), Type 11 Scrollbar = the stack-split slider

PostInit ends (decomp 198307198310) by hiding all four: m_pSelObjectHealthMeter/ManaMeter/StackSizeEntryBox/StackSizeSlider → SetVisible(0). So the 2 Meters and the Scrollbar are NOT toolbar paging or persistent vitals — they are the on-demand "selected object" readout + the stack-split slider, hidden until needed. CONFIRMED.

Panel-launcher buttons (open inventory/spellbook/etc.) wired into m_buttonInfoArray with a panelID attribute (0x10000029): 0x10000197, 0x10000198, 0x10000199, 0x1000055A, 0x1000019A, 0x1000019B, 0x100001B1 (decomp 198179198303). 0x100001B1 (X=238 W=63, sprite 0x06004CF7 Alphablend, with child 0x1000046C = m_pInventoryButtonDragOverlay) is the inventory button that also serves as a "drop item into your pack" target (see §5). The 0x1000019C/0x10000196 Type-3 elements (sprites 0x0600112B/0x0600112C) are decorative dividers; the 0x10000194 element drives UpdateAmmoNumber (the ammo-count readout, decomp 198081). Text0x34 in the pre-dump label = the 0x34 (52) text/field/image sub-elements across this whole tree (chrome + the above); they are NOT 52 slots.

3. Shortcut slot model (Q4) — CONFIRMED

A slot holds an item, the player module holds the model. Each m_shortcutSlots[i] is a UIElement_ItemList; UseShortcut/RemoveShortcutInSlotNum read the item via UIElement_ListBox::GetItem(slot, 0) then DynamicCast(0x10000032) (= UIElement_UIItem) and read the object id at field offset +0x5FC on the UIItem (decomp 196415, 196519, 196811: *(uint32_t*)((char*)eax_1 + 0x5fc)). That +0x5FC is the weenie/object id the slot points at. UNVERIFIED exact field name (offset only); LIKELY the UIItem's bound object id.

ShortCutData (the persistent unit) — verbatim header (acclient.h:36484):

struct __cppobj ShortCutData : PackObj {
  int index_;            // slot number (0..17)
  unsigned int objectID_;// item guid (0 if spell shortcut)
  unsigned int spellID_; // spell id (0 for item shortcut)
};

Constructed CShortCutData(&var_10, index, objectID, spellID) (decomp 489341: index_=arg2; objectID_=arg3; spellID_=arg4). For an item shortcut the toolbar always passes spellID=0 (CShortCutData(&var_10, i_1, arg2, 0) in AddShortcut, decomp 196874).

Number of slots / bars: 18 slots in 2 visible rows of 9 (top row = hotkeys 1-9, bottom = no hotkeys but addressable via UseShortcut(0xC..0x11)). There is no separate "bar paging" — all 18 are always present; the layout just stacks two rows. CONFIRMED (§2a).

Item vs spell shortcuts. The data model has a spellID_ slot, but in practice the toolbar holds only items. Confirmation from three angles:

  1. The toolbar's add paths only ever construct item shortcuts (AddShortcut/CreateShortcutToItem pass spellID=0).
  2. Spell shortcuts live in a different list — the spellbook's m_spellList via UIElement_ItemList::ItemList_InsertSpellShortcut (decomp 232294) and the spell-bar hotbars (the SpellLists8 / hotbar_spells block, separate from SHORTCUT). CM_Magic::SendNotice_AddSpellShortcut (decomp 682275) is a local UI notice (dispatched via gmGlobalEventHandler to notice handlers), not a wire send and not routed to gmToolbarUI.
  3. Chorizite's own comment on ShortCutData.SpellId: "May not have been used in prod? … I don't think you could put spells in shortcut spot…" (ShortCutData.generated.cs:34). CONFIRMED — the toolbar is item-only; the spellID_/spell-bar machinery is a separate spellbook concern (out of scope for the action-bar widget).

IsShortcutEligible(ACCWeenieObject*) (decomp 196261, __stdcall): returns true unless the object is null, OR it's the player itself / a creature you don't own, OR it's currently inside the open vendor's container. Logic (decomp 196268196300):

  • if (pwd._bitfield & 4) == 0 (not "owned"?) and not a player → fall through; else require IsPlayer().
  • then if ((InqType() & 0x10) != 0) (Creature type bit) require IsPlayer() to continue;
  • then read pwd._containerID; eligible (return 1) iff _containerID == 0 OR _containerID != UISystem->vendorID — i.e. anything not sitting in the currently-open vendor window is eligible. CONFIRMED (paraphrase of the branch tree).

IsShortcutSlotAvailable(slot) (decomp 196575): slot in range AND UIElement_ItemList::GetNumUIItems(slot)==0 (empty). CONFIRMED.

Activation — UseShortcut(slot, useFlag) (decomp 196395):

  1. Get the UIItem in the slot; read its object id from +0x5FC.
  2. If a target mode is active (UISystem->targetMode != TARGET_MODE_NONE, e.g. a spell awaiting a target): ClientUISystem::ExecuteTargetModeForItem(objId, targetMode) then clear target mode. (decomp 196412196421)
  3. Else if useFlag != 0: ItemHolder::UseObject(objId, 0, 0) — the standard use-item action. (decomp 196429)
  4. Else (useFlag==0): ACCWeenieObject::SetSelectedObject(objId, 0) — just select it. (decomp 196433)

So toolbar activation is the ordinary use-item path, not a bespoke message. ItemHolder::UseObject (decomp 402923) has a 0.2 s throttle (m_timeLastUsed + 0.2, decomp 402933) and then dispatches the use via the inventory-request path (DetermineUseResult → 0x0036 "Use" or 0x0035 "UseWithTarget"). LIKELY (the exact 0x0035/0x0036 branch is deep in UseObject; the throttle + dispatch are CONFIRMED, the opcode selection is inferred from acdream's existing InteractRequests.cs opcodes 0x0035/0x0036).

4. Wire + persistence (Q5)

4a. Persistence = a character option in PlayerDescription (login restore)

Shortcuts are saved server-side (ACE: CharacterPropertiesShortcutBar, Player_Character.cs:235) and shipped to the client inside the PlayerDescription login message in the CharacterOptionDataFlag::SHORTCUT (0x1) block — count:u32 then count × ShortCutData. CONFIRMED in three refs:

  • holtburger events.rs:514-524 (PlayerDescriptionEventData.shortcuts, "List of user-defined shortcuts for the action bar" line 124).
  • ACE Player_Character.cs:238 GetShortcuts() reads Character.GetShortcuts(...)List<Shortcut> for the description.
  • acdream already parses this: PlayerDescriptionParser.cs:345-356 reads count then ShortcutEntry(Index, ObjectGuid, SpellId, Layer) per entry, exposed on Parsed.Shortcuts.

Client-side restore: gmToolbarUI::UpdateFromPlayerDesc (decomp 198838) → FlushShortcuts(), gets the CPlayerModule's ShortCutManager, then for (i=0; i<0x12; i++) { objId = shortCuts_[i]->objectID_ (+8); if (objId) AddShortcut(this, objId, i, 0); } (decomp 198879198893). The 0 final arg = do NOT echo to server (it's already persisted). CONFIRMED.

4b. Live mutation — two C2S game actions

Opcode Name Dir Trigger ACE handler Chorizite type acdream parse status
0x019C AddShortCut C→S AddShortcut(…, send=1) builds CShortCutData(slot,objId,0)CM_Character::Event_AddShortCut GameActionAddShortcut.HandlePlayer.HandleActionAddShortcut(shortcut)Character.AddOrUpdateShortcut(Index,ObjectId) Character_AddShortCut { ShortCutData Shortcut } builder present (outbound InventoryActions.BuildAddShortcut, see note)
0x019D RemoveShortCut C→S RemoveShortcut(…, send=1)CM_Character::Event_RemoveShortCut(slotIndex) GameActionRemoveShortcut.HandlePlayer.HandleActionRemoveShortcut(index)Character.TryRemoveShortcut(index) Character_RemoveShortCut { uint Index } builder present (InventoryActions.BuildRemoveShortcut)
(—) shortcut list S→C login part of PlayerDescription SHORTCUT block ShortCutData in description parsed (PlayerDescriptionParser.cs:345)

Opcode values triple-confirmed: decomp Event_AddShortCut packs *(uint32_t*)var_c = 0x19c (decomp 679733) and Event_RemoveShortCut packs 0x19d (decomp 680332); ACE GameActionType.cs:77-78 (AddShortCut=0x019C, RemoveShortCut=0x019D); holtburger opcodes.rs:371-374 (commented, same values).

Wire field order — ShortCutData payload (16 bytes), CONFIRMED across 3 refs:

Index    : u32   (slot 0..17)
ObjectId : u32   (item guid; 0 for spell)
SpellId  : u16   (LayeredSpell.id; 0 for item)
Layer    : u16   (LayeredSpell.layer; 0 for item)
  • Chorizite ShortCutData.generated.cs:41-46 (Index, ObjectId, then LayeredSpellId.Read = u16 id + u16 layer).
  • ACE Shortcut.cs:33-42 ReadShortcut (Index, ObjectId, ReadLayeredSpell).
  • holtburger shortcuts.rs:13-34 (index u32, object_id Guid, spell_id u16, layer u16). RemoveShortCut payload = just Index:u32 (Chorizite Character_RemoveShortCut.generated.cs:33; ACE GameActionRemoveShortcut.cs:9; decomp packs *(uint32_t*)eax_3 = arg1 at 680335).

⚠ acdream builder field-naming bug to fix at port time (not a wire bug). InventoryActions.BuildAddShortcut(seq, slotIndex, objectType, targetId) (InventoryActions.cs:99-110) writes 24 bytes = 8-byte envelope (0xF7B1 + seq) + slotIndex(u32) + objectType(u32) + targetId(u32). The byte layout is correct for item shortcuts (slot, then guid, then a final dword that for items is 0 = SpellId|Layer), but the parameter names are wrong/misleading: the 2nd field is Index, the 3rd is ObjectId, and the 4th dword is SpellId(u16)|Layer(u16) — there is no separate "objectType". A faithful builder should take (seq, uint index, uint objectGuid, ushort spellId, ushort layer) and pack the spell as two u16s. For the toolbar's item-only use, callers must pass objectGuid as the 3rd arg and 0 as the 4th. LIKELY a latent bug if anyone wired a "objectType" semantic; flag in the divergence register when the toolbar lands. (CONFIRMED file contents; the "bug" judgment is mine.)

ACE's reorder note (important UX contract): "When a shortcut is added on top of an existing item, the client automatically sends the RemoveShortcut command for that existing item first, then will add the new item, and re-add the existing item to the appropriate place." (Player_Character.cs:254). This is exactly the HandleDropRelease sequence in §5. CONFIRMED.

5. Drag-drop for the toolbar (Q6) — CONFIRMED

gmToolbarUI multiply-inherits ItemListDragHandler (constructor sets the ItemListDragHandler::vftable, decomp 196680) and registers itself as the drag handler on every slot's UIElement_ItemList in InitShortcutArray (RegisterItemListDragHandler(slot, &this->vtable), decomp 197069 etc.). Drops land in gmToolbarUI::HandleDropRelease (decomp 197971):

  1. Read source UIItem (ebp = msg.dwParam1+8) and drop-target element (ebx = msg.dwParam1+0x10). (decomp 197974197976)
  2. If the target is the inventory button (ebx->m_desc.m_elementID == 0x100001B1): this is "drop item into my pack." InqDropIconInfo extracts the dragged object id; then if owned by player → CPlayerSystem::PlaceInBackpack(objId, 0), else → ItemHolder::AttemptToPlaceInContainer(objId, playerId, …). (decomp 198031198056) — i.e. dropping on the inventory button moves the real item into your pack, it does not create a shortcut.
  3. Else (target is a shortcut slot): find which slot i is the ancestor of the drop target (IsAncestorOfMe(ebx, m_shortcutSlots[i]), decomp 197991), InqDropIconInfo(ebp, &objId, &var_4, &flags). Then on objId != 0:
    • drop flags (flags & 0xE) == 0 (a fresh drag from inventory, not a within-bar move): RemoveShortcutInSlotNum(i, 1) (evict whatever was there, returns its objId eax_13), CreateShortcutToItem(objId, i, 1, 0) (place the dragged item in slot i, send=1). If the evicted eax_13 was a different item, GetFirstEmptyShortcutToTheRightOf(i) and AddShortcut(eax_13, thatSlot, 1) to relocate it. (decomp 198007198018)
    • else if (flags & 4) != 0 (a within-bar reorder, m_lastShortcutNumDragged is the source slot): RemoveShortcutInSlotNum(i, 1)AddShortcut(objId, i, 1); if an item was displaced and IsShortcutSlotAvailable(m_lastShortcutNumDragged), put the displaced item back into the vacated source slot (AddShortcut(eax_15, m_lastShortcutNumDragged, 1)). (decomp 198020198027)

This is precisely ACE's "remove the existing one, add the new one, re-add the existing item to the appropriate place." CONFIRMED.

Slot-resolution helpers (Q6 core):

  • CreateShortcutToItem(objId, slotOrNeg1, send, fromServer) (decomp 196905): null-check; get ACCWeenieObject; if IsShortcutEligible. If slot != 0xFFFFFFFFRemoveShortcut(objId,1); AddShortcut(objId, slot, 1) (decomp 196928196930). Else (slot unspecified) it scans for a home (the loop at 196954+, with a "no empty slot" DisplayStringInfo notice when full, decomp 196945196949). This is the entry called by RecvNotice_AddShortcut and the keyboard "add selected to toolbar" (0x1000010DCreateShortcutToItem(selectedID, 0xFFFFFFFF, 1, 0), decomp 197613).
  • AddShortcut(objId, slot, send) (decomp 196825): if slot out of range, find the first empty slot (linear scan, decomp 196836196848). Then ItemList_Flush(slot); ItemList_AddItem(slot, objId); SetShortcutNum(weenie, slot) (or SetDelayedShortcutNum if the weenie isn't loaded yet, decomp 196861196867). If send, build CShortCutData(slot, objId, 0)Event_AddShortCut (wire) + PlayerModule::AddShortCut (local model) (decomp 196873196876).
  • RemoveShortcut(objId, send) (decomp 196462): scan slots for the one containing objId (ItemList_IsInList), ItemList_Flush, SetShortcutNum(weenie, 0xFFFFFFFF); if send, Event_RemoveShortCut(slotIndex) + PlayerModule::RemoveShortCut(slotIndex); returns the slot index (or 0xFFFFFFFF). (decomp 196471196496)
  • RemoveShortcutInSlotNum(slot, send) (decomp 196502): read the UIItem objId at +0x5FC, RemoveShortcut(objId, send), return the evicted objId. (decomp 196519196524)
  • GetFirstEmptyShortcutToTheRightOf(slot) (decomp 196536): scan slot+1 .. end for an empty ItemList (GetNumUIItems==0); if none, wrap-scan 0 .. slot; return 0xFFFFFFFF if the bar is full. (decomp 196539196569)
  • FlushShortcuts() (decomp 196442): ItemList_Flush every slot (visual clear; does NOT touch the server). Used by login restore. (decomp 196451196457)

6. New toolkit widgets this introduces

The toolbar needs the same item-slot spine the inventory/paperdoll need; it adds the slot-grid + drag-handler concept on top.

Widget dat Type it registers at leaf vs container Purpose
UiItemSlot (port of UIElement_UIItem, class 0x10000032) resolves to a class id, not a numeric toolkit Type (it's a UIElement subclass 0x10000032, registered via RegisterElementClass, not Types 1-0x12); in acdream's factory this is a new behavioral leaf widget leaf (ConsumesDatChildren=>true) the item-in-a-slot: icon from weenie IconId (+ underlay/overlay/highlight), stack-size + selection state, holds the bound object id (retail +0x5FC). Shared with inventory + paperdoll — build once.
UiItemList (port of UIElement_ItemList / UIElement_ListBox, class 0x10000031) new behavioral widget at class 0x10000031 (the dump shows it as the slot prototype 0x100001B2's resolved class; Type-5 ListBox is the generic relative but item lists are the specialized 0x10000031) leaf wrt the importer (it manages its own UIItem children procedurally) a 1-cell (toolbar) or N-cell (inventory) container of UiItemSlots; exposes AddItem/Flush/IsInList/GetNumUIItems/GetItem. Shared.
ToolbarController (the gmToolbarUI::PostInit-style binder) not a widget — a controller (like VitalsController/ChatWindowController) n/a finds the 18 slots by id, the use/examine buttons, the selected-object meters/name, the stack slider; binds UseShortcut/AddShortcut/RemoveShortcut; restores from Parsed.Shortcuts; sends 0x019C/0x019D.
drag-handler seam n/a (an interface on UiItemList + the controller) n/a port of ItemListDragHandlerOnItemListDragOver / HandleDropRelease (slot resolution from §5). The toolkit's UiRoot already has drag-drop input plumbing (per the d2b memory: "UiRoot already has full input (focus/capture/drag-drop/tooltip/click)"), so this is a binding, not new infra.

Reuses (no new widget needed): UiMeter (Type 7) for the two selected-object bars; UiText/UiField (Type 12 / the controller-placed editable) for the name + stack-size box; UiScrollbar (Type 11) for the stack slider; UiButton (Type 1) for Use/Examine/panel-launchers; UiDatElement for chrome. The window-manager (open/close/z-order/persist + grip/dragbar drag from D.2b Plan-2) is needed for show/hide + persisting position, same as inventory/paperdoll — it is not toolbar-specific.

7. Open questions / UNVERIFIED

  • UIElement_UIItem +0x5FC field name — confirmed as the bound object id by offset only; the symbolic field name is UNVERIFIED. Cross-check against the spine doc's UIItem port if/when it exists, or grep UIElement_UIItem::SetShortcutNum/UIItem_GetState.
  • Exact use-item opcode UseObject sends (0x0035 vs 0x0036)ItemHolder::UseObject throttle + dispatch CONFIRMED; the precise opcode branch (DetermineUseResult) was not traced to the send. acdream's InteractRequests.cs already has both (0x0035 UseWithTarget, 0x0036 Use); reconcile when wiring activation.
  • UseShortcut target-mode pathClientUISystem::ExecuteTargetModeForItem (for "use item on a target", e.g. a healing kit) is out of scope for the action-bar widget itself; it depends on the target-mode subsystem (cursor target picking). File as a follow-up.
  • SetDelayedShortcutNum — the "weenie not loaded yet" deferral path (AddShortcut decomp 196867) needs a small state machine on the slot to re-bind once CreateObject for that guid arrives. Note for the controller port; not yet detailed here.
  • Root element Type value — the dump prints the root's Type = 268435463 (=0x10000007) for 0x10000191 but some other top-level dump fields print Type = 268435463 ambiguously; I read it as the panel class id, consistent with GetUIElementType. LIKELY; verify with ElementReader.Merge when the importer runs over 0x21000016.
  • Spell-on-toolbar — declared dead (Chorizite + the toolbar's item-only add paths). If a future server/ACE variant DOES persist a spell shortcut (spellID_!=0), the UiItemSlot would need a spell-icon branch. Low priority; the wire field exists so parsing already handles it.

8. MEMORY.md index line

  • Action bar / quick slots (gmToolbarUI) deep dive — 18 item slots (2 rows of 9, ids 0x100001A7-AF+0x100006B7-BF) = UIElement_ItemList(0x10000031) of one UIElement_UIItem(0x10000032); model ShortCutManager::shortCuts_[18] persisted in PlayerDescription's SHORTCUT block (acdream already parses it); live mutate via AddShortCut 0x019C/RemoveShortCut 0x019D (acdream builders present — fix BuildAddShortcut field naming); activation = ordinary use-item (ItemHolder::UseObject, no special wire); the 2 Meters + Scrollbar in 0x21000016 are the hidden selected-object Health/Mana bars + the stack-split slider, NOT paging; drag-drop via gmToolbarUI : ItemListDragHandler::HandleDropRelease (CreateShortcutToItem/GetFirstEmptyShortcutToTheRightOf). New toolkit widgets: UiItemSlot + UiItemList (shared spine) + ToolbarController.