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:
parent
d2bb5af453
commit
5acc3f01cf
23 changed files with 2635 additions and 168 deletions
|
|
@ -1424,14 +1424,445 @@ public sealed class ItemInteractionControllerTests
|
|||
{
|
||||
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));
|
||||
var pending = new List<PendingBackpackPlacement>();
|
||||
h.Controller.PendingBackpackPlacementRequested += pending.Add;
|
||||
|
||||
Assert.True(h.Controller.PlaceWorldItemInBackpack(item));
|
||||
|
||||
Assert.Equal(new[] { (item, Player, 0) }, pending);
|
||||
Assert.Collection(
|
||||
pending,
|
||||
placement =>
|
||||
{
|
||||
Assert.NotEqual(0u, placement.Token);
|
||||
Assert.Equal(item, placement.ItemId);
|
||||
Assert.Equal(Player, placement.ContainerId);
|
||||
Assert.Equal(0, placement.Placement);
|
||||
});
|
||||
Assert.Equal(new[] { (item, Player, 0) }, h.BackpackPlacements);
|
||||
Assert.True(h.Controller.TryGetPendingInventoryRequest(out var request));
|
||||
Assert.Equal(InventoryRequestKind.Pickup, request.Kind);
|
||||
Assert.False(request.Dispatched);
|
||||
|
||||
Assert.True(h.Controller.TryDispatchInventoryRequest(
|
||||
InventoryRequestKind.Pickup,
|
||||
item,
|
||||
static () => true,
|
||||
pending[0].Token));
|
||||
Assert.True(h.Controller.TryGetPendingInventoryRequest(out request));
|
||||
Assert.True(request.Dispatched);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PendingRequestSerializesEveryLaterItemWithoutSecondWire()
|
||||
{
|
||||
var h = new Harness();
|
||||
const uint item = 0x70000A0Du;
|
||||
const uint laterItem = 0x70000A0Eu;
|
||||
h.Objects.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = item,
|
||||
Name = "Loot",
|
||||
Type = ItemType.Misc,
|
||||
});
|
||||
var requested = new List<PendingBackpackPlacement>();
|
||||
var cancelled = new List<PendingBackpackPlacement>();
|
||||
h.Controller.PendingBackpackPlacementRequested += requested.Add;
|
||||
h.Controller.PendingBackpackPlacementCancelled += cancelled.Add;
|
||||
|
||||
Assert.True(h.Controller.PlaceWorldItemInBackpack(item));
|
||||
PendingBackpackPlacement first = Assert.Single(requested);
|
||||
Assert.True(h.Controller.PlaceWorldItemInBackpack(item));
|
||||
Assert.True(h.Controller.PlaceWorldItemInBackpack(laterItem));
|
||||
|
||||
Assert.Single(requested);
|
||||
Assert.Empty(cancelled);
|
||||
Assert.Equal(new[] { (item, Player, 0) }, h.BackpackPlacements);
|
||||
Assert.Equal(
|
||||
new[]
|
||||
{
|
||||
"Already attempting to place Loot here",
|
||||
"Already attempting to place Loot here",
|
||||
},
|
||||
h.SystemMessages);
|
||||
Assert.True(h.Controller.TryGetPendingBackpackPlacement(item, out var current));
|
||||
Assert.Equal(first, current);
|
||||
|
||||
h.Objects.RejectMove(item, weenieError: 0x29u);
|
||||
Assert.False(h.Controller.TryGetPendingBackpackPlacement(item, out _));
|
||||
Assert.Empty(cancelled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BusyInventoryTransactionPreventsPickupReservationAndWireRequest()
|
||||
{
|
||||
var h = new Harness();
|
||||
const uint item = 0x70000A0Fu;
|
||||
h.Controller.IncrementBusyCount();
|
||||
|
||||
Assert.True(h.Controller.PlaceWorldItemInBackpack(item));
|
||||
|
||||
Assert.Empty(h.BackpackPlacements);
|
||||
Assert.False(h.Controller.TryGetPendingBackpackPlacement(item, out _));
|
||||
Assert.Equal(
|
||||
new[] { ItemInteractionController.InventoryRequestBusyMessage },
|
||||
h.SystemMessages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PendingPickupPreventsContainedUseRequest()
|
||||
{
|
||||
var h = new Harness();
|
||||
const uint pickup = 0x70000A10u;
|
||||
const uint contained = 0x50000A11u;
|
||||
h.AddContained(contained, item => item.Useability = ItemUseability.Contained);
|
||||
|
||||
Assert.True(h.Controller.PlaceWorldItemInBackpack(pickup));
|
||||
h.Now += 200;
|
||||
h.Controller.ActivateItem(contained);
|
||||
|
||||
Assert.Empty(h.Uses);
|
||||
Assert.True(h.Controller.TryGetPendingBackpackPlacement(pickup, out _));
|
||||
Assert.Equal(
|
||||
new[] { ItemInteractionController.InventoryRequestBusyMessage },
|
||||
h.SystemMessages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PendingPickupPreventsPaperdollWieldRequest()
|
||||
{
|
||||
var h = new Harness();
|
||||
const uint pickup = 0x70000A12u;
|
||||
const uint helm = 0x50000A13u;
|
||||
h.AddContained(helm, item => item.ValidLocations = EquipMask.HeadWear);
|
||||
|
||||
Assert.True(h.Controller.PlaceWorldItemInBackpack(pickup));
|
||||
Assert.False(h.Controller.WieldFromPaperdoll(helm, EquipMask.HeadWear));
|
||||
|
||||
Assert.Empty(h.Wields);
|
||||
Assert.Equal(EquipMask.None, h.Objects.Get(helm)!.CurrentlyEquippedLocation);
|
||||
Assert.Equal(
|
||||
new[] { ItemInteractionController.InventoryRequestBusyMessage },
|
||||
h.SystemMessages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PendingPickupPreventsConfirmedUseRequest()
|
||||
{
|
||||
var h = new Harness();
|
||||
const uint pickup = 0x70000A14u;
|
||||
const uint item = 0x50000A15u;
|
||||
h.AddContained(item, contained => contained.Useability = ItemUseability.Contained);
|
||||
|
||||
Assert.True(h.Controller.PlaceWorldItemInBackpack(pickup));
|
||||
Assert.False(h.Controller.ExecuteConfirmedUse(item));
|
||||
|
||||
Assert.Empty(h.Uses);
|
||||
Assert.Equal(0, h.Controller.BusyCount);
|
||||
Assert.Equal(
|
||||
new[] { ItemInteractionController.InventoryRequestBusyMessage },
|
||||
h.SystemMessages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GlobalInventoryRequestRejectsPickupUntilMatchingMoveResponse()
|
||||
{
|
||||
var h = new Harness();
|
||||
const uint moving = 0x50000A16u;
|
||||
const uint unrelated = 0x50000A17u;
|
||||
const uint pickup = 0x70000A18u;
|
||||
h.AddContained(moving);
|
||||
h.AddContained(unrelated);
|
||||
|
||||
Assert.True(h.Controller.TryDispatchInventoryRequest(
|
||||
InventoryRequestKind.PutInContainer,
|
||||
moving,
|
||||
static () => true));
|
||||
Assert.True(h.Controller.PlaceWorldItemInBackpack(pickup));
|
||||
|
||||
Assert.Empty(h.BackpackPlacements);
|
||||
Assert.True(h.Controller.TryGetPendingInventoryRequest(out var pending));
|
||||
Assert.Equal(moving, pending.ItemId);
|
||||
Assert.Equal(
|
||||
new[] { ItemInteractionController.InventoryRequestBusyMessage },
|
||||
h.SystemMessages);
|
||||
|
||||
h.Objects.ApplyConfirmedServerMove(unrelated, Pack, 0u, 0);
|
||||
Assert.True(h.Controller.TryGetPendingInventoryRequest(out _));
|
||||
|
||||
h.Objects.ApplyConfirmedServerMove(moving, Pack, 0u, 0);
|
||||
Assert.False(h.Controller.TryGetPendingInventoryRequest(out _));
|
||||
Assert.True(h.Controller.PlaceWorldItemInBackpack(pickup));
|
||||
Assert.Equal(new[] { (pickup, Player, 0) }, h.BackpackPlacements);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SplitRequestReleasesOnlyOnSourceStackResponse()
|
||||
{
|
||||
var h = new Harness();
|
||||
const uint source = 0x50000A19u;
|
||||
const uint unrelated = 0x50000A1Au;
|
||||
h.AddContained(source, item => item.StackSize = 10);
|
||||
h.AddContained(unrelated, item => item.StackSize = 10);
|
||||
|
||||
Assert.True(h.Controller.TryDispatchInventoryRequest(
|
||||
InventoryRequestKind.SplitToContainer,
|
||||
source,
|
||||
static () => true));
|
||||
h.Objects.UpdateStackSize(unrelated, 9, 0);
|
||||
Assert.True(h.Controller.TryGetPendingInventoryRequest(out _));
|
||||
|
||||
h.Objects.UpdateStackSize(source, 9, 0);
|
||||
Assert.False(h.Controller.TryGetPendingInventoryRequest(out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MatchingInventoryFailureReleasesGlobalRequest()
|
||||
{
|
||||
var h = new Harness();
|
||||
const uint source = 0x50000A1Bu;
|
||||
h.AddContained(source);
|
||||
|
||||
Assert.True(h.Controller.TryDispatchInventoryRequest(
|
||||
InventoryRequestKind.PutInContainer,
|
||||
source,
|
||||
static () => true));
|
||||
|
||||
h.Objects.RejectMove(source, weenieError: 0x29u);
|
||||
|
||||
Assert.False(h.Controller.TryGetPendingInventoryRequest(out _));
|
||||
Assert.True(h.Controller.CanMakeInventoryRequest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProvisionalInventoryOwnerRejectsReentrantSecondDispatch()
|
||||
{
|
||||
var h = new Harness();
|
||||
const uint first = 0x50000A1Cu;
|
||||
const uint second = 0x50000A1Du;
|
||||
h.AddContained(first);
|
||||
h.AddContained(second);
|
||||
bool secondDispatched = true;
|
||||
|
||||
Assert.True(h.Controller.TryDispatchInventoryRequest(
|
||||
InventoryRequestKind.PutInContainer,
|
||||
first,
|
||||
() =>
|
||||
{
|
||||
secondDispatched = h.Controller.TryDispatchInventoryRequest(
|
||||
InventoryRequestKind.PutInContainer,
|
||||
second,
|
||||
static () => true);
|
||||
return true;
|
||||
}));
|
||||
|
||||
Assert.False(secondDispatched);
|
||||
Assert.True(h.Controller.TryGetPendingInventoryRequest(out var pending));
|
||||
Assert.Equal(first, pending.ItemId);
|
||||
Assert.True(pending.Dispatched);
|
||||
Assert.Equal(
|
||||
new[] { ItemInteractionController.InventoryRequestBusyMessage },
|
||||
h.SystemMessages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SynchronousMatchingResponseDoesNotResurrectProvisionalOwner()
|
||||
{
|
||||
var h = new Harness();
|
||||
const uint item = 0x50000A1Eu;
|
||||
h.AddContained(item);
|
||||
|
||||
Assert.True(h.Controller.TryDispatchInventoryRequest(
|
||||
InventoryRequestKind.PutInContainer,
|
||||
item,
|
||||
() => h.Objects.ApplyConfirmedServerMove(item, Player, 0u, 0)));
|
||||
|
||||
Assert.False(h.Controller.TryGetPendingInventoryRequest(out _));
|
||||
Assert.True(h.Controller.CanMakeInventoryRequest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OptimisticMoveKeepsGlobalOwnerUntilAuthoritativeResponse()
|
||||
{
|
||||
var h = new Harness();
|
||||
const uint item = 0x50000A20u;
|
||||
const uint second = 0x50000A21u;
|
||||
h.AddContained(item);
|
||||
h.AddContained(second);
|
||||
|
||||
Assert.True(h.Controller.TryDispatchInventoryRequest(
|
||||
InventoryRequestKind.PutInContainer,
|
||||
item,
|
||||
() => h.Objects.MoveItemOptimistic(item, Player, 0)));
|
||||
|
||||
Assert.True(h.Controller.TryGetPendingInventoryRequest(out var pending));
|
||||
Assert.Equal(item, pending.ItemId);
|
||||
Assert.True(pending.Dispatched);
|
||||
Assert.False(h.Controller.TryDispatchInventoryRequest(
|
||||
InventoryRequestKind.PutInContainer,
|
||||
second,
|
||||
static () => true));
|
||||
|
||||
Assert.True(h.Objects.ApplyConfirmedServerMove(item, Player, 0u, 0));
|
||||
Assert.False(h.Controller.TryGetPendingInventoryRequest(out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AuthoritativeResponseReleasesGlobalOwnerBeforePublishingPlacementResolution()
|
||||
{
|
||||
var h = new Harness();
|
||||
const uint first = 0x70000A22u;
|
||||
const uint second = 0x50000A23u;
|
||||
h.Objects.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = first,
|
||||
Name = "Loot",
|
||||
Type = ItemType.Misc,
|
||||
});
|
||||
h.AddContained(second);
|
||||
bool secondDispatched = false;
|
||||
h.Controller.PendingBackpackPlacementResolved += _ =>
|
||||
secondDispatched = h.Controller.TryDispatchInventoryRequest(
|
||||
InventoryRequestKind.PutInContainer,
|
||||
second,
|
||||
static () => true);
|
||||
|
||||
Assert.True(h.Controller.TryDispatchPendingBackpackPlacement(
|
||||
first,
|
||||
Player,
|
||||
0,
|
||||
InventoryRequestKind.Pickup,
|
||||
static () => true));
|
||||
|
||||
Assert.True(h.Objects.ApplyConfirmedServerMove(first, Player, 0u, 0));
|
||||
|
||||
Assert.True(secondDispatched);
|
||||
Assert.True(h.Controller.TryGetPendingInventoryRequest(out var pending));
|
||||
Assert.Equal(second, pending.ItemId);
|
||||
Assert.True(pending.Dispatched);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FailedMoveReleasesOldOwnerOnceAndPreservesReentrantSameItemRequest()
|
||||
{
|
||||
var h = new Harness();
|
||||
const uint item = 0x50000A24u;
|
||||
h.AddContained(item);
|
||||
bool replacementDispatched = false;
|
||||
h.Controller.PendingBackpackPlacementResolved += _ =>
|
||||
replacementDispatched = h.Controller.TryDispatchInventoryRequest(
|
||||
InventoryRequestKind.PutInContainer,
|
||||
item,
|
||||
static () => true);
|
||||
|
||||
Assert.True(h.Controller.TryDispatchPendingBackpackPlacement(
|
||||
item,
|
||||
Player,
|
||||
0,
|
||||
InventoryRequestKind.PutInContainer,
|
||||
() => h.Objects.MoveItemOptimistic(item, Player, 0)));
|
||||
|
||||
Assert.True(h.Objects.RejectMove(item, weenieError: 0x29u));
|
||||
|
||||
Assert.True(replacementDispatched);
|
||||
Assert.True(h.Controller.TryGetPendingInventoryRequest(out var pending));
|
||||
Assert.Equal(item, pending.ItemId);
|
||||
Assert.True(pending.Dispatched);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResponseClearsGlobalAndLocalStateBeforeReadinessNotification()
|
||||
{
|
||||
var h = new Harness();
|
||||
const uint first = 0x70000A25u;
|
||||
const uint second = 0x70000A26u;
|
||||
h.Objects.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = first,
|
||||
Name = "First loot",
|
||||
Type = ItemType.Misc,
|
||||
});
|
||||
h.Objects.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = second,
|
||||
Name = "Second loot",
|
||||
Type = ItemType.Misc,
|
||||
});
|
||||
Assert.True(h.Controller.TryDispatchPendingBackpackPlacement(
|
||||
first,
|
||||
Player,
|
||||
0,
|
||||
InventoryRequestKind.Pickup,
|
||||
static () => true));
|
||||
|
||||
var eventOrder = new List<string>();
|
||||
h.Controller.PendingBackpackPlacementResolved += placement =>
|
||||
{
|
||||
if (placement.ItemId == first)
|
||||
eventOrder.Add("resolved-first");
|
||||
};
|
||||
h.Controller.PendingBackpackPlacementRequested += placement =>
|
||||
{
|
||||
if (placement.ItemId == second)
|
||||
eventOrder.Add("requested-second");
|
||||
};
|
||||
bool attempted = false;
|
||||
bool secondDispatched = false;
|
||||
h.Controller.StateChanged += () =>
|
||||
{
|
||||
if (attempted || !h.Controller.CanMakeInventoryRequest)
|
||||
return;
|
||||
attempted = true;
|
||||
eventOrder.Add("ready");
|
||||
secondDispatched = h.Controller.TryDispatchPendingBackpackPlacement(
|
||||
second,
|
||||
Player,
|
||||
0,
|
||||
InventoryRequestKind.Pickup,
|
||||
static () => true);
|
||||
};
|
||||
|
||||
Assert.True(h.Objects.ApplyConfirmedServerMove(first, Player, 0u, 0));
|
||||
|
||||
Assert.True(attempted);
|
||||
Assert.True(secondDispatched);
|
||||
Assert.True(h.Controller.TryGetPendingBackpackPlacement(second, out _));
|
||||
Assert.True(h.Controller.TryGetPendingInventoryRequest(out var pending));
|
||||
Assert.Equal(second, pending.ItemId);
|
||||
Assert.True(pending.Dispatched);
|
||||
Assert.Equal(
|
||||
new[] { "resolved-first", "ready", "requested-second" },
|
||||
eventOrder);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReentrantPendingPublicationCancellationPreventsWireDispatch()
|
||||
{
|
||||
var h = new Harness();
|
||||
const uint item = 0x70000A1Fu;
|
||||
h.Objects.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = item,
|
||||
Name = "Loot",
|
||||
Type = ItemType.Misc,
|
||||
});
|
||||
bool wireDispatched = false;
|
||||
h.Controller.PendingBackpackPlacementRequested += pending =>
|
||||
h.Controller.CancelPendingBackpackPlacement(pending.ItemId, pending.Token);
|
||||
|
||||
Assert.False(h.Controller.TryDispatchPendingBackpackPlacement(
|
||||
item,
|
||||
Player,
|
||||
0,
|
||||
InventoryRequestKind.Pickup,
|
||||
() =>
|
||||
{
|
||||
wireDispatched = true;
|
||||
return true;
|
||||
}));
|
||||
|
||||
Assert.False(wireDispatched);
|
||||
Assert.False(h.Controller.TryGetPendingBackpackPlacement(item, out _));
|
||||
Assert.False(h.Controller.TryGetPendingInventoryRequest(out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue