feat(ui): complete retail quick-slot input

Port global toolbar use/select/create actions, migrate the collapsed Ctrl-number bindings, and route them through a focused retained-UI controller. Preserve shortcut aliases as aliases across inventory and paperdoll drops with retail's neutral/accept/reject drag states, preventing physical item moves such as unwielding an equipped helmet.

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-11 08:59:49 +02:00
parent 6fcc510d5d
commit e65119f0c6
24 changed files with 650 additions and 69 deletions

View file

@ -312,9 +312,44 @@ before introducing a public enum.
it first attempts `PlaceInBackpack`; with the flag clear it reports the appropriate
not-owned/ineligible/full/duplicate message. For a specified slot it removes any
existing occurrence of the same object and adds it at that slot. For auto-slot it
finds an empty slot; a duplicate does not get added again.
passes `-1` to `AddShortcut`; `AddShortcut @ 0x004BD9A0` scans from slot zero and
stops at the first empty slot. A duplicate does not get added again.
### 4.2 Remove-on-lift
### 4.2 Shortcut aliases are not physical items
Retail preserves the dragged element's source identity in `DropItemFlags`. The
shared inventory-list path and paperdoll path both require
`(flags & 0xE) == 0` before they show an accept/reject state or perform a physical
item move. A shortcut alias therefore remains visually neutral over inventory or
paperdoll, and releasing it there performs no item operation. The shortcut was
already removed by `RecvNotice_ItemListBeginDrag`, so the net result is only
removing the alias from the toolbar.
```text
UIElement_ItemList.ItemList_DragOver(source, target): // 0x004E3400
(objectId, _, flags) = InqDropIconInfo(source)
if no custom handler and (flags & 0xE) == 0:
set the target's physical-item accept/reject state
// alias source: leave the target neutral
UIElement_ItemList.HandleDropRelease(message): // 0x004E4790
(objectId, _, flags) = InqDropIconInfo(source)
if objectId != 0 and (flags & 0xE) == 0:
AcceptDragObject(objectId, flags & 1)
// alias source: no physical item operation
gmPaperDollUI.OnItemListDragOver / HandleDropRelease: // 0x004A4270 / 0x004A4D80
apply the same (flags & 0xE) == 0 gate before wield policy
gmToolbarUI.OnItemListDragOver: // 0x004BD150
accept when (flags & 0xE) == 0 or (flags & 4) != 0
```
In acdream's typed payload, `ItemDragSource.ShortcutBar` is the `flags & 4`
alias case. Inventory, equipment, and world placement controllers must never
reinterpret that payload as the underlying physical object.
### 4.3 Remove-on-lift
```text
RecvNotice_ItemListBeginDrag(sourceList): // 0x004BD930
@ -329,7 +364,7 @@ RecvNotice_ItemListBeginDrag(sourceList): // 0x004BD930
then, when `send=true`, sends `Event_RemoveShortCut(index)` and removes that slot
from `PlayerModule`. Releasing a lifted shortcut off the toolbar leaves it removed.
### 4.3 Empty-slot search is cyclic to the right
### 4.4 Empty-slot search is cyclic to the right
```text
GetFirstEmptyShortcutToTheRightOf(slot): // 0x004BD560
@ -341,7 +376,7 @@ GetFirstEmptyShortcutToTheRightOf(slot): // 0x004BD560
It is not an inventory grid index, the original source index, or a global
first-empty search unless wraparound reaches slot zero.
### 4.4 Drop release branches
### 4.5 Drop release branches
`InqDropIconInfo` supplies the dragged object id and `DropItemFlags`. The exact
source-flag enum names are not present in the named body, but the masks and branch
@ -387,7 +422,7 @@ an occupied slot sends the displaced item to the first empty slot cyclically to
the right of the target. Only a dragged shortcut alias attempts to restore the
displaced item to `m_lastShortcutNumDragged`.
### 4.5 Full-stack merge rekey
### 4.6 Full-stack merge rekey
```text
RecvNotice_FullMergingItem(oldId, newId): // 0x004BE9B0