fix(interaction): close selection lifecycle review gaps

Bind queued actions and pending inventory requests to exact live incarnations, separate optimistic placement from authoritative responses, and serialize retail-style inventory ownership across UI surfaces.

Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-21 09:01:02 +02:00
parent d2bb5af453
commit 5acc3f01cf
23 changed files with 2635 additions and 168 deletions

View file

@ -301,6 +301,97 @@ public class ToolbarControllerTests
Assert.Equal(new[] { (item, player, 0) }, puts);
}
[Fact]
public void InventoryButton_unownedGroundDropUsesDirectAttemptWithoutDestinationProjection()
{
const uint player = 0x5000u;
const uint item = 0x70005001u;
var (layout, _, _) = FakeToolbar();
var repo = new ClientObjectTable();
repo.AddOrUpdate(new ClientObject { ObjectId = item, Name = "Loot", Type = ItemType.Misc });
var placements = new List<(uint Item, uint Container, int Placement)>();
var directPuts = new List<(uint Item, uint Container, int Placement)>();
using var interaction = new ItemInteractionController(
repo,
playerGuid: () => player,
sendUse: null,
sendUseWithTarget: null,
sendWield: null,
sendDrop: null,
placeInBackpack: (i, c, p) => placements.Add((i, c, p)));
ToolbarController.Bind(
layout,
repo,
() => Array.Empty<ShortcutEntry>(),
iconIds: static (_, _, _, _, _) => 0u,
useItem: static _ => { },
itemInteraction: interaction,
playerGuid: () => player,
sendPutItemInContainer: (i, c, p) => directPuts.Add((i, c, p)));
var button = (UiButton)layout.FindElement(InventoryButtonId)!;
var payload = new ItemDragPayload(item, ItemDragSource.Ground, 0, new UiItemSlot());
Assert.True(button.OnEvent(new UiEvent(0u, button, UiEventType.DragEnter, Payload: payload)));
Assert.True(button.OnEvent(new UiEvent(0u, button, UiEventType.DropReleased, Payload: payload)));
Assert.Empty(placements);
Assert.Equal(new[] { (item, player, 0) }, directPuts);
Assert.False(interaction.TryGetPendingBackpackPlacement(item, out _));
Assert.True(interaction.TryGetPendingInventoryRequest(out var request));
Assert.Equal(item, request.ItemId);
}
[Fact]
public void InventoryButton_ownedDropPublishesDestinationAndGloballyRejectsSecondRequest()
{
const uint player = 0x5000u;
const uint first = 0x70005002u;
const uint second = 0x70005003u;
var (layout, _, _) = FakeToolbar();
var repo = new ClientObjectTable();
const uint pack = 0x5004u;
repo.AddOrUpdate(new ClientObject { ObjectId = player, Name = "Player", Type = ItemType.Creature });
repo.AddOrUpdate(new ClientObject { ObjectId = pack, Name = "Pack", Type = ItemType.Container });
repo.MoveItem(pack, player, 0);
repo.AddOrUpdate(new ClientObject { ObjectId = first, Name = "First Loot", Type = ItemType.Misc });
repo.AddOrUpdate(new ClientObject { ObjectId = second, Name = "Second Loot", Type = ItemType.Misc });
repo.MoveItem(first, pack, 0);
repo.MoveItem(second, pack, 1);
var puts = new List<(uint Item, uint Container, int Placement)>();
var messages = new List<string>();
using var interaction = new ItemInteractionController(
repo,
playerGuid: () => player,
sendUse: null,
sendUseWithTarget: null,
sendWield: null,
sendDrop: null,
systemMessage: messages.Add);
ToolbarController.Bind(
layout,
repo,
() => Array.Empty<ShortcutEntry>(),
iconIds: static (_, _, _, _, _) => 0u,
useItem: static _ => { },
itemInteraction: interaction,
playerGuid: () => player,
sendPutItemInContainer: (i, c, p) => puts.Add((i, c, p)));
var button = (UiButton)layout.FindElement(InventoryButtonId)!;
foreach (uint item in new[] { first, second })
{
var payload = new ItemDragPayload(item, ItemDragSource.Inventory, 0, new UiItemSlot());
button.OnEvent(new UiEvent(0u, button, UiEventType.DragEnter, Payload: payload));
button.OnEvent(new UiEvent(0u, button, UiEventType.DropReleased, Payload: payload));
}
Assert.Equal(new[] { (first, player, 0) }, puts);
Assert.True(interaction.TryGetPendingBackpackPlacement(first, out _));
Assert.True(interaction.TryGetPendingInventoryRequest(out var request));
Assert.Equal(first, request.ItemId);
Assert.Equal(new[] { ItemInteractionController.InventoryRequestBusyMessage }, messages);
}
[Fact]
public void InventoryButton_shortcutAliasDrop_staysNeutralAndDoesNotMovePhysicalItem()
{