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

@ -204,7 +204,7 @@ AP-94..AP-110 for the confirmed retail-UI completion gaps.
| ~~AP-97~~ | **RETIRED 2026-07-10 (Wave 1 retained-widget foundation)** — the prior generic Device-timer premise was a decomp misread. Named retail polls the hovered element's tooltip deadline, then broadcasts global UI time message `3`; it does not expose the assumed arbitrary timer queue. `UiRoot.Tick` now preserves that order and subtree removal clears input/time ownership before another pulse. | `src/AcDream.App/UI/UiRoot.cs`; `IUiGlobalTimeListener.cs` | — | — | `UIElementManager::UseTime` and hover/focus paths, pinned in `docs/research/2026-07-10-retained-widget-foundations-pseudocode.md` |
| ~~AP-98~~ | **RETIRED 2026-07-10 (Wave 2.3)** — typed handles/manager own lifecycle; every production/Studio window uses `RetailWindowFrame`; schema-v2 `UiWindowLayout` persists outer bounds, visibility, toolbar collapse, and chat maximize by character/resolution with legacy-radar migration, nearest-resolution fallback, clamping, and a pre-login `default` write guard. Chat maximize uses DAT 100360 limits and preserves its lower edge when growing upward. | `src/AcDream.App/UI/RetailWindowManager.cs`; `RetailWindowLayoutPersistence.cs`; `RetailWindowHandle.cs`; `Layout/RetailWindowFrame.cs`; `ChatWindowController.cs`; `SettingsStore.cs` | — | — | `gmMainChatUI::HandleMaximizeButton @ 0x004CCE50`; retail `saveui/loadui` behavior |
| ~~AP-99~~ | **RETIRED 2026-07-11 (Wave 3.2)** — Core `ItemInteractionPolicy` ports the complete ordered `DetermineUseResult`/`UseObject`/`AttemptPlaceIn3D` matrix; PWD flags and `CombatUse` survive CreateObject; component-pack membership comes from portal.dat; App owns throttle, UseDone-balanced busy state, target mode, wire/optimistic dispatch, and typed confirmation/auxiliary seams. The two corrupt decompiler operands were pinned from matching x86 as `BF_REQUIRES_PACKSLOT` and `BF_VENDOR`. | `src/AcDream.Core/Items/ItemInteractionPolicy.cs`; `src/AcDream.App/UI/ItemInteractionController.cs` | — | — | `ItemHolder::DetermineUseResult @ 0x00588460`; `UseObject @ 0x00588A80`; `AttemptPlaceIn3D @ 0x00588600` |
| AP-101 | Toolbar quick-slot actions and most panel/Use/Examine controls are not fully dispatched; selected-object mana and stack entry/slider are absent | `src/AcDream.App/UI/Layout/ToolbarController.cs`; `GameWindow.OnInputAction` | Item click/use, health/name, some panel toggles, and all four combat indicator buttons work; combat clicks and the bound key share `ToggleLiveCombatMode` | Remaining bound retail keys/buttons do nothing and item/stack selections show incomplete state | `gmToolbarUI @ 0x004BD0C0..0x004BF380`; LayoutDesc `0x21000016` |
| AP-101 | Most toolbar panel/Use/Examine controls are not fully dispatched; selected-object mana and stack entry/slider are absent | `src/AcDream.App/UI/Layout/ToolbarController.cs`; `GameWindow.OnInputAction` | Item click/use, health/name, quick-slot use/select/create actions, some panel toggles, and all four combat indicator buttons work; combat clicks and the bound key share `ToggleLiveCombatMode` | Remaining bound retail buttons do nothing and item/stack selections show incomplete state | `gmToolbarUI @ 0x004BD0C0..0x004BF380`; LayoutDesc `0x21000016` |
| AP-102 | Inventory-to-occupied toolbar drop treats the inventory source index as though it were a vacated toolbar slot for the displaced shortcut | `src/AcDream.App/UI/Layout/ToolbarController.cs` drop planner | Normal empty-slot and toolbar-reorder paths work; Wave 4 makes the transaction source-aware | The displaced shortcut is lost or moved to an unrelated slot | `gmToolbarUI::HandleDropRelease @ 0x004BE7C0` |
| AP-103 | Shortcut session storage keeps only object guid and discards raw non-object fields; parser/builder names the final uint as `u16 spellId/u16 layer` instead of preserving retail `spellID_` as raw data | `src/AcDream.Core/Items/ShortcutStore.cs`; `src/AcDream.Core.Net/Messages/PlayerDescriptionParser.cs`; `InventoryActions.cs` | `gmToolbarUI` itself displays only nonzero object ids, so current item shortcuts work | Round-trip mutation corrupts unknown/raw fields and encourages invented spell support in the item toolbar | `ShortCutData` in `acclient.h:36484`; `gmToolbarUI::UpdateFromPlayerDesc @ 0x004BF810` |
| AP-104 | Vitals detail element `0x100004A9` and root `HideDetail`/`ShowDetail` transitions are not wired | `src/AcDream.App/UI/Layout/VitalsController.cs` | Compact vitals values/bars are correct | Detail click does nothing and expanded retail state is unreachable | `gmVitalsUI::ListenToElementMessage @ 0x004BFC00`; `PostInit @ 0x004BFCE0` |

View file

@ -485,6 +485,8 @@ behavior. Estimated 1726 days focused work, 35 weeks calendar.
- **✓ SHIPPED — UI Studio** (2026-06-26, branch `claude/hopeful-maxwell-214a12`, ~33693c6→HEAD). Standalone `AcDream.App ui-studio <datdir> [--layout 0xNNNN | --dump <slug>] [--screenshot <png>]` Silk tool that previews any panel through the **production renderer** (`RenderBootstrap.cs` + GameWindow untouched). Sources: 26-window retail dump (`docs/research/2026-06-25-retail-ui-layout-dump.json`) via `--dump`, or live dat import + fixtures via `--layout`. Interactive canvas (click-routing to `UiHost`, Interact/Inspect ImGui toggle), headless `--screenshot`. Architecture: `src/AcDream.App/Studio/` (`StudioWindow`, `PanelFbo`, `FixtureProvider`, `LayoutSource`) + `src/AcDream.App/Rendering/RenderBootstrap.cs`. ISSUES #156 (inventory all-black in studio) and #157 (GameWindow font resolver) filed.
- **✓ SHIPPED — Importer dat-fidelity** (Fix A/B/C, 2026-06-26, same branch). **Boundary established: look = importer (font/justification/color from dat); state/behavior = runtime controller logic.** Fix A: justification property `0x14`/`0x15``UiText` Centered/RightAligned/VerticalJustify. Fix B: FontColor property `0x1B``UiText.DefaultColor`; character colors proved RUNTIME (no importer fix). Fix C: FontDid → per-element dat font via a resolver in `DatWidgetFactory`**STUDIO path only**; GameWindow passes `null` (Issue #157). Fix 4 (UIState-group activation): INVESTIGATED — no importer fix exists; the dat has no Visible encoding; runtime `gm*UI` is correct.
- **✓ SHIPPED — Wave 3.3 unified primary-click router.** `ItemInteractionController.OfferPrimaryClick` returns `NotActive` / `ConsumedSuccess` / `ConsumedRejected`; inventory, bags, main pack, paperdoll, toolbar, radar, and world clicks all offer target mode before local fallback. Rejected targets cannot drift selection or open/use the clicked object. Issue #197 and AP-107 retired; retained matrix and live ACE-provisioned healing gate user-confirmed. The 3-D doll routes through authored hit mask `0x100001D6` (resolved `UiButton` above viewport `0x100001D5`), and local health prefers authoritative private-vital state.
- **✓ SHIPPED — Wave 4.1 retail quick-slot input.** Bare `1..9` use toolbar slots 0..8, Ctrl+`1..9` selects them, Alt+`5..9` uses slots 13..17, and `0` creates a shortcut to the selected owned/eligible object in retail's first empty slot. `ToolbarInputController` is the focused semantic adapter; `ToolbarController` owns `UseShortcut` target-before-use/select ordering and auto-slot eligibility. Keybinding schema v2 migrates only the exact old default Ctrl+number chords, so custom bindings retain their chosen action. Full automated conformance and live user gate passed 2026-07-11.
- **✓ SHIPPED — Wave 4.3a shortcut-alias boundary.** Drag feedback now preserves retail's neutral/accept/reject distinction. Inventory and paperdoll apply the `(DropItemFlags & 0xE) == 0` physical-item gate, so a toolbar alias can reorder within the toolbar but can never move, unwield, wield, or drop the object it references. Live equipped-helmet gate passed 2026-07-11: inventory drop removed only the shortcut.
- **✓ SHIPPED — Character window** (`LayoutDesc 0x2100002E`, `CharacterStatController`, 2026-06-26, same branch). **Visually user-confirmed 2026-06-26 — Attributes tab reads as retail.** Three tabs, header (name/heritage/PK), large-gold level number (dat font, `largeDatFont` 18px), "Total Experience (XP):" + "XP for next level:" captions, 9-row attribute list (icons + right-aligned values + Health/Stamina/Mana vitals), click-to-select (top/bottom selection bars + footer State-B "{Attr}: {value}" / "Experience To Raise: Infinity!" + affordability-gated raise triangles), centered footer. User noted "still needs some polish for later" — deferred to Issue #158.
- **✓ SHIPPED — D.5.4 — Client object/item data model (foundation).** Shipped 2026-06-18 (`b506f53`..`a33e897`, 11 commits). Renamed `ItemRepository``ClientObjectTable` / `ItemInstance``ClientObject`; broadened the table to hold EVERY server object (retail `weenie_object_table` shape). `CreateObject` is now the canonical merge-upsert (`ClientObjectTable.Ingest`, retail `SetWeenieDesc` semantics) via a new Core.Net `ObjectTableWiring` (off GameWindow); `DeleteObject` evicts; `PlayerDescription` is a membership manifest (`RecordMembership`); live container-membership index (`GetContents`, retail `object_inventory_table`). `_liveEntityInfoByGuid` retired (selection/describe resolve from the one table). Root fix: the old enrich-existing-only `EnrichItem` dropped `CreateObject`s for items with no `PlayerDescription` stub — live-Coldeve 4/6 hotbar slots blank; items are now created, not dropped. **Crux resolved:** retail is TWO tables (`object_table` + `weenie_object_table`), NOT one — acdream's `WorldEntity` (3D system) + `ClientObjectTable` (data/UI) split was already architecturally faithful; the fix was the ingestion path, not a table unification. 2671 tests green.
- **Roadmap correction (2026-07-10):** the completion order is now the architecture-first campaign in `docs/superpowers/plans/2026-07-10-retail-ui-fidelity-completion.md`. Retail `gmToolbarUI` is object-only: preserve `ShortCutData.index_`, `objectID_`, and `spellID_`, but do not invent spell glyphs on this bar. `PlayerModule::favorite_spells_[8]` feeds separate spell bars.

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

View file

@ -442,11 +442,11 @@ skipped.
### 4.1 Wire retail quick-slot input
- [ ] Add a focused retained-UI input action controller which maps
- [x] Add a focused retained-UI input action controller which maps
`UseQuickSlot_1..9`, `UseQuickSlot_14..18`, and `CreateShortcut` to
`ToolbarController` methods.
- [ ] Port use-vs-select semantics from `gmToolbarUI::ListenToGlobalMessage`.
- [ ] Keep `GameWindow.OnInputAction` to one delegation seam.
- [x] Port use-vs-select semantics from `gmToolbarUI::ListenToGlobalMessage`.
- [x] Keep `GameWindow.OnInputAction` to one delegation seam.
### 4.2 Preserve the exact shortcut model; keep the toolbar object-only
@ -473,6 +473,15 @@ skipped.
- [ ] On full-stack merge, rekey every matching object shortcut and preserve the
non-object fields of its raw `ShortCutData` entry.
**Wave 4.3 partial implementation (2026-07-11):** drag-over feedback is now an
explicit neutral/accept/reject result instead of a Boolean. Inventory and
paperdoll enforce retail's `(DropItemFlags & 0xE) == 0` physical-item gate, so a
toolbar alias released on either surface only leaves its remove-on-lift shortcut
removal in place; it cannot move, unwield, or wield the underlying object. The
remaining source-aware occupied-toolbar transaction and raw-field preservation
stay in Wave 4.2/4.3. Live gate passed with an equipped helmet: dropping its
toolbar alias onto inventory removed only the shortcut and left the helmet worn.
### 4.4 Finish toolbar controls
- [ ] Bind all seven retail panel buttons by their DAT panel-id attribute plus combat,
@ -486,13 +495,20 @@ skipped.
- [ ] Add item-mana query/cancel state, health query cancellation, and one shared
split-quantity owner for the entry, slider, and item drag payload.
**Partial implementation (2026-07-11):** the four stance-specific combat
**Wave 4.1 implementation (2026-07-11):** bare `1..9` now use slots 0..8,
Ctrl+`1..9` selects them, Alt+`5..9` uses slots 13..17, and `0` creates a
shortcut to the selected owned/eligible object in the first empty slot. A focused
`ToolbarInputController` maps semantic actions; `ToolbarController` owns the
retail target-before-use/select ordering and auto-slot eligibility; and
`GameWindow.OnInputAction` has one retained-runtime delegation seam. Keybinding
schema v2 migrates only the exact old default Ctrl+number bindings, preserving
custom user bindings. The four stance-specific combat
indicator buttons `0x10000192..0x10000195` now all dispatch one injected combat
toggle command. The key binding and retained toolbar therefore converge on
`GameWindow.ToggleLiveCombatMode`, matching `gmToolbarUI::ListenToElementMessage
@ 0x004BEE90`; the visible indicator continues to update from authoritative
`CombatState`. Use, Examine, remaining panel buttons, quick slots, and selected
object residuals remain in this wave.
`CombatState`. Use, Examine, remaining panel buttons, raw shortcut preservation,
drag/drop edge cases, and selected-object residuals remain in this wave.
**Oracles:** `gmToolbarUI @ 0x004BD0C0..0x004BF380`,
`UIElement_UIItem::SetShortcutNum`, shortcut wire builders.