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>
191 lines
27 KiB
Markdown
191 lines
27 KiB
Markdown
# 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 legend** — **CONFIRMED** = 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 197054–197560):
|
||
|
||
| 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..0x1000004D` → `UseShortcut(this, msg-0x10000042, 1)` → slots **0–11**, **use** (arg3=1). (decomp 197576–197591)
|
||
- `0x1000004E..0x10000059` → `UseShortcut(this, msg-0x1000004E, 0)` → slots **0–11**, **select** (arg3=0). (decomp 197592–197606)
|
||
- `0x10000132..0x10000137` → `UseShortcut(this, 0xC..0x11, 1)` → slots **12–17**, **use**. (decomp 197616–197645)
|
||
- `0x10000138..0x1000013D` → `UseShortcut(this, 0xC..0x11, 0)` → slots **12–17**, **select**. (decomp 197646–197674)
|
||
|
||
The slot count `18` is independently confirmed by the header struct `ShortCutManager::shortCuts_[18]` (`acclient.h` line 36492–36494: `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 198307–198310) 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 198179–198303). `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`):
|
||
```c
|
||
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 196268–196300):
|
||
- 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 196412–196421)
|
||
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 198879–198893). 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.Handle` → `Player.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.Handle` → `Player.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 197974–197976)
|
||
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 198031–198056) — 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 198007–198018)
|
||
- **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 198020–198027)
|
||
|
||
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 != 0xFFFFFFFF` → `RemoveShortcut(objId,1); AddShortcut(objId, slot, 1)` (decomp 196928–196930). Else (slot unspecified) it scans for a home (the loop at 196954+, with a "no empty slot" `DisplayStringInfo` notice when full, decomp 196945–196949). This is the entry called by `RecvNotice_AddShortcut` and the keyboard "add selected to toolbar" (`0x1000010D` → `CreateShortcutToItem(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 196836–196848). Then `ItemList_Flush(slot); ItemList_AddItem(slot, objId); SetShortcutNum(weenie, slot)` (or `SetDelayedShortcutNum` if the weenie isn't loaded yet, decomp 196861–196867). If `send`, build `CShortCutData(slot, objId, 0)` → `Event_AddShortCut` (wire) + `PlayerModule::AddShortCut` (local model) (decomp 196873–196876).
|
||
- **`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 196471–196496)
|
||
- **`RemoveShortcutInSlotNum(slot, send)`** (decomp 196502): read the `UIItem` objId at `+0x5FC`, `RemoveShortcut(objId, send)`, return the evicted objId. (decomp 196519–196524)
|
||
- **`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 196539–196569)
|
||
- **`FlushShortcuts()`** (decomp 196442): `ItemList_Flush` every slot (visual clear; does NOT touch the server). Used by login restore. (decomp 196451–196457)
|
||
|
||
## 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 `UiItemSlot`s; 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 `ItemListDragHandler` — `OnItemListDragOver` / `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 path** — `ClientUISystem::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](research/2026-06-16-action-bar-toolbar-deep-dive.md) — 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`.
|