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:
parent
6fcc510d5d
commit
e65119f0c6
24 changed files with 650 additions and 69 deletions
|
|
@ -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-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 100–360 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-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 100–360 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-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-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-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` |
|
| 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` |
|
||||||
|
|
|
||||||
|
|
@ -485,6 +485,8 @@ behavior. Estimated 17–26 days focused work, 3–5 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 — 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 — 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 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 — 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.
|
- **✓ 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.
|
- **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.
|
||||||
|
|
|
||||||
|
|
@ -312,9 +312,44 @@ before introducing a public enum.
|
||||||
it first attempts `PlaceInBackpack`; with the flag clear it reports the appropriate
|
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
|
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
|
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
|
```text
|
||||||
RecvNotice_ItemListBeginDrag(sourceList): // 0x004BD930
|
RecvNotice_ItemListBeginDrag(sourceList): // 0x004BD930
|
||||||
|
|
@ -329,7 +364,7 @@ RecvNotice_ItemListBeginDrag(sourceList): // 0x004BD930
|
||||||
then, when `send=true`, sends `Event_RemoveShortCut(index)` and removes that slot
|
then, when `send=true`, sends `Event_RemoveShortCut(index)` and removes that slot
|
||||||
from `PlayerModule`. Releasing a lifted shortcut off the toolbar leaves it removed.
|
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
|
```text
|
||||||
GetFirstEmptyShortcutToTheRightOf(slot): // 0x004BD560
|
GetFirstEmptyShortcutToTheRightOf(slot): // 0x004BD560
|
||||||
|
|
@ -341,7 +376,7 @@ GetFirstEmptyShortcutToTheRightOf(slot): // 0x004BD560
|
||||||
It is not an inventory grid index, the original source index, or a global
|
It is not an inventory grid index, the original source index, or a global
|
||||||
first-empty search unless wraparound reaches slot zero.
|
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
|
`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
|
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
|
the right of the target. Only a dragged shortcut alias attempts to restore the
|
||||||
displaced item to `m_lastShortcutNumDragged`.
|
displaced item to `m_lastShortcutNumDragged`.
|
||||||
|
|
||||||
### 4.5 Full-stack merge rekey
|
### 4.6 Full-stack merge rekey
|
||||||
|
|
||||||
```text
|
```text
|
||||||
RecvNotice_FullMergingItem(oldId, newId): // 0x004BE9B0
|
RecvNotice_FullMergingItem(oldId, newId): // 0x004BE9B0
|
||||||
|
|
|
||||||
|
|
@ -442,11 +442,11 @@ skipped.
|
||||||
|
|
||||||
### 4.1 Wire retail quick-slot input
|
### 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
|
`UseQuickSlot_1..9`, `UseQuickSlot_14..18`, and `CreateShortcut` to
|
||||||
`ToolbarController` methods.
|
`ToolbarController` methods.
|
||||||
- [ ] Port use-vs-select semantics from `gmToolbarUI::ListenToGlobalMessage`.
|
- [x] Port use-vs-select semantics from `gmToolbarUI::ListenToGlobalMessage`.
|
||||||
- [ ] Keep `GameWindow.OnInputAction` to one delegation seam.
|
- [x] Keep `GameWindow.OnInputAction` to one delegation seam.
|
||||||
|
|
||||||
### 4.2 Preserve the exact shortcut model; keep the toolbar object-only
|
### 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
|
- [ ] On full-stack merge, rekey every matching object shortcut and preserve the
|
||||||
non-object fields of its raw `ShortCutData` entry.
|
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
|
### 4.4 Finish toolbar controls
|
||||||
|
|
||||||
- [ ] Bind all seven retail panel buttons by their DAT panel-id attribute plus combat,
|
- [ ] 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
|
- [ ] Add item-mana query/cancel state, health query cancellation, and one shared
|
||||||
split-quantity owner for the entry, slider, and item drag payload.
|
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
|
indicator buttons `0x10000192..0x10000195` now all dispatch one injected combat
|
||||||
toggle command. The key binding and retained toolbar therefore converge on
|
toggle command. The key binding and retained toolbar therefore converge on
|
||||||
`GameWindow.ToggleLiveCombatMode`, matching `gmToolbarUI::ListenToElementMessage
|
`GameWindow.ToggleLiveCombatMode`, matching `gmToolbarUI::ListenToElementMessage
|
||||||
@ 0x004BEE90`; the visible indicator continues to update from authoritative
|
@ 0x004BEE90`; the visible indicator continues to update from authoritative
|
||||||
`CombatState`. Use, Examine, remaining panel buttons, quick slots, and selected
|
`CombatState`. Use, Examine, remaining panel buttons, raw shortcut preservation,
|
||||||
object residuals remain in this wave.
|
drag/drop edge cases, and selected-object residuals remain in this wave.
|
||||||
|
|
||||||
**Oracles:** `gmToolbarUI @ 0x004BD0C0..0x004BF380`,
|
**Oracles:** `gmToolbarUI @ 0x004BD0C0..0x004BF380`,
|
||||||
`UIElement_UIItem::SetShortcutNum`, shortcut wire builders.
|
`UIElement_UIItem::SetShortcutNum`, shortcut wire builders.
|
||||||
|
|
|
||||||
|
|
@ -11781,6 +11781,13 @@ public sealed class GameWindow : IDisposable
|
||||||
if (activation != AcDream.UI.Abstractions.Input.ActivationType.Press
|
if (activation != AcDream.UI.Abstractions.Input.ActivationType.Press
|
||||||
&& activation != AcDream.UI.Abstractions.Input.ActivationType.DoubleClick) return;
|
&& activation != AcDream.UI.Abstractions.Input.ActivationType.DoubleClick) return;
|
||||||
|
|
||||||
|
// Wave 4.1: one retained-UI delegation seam. The runtime routes only
|
||||||
|
// toolbar-owned semantic actions; every other action continues through
|
||||||
|
// the game switch below. Keyboard capture is already enforced at the
|
||||||
|
// InputDispatcher source, matching retail's stack-entry focus gate.
|
||||||
|
if (_retailUiRuntime?.HandleInputAction(action) == true)
|
||||||
|
return;
|
||||||
|
|
||||||
// K-fix1 (2026-04-26): Q is autorun TOGGLE, not hold-to-run. Press
|
// K-fix1 (2026-04-26): Q is autorun TOGGLE, not hold-to-run. Press
|
||||||
// Q to start, press Q again to stop. Pressing Backup / Stop /
|
// Q to start, press Q again to stop. Pressing Backup / Stop /
|
||||||
// StrafeLeft / StrafeRight while autorun is active also cancels it
|
// StrafeLeft / StrafeRight while autorun is active also cancels it
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,23 @@
|
||||||
namespace AcDream.App.UI;
|
namespace AcDream.App.UI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Visual result of a drag-over query. Retail handlers can consume a drag-over
|
||||||
|
/// message without setting either accept or reject art; shortcut aliases over a
|
||||||
|
/// physical item list use that neutral path.
|
||||||
|
/// </summary>
|
||||||
|
public enum ItemDragAcceptance
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Accept,
|
||||||
|
Reject,
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A panel controller implements this and registers itself on each of its
|
/// A panel controller implements this and registers itself on each of its
|
||||||
/// <see cref="UiItemList"/>s. Port of retail's <c>m_dragHandler</c> vtable
|
/// <see cref="UiItemList"/>s. Port of retail's <c>m_dragHandler</c> vtable
|
||||||
/// (<c>RegisterItemListDragHandler</c>, decomp 230461; confirmed acclient
|
/// (<c>RegisterItemListDragHandler</c>, decomp 230461; confirmed acclient
|
||||||
/// 0x004a539e + the gmToolbarUI block 0x004bdd89).
|
/// 0x004a539e + the gmToolbarUI block 0x004bdd89).
|
||||||
/// <para><see cref="OnDragOver"/> decides the accept/reject OVERLAY only (advisory).
|
/// <para><see cref="OnDragOver"/> decides the neutral/accept/reject overlay only (advisory).
|
||||||
/// <see cref="HandleDropRelease"/> is authoritative — it performs the action, or
|
/// <see cref="HandleDropRelease"/> is authoritative — it performs the action, or
|
||||||
/// no-ops to reject.</para>
|
/// no-ops to reject.</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -17,8 +29,8 @@ public interface IItemListDragHandler
|
||||||
/// HandleDropRelease (place) or the drag ends off-target (stays removed). No restore on cancel.</summary>
|
/// HandleDropRelease (place) or the drag ends off-target (stays removed). No restore on cancel.</summary>
|
||||||
void OnDragLift(UiItemList sourceList, UiItemSlot sourceCell, ItemDragPayload payload);
|
void OnDragLift(UiItemList sourceList, UiItemSlot sourceCell, ItemDragPayload payload);
|
||||||
|
|
||||||
/// <summary>True ⇒ the target cell shows the accept (green) frame; false ⇒ reject (red).</summary>
|
/// <summary>Returns the advisory target-cell overlay state.</summary>
|
||||||
bool OnDragOver(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload);
|
ItemDragAcceptance OnDragOver(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload);
|
||||||
|
|
||||||
/// <summary>Perform the drop (issue the per-panel wire action), or no-op to reject.</summary>
|
/// <summary>Perform the drop (issue the per-panel wire action), or no-op to reject.</summary>
|
||||||
void HandleDropRelease(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload);
|
void HandleDropRelease(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload);
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,17 @@ public sealed class ItemInteractionController : IDisposable
|
||||||
public bool IsPendingSource(uint itemGuid)
|
public bool IsPendingSource(uint itemGuid)
|
||||||
=> itemGuid != 0 && itemGuid == PendingSourceItem;
|
=> itemGuid != 0 && itemGuid == PendingSourceItem;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retail <c>ACCWeenieObject::IsOwnedByPlayer</c> projection shared with
|
||||||
|
/// toolbar shortcut creation. Ownership includes self, equipped items, and
|
||||||
|
/// arbitrarily nested carried containers.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsOwnedByPlayer(uint itemGuid)
|
||||||
|
=> _objects.Get(itemGuid) is { } item
|
||||||
|
&& (item.ObjectId == _playerGuid()
|
||||||
|
|| IsCarriedByPlayer(item)
|
||||||
|
|| IsEquippedByPlayer(item));
|
||||||
|
|
||||||
public bool IsCurrentTargetCompatible(uint targetGuid)
|
public bool IsCurrentTargetCompatible(uint targetGuid)
|
||||||
{
|
{
|
||||||
if (!IsTargetModeActive || targetGuid == 0) return false;
|
if (!IsTargetModeActive || targetGuid == 0) return false;
|
||||||
|
|
|
||||||
|
|
@ -379,24 +379,41 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
||||||
/// until the server confirms the move. Retail dims the source; we leave it + the floating ghost.</summary>
|
/// until the server confirms the move. Retail dims the source; we leave it + the floating ghost.</summary>
|
||||||
public void OnDragLift(UiItemList sourceList, UiItemSlot sourceCell, ItemDragPayload payload) { }
|
public void OnDragLift(UiItemList sourceList, UiItemSlot sourceCell, ItemDragPayload payload) { }
|
||||||
|
|
||||||
/// <summary>Advisory accept/reject overlay (green insert-arrow / red circle). Grid drops are always
|
/// <summary>Advisory neutral/accept/reject overlay. Shortcut aliases stay neutral; physical grid
|
||||||
/// valid (reorder/insert); a side-bag/main-pack drop is valid unless that container is KNOWN-full.</summary>
|
/// drops accept; a side-bag/main-pack drop rejects only when that container is known full.</summary>
|
||||||
public bool OnDragOver(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload)
|
public ItemDragAcceptance OnDragOver(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload)
|
||||||
{
|
{
|
||||||
if (payload.ObjId == 0) return false;
|
// UIElement_ItemList::ItemList_DragOver @ 0x004E3400 only evaluates
|
||||||
if (targetList == _contentsGrid) return true;
|
// physical targets when (DropItemFlags & 0xE) == 0. A toolbar alias
|
||||||
|
// carries bit 4: leave the inventory target neutral while the alias's
|
||||||
|
// remove-on-lift stands.
|
||||||
|
if (payload.SourceKind == ItemDragSource.ShortcutBar)
|
||||||
|
return ItemDragAcceptance.None;
|
||||||
|
if (payload.ObjId == 0)
|
||||||
|
return ItemDragAcceptance.Reject;
|
||||||
|
if (targetList == _contentsGrid)
|
||||||
|
return ItemDragAcceptance.Accept;
|
||||||
if (targetList == _containerList || targetList == _topContainer)
|
if (targetList == _containerList || targetList == _topContainer)
|
||||||
{
|
{
|
||||||
if (targetCell.ItemId == 0 || targetCell.ItemId == payload.ObjId) return false;
|
if (targetCell.ItemId == 0 || targetCell.ItemId == payload.ObjId)
|
||||||
return !IsContainerFull(targetCell.ItemId);
|
return ItemDragAcceptance.Reject;
|
||||||
|
return IsContainerFull(targetCell.ItemId)
|
||||||
|
? ItemDragAcceptance.Reject
|
||||||
|
: ItemDragAcceptance.Accept;
|
||||||
}
|
}
|
||||||
return false;
|
return ItemDragAcceptance.Reject;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Perform the move: resolve (container, placement) from the target, optimistically move
|
/// <summary>Perform the move: resolve (container, placement) from the target, optimistically move
|
||||||
/// locally (instant), and send PutItemInContainer. Retail: HandleDropRelease + ItemList_InsertItem.</summary>
|
/// locally (instant), and send PutItemInContainer. Retail: HandleDropRelease + ItemList_InsertItem.</summary>
|
||||||
public void HandleDropRelease(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload)
|
public void HandleDropRelease(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload)
|
||||||
{
|
{
|
||||||
|
// UIElement_ItemList::HandleDropRelease @ 0x004E4790 applies the same
|
||||||
|
// (flags & 0xE) == 0 gate before AcceptDragObject. A shortcut alias is
|
||||||
|
// not the equipped/contained object it points at.
|
||||||
|
if (payload.SourceKind == ItemDragSource.ShortcutBar)
|
||||||
|
return;
|
||||||
|
|
||||||
uint item = payload.ObjId;
|
uint item = payload.ObjId;
|
||||||
if (item == 0) return;
|
if (item == 0) return;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -259,11 +259,18 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
|
||||||
|
|
||||||
/// <summary>Accept iff the dragged item can be worn here (its ValidLocations intersects the slot mask).
|
/// <summary>Accept iff the dragged item can be worn here (its ValidLocations intersects the slot mask).
|
||||||
/// Retail: gmPaperDollUI::OnItemListDragOver (decomp 174302).</summary>
|
/// Retail: gmPaperDollUI::OnItemListDragOver (decomp 174302).</summary>
|
||||||
public bool OnDragOver(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload)
|
public ItemDragAcceptance OnDragOver(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload)
|
||||||
{
|
{
|
||||||
|
// gmPaperDollUI::OnItemListDragOver @ 0x004A4270 ignores shortcut
|
||||||
|
// aliases through its (DropItemFlags & 0xE) == 0 gate.
|
||||||
|
if (payload.SourceKind == ItemDragSource.ShortcutBar)
|
||||||
|
return ItemDragAcceptance.None;
|
||||||
var item = _objects.Get(payload.ObjId);
|
var item = _objects.Get(payload.ObjId);
|
||||||
if (item is null) return false;
|
if (item is null)
|
||||||
return (item.ValidLocations & MaskFor(targetList)) != EquipMask.None;
|
return ItemDragAcceptance.Reject;
|
||||||
|
return (item.ValidLocations & MaskFor(targetList)) != EquipMask.None
|
||||||
|
? ItemDragAcceptance.Accept
|
||||||
|
: ItemDragAcceptance.Reject;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Wield the dropped item: optimistic local equip (instant) + GetAndWieldItem 0x001A.
|
/// <summary>Wield the dropped item: optimistic local equip (instant) + GetAndWieldItem 0x001A.
|
||||||
|
|
@ -271,6 +278,10 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
|
||||||
/// chosen finger). Retail: gmPaperDollUI HandleDropRelease → GetAndWieldItem.</summary>
|
/// chosen finger). Retail: gmPaperDollUI HandleDropRelease → GetAndWieldItem.</summary>
|
||||||
public void HandleDropRelease(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload)
|
public void HandleDropRelease(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload)
|
||||||
{
|
{
|
||||||
|
// gmPaperDollUI::HandleDropRelease @ 0x004A4D80 applies the same flag
|
||||||
|
// gate before accepting/wielding the physical object.
|
||||||
|
if (payload.SourceKind == ItemDragSource.ShortcutBar)
|
||||||
|
return;
|
||||||
var item = _objects.Get(payload.ObjId);
|
var item = _objects.Get(payload.ObjId);
|
||||||
if (item is null) return;
|
if (item is null) return;
|
||||||
EquipMask wieldMask = ItemEquipRules.ResolvePaperdollDropWieldMask(item, MaskFor(targetList));
|
EquipMask wieldMask = ItemEquipRules.ResolvePaperdollDropWieldMask(item, MaskFor(targetList));
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
||||||
private readonly Action<uint, uint>? _sendAddShortcut; // (index, objectGuid)
|
private readonly Action<uint, uint>? _sendAddShortcut; // (index, objectGuid)
|
||||||
private readonly Action<uint>? _sendRemoveShortcut; // (index)
|
private readonly Action<uint>? _sendRemoveShortcut; // (index)
|
||||||
private readonly ItemInteractionController? _itemInteraction;
|
private readonly ItemInteractionController? _itemInteraction;
|
||||||
|
private readonly Action<uint>? _selectItem;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
// Digit sprite DID arrays for slot labels (top row, numbers 1-9).
|
// Digit sprite DID arrays for slot labels (top row, numbers 1-9).
|
||||||
|
|
@ -98,7 +99,8 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
||||||
ItemInteractionController? itemInteraction = null,
|
ItemInteractionController? itemInteraction = null,
|
||||||
Action<uint, uint>? sendAddShortcut = null,
|
Action<uint, uint>? sendAddShortcut = null,
|
||||||
Action<uint>? sendRemoveShortcut = null,
|
Action<uint>? sendRemoveShortcut = null,
|
||||||
Action? toggleCombat = null)
|
Action? toggleCombat = null,
|
||||||
|
Action<uint>? selectItem = null)
|
||||||
{
|
{
|
||||||
_repo = repo;
|
_repo = repo;
|
||||||
_combatState = combatState;
|
_combatState = combatState;
|
||||||
|
|
@ -111,6 +113,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
||||||
_itemInteraction = itemInteraction;
|
_itemInteraction = itemInteraction;
|
||||||
_sendAddShortcut = sendAddShortcut;
|
_sendAddShortcut = sendAddShortcut;
|
||||||
_sendRemoveShortcut = sendRemoveShortcut;
|
_sendRemoveShortcut = sendRemoveShortcut;
|
||||||
|
_selectItem = selectItem;
|
||||||
|
|
||||||
for (int i = 0; i < SlotIds.Length; i++)
|
for (int i = 0; i < SlotIds.Length; i++)
|
||||||
{
|
{
|
||||||
|
|
@ -233,11 +236,12 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
||||||
ItemInteractionController? itemInteraction = null,
|
ItemInteractionController? itemInteraction = null,
|
||||||
Action<uint, uint>? sendAddShortcut = null,
|
Action<uint, uint>? sendAddShortcut = null,
|
||||||
Action<uint>? sendRemoveShortcut = null,
|
Action<uint>? sendRemoveShortcut = null,
|
||||||
Action? toggleCombat = null)
|
Action? toggleCombat = null,
|
||||||
|
Action<uint>? selectItem = null)
|
||||||
{
|
{
|
||||||
var c = new ToolbarController(layout, repo, shortcuts, iconIds, useItem, combatState,
|
var c = new ToolbarController(layout, repo, shortcuts, iconIds, useItem, combatState,
|
||||||
peaceDigits, warDigits, emptyDigits, itemInteraction,
|
peaceDigits, warDigits, emptyDigits, itemInteraction,
|
||||||
sendAddShortcut, sendRemoveShortcut, toggleCombat);
|
sendAddShortcut, sendRemoveShortcut, toggleCombat, selectItem);
|
||||||
c.Populate();
|
c.Populate();
|
||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
|
|
@ -387,6 +391,95 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Port of <c>gmToolbarUI::UseShortcut @ 0x004BD350</c>. Target mode
|
||||||
|
/// is one-shot and precedes both normal use and selection.
|
||||||
|
/// </summary>
|
||||||
|
public bool UseShortcut(int slot, bool use)
|
||||||
|
{
|
||||||
|
if ((uint)slot >= _slots.Length || _slots[slot]?.Cell is not { } cell)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
uint itemId = cell.ItemId;
|
||||||
|
if (_itemInteraction?.IsTargetModeActive == true)
|
||||||
|
{
|
||||||
|
if (itemId != 0)
|
||||||
|
_itemInteraction.OfferPrimaryClick(itemId);
|
||||||
|
else
|
||||||
|
_itemInteraction.CancelTargetMode();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemId == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (use)
|
||||||
|
{
|
||||||
|
if (_itemInteraction is not null)
|
||||||
|
_itemInteraction.ActivateItem(itemId);
|
||||||
|
else
|
||||||
|
_useItem(itemId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_selectItem?.Invoke(itemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Auto-slot form of retail <c>gmToolbarUI::CreateShortcutToItem @
|
||||||
|
/// 0x004BDAC0</c>, used by the global CreateShortcut action. It rejects
|
||||||
|
/// stuck/non-player objects, non-player creatures, unowned objects,
|
||||||
|
/// duplicates, and a full bar before emitting AddShortcut.
|
||||||
|
/// </summary>
|
||||||
|
public bool CreateShortcutToItem(uint itemId)
|
||||||
|
{
|
||||||
|
if (itemId == 0 || _repo.Get(itemId) is not { } item)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var flags = (PublicWeenieFlags)(item.PublicWeenieBitfield ?? 0u);
|
||||||
|
bool isPlayer = itemId == _itemInteraction?.PlayerGuid
|
||||||
|
|| (flags & PublicWeenieFlags.Player) != 0;
|
||||||
|
if ((flags & PublicWeenieFlags.Stuck) != 0 && !isPlayer)
|
||||||
|
return false;
|
||||||
|
if ((item.Type & ItemType.Creature) != 0 && !isPlayer)
|
||||||
|
return false;
|
||||||
|
if (_itemInteraction?.IsOwnedByPlayer(itemId) != true)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
EnsureStoreLoadedForMutation();
|
||||||
|
for (int slot = 0; slot < _slots.Length; slot++)
|
||||||
|
if (_store.Get(slot) == itemId)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
int empty = -1;
|
||||||
|
for (int slot = 0; slot < _slots.Length; slot++)
|
||||||
|
{
|
||||||
|
if (_store.IsEmpty(slot))
|
||||||
|
{
|
||||||
|
empty = slot;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (empty < 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
_store.Set(empty, itemId);
|
||||||
|
_sendAddShortcut?.Invoke((uint)empty, itemId);
|
||||||
|
Populate();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureStoreLoadedForMutation()
|
||||||
|
{
|
||||||
|
if (_storeLoaded)
|
||||||
|
return;
|
||||||
|
_store.Load(System.Linq.Enumerable.Select(_shortcuts(), e => ((int)e.Index, e.ObjectGuid)));
|
||||||
|
_storeLoaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
// ── IItemListDragHandler (B.2 live handler) ──────────────────────────────
|
// ── IItemListDragHandler (B.2 live handler) ──────────────────────────────
|
||||||
// Retail: gmToolbarUI is the m_dragHandler for every shortcut slot list.
|
// Retail: gmToolbarUI is the m_dragHandler for every shortcut slot list.
|
||||||
// Retail model (remove-on-lift / place-on-drop / no-restore):
|
// Retail model (remove-on-lift / place-on-drop / no-restore):
|
||||||
|
|
@ -407,8 +500,8 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public bool OnDragOver(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload)
|
public ItemDragAcceptance OnDragOver(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload)
|
||||||
=> payload.ObjId != 0;
|
=> payload.ObjId != 0 ? ItemDragAcceptance.Accept : ItemDragAcceptance.None;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void HandleDropRelease(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload)
|
public void HandleDropRelease(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload)
|
||||||
|
|
|
||||||
71
src/AcDream.App/UI/Layout/ToolbarInputController.cs
Normal file
71
src/AcDream.App/UI/Layout/ToolbarInputController.cs
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
using AcDream.Core.Selection;
|
||||||
|
using AcDream.UI.Abstractions.Input;
|
||||||
|
|
||||||
|
namespace AcDream.App.UI.Layout;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Focused retained-toolbar adapter for retail global input actions. The input
|
||||||
|
/// dispatcher owns chords/scopes; this class only maps semantic actions to the
|
||||||
|
/// exact <c>gmToolbarUI::ListenToGlobalMessage @ 0x004BE4E0</c> slot intent.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ToolbarInputController
|
||||||
|
{
|
||||||
|
private readonly ToolbarController _toolbar;
|
||||||
|
private readonly SelectionState _selection;
|
||||||
|
|
||||||
|
public ToolbarInputController(ToolbarController toolbar, SelectionState selection)
|
||||||
|
{
|
||||||
|
_toolbar = toolbar ?? throw new ArgumentNullException(nameof(toolbar));
|
||||||
|
_selection = selection ?? throw new ArgumentNullException(nameof(selection));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Returns true when <paramref name="action"/> belongs to the toolbar.</summary>
|
||||||
|
public bool Handle(InputAction action)
|
||||||
|
{
|
||||||
|
if (TryMapShortcut(action, out int slot, out bool use))
|
||||||
|
{
|
||||||
|
_toolbar.UseShortcut(slot, use);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action == InputAction.CreateShortcut)
|
||||||
|
{
|
||||||
|
_toolbar.CreateShortcutToItem(_selection.SelectedObjectId ?? 0u);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool TryMapShortcut(InputAction action, out int slot, out bool use)
|
||||||
|
{
|
||||||
|
int value = (int)action;
|
||||||
|
if (value >= (int)InputAction.UseQuickSlot_1
|
||||||
|
&& value <= (int)InputAction.UseQuickSlot_9)
|
||||||
|
{
|
||||||
|
slot = value - (int)InputAction.UseQuickSlot_1;
|
||||||
|
use = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value >= (int)InputAction.SelectQuickSlot_1
|
||||||
|
&& value <= (int)InputAction.SelectQuickSlot_9)
|
||||||
|
{
|
||||||
|
slot = value - (int)InputAction.SelectQuickSlot_1;
|
||||||
|
use = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value >= (int)InputAction.UseQuickSlot_14
|
||||||
|
&& value <= (int)InputAction.UseQuickSlot_18)
|
||||||
|
{
|
||||||
|
slot = 13 + value - (int)InputAction.UseQuickSlot_14;
|
||||||
|
use = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
slot = -1;
|
||||||
|
use = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -149,6 +149,7 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
public ItemInteractionController ItemInteraction => _bindings.Inventory.ItemInteraction;
|
public ItemInteractionController ItemInteraction => _bindings.Inventory.ItemInteraction;
|
||||||
public CharacterSheetProvider CharacterSheetProvider => _bindings.Character.Provider;
|
public CharacterSheetProvider CharacterSheetProvider => _bindings.Character.Provider;
|
||||||
public ToolbarController? ToolbarController { get; private set; }
|
public ToolbarController? ToolbarController { get; private set; }
|
||||||
|
public ToolbarInputController? ToolbarInputController { get; private set; }
|
||||||
public SelectedObjectController? SelectedObjectController { get; private set; }
|
public SelectedObjectController? SelectedObjectController { get; private set; }
|
||||||
public UiViewport? PaperdollViewportWidget { get; private set; }
|
public UiViewport? PaperdollViewportWidget { get; private set; }
|
||||||
public UiNineSlicePanel? InventoryFrame { get; private set; }
|
public UiNineSlicePanel? InventoryFrame { get; private set; }
|
||||||
|
|
@ -176,6 +177,9 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
|
|
||||||
public void Draw(System.Numerics.Vector2 screenSize) => Host.Draw(screenSize);
|
public void Draw(System.Numerics.Vector2 screenSize) => Host.Draw(screenSize);
|
||||||
|
|
||||||
|
public bool HandleInputAction(AcDream.UI.Abstractions.Input.InputAction action)
|
||||||
|
=> ToolbarInputController?.Handle(action) == true;
|
||||||
|
|
||||||
public void UpdateCursor(IEnumerable<IMouse> mice)
|
public void UpdateCursor(IEnumerable<IMouse> mice)
|
||||||
{
|
{
|
||||||
CursorFeedback feedback = _bindings.Cursor.Feedback.Update(Host.Root);
|
CursorFeedback feedback = _bindings.Cursor.Feedback.Update(Host.Root);
|
||||||
|
|
@ -360,7 +364,9 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
ToolbarController = Layout.ToolbarController.Bind(
|
ToolbarController = Layout.ToolbarController.Bind(
|
||||||
layout, b.Objects, b.Shortcuts, b.ResolveIcon, b.UseItem, b.Combat,
|
layout, b.Objects, b.Shortcuts, b.ResolveIcon, b.UseItem, b.Combat,
|
||||||
peace, war, empty, b.ItemInteraction, b.SendAddShortcut, b.SendRemoveShortcut,
|
peace, war, empty, b.ItemInteraction, b.SendAddShortcut, b.SendRemoveShortcut,
|
||||||
toggleCombat: b.ToggleCombat);
|
toggleCombat: b.ToggleCombat,
|
||||||
|
selectItem: guid => b.Selection.Select(guid, SelectionChangeSource.Toolbar));
|
||||||
|
ToolbarInputController = new ToolbarInputController(ToolbarController, b.Selection);
|
||||||
SelectedObjectController = Layout.SelectedObjectController.Bind(
|
SelectedObjectController = Layout.SelectedObjectController.Bind(
|
||||||
layout,
|
layout,
|
||||||
b.Selection,
|
b.Selection,
|
||||||
|
|
|
||||||
|
|
@ -199,9 +199,15 @@ public sealed class UiItemSlot : UiElement
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case UiEventType.DragEnter: // pointer entered me mid-drag → ask the list's handler
|
case UiEventType.DragEnter: // pointer entered me mid-drag → ask the list's handler
|
||||||
_dragAccept = (FindList() is { DragHandler: { } h } list
|
_dragAccept = FindList() is { DragHandler: { } h } list
|
||||||
&& e.Payload is ItemDragPayload p && h.OnDragOver(list, this, p))
|
&& e.Payload is ItemDragPayload p
|
||||||
? DragAcceptState.Accept : DragAcceptState.Reject;
|
? h.OnDragOver(list, this, p) switch
|
||||||
|
{
|
||||||
|
ItemDragAcceptance.Accept => DragAcceptState.Accept,
|
||||||
|
ItemDragAcceptance.Reject => DragAcceptState.Reject,
|
||||||
|
_ => DragAcceptState.None,
|
||||||
|
}
|
||||||
|
: DragAcceptState.Reject;
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case UiEventType.DragOver: // UiRoot fires this on LEAVE → neutral
|
case UiEventType.DragOver: // UiRoot fires this on LEAVE → neutral
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,10 @@ namespace AcDream.UI.Abstractions.Input;
|
||||||
/// debug bindings that have no retail equivalent.
|
/// debug bindings that have no retail equivalent.
|
||||||
///
|
///
|
||||||
/// <para>
|
/// <para>
|
||||||
/// K.1a defines the enum but only the existing acdream-current chords
|
/// K.1a defined the enum and K.1c flipped the bindings table to the full
|
||||||
/// resolve to actions; K.1c flips the bindings table to the full retail
|
/// retail preset. Runtime controllers subscribe by subsystem; actions whose
|
||||||
/// preset. Several actions in this enum (<c>UseQuickSlot_*</c>,
|
/// owning panel has not landed yet (for example <c>UseSpellSlot_*</c>) may
|
||||||
/// <c>Combat*</c>, <c>UseSpellSlot_*</c>) have NO subscribers in K.1 —
|
/// intentionally remain undispatched.
|
||||||
/// the chord fires the event but nothing acts on it. Phase L panels
|
|
||||||
/// will subscribe.
|
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum InputAction
|
public enum InputAction
|
||||||
|
|
@ -142,12 +140,21 @@ public enum InputAction
|
||||||
UseQuickSlot_7,
|
UseQuickSlot_7,
|
||||||
UseQuickSlot_8,
|
UseQuickSlot_8,
|
||||||
UseQuickSlot_9,
|
UseQuickSlot_9,
|
||||||
|
SelectQuickSlot_1,
|
||||||
|
SelectQuickSlot_2,
|
||||||
|
SelectQuickSlot_3,
|
||||||
|
SelectQuickSlot_4,
|
||||||
|
SelectQuickSlot_5,
|
||||||
|
SelectQuickSlot_6,
|
||||||
|
SelectQuickSlot_7,
|
||||||
|
SelectQuickSlot_8,
|
||||||
|
SelectQuickSlot_9,
|
||||||
UseQuickSlot_14,
|
UseQuickSlot_14,
|
||||||
UseQuickSlot_15,
|
UseQuickSlot_15,
|
||||||
UseQuickSlot_16,
|
UseQuickSlot_16,
|
||||||
UseQuickSlot_17,
|
UseQuickSlot_17,
|
||||||
UseQuickSlot_18,
|
UseQuickSlot_18,
|
||||||
/// <summary>Drop-target shortcut creation (retail 0 / drag-drop).</summary>
|
/// <summary>Create a toolbar shortcut to the selected object (retail 0).</summary>
|
||||||
CreateShortcut,
|
CreateShortcut,
|
||||||
|
|
||||||
// ── Chat ──────────────────────────────────────────────
|
// ── Chat ──────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ namespace AcDream.UI.Abstractions.Input;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class KeyBindings
|
public sealed class KeyBindings
|
||||||
{
|
{
|
||||||
private const int CurrentSchemaVersion = 1;
|
private const int CurrentSchemaVersion = 2;
|
||||||
|
|
||||||
private readonly List<Binding> _bindings = new();
|
private readonly List<Binding> _bindings = new();
|
||||||
|
|
||||||
|
|
@ -212,13 +212,18 @@ public sealed class KeyBindings
|
||||||
b.Add(new(new KeyChord(Key.Escape, ModifierMask.Shift), InputAction.LOGOUT));
|
b.Add(new(new KeyChord(Key.Escape, ModifierMask.Shift), InputAction.LOGOUT));
|
||||||
|
|
||||||
// ── QuickslotCommands ──────────────────────────────────
|
// ── QuickslotCommands ──────────────────────────────────
|
||||||
// Number1..9 → UseQuickSlot_1..9 with bare-mod and Ctrl-mod chords.
|
// Retail gmToolbarUI::ListenToGlobalMessage @ 0x004BE4E0 receives
|
||||||
|
// distinct action-id ranges: bare 1..9 USE slots 0..8, while
|
||||||
|
// Ctrl+1..9 SELECT those slots. The keymap repeats the display name
|
||||||
|
// UseQuickSlot_N for both bindings, so our semantic action layer must
|
||||||
|
// preserve the differing intent explicitly.
|
||||||
for (int i = 1; i <= 9; i++)
|
for (int i = 1; i <= 9; i++)
|
||||||
{
|
{
|
||||||
var k = (Key)((int)Key.Number0 + i); // Number1..Number9
|
var k = (Key)((int)Key.Number0 + i); // Number1..Number9
|
||||||
var action = (InputAction)((int)InputAction.UseQuickSlot_1 + i - 1);
|
var useAction = (InputAction)((int)InputAction.UseQuickSlot_1 + i - 1);
|
||||||
b.Add(new(new KeyChord(k, ModifierMask.None), action));
|
var selectAction = (InputAction)((int)InputAction.SelectQuickSlot_1 + i - 1);
|
||||||
b.Add(new(new KeyChord(k, ModifierMask.Ctrl), action));
|
b.Add(new(new KeyChord(k, ModifierMask.None), useAction));
|
||||||
|
b.Add(new(new KeyChord(k, ModifierMask.Ctrl), selectAction));
|
||||||
}
|
}
|
||||||
// Alt+5..9 → UseQuickSlot_14..18.
|
// Alt+5..9 → UseQuickSlot_14..18.
|
||||||
for (int i = 5; i <= 9; i++)
|
for (int i = 5; i <= 9; i++)
|
||||||
|
|
@ -377,9 +382,9 @@ public sealed class KeyBindings
|
||||||
using var stream = File.OpenRead(path);
|
using var stream = File.OpenRead(path);
|
||||||
var doc = JsonDocument.Parse(stream);
|
var doc = JsonDocument.Parse(stream);
|
||||||
var root = doc.RootElement;
|
var root = doc.RootElement;
|
||||||
// version is currently advisory — read so future migrations
|
// Missing/older versions are valid and drive narrow migrations;
|
||||||
// can branch on it; the field's presence is non-fatal.
|
// the field's presence remains non-fatal.
|
||||||
_ = root.TryGetProperty("version", out var vEl) ? vEl.GetInt32() : 0;
|
int version = root.TryGetProperty("version", out var vEl) ? vEl.GetInt32() : 0;
|
||||||
|
|
||||||
var defaults = RetailDefaults();
|
var defaults = RetailDefaults();
|
||||||
var loaded = new KeyBindings();
|
var loaded = new KeyBindings();
|
||||||
|
|
@ -413,7 +418,9 @@ public sealed class KeyBindings
|
||||||
{
|
{
|
||||||
device = (byte)dEl.GetInt32();
|
device = (byte)dEl.GetInt32();
|
||||||
}
|
}
|
||||||
loaded.Add(new(new KeyChord(silkKey, mods, device), action, activation));
|
var chord = new KeyChord(silkKey, mods, device);
|
||||||
|
action = MigrateLegacyQuickSlotIntent(version, action, chord, activation);
|
||||||
|
loaded.Add(new(chord, action, activation));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -481,6 +488,34 @@ public sealed class KeyBindings
|
||||||
File.WriteAllText(path, json);
|
File.WriteAllText(path, json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Schema v1 repeated <c>UseQuickSlot_N</c> for bare and Ctrl chords,
|
||||||
|
/// losing retail's use-vs-select distinction. Migrate only the exact old
|
||||||
|
/// default Ctrl+matching-number shape; arbitrary user rebindings remain
|
||||||
|
/// attached to the action the user chose.
|
||||||
|
/// </summary>
|
||||||
|
private static InputAction MigrateLegacyQuickSlotIntent(
|
||||||
|
int version,
|
||||||
|
InputAction action,
|
||||||
|
KeyChord chord,
|
||||||
|
ActivationType activation)
|
||||||
|
{
|
||||||
|
if (version >= 2
|
||||||
|
|| activation != ActivationType.Press
|
||||||
|
|| chord.Device != 0
|
||||||
|
|| chord.Modifiers != ModifierMask.Ctrl)
|
||||||
|
return action;
|
||||||
|
|
||||||
|
int offset = (int)action - (int)InputAction.UseQuickSlot_1;
|
||||||
|
if ((uint)offset >= 9u)
|
||||||
|
return action;
|
||||||
|
|
||||||
|
var expectedKey = (Key)((int)Key.Number1 + offset);
|
||||||
|
return chord.Key == expectedKey
|
||||||
|
? (InputAction)((int)InputAction.SelectQuickSlot_1 + offset)
|
||||||
|
: action;
|
||||||
|
}
|
||||||
|
|
||||||
private static ModifierMask ParseModifiers(JsonElement bindingEl)
|
private static ModifierMask ParseModifiers(JsonElement bindingEl)
|
||||||
{
|
{
|
||||||
if (!bindingEl.TryGetProperty("mod", out var modEl)) return ModifierMask.None;
|
if (!bindingEl.TryGetProperty("mod", out var modEl)) return ModifierMask.None;
|
||||||
|
|
|
||||||
|
|
@ -172,6 +172,11 @@ public sealed class SettingsPanel : IPanel
|
||||||
InputAction.UseQuickSlot_1, InputAction.UseQuickSlot_2, InputAction.UseQuickSlot_3,
|
InputAction.UseQuickSlot_1, InputAction.UseQuickSlot_2, InputAction.UseQuickSlot_3,
|
||||||
InputAction.UseQuickSlot_4, InputAction.UseQuickSlot_5, InputAction.UseQuickSlot_6,
|
InputAction.UseQuickSlot_4, InputAction.UseQuickSlot_5, InputAction.UseQuickSlot_6,
|
||||||
InputAction.UseQuickSlot_7, InputAction.UseQuickSlot_8, InputAction.UseQuickSlot_9,
|
InputAction.UseQuickSlot_7, InputAction.UseQuickSlot_8, InputAction.UseQuickSlot_9,
|
||||||
|
InputAction.SelectQuickSlot_1, InputAction.SelectQuickSlot_2, InputAction.SelectQuickSlot_3,
|
||||||
|
InputAction.SelectQuickSlot_4, InputAction.SelectQuickSlot_5, InputAction.SelectQuickSlot_6,
|
||||||
|
InputAction.SelectQuickSlot_7, InputAction.SelectQuickSlot_8, InputAction.SelectQuickSlot_9,
|
||||||
|
InputAction.UseQuickSlot_14, InputAction.UseQuickSlot_15, InputAction.UseQuickSlot_16,
|
||||||
|
InputAction.UseQuickSlot_17, InputAction.UseQuickSlot_18,
|
||||||
InputAction.CreateShortcut,
|
InputAction.CreateShortcut,
|
||||||
});
|
});
|
||||||
RenderSection(renderer, "Emotes", new[]
|
RenderSection(renderer, "Emotes", new[]
|
||||||
|
|
|
||||||
|
|
@ -8,15 +8,15 @@ public class DragDropSpineTests
|
||||||
// A spy handler used across the spine tests.
|
// A spy handler used across the spine tests.
|
||||||
private sealed class SpyHandler : IItemListDragHandler
|
private sealed class SpyHandler : IItemListDragHandler
|
||||||
{
|
{
|
||||||
public bool AcceptResult = true;
|
public ItemDragAcceptance Acceptance = ItemDragAcceptance.Accept;
|
||||||
public (UiItemList list, UiItemSlot cell, ItemDragPayload payload)? LastOver;
|
public (UiItemList list, UiItemSlot cell, ItemDragPayload payload)? LastOver;
|
||||||
public (UiItemList list, UiItemSlot cell, ItemDragPayload payload)? LastDrop;
|
public (UiItemList list, UiItemSlot cell, ItemDragPayload payload)? LastDrop;
|
||||||
public (UiItemList list, UiItemSlot cell, ItemDragPayload payload)? LastLift;
|
public (UiItemList list, UiItemSlot cell, ItemDragPayload payload)? LastLift;
|
||||||
public void OnDragLift(UiItemList list, UiItemSlot cell, ItemDragPayload p)
|
public void OnDragLift(UiItemList list, UiItemSlot cell, ItemDragPayload p)
|
||||||
{ LastLift = (list, cell, p); }
|
{ LastLift = (list, cell, p); }
|
||||||
|
|
||||||
public bool OnDragOver(UiItemList list, UiItemSlot cell, ItemDragPayload p)
|
public ItemDragAcceptance OnDragOver(UiItemList list, UiItemSlot cell, ItemDragPayload p)
|
||||||
{ LastOver = (list, cell, p); return AcceptResult; }
|
{ LastOver = (list, cell, p); return Acceptance; }
|
||||||
|
|
||||||
public void HandleDropRelease(UiItemList list, UiItemSlot cell, ItemDragPayload p)
|
public void HandleDropRelease(UiItemList list, UiItemSlot cell, ItemDragPayload p)
|
||||||
{ LastDrop = (list, cell, p); }
|
{ LastDrop = (list, cell, p); }
|
||||||
|
|
@ -92,7 +92,7 @@ public class DragDropSpineTests
|
||||||
public void DragEnter_setsAcceptOverlay_whenHandlerAccepts()
|
public void DragEnter_setsAcceptOverlay_whenHandlerAccepts()
|
||||||
{
|
{
|
||||||
var (_, cell, h) = ListWithHandler();
|
var (_, cell, h) = ListWithHandler();
|
||||||
h.AcceptResult = true;
|
h.Acceptance = ItemDragAcceptance.Accept;
|
||||||
cell.OnEvent(new UiEvent(0u, cell, UiEventType.DragEnter, Payload: SomePayload()));
|
cell.OnEvent(new UiEvent(0u, cell, UiEventType.DragEnter, Payload: SomePayload()));
|
||||||
Assert.Equal(UiItemSlot.DragAcceptState.Accept, cell.DragAcceptVisual);
|
Assert.Equal(UiItemSlot.DragAcceptState.Accept, cell.DragAcceptVisual);
|
||||||
}
|
}
|
||||||
|
|
@ -101,11 +101,20 @@ public class DragDropSpineTests
|
||||||
public void DragEnter_setsRejectOverlay_whenHandlerRejects()
|
public void DragEnter_setsRejectOverlay_whenHandlerRejects()
|
||||||
{
|
{
|
||||||
var (_, cell, h) = ListWithHandler();
|
var (_, cell, h) = ListWithHandler();
|
||||||
h.AcceptResult = false;
|
h.Acceptance = ItemDragAcceptance.Reject;
|
||||||
cell.OnEvent(new UiEvent(0u, cell, UiEventType.DragEnter, Payload: SomePayload()));
|
cell.OnEvent(new UiEvent(0u, cell, UiEventType.DragEnter, Payload: SomePayload()));
|
||||||
Assert.Equal(UiItemSlot.DragAcceptState.Reject, cell.DragAcceptVisual);
|
Assert.Equal(UiItemSlot.DragAcceptState.Reject, cell.DragAcceptVisual);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DragEnter_keepsNeutralOverlay_whenHandlerIgnoresAlias()
|
||||||
|
{
|
||||||
|
var (_, cell, h) = ListWithHandler();
|
||||||
|
h.Acceptance = ItemDragAcceptance.None;
|
||||||
|
cell.OnEvent(new UiEvent(0u, cell, UiEventType.DragEnter, Payload: SomePayload()));
|
||||||
|
Assert.Equal(UiItemSlot.DragAcceptState.None, cell.DragAcceptVisual);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DragOver_resetsOverlayToNeutral()
|
public void DragOver_resetsOverlayToNeutral()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -591,8 +591,10 @@ public class InventoryControllerTests
|
||||||
objects.AddOrUpdate(new ClientObject { ObjectId = 0xFFFFu });
|
objects.AddOrUpdate(new ClientObject { ObjectId = 0xFFFFu });
|
||||||
var ctrl = (IItemListDragHandler)Bind(layout, objects);
|
var ctrl = (IItemListDragHandler)Bind(layout, objects);
|
||||||
|
|
||||||
Assert.False(ctrl.OnDragOver(containers, containers.GetItem(0)!, Payload(0xFFFFu))); // full bag → red
|
Assert.Equal(ItemDragAcceptance.Reject,
|
||||||
Assert.True(ctrl.OnDragOver(grid, grid.GetItem(0)!, Payload(0xFFFFu))); // grid → green
|
ctrl.OnDragOver(containers, containers.GetItem(0)!, Payload(0xFFFFu))); // full bag → red
|
||||||
|
Assert.Equal(ItemDragAcceptance.Accept,
|
||||||
|
ctrl.OnDragOver(grid, grid.GetItem(0)!, Payload(0xFFFFu))); // grid → green
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -616,7 +618,38 @@ public class InventoryControllerTests
|
||||||
var ctrl = (IItemListDragHandler)Bind(layout, objects);
|
var ctrl = (IItemListDragHandler)Bind(layout, objects);
|
||||||
|
|
||||||
// GetContents(0xC) is empty (a closed bag's contents aren't indexed until opened) → not known-full → accept.
|
// GetContents(0xC) is empty (a closed bag's contents aren't indexed until opened) → not known-full → accept.
|
||||||
Assert.True(ctrl.OnDragOver(containers, containers.GetItem(0)!, Payload(0xFFFFu)));
|
Assert.Equal(ItemDragAcceptance.Accept,
|
||||||
|
ctrl.OnDragOver(containers, containers.GetItem(0)!, Payload(0xFFFFu)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ShortcutAliasDropOnInventory_removesOnlyAlias_andNeverMovesEquippedItem()
|
||||||
|
{
|
||||||
|
var (layout, grid, _, _, _, _, _, _) = BuildLayout();
|
||||||
|
var objects = new ClientObjectTable();
|
||||||
|
const uint helmet = 0xD00Du;
|
||||||
|
objects.AddOrUpdate(new ClientObject
|
||||||
|
{
|
||||||
|
ObjectId = helmet,
|
||||||
|
Type = ItemType.Armor,
|
||||||
|
ValidLocations = EquipMask.HeadWear,
|
||||||
|
});
|
||||||
|
objects.MoveItem(helmet, Player, newSlot: 0, newEquipLocation: EquipMask.HeadWear);
|
||||||
|
var puts = new List<(uint item, uint container, int placement)>();
|
||||||
|
var ctrl = (IItemListDragHandler)Bind(layout, objects, puts: puts);
|
||||||
|
var payload = new ItemDragPayload(
|
||||||
|
helmet,
|
||||||
|
ItemDragSource.ShortcutBar,
|
||||||
|
SourceSlot: 3,
|
||||||
|
SourceCell: new UiItemSlot());
|
||||||
|
|
||||||
|
Assert.Equal(ItemDragAcceptance.None,
|
||||||
|
ctrl.OnDragOver(grid, grid.GetItem(0)!, payload));
|
||||||
|
ctrl.HandleDropRelease(grid, grid.GetItem(0)!, payload);
|
||||||
|
|
||||||
|
Assert.Empty(puts);
|
||||||
|
Assert.Equal(Player, objects.Get(helmet)!.ContainerId);
|
||||||
|
Assert.Equal(EquipMask.HeadWear, objects.Get(helmet)!.CurrentlyEquippedLocation);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -99,8 +99,29 @@ public class PaperdollControllerTests
|
||||||
SeedPackItem(objects, 0xC01u, EquipMask.HeadWear); // a helm in the pack
|
SeedPackItem(objects, 0xC01u, EquipMask.HeadWear); // a helm in the pack
|
||||||
var ctrl = Bind(layout, objects);
|
var ctrl = Bind(layout, objects);
|
||||||
var payload = new ItemDragPayload(0xC01u, ItemDragSource.Inventory, 0, lists[HeadSlot].Cell);
|
var payload = new ItemDragPayload(0xC01u, ItemDragSource.Inventory, 0, lists[HeadSlot].Cell);
|
||||||
Assert.True(ctrl.OnDragOver(lists[HeadSlot], lists[HeadSlot].Cell, payload)); // helm → head OK
|
Assert.Equal(ItemDragAcceptance.Accept,
|
||||||
Assert.False(ctrl.OnDragOver(lists[ShieldSlot], lists[ShieldSlot].Cell, payload)); // helm → shield NO
|
ctrl.OnDragOver(lists[HeadSlot], lists[HeadSlot].Cell, payload)); // helm → head OK
|
||||||
|
Assert.Equal(ItemDragAcceptance.Reject,
|
||||||
|
ctrl.OnDragOver(lists[ShieldSlot], lists[ShieldSlot].Cell, payload)); // helm → shield NO
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ShortcutAlias_isNeutral_andNeverWieldsThroughPaperdoll()
|
||||||
|
{
|
||||||
|
var (layout, lists) = BuildLayout();
|
||||||
|
var objects = new ClientObjectTable();
|
||||||
|
SeedPackItem(objects, 0xC02u, EquipMask.HeadWear);
|
||||||
|
var wields = new List<(uint item, uint mask)>();
|
||||||
|
var ctrl = Bind(layout, objects, wields);
|
||||||
|
var payload = new ItemDragPayload(
|
||||||
|
0xC02u, ItemDragSource.ShortcutBar, 2, lists[HeadSlot].Cell);
|
||||||
|
|
||||||
|
Assert.Equal(ItemDragAcceptance.None,
|
||||||
|
ctrl.OnDragOver(lists[HeadSlot], lists[HeadSlot].Cell, payload));
|
||||||
|
ctrl.HandleDropRelease(lists[HeadSlot], lists[HeadSlot].Cell, payload);
|
||||||
|
|
||||||
|
Assert.Empty(wields);
|
||||||
|
Assert.Equal(EquipMask.None, objects.Get(0xC02u)!.CurrentlyEquippedLocation);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -254,6 +254,124 @@ public class ToolbarControllerTests
|
||||||
Assert.Equal(CombatIds.Length, toggles);
|
Assert.Equal(CombatIds.Length, toggles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UseShortcut_usesOrSelectsAccordingToRetailIntent()
|
||||||
|
{
|
||||||
|
const uint itemId = 0x50001001u;
|
||||||
|
var (layout, _, _) = FakeToolbar();
|
||||||
|
var repo = new ClientObjectTable();
|
||||||
|
repo.AddOrUpdate(new ClientObject { ObjectId = itemId, Type = ItemType.Misc });
|
||||||
|
var shortcuts = new[]
|
||||||
|
{
|
||||||
|
new PlayerDescriptionParser.ShortcutEntry(0, itemId, 0, 0),
|
||||||
|
};
|
||||||
|
uint used = 0;
|
||||||
|
uint selected = 0;
|
||||||
|
var controller = ToolbarController.Bind(
|
||||||
|
layout,
|
||||||
|
repo,
|
||||||
|
() => shortcuts,
|
||||||
|
iconIds: (_, _, _, _, _) => 1u,
|
||||||
|
useItem: id => used = id,
|
||||||
|
selectItem: id => selected = id);
|
||||||
|
|
||||||
|
Assert.True(controller.UseShortcut(0, use: true));
|
||||||
|
Assert.Equal(itemId, used);
|
||||||
|
Assert.Equal(0u, selected);
|
||||||
|
|
||||||
|
Assert.True(controller.UseShortcut(0, use: false));
|
||||||
|
Assert.Equal(itemId, selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UseShortcut_targetModePrecedesUseAndClearsOneShotMode()
|
||||||
|
{
|
||||||
|
const uint player = 0x50000001u;
|
||||||
|
const uint pack = 0x50000002u;
|
||||||
|
const uint source = 0x50000003u;
|
||||||
|
const uint target = 0x50000004u;
|
||||||
|
var (layout, _, _) = FakeToolbar();
|
||||||
|
var repo = new ClientObjectTable();
|
||||||
|
repo.AddOrUpdate(new ClientObject { ObjectId = player, Type = ItemType.Creature });
|
||||||
|
repo.AddOrUpdate(new ClientObject { ObjectId = pack, Type = ItemType.Container });
|
||||||
|
repo.MoveItem(pack, player, 0);
|
||||||
|
repo.AddOrUpdate(new ClientObject
|
||||||
|
{
|
||||||
|
ObjectId = source,
|
||||||
|
Type = ItemType.Misc,
|
||||||
|
Useability = 0x00220008u,
|
||||||
|
TargetType = (uint)ItemType.Creature,
|
||||||
|
});
|
||||||
|
repo.MoveItem(source, pack, 0);
|
||||||
|
repo.AddOrUpdate(new ClientObject { ObjectId = target, Type = ItemType.Creature });
|
||||||
|
uint sentSource = 0;
|
||||||
|
uint sentTarget = 0;
|
||||||
|
var interaction = new ItemInteractionController(
|
||||||
|
repo,
|
||||||
|
() => player,
|
||||||
|
sendUse: null,
|
||||||
|
sendUseWithTarget: (s, t) => (sentSource, sentTarget) = (s, t),
|
||||||
|
sendWield: null,
|
||||||
|
sendDrop: null,
|
||||||
|
nowMs: () => 1_000);
|
||||||
|
var shortcuts = new[]
|
||||||
|
{
|
||||||
|
new PlayerDescriptionParser.ShortcutEntry(0, target, 0, 0),
|
||||||
|
};
|
||||||
|
var controller = ToolbarController.Bind(
|
||||||
|
layout,
|
||||||
|
repo,
|
||||||
|
() => shortcuts,
|
||||||
|
iconIds: (_, _, _, _, _) => 1u,
|
||||||
|
useItem: _ => { },
|
||||||
|
itemInteraction: interaction);
|
||||||
|
|
||||||
|
Assert.True(interaction.ActivateItem(source));
|
||||||
|
Assert.True(controller.UseShortcut(0, use: false));
|
||||||
|
|
||||||
|
Assert.Equal(source, sentSource);
|
||||||
|
Assert.Equal(target, sentTarget);
|
||||||
|
Assert.False(interaction.IsTargetModeActive);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateShortcutToItem_addsFirstEmptyOwnedEligibleItemOnce()
|
||||||
|
{
|
||||||
|
const uint player = 0x50000001u;
|
||||||
|
const uint pack = 0x50000002u;
|
||||||
|
const uint item = 0x50000003u;
|
||||||
|
var (layout, slots, _) = FakeToolbar();
|
||||||
|
var repo = new ClientObjectTable();
|
||||||
|
repo.AddOrUpdate(new ClientObject { ObjectId = player, Type = ItemType.Creature });
|
||||||
|
repo.AddOrUpdate(new ClientObject { ObjectId = pack, Type = ItemType.Container });
|
||||||
|
repo.MoveItem(pack, player, 0);
|
||||||
|
repo.AddOrUpdate(new ClientObject { ObjectId = item, Type = ItemType.Misc });
|
||||||
|
repo.MoveItem(item, pack, 0);
|
||||||
|
var interaction = new ItemInteractionController(
|
||||||
|
repo,
|
||||||
|
() => player,
|
||||||
|
sendUse: null,
|
||||||
|
sendUseWithTarget: null,
|
||||||
|
sendWield: null,
|
||||||
|
sendDrop: null);
|
||||||
|
var sends = new List<(uint Slot, uint Item)>();
|
||||||
|
var controller = ToolbarController.Bind(
|
||||||
|
layout,
|
||||||
|
repo,
|
||||||
|
() => Array.Empty<PlayerDescriptionParser.ShortcutEntry>(),
|
||||||
|
iconIds: (_, _, _, _, _) => 1u,
|
||||||
|
useItem: _ => { },
|
||||||
|
itemInteraction: interaction,
|
||||||
|
sendAddShortcut: (slot, id) => sends.Add((slot, id)));
|
||||||
|
|
||||||
|
Assert.True(controller.CreateShortcutToItem(item));
|
||||||
|
Assert.Equal(item, slots[Row1[0]].Cell.ItemId);
|
||||||
|
Assert.Equal(new[] { (0u, item) }, sends);
|
||||||
|
|
||||||
|
Assert.False(controller.CreateShortcutToItem(item));
|
||||||
|
Assert.Single(sends);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// At bind time (default NonCombat), only the peace indicator (0x10000192) is visible;
|
/// At bind time (default NonCombat), only the peace indicator (0x10000192) is visible;
|
||||||
/// the melee/missile/magic indicators (0x10000193/4/5) are hidden.
|
/// the melee/missile/magic indicators (0x10000193/4/5) are hidden.
|
||||||
|
|
@ -620,7 +738,7 @@ public class ToolbarControllerTests
|
||||||
|
|
||||||
var list = slots[Row1[0]];
|
var list = slots[Row1[0]];
|
||||||
var payload = new ItemDragPayload(0x5001u, ItemDragSource.Inventory, 0, new UiItemSlot());
|
var payload = new ItemDragPayload(0x5001u, ItemDragSource.Inventory, 0, new UiItemSlot());
|
||||||
Assert.True(ctrl.OnDragOver(list, list.Cell, payload));
|
Assert.Equal(ItemDragAcceptance.Accept, ctrl.OnDragOver(list, list.Cell, payload));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>OnDragOver rejects a ghost payload with ObjId 0 (the guard against an
|
/// <summary>OnDragOver rejects a ghost payload with ObjId 0 (the guard against an
|
||||||
|
|
@ -634,7 +752,7 @@ public class ToolbarControllerTests
|
||||||
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { });
|
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { });
|
||||||
var list = slots[Row1[0]];
|
var list = slots[Row1[0]];
|
||||||
var payload = new ItemDragPayload(0u, ItemDragSource.Inventory, 0, new UiItemSlot());
|
var payload = new ItemDragPayload(0u, ItemDragSource.Inventory, 0, new UiItemSlot());
|
||||||
Assert.False(ctrl.OnDragOver(list, list.Cell, payload));
|
Assert.Equal(ItemDragAcceptance.None, ctrl.OnDragOver(list, list.Cell, payload));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── B.2: live drag handler (store + reorder/remove + wire) ───────────────
|
// ── B.2: live drag handler (store + reorder/remove + wire) ───────────────
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
using AcDream.App.UI.Layout;
|
||||||
|
using AcDream.UI.Abstractions.Input;
|
||||||
|
|
||||||
|
namespace AcDream.App.Tests.UI.Layout;
|
||||||
|
|
||||||
|
public sealed class ToolbarInputControllerTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData(InputAction.UseQuickSlot_1, 0, true)]
|
||||||
|
[InlineData(InputAction.UseQuickSlot_9, 8, true)]
|
||||||
|
[InlineData(InputAction.SelectQuickSlot_1, 0, false)]
|
||||||
|
[InlineData(InputAction.SelectQuickSlot_9, 8, false)]
|
||||||
|
[InlineData(InputAction.UseQuickSlot_14, 13, true)]
|
||||||
|
[InlineData(InputAction.UseQuickSlot_18, 17, true)]
|
||||||
|
public void ShortcutActions_mapRetailSlotAndIntent(InputAction action, int slot, bool use)
|
||||||
|
{
|
||||||
|
Assert.True(ToolbarInputController.TryMapShortcut(action, out int actualSlot, out bool actualUse));
|
||||||
|
Assert.Equal(slot, actualSlot);
|
||||||
|
Assert.Equal(use, actualUse);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(InputAction.CreateShortcut)]
|
||||||
|
[InlineData(InputAction.CombatToggleCombat)]
|
||||||
|
[InlineData(InputAction.ToggleChatEntry)]
|
||||||
|
public void NonSlotActions_doNotMapAsShortcut(InputAction action)
|
||||||
|
{
|
||||||
|
Assert.False(ToolbarInputController.TryMapShortcut(action, out int slot, out bool use));
|
||||||
|
Assert.Equal(-1, slot);
|
||||||
|
Assert.False(use);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,8 +16,8 @@ public sealed class RetailUiAutomationProbeTests
|
||||||
public void OnDragLift(UiItemList sourceList, UiItemSlot sourceCell, ItemDragPayload payload)
|
public void OnDragLift(UiItemList sourceList, UiItemSlot sourceCell, ItemDragPayload payload)
|
||||||
=> LastLift = (sourceList, sourceCell, payload);
|
=> LastLift = (sourceList, sourceCell, payload);
|
||||||
|
|
||||||
public bool OnDragOver(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload)
|
public ItemDragAcceptance OnDragOver(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload)
|
||||||
=> true;
|
=> ItemDragAcceptance.Accept;
|
||||||
|
|
||||||
public void HandleDropRelease(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload)
|
public void HandleDropRelease(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload)
|
||||||
=> LastDrop = (targetList, targetCell, payload);
|
=> LastDrop = (targetList, targetCell, payload);
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,38 @@ public class KeyBindingsJsonTests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LoadOrDefault_migratesV1CtrlNumberQuickSlotFromUseToSelect()
|
||||||
|
{
|
||||||
|
var path = TempFile();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const string json = """
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"actions": {
|
||||||
|
"UseQuickSlot_5": [
|
||||||
|
{ "key": "Number5" },
|
||||||
|
{ "key": "Number5", "mod": "Ctrl" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
File.WriteAllText(path, json);
|
||||||
|
|
||||||
|
var loaded = KeyBindings.LoadOrDefault(path);
|
||||||
|
|
||||||
|
Assert.Equal(InputAction.UseQuickSlot_5,
|
||||||
|
loaded.Find(new KeyChord(Key.Number5, ModifierMask.None), ActivationType.Press)?.Action);
|
||||||
|
Assert.Equal(InputAction.SelectQuickSlot_5,
|
||||||
|
loaded.Find(new KeyChord(Key.Number5, ModifierMask.Ctrl), ActivationType.Press)?.Action);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (File.Exists(path)) File.Delete(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void LoadOrDefault_unknown_action_name_skipped_silently()
|
public void LoadOrDefault_unknown_action_name_skipped_silently()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -109,12 +109,14 @@ public class KeyBindingsRetailTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void UseQuickSlot_5_bound_to_Number5_with_and_without_Ctrl()
|
public void QuickSlot_5_bareUsesAndCtrlSelects()
|
||||||
{
|
{
|
||||||
var b = KeyBindings.RetailDefaults();
|
var b = KeyBindings.RetailDefaults();
|
||||||
var binds = b.ForAction(InputAction.UseQuickSlot_5).ToList();
|
var bare = b.Find(new KeyChord(Key.Number5, ModifierMask.None), ActivationType.Press);
|
||||||
Assert.Contains(binds, x => x.Chord == new KeyChord(Key.Number5, ModifierMask.None));
|
var ctrl = b.Find(new KeyChord(Key.Number5, ModifierMask.Ctrl), ActivationType.Press);
|
||||||
Assert.Contains(binds, x => x.Chord == new KeyChord(Key.Number5, ModifierMask.Ctrl));
|
|
||||||
|
Assert.Equal(InputAction.UseQuickSlot_5, bare?.Action);
|
||||||
|
Assert.Equal(InputAction.SelectQuickSlot_5, ctrl?.Action);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue