diff --git a/docs/ISSUES.md b/docs/ISSUES.md index 8223e4c9..1c0d88fd 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -147,6 +147,10 @@ assessment. `SelectRight` is now a release-completed configurable click with the shared three-pixel drag threshold, then pulses, selects, and appraises the picked world object through the same owners as the magnifying glass and E key. RMB camera drags and empty-world releases do not issue appraisal requests. +The separate retained ItemList path now does the same for occupied backpack, +side-bag, loot-container, paperdoll-slot, and physical shortcut cells; it +selects the item before entering the shared appraisal owner, and RMB movement +neither appraises nor lifts the item. **Files:** `src/AcDream.Core.Net/Messages/AppraiseInfoParser.cs`; `src/AcDream.App/UI/RetailUiRuntime.cs`; @@ -170,6 +174,8 @@ the top and assessed melee/missile/armor/magic items show their retail-ordered stats and full DAT spell descriptions. Right-clicking a visible world object opens the same examination window; right-dragging the camera does not. +Right-clicking an occupied retained item cell selects and examines it without +using, equipping, looting, or dragging it. The Black Phyntos Hive specifically reads `Value: ???`, `Burden: Unknown`, then its description after one retail paragraph break, without the bogus 255-item/255-container line. diff --git a/docs/plans/2026-04-11-roadmap.md b/docs/plans/2026-04-11-roadmap.md index d1bc8206..1f3ffe11 100644 --- a/docs/plans/2026-04-11-roadmap.md +++ b/docs/plans/2026-04-11-roadmap.md @@ -33,7 +33,9 @@ presentation; its connected gate passed. The existing magnifying-glass Assess command was already retail-faithful. World right-click assessment now follows the configurable `SelectRight` action's retail release-completed gesture, including drag cancellation, then reuses the canonical -picker/selection/appraisal path. Slice 3's first connected gate exposed +picker/selection/appraisal path. The parallel retained ItemList branch now +selects and appraises occupied inventory, external-container, paperdoll, and +physical-shortcut cells through the same request owner. Slice 3's first connected gate exposed three deeper defects, now corrected: the response enum mislabeled ACE's creature bit `0x0100` and dropped monster packets while retaining the busy cursor; the imported 310 x 400 examination layout was incorrectly hosted as diff --git a/docs/plans/2026-05-12-milestones.md b/docs/plans/2026-05-12-milestones.md index 855dcb2f..7b18b7a5 100644 --- a/docs/plans/2026-05-12-milestones.md +++ b/docs/plans/2026-05-12-milestones.md @@ -101,6 +101,9 @@ passed. The pre-existing Assess command has been reconciled against retail. The same slice now includes retail world right-click assessment through the configurable `SelectRight` complete-click gesture; RMB orbit drags cancel instead of appraising the release point. +Retail's separate retained ItemList right-click branch now covers physical +backpack, loot, paperdoll, and toolbar item cells through the same selection +and appraisal owner. Slice 3's first connected gate found and corrected the real remaining defects: ACE creature flag `0x0100` was mislabeled as a weapon profile and left the appraisal busy reference held, retail's examination UI had been mounted as a diff --git a/docs/plans/2026-07-23-world-interaction-completion.md b/docs/plans/2026-07-23-world-interaction-completion.md index 657a4bfd..5074e262 100644 --- a/docs/plans/2026-07-23-world-interaction-completion.md +++ b/docs/plans/2026-07-23-world-interaction-completion.md @@ -236,6 +236,11 @@ routes through the existing world picker, lighting pulse, canonical no-op, right-drag camera orbit does not appraise its release point, and the independent configurable `SelectionExamine` action now reaches the same request/target-mode path. +The retained follow-up ports the separate +`UIElement_ItemList::ListenToElementMessage @ 0x004E4D50` branch: an occupied +backpack, side-bag, loot, paperdoll-slot, or physical toolbar cell now selects +its item and enters that same appraisal owner on a completed right-click. +Right-button movement cancels the click and can never begin an item drag. ## Slice 1 — spell-bar overflow arrows diff --git a/docs/research/2026-07-17-retail-world-selection-pseudocode.md b/docs/research/2026-07-17-retail-world-selection-pseudocode.md index f1377b6b..873166ba 100644 --- a/docs/research/2026-07-17-retail-world-selection-pseudocode.md +++ b/docs/research/2026-07-17-retail-world-selection-pseudocode.md @@ -221,6 +221,40 @@ camera drag does not appraise the object under the release point. Acdream maps the configurable `SelectRight` action to the same complete-click gesture and keeps `SelectionExamine` independently configurable (retail default E). +### Retained physical-item lists + +`UIElement_ItemList::ListenToElementMessage @ 0x004E4D50` + +```text +on ItemList right-click: + item = UIItem under the mouse + if item.itemID != 0: + if this list uses single selection: + update the list selection + SetSelectedObject(item.itemID) + ClientUISystem.ExamineObject(item.itemID) + else if item.spellID != 0: + ClientUISystem.ExamineSpell(item.spellID) +``` + +`gmPaperDollUI::ListenToElementMessage @ 0x004A5C30` repeats the physical-item +branch for a worn item resolved through the authored paperdoll click map: + +```text +on paperdoll right-click: + itemID = GetPaperDollItemUnderMouse(mouse) + if itemID != 0: + SetSelectedObject(itemID) + ClientUISystem.ExamineObject(itemID) +``` + +Acdream's shared retained `UiItemList` owns the corresponding physical-item +notice. Backpack, side-bag, external-container, paperdoll-slot, and physical +toolbar cells provide their selection source and route the request through the +same `ItemInteractionController` appraisal owner used by world SmartBox and +the toolbar magnifying glass. RMB motion beyond the shared three-pixel click +threshold neither appraises nor begins an item drag. + ## SmartBox click lighting pulse `UIElement_SmartBoxWrapper::RecvNotice_SmartBoxObjectFound @ 0x004E5AD0` diff --git a/src/AcDream.App/UI/Layout/ExternalContainerController.cs b/src/AcDream.App/UI/Layout/ExternalContainerController.cs index 35371745..117bf7cb 100644 --- a/src/AcDream.App/UI/Layout/ExternalContainerController.cs +++ b/src/AcDream.App/UI/Layout/ExternalContainerController.cs @@ -83,6 +83,9 @@ public sealed class ExternalContainerController : IItemListDragHandler, IRetaine ConfigureList(_topContainer, ContainerCellSize, horizontalScroll: false, containerEmptySprite); ConfigureList(_containerList, ContainerCellSize, horizontalScroll: true, containerEmptySprite); ConfigureList(_contentsList, ItemCellSize, horizontalScroll: true, contentsEmptySprite); + _topContainer.ExamineItemRequested = ExamineItem; + _containerList.ExamineItemRequested = ExamineItem; + _contentsList.ExamineItemRequested = ExamineItem; _contentsList.RegisterDragHandler(this); if (layout.FindElement(ContentsScrollbarId) is UiScrollbar scrollbar) @@ -375,6 +378,12 @@ public sealed class ExternalContainerController : IItemListDragHandler, IRetaine private void Select(uint guid) => _selection.Select(guid, SelectionChangeSource.ExternalContainer); + private void ExamineItem(uint guid) + { + Select(guid); + _itemInteraction.ExamineSelectedOrEnterMode(guid); + } + private void HandlePrimaryClick(uint guid, Action fallback) { if (_itemInteraction.OfferPrimaryClick(guid) != ItemPrimaryClickResult.NotActive) @@ -564,5 +573,8 @@ public sealed class ExternalContainerController : IItemListDragHandler, IRetaine _objects.Cleared -= OnObjectsCleared; _selection.Changed -= OnSelectionChanged; _itemInteraction.StateChanged -= OnInteractionStateChanged; + _topContainer.ExamineItemRequested = null; + _containerList.ExamineItemRequested = null; + _contentsList.ExamineItemRequested = null; } } diff --git a/src/AcDream.App/UI/Layout/InventoryController.cs b/src/AcDream.App/UI/Layout/InventoryController.cs index 02e660a6..774678ee 100644 --- a/src/AcDream.App/UI/Layout/InventoryController.cs +++ b/src/AcDream.App/UI/Layout/InventoryController.cs @@ -153,6 +153,9 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo _contentsGrid?.RegisterDragHandler(this); _containerList?.RegisterDragHandler(this); _topContainer?.RegisterDragHandler(this); + if (_contentsGrid is not null) _contentsGrid.ExamineItemRequested = ExamineItem; + if (_containerList is not null) _containerList.ExamineItemRequested = ExamineItem; + if (_topContainer is not null) _topContainer.ExamineItemRequested = ExamineItem; // Burden meter: vertical 11×58 bar (gmBackpackUI m_burdenMeter, retail direction 4). _burdenMeter = layout.FindElement(BurdenMeterId) as UiMeter; @@ -778,6 +781,17 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo _selection.Select(guid, SelectionChangeSource.Inventory); } + /// + /// Retail ItemList right-click: select the physical item, then enter the + /// shared appraisal request owner. This is intentionally independent from + /// left-click target mode and double-click Use/AutoWield. + /// + private void ExamineItem(uint guid) + { + SelectItem(guid); + _itemInteraction?.ExamineSelectedOrEnterMode(guid); + } + /// Open a container (side bag or main pack): it becomes both the selected item and the /// open container. Sends Use to open a side bag server-side (ViewContents arrives async). /// Owned packs are not ClientUISystem::groundObject and never send @@ -917,6 +931,9 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo _objects.ObjectUpdated -= OnObjectChanged; _objects.Cleared -= OnObjectsCleared; _selection.Changed -= OnSelectionChanged; + if (_contentsGrid is not null) _contentsGrid.ExamineItemRequested = null; + if (_containerList is not null) _containerList.ExamineItemRequested = null; + if (_topContainer is not null) _topContainer.ExamineItemRequested = null; if (_itemInteraction is not null) { _itemInteraction.StateChanged -= OnInteractionStateChanged; diff --git a/src/AcDream.App/UI/Layout/PaperdollController.cs b/src/AcDream.App/UI/Layout/PaperdollController.cs index c68b2ddc..f1dac2fd 100644 --- a/src/AcDream.App/UI/Layout/PaperdollController.cs +++ b/src/AcDream.App/UI/Layout/PaperdollController.cs @@ -84,6 +84,7 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo var (element, mask, _, unlockBit) = PaperdollSlotBackgrounds.Definitions[i]; if (layout.FindElement(element) is not UiItemList list) continue; list.RegisterDragHandler(this); + list.ExamineItemRequested = ExamineItem; list.Cell.SourceKind = ItemDragSource.Equipment; list.Cell.SlotIndex = i; // definition position = equipped drag-payload SourceSlot list.Cell.EmptySprite = emptySlotSprites is not null @@ -350,6 +351,12 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo foreach (var a in _armorSlots) a.Visible = _viewState.ArmorSlotsVisible; } + private void ExamineItem(uint itemId) + { + _selection.Select(itemId, SelectionChangeSource.Paperdoll); + _itemInteraction.ExamineSelectedOrEnterMode(itemId); + } + /// Detach event handlers (idempotent). public void Dispose() { @@ -361,6 +368,8 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo _objects.ObjectUpdated -= OnObjectChanged; _objects.Cleared -= OnObjectsCleared; _selection.Changed -= OnSelectionChanged; + foreach (var (_, list) in _slots) + list.ExamineItemRequested = null; if (_dollViewport is UiViewport doll) doll.ClickedAt = null; switch (_dollDragMask) diff --git a/src/AcDream.App/UI/Layout/ToolbarController.cs b/src/AcDream.App/UI/Layout/ToolbarController.cs index 5e1fd48a..4df5a9b3 100644 --- a/src/AcDream.App/UI/Layout/ToolbarController.cs +++ b/src/AcDream.App/UI/Layout/ToolbarController.cs @@ -138,6 +138,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont if (_slots[i] is { } list) { WireClick(list); + list.ExamineItemRequested = ExamineItem; // B.1 drag-drop spine: this controller is the drop handler for every // toolbar slot list; each cell knows its slot index + that it's a // shortcut-bar source (retail UIElement_ItemList::RegisterItemListDragHandler). @@ -531,6 +532,15 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont }; } + private void ExamineItem(uint itemId) + { + if (_selectItem is not null) + _selectItem(itemId); + else + _selection?.Select(itemId, SelectionChangeSource.Toolbar); + _itemInteraction?.ExamineSelectedOrEnterMode(itemId); + } + private static ItemDragAcceptance InventoryButtonDragOver(ItemDragPayload payload) => payload.ObjId != 0 && payload.SourceKind != ItemDragSource.ShortcutBar ? ItemDragAcceptance.Accept @@ -765,6 +775,9 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont _combatState.CombatModeChanged -= SetCombatMode; if (_selection is not null) _selection.Changed -= OnSelectionChanged; + foreach (UiItemList? list in _slots) + if (list is not null) + list.ExamineItemRequested = null; _repo.ObjectAdded -= OnRepositoryObjectChanged; _repo.ObjectUpdated -= OnRepositoryObjectChanged; _repo.ObjectRemoved -= OnRepositoryObjectChanged; diff --git a/src/AcDream.App/UI/UiItemList.cs b/src/AcDream.App/UI/UiItemList.cs index 3ddfa90e..50b38c36 100644 --- a/src/AcDream.App/UI/UiItemList.cs +++ b/src/AcDream.App/UI/UiItemList.cs @@ -73,6 +73,15 @@ public sealed class UiItemList : UiElement /// public Action? CatalogDropped { get; set; } + /// + /// Appraisal command supplied by the owning panel controller for physical + /// item cells. Retail UIElement_ItemList::ListenToElementMessage + /// @ 0x004E4D50 handles its right-click branch at 0x004E4ECA: + /// select the occupied UIItem's itemID, then call + /// ClientUISystem::ExamineObject. + /// + public Action? ExamineItemRequested { get; set; } + private uint _cellEmptySprite; /// Empty-slot sprite for THIS list's cells, resolved from the dat cell template /// (retail attribute 0x1000000e -> catalog 0x21000037 prototype's ItemSlot_Empty; see diff --git a/src/AcDream.App/UI/UiItemSlot.cs b/src/AcDream.App/UI/UiItemSlot.cs index 963b604c..11b38ed0 100644 --- a/src/AcDream.App/UI/UiItemSlot.cs +++ b/src/AcDream.App/UI/UiItemSlot.cs @@ -274,6 +274,11 @@ public class UiItemSlot : UiElement case UiEventType.DoubleClick: DoubleClicked?.Invoke(); return true; + case UiEventType.RightClick: + if (ItemId != 0 + && FindList() is { ExamineItemRequested: { } examine }) + examine(ItemId); + return true; case UiEventType.DragBegin: // Notify the source list's handler so it can lift (remove + wire) — retail diff --git a/src/AcDream.App/UI/UiRoot.cs b/src/AcDream.App/UI/UiRoot.cs index 7b2aa7d4..b5afda8a 100644 --- a/src/AcDream.App/UI/UiRoot.cs +++ b/src/AcDream.App/UI/UiRoot.cs @@ -496,7 +496,10 @@ public sealed class UiRoot : UiElement } else { - _dragCandidate = true; + // Retail item drag begins from left-button movement. A right-button + // press remains a complete-click candidate for ItemList appraisal; + // it must never lift or move an inventory item. + _dragCandidate = btn == UiMouseButton.Left; } // Dispatch raw MouseDown event (retail uses WM_LBUTTONDOWN = 0x201). @@ -594,7 +597,10 @@ public sealed class UiRoot : UiElement _lastClickX = x; _lastClickY = y; } - else if (btn == UiMouseButton.Right && ContainsAbsolute(target, x, y)) + else if (btn == UiMouseButton.Right + && ContainsAbsolute(target, x, y) + && Math.Abs(x - _pressX) <= DragDistanceThreshold + && Math.Abs(y - _pressY) <= DragDistanceThreshold) { var click = new UiEvent(target.EventId, target, UiEventType.RightClick, Data0: (int)flags); diff --git a/tests/AcDream.App.Tests/UI/DragDropSpineTests.cs b/tests/AcDream.App.Tests/UI/DragDropSpineTests.cs index 2b24b375..2fee034a 100644 --- a/tests/AcDream.App.Tests/UI/DragDropSpineTests.cs +++ b/tests/AcDream.App.Tests/UI/DragDropSpineTests.cs @@ -275,6 +275,35 @@ public class DragDropSpineTests Assert.True(used); } + [Fact] + public void RightClick_withoutDrag_requestsItemListAppraisal() + { + var (root, list, _) = RootWithBoundSlot(0x5001u); + var examined = new List(); + list.ExamineItemRequested = examined.Add; + + root.OnMouseDown(UiMouseButton.Right, 10, 10); + root.OnMouseUp(UiMouseButton.Right, 10, 10); + + Assert.Equal(new uint[] { 0x5001u }, examined); + Assert.Null(root.DragSource); + } + + [Fact] + public void RightButtonMovement_cancelsAppraisal_andNeverStartsItemDrag() + { + var (root, list, _) = RootWithBoundSlot(0x5001u); + var examined = new List(); + list.ExamineItemRequested = examined.Add; + + root.OnMouseDown(UiMouseButton.Right, 10, 10); + root.OnMouseMove(14, 10); + root.OnMouseUp(UiMouseButton.Right, 14, 10); + + Assert.Empty(examined); + Assert.Null(root.DragSource); + } + [Fact] public void CompletedDrag_doesNotFireUse() { diff --git a/tests/AcDream.App.Tests/UI/Layout/ExternalContainerControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/ExternalContainerControllerTests.cs index ec06a3db..20cb375c 100644 --- a/tests/AcDream.App.Tests/UI/Layout/ExternalContainerControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/ExternalContainerControllerTests.cs @@ -57,6 +57,7 @@ public sealed class ExternalContainerControllerTests public readonly List Uses = new(); public readonly List NoLonger = new(); public readonly List Pickups = new(); + public readonly List Examines = new(); public readonly List<(uint Item, uint Container, int Placement)> Puts = new(); public readonly List<(uint Item, uint Container, uint Placement, uint Amount)> Splits = new(); public readonly UiRoot Screen = new() { Width = 800f, Height = 600f }; @@ -110,6 +111,7 @@ public sealed class ExternalContainerControllerTests sendUseWithTarget: null, sendWield: null, sendDrop: null, + sendExamine: Examines.Add, groundObjectId: () => State.CurrentContainerId, placeInBackpack: (item, _, _) => Pickups.Add(item), sendPutItemInContainer: (item, container, placement) => @@ -183,6 +185,20 @@ public sealed class ExternalContainerControllerTests Assert.Equal(new uint[] { Item }, h.Pickups); } + [Fact] + public void RightClickLoot_selectsAndExaminesWithoutPickingUp() + { + using var h = new Harness(); + h.Open(Chest, new ContainerContentEntry(Item, 0u)); + + UiItemSlot cell = h.Contents.GetItem(0)!; + cell.OnEvent(new UiEvent(0u, cell, UiEventType.RightClick)); + + Assert.Equal(Item, h.Selection.SelectedObjectId); + Assert.Equal(new uint[] { Item }, h.Examines); + Assert.Empty(h.Pickups); + } + [Fact] public void CloseAndRangeExit_SendUseOnceWithoutNoLongerViewing() { diff --git a/tests/AcDream.App.Tests/UI/Layout/InventoryControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/InventoryControllerTests.cs index 5f717fc4..1c7e106e 100644 --- a/tests/AcDream.App.Tests/UI/Layout/InventoryControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/InventoryControllerTests.cs @@ -62,7 +62,8 @@ public class InventoryControllerTests string? ownerName = null, Action? onClose = null, SelectionState? selection = null, - StackSplitQuantityState? stackSplitQuantity = null) + StackSplitQuantityState? stackSplitQuantity = null, + ItemInteractionController? itemInteraction = null) => InventoryController.Bind(layout, objects, () => Player, iconIds: (_, _, _, _, _) => 0u, strength: () => strength, datFont: null, @@ -76,7 +77,8 @@ public class InventoryControllerTests notifyMergeAttempt: mergeNotices is null ? null : (s, t) => mergeNotices.Add((s, t)), onClose: onClose, selection: selection ?? new SelectionState(), - stackSplitQuantity: stackSplitQuantity); + stackSplitQuantity: stackSplitQuantity, + itemInteraction: itemInteraction); private static UiButton MakeButton(uint id) { @@ -494,6 +496,37 @@ public class InventoryControllerTests Assert.True(grid.GetItem(1)!.Selected); } + [Fact] + public void RightClickGridItem_selectsAndUsesSharedAppraisalOwner() + { + var (layout, grid, _, _, _, _, _, _) = BuildLayout(); + var objects = new ClientObjectTable(); + SeedContained(objects, 0xAu, Player, slot: 0); + var selection = new SelectionState(); + var appraisals = new List(); + using var interaction = new ItemInteractionController( + objects, + playerGuid: () => Player, + sendUse: null, + sendUseWithTarget: null, + sendWield: null, + sendDrop: null, + sendExamine: appraisals.Add); + Bind( + layout, + objects, + selection: selection, + itemInteraction: interaction); + + grid.GetItem(0)!.OnEvent( + new UiEvent(0u, grid.GetItem(0), UiEventType.RightClick)); + + Assert.Equal(0xAu, selection.SelectedObjectId); + Assert.True(grid.GetItem(0)!.Selected); + Assert.Equal(new uint[] { 0xAu }, appraisals); + Assert.Equal(1, interaction.BusyCount); + } + [Fact] public void TargetMode_suppressesSelectedSquare_onPendingSource() { diff --git a/tests/AcDream.App.Tests/UI/Layout/PaperdollControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/PaperdollControllerTests.cs index 1d1c4775..34383d1c 100644 --- a/tests/AcDream.App.Tests/UI/Layout/PaperdollControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/PaperdollControllerTests.cs @@ -47,6 +47,7 @@ public class PaperdollControllerTests IReadOnlyDictionary? emptySlotSprites = null, List<(uint item, uint container, int placement)>? puts = null, List? systemMessages = null, + List? examines = null, Action? configureInteraction = null) { var itemInteraction = new ItemInteractionController( @@ -56,6 +57,7 @@ public class PaperdollControllerTests sendUseWithTarget: null, sendWield: wields is null ? null : (i, m) => wields.Add((i, m)), sendDrop: null, + sendExamine: examines is null ? null : examines.Add, sendPutItemInContainer: puts is null ? null : (item, container, placement) => puts.Add((item, container, placement)), @@ -116,6 +118,28 @@ public class PaperdollControllerTests Assert.Equal(0u, lists[ShieldSlot].Cell.ItemId); // shield slot empty } + [Fact] + public void RightClickEquippedSlot_selectsAndExaminesItem() + { + var (layout, lists) = BuildLayout(); + var objects = new ClientObjectTable(); + SeedEquipped(objects, 0xA01u, EquipMask.HeadWear); + var selection = new SelectionState(); + var appraisals = new List(); + Bind( + layout, + objects, + selection: selection, + examines: appraisals); + + UiItemSlot cell = lists[HeadSlot].Cell; + cell.OnEvent(new UiEvent(0u, cell, UiEventType.RightClick)); + + Assert.Equal(0xA01u, selection.SelectedObjectId); + Assert.True(cell.Selected); + Assert.Equal(new uint[] { 0xA01u }, appraisals); + } + [Fact] public void SessionTableClearRemovesOldEquipmentAndLocksAetheriaSlots() { diff --git a/tests/AcDream.App.Tests/UI/Layout/ToolbarControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/ToolbarControllerTests.cs index c9e0095d..fbbcd8f6 100644 --- a/tests/AcDream.App.Tests/UI/Layout/ToolbarControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/ToolbarControllerTests.cs @@ -763,6 +763,42 @@ public class ToolbarControllerTests Assert.Equal(itemId, selected); } + [Fact] + public void RightClickPhysicalShortcut_selectsAndExaminesItem() + { + const uint player = 0x50000001u; + const uint itemId = 0x50001001u; + var (layout, slots, _) = FakeToolbar(); + var repo = new ClientObjectTable(); + repo.AddOrUpdate(new ClientObject { ObjectId = itemId, Type = ItemType.Misc }); + var shortcuts = new[] { new ShortcutEntry(0, itemId, 0) }; + var selection = new SelectionState(); + var appraisals = new List(); + using var interaction = new ItemInteractionController( + repo, + playerGuid: () => player, + sendUse: null, + sendUseWithTarget: null, + sendWield: null, + sendDrop: null, + sendExamine: appraisals.Add); + using var controller = ToolbarController.Bind( + layout, + repo, + () => shortcuts, + iconIds: (_, _, _, _, _) => 1u, + useItem: _ => { }, + itemInteraction: interaction, + selectItem: id => selection.Select(id, SelectionChangeSource.Toolbar), + selection: selection); + + UiItemSlot cell = slots[Row1[0]].Cell; + cell.OnEvent(new UiEvent(0u, cell, UiEventType.RightClick)); + + Assert.Equal(itemId, selection.SelectedObjectId); + Assert.Equal(new uint[] { itemId }, appraisals); + } + [Fact] public void UseShortcut_targetModePrecedesUseAndClearsOneShotMode() {