diff --git a/docs/ISSUES.md b/docs/ISSUES.md index 65106aa7..10a8dc8c 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -69,7 +69,7 @@ Copy this block when adding a new issue: ## #231 — F-key pickup omits retail pending destination-slot presentation -**Status:** IN-PROGRESS — isolated as Slice 1 corrective commit +**Status:** FIXED (2026-07-21) — keyboard pickup uses the shared ItemHolder path **Severity:** MEDIUM **Filed:** 2026-07-21 **Component:** selection / inventory / retained UI @@ -98,6 +98,11 @@ destination/placement pending reservation, then sends one authoritative request. Server confirmation settles it; server failure removes it without speculative ownership mutation. +**Resolution:** `ItemInteractionController.PlaceWorldItemInBackpack` now owns +the shared pending-placement notification and request dispatch. Both +double-click activation and the F-key route enter that method, so they publish +the same destination container and placement before the server request. + --- ## #230 — Selection hits and deferred actions can cross live GUID incarnations diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index d5e07d95..dd267485 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -13588,7 +13588,8 @@ public sealed class GameWindow : IDisposable case AcDream.UI.Abstractions.Input.InputAction.SelectionPickUp: if (_selection.SelectedObjectId is uint pickupTarget) - _outboundInteractions.Enqueue(() => SendPickUp(pickupTarget)); + _outboundInteractions.Enqueue(() => + _itemInteractionController?.PlaceWorldItemInBackpack(pickupTarget)); else _debugVm?.AddToast("Nothing selected"); break; @@ -13973,9 +13974,6 @@ public sealed class GameWindow : IDisposable Console.WriteLine($"[D.5.1] toolbar use-item guid=0x{guid:X8} seq={seq}"); } - private void SendPickUp(uint itemGuid) - => SendPickUp(itemGuid, _playerServerGuid, placement: 0); - private void SendPickUp(uint itemGuid, uint destinationContainerId, int placement) { if (_liveSession is null diff --git a/src/AcDream.App/UI/ItemInteractionController.cs b/src/AcDream.App/UI/ItemInteractionController.cs index 39dbcd42..45b15389 100644 --- a/src/AcDream.App/UI/ItemInteractionController.cs +++ b/src/AcDream.App/UI/ItemInteractionController.cs @@ -303,6 +303,25 @@ public sealed class ItemInteractionController : IDisposable return ExecuteUseActions(decision.Actions); } + /// + /// Retail keyboard pickup entry point. CPlayerSystem::PlaceInBackpack + /// publishes the waiting destination slot before issuing the move request, + /// exactly like double-click pickup through ItemHolder. + /// + public bool PlaceWorldItemInBackpack(uint itemGuid) + { + if (itemGuid == 0u || _placeInBackpack is null) + return false; + + uint containerId = _backpackContainerId(); + if (containerId == 0u) + containerId = _playerGuid(); + const int placement = 0; + PendingBackpackPlacementRequested?.Invoke(itemGuid, containerId, placement); + _placeInBackpack(itemGuid, containerId, placement); + return true; + } + /// /// Retail paperdoll drop entry point. The paperdoll resolves the exact /// target side/location; this shared interaction owner performs AutoWield's @@ -418,18 +437,7 @@ public sealed class ItemInteractionController : IDisposable switch (action.Kind) { case ItemPolicyActionKind.PlaceInBackpack: - if (_placeInBackpack is null) - break; - uint containerId = _backpackContainerId(); - if (containerId == 0u) - containerId = _playerGuid(); - const int placement = 0; - PendingBackpackPlacementRequested?.Invoke( - action.ObjectId, - containerId, - placement); - _placeInBackpack(action.ObjectId, containerId, placement); - acted = true; + acted |= PlaceWorldItemInBackpack(action.ObjectId); break; case ItemPolicyActionKind.WieldRight: case ItemPolicyActionKind.WieldLeft: diff --git a/tests/AcDream.App.Tests/UI/ItemInteractionControllerTests.cs b/tests/AcDream.App.Tests/UI/ItemInteractionControllerTests.cs index 58d2a4d6..f2d3fd0d 100644 --- a/tests/AcDream.App.Tests/UI/ItemInteractionControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/ItemInteractionControllerTests.cs @@ -1419,6 +1419,21 @@ public sealed class ItemInteractionControllerTests Assert.Equal(2, h.Uses.Count); } + [Fact] + public void KeyboardPickup_PublishesPendingDestinationBeforeRequest() + { + var h = new Harness(); + const uint item = 0x70000A0Bu; + var pending = new List<(uint Item, uint Container, int Placement)>(); + h.Controller.PendingBackpackPlacementRequested += + (objectId, container, placement) => pending.Add((objectId, container, placement)); + + Assert.True(h.Controller.PlaceWorldItemInBackpack(item)); + + Assert.Equal(new[] { (item, Player, 0) }, pending); + Assert.Equal(new[] { (item, Player, 0) }, h.BackpackPlacements); + } + [Fact] public void ResetSession_ClearsTargetBusyThrottleAndConsumedClickState() {