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()
{