refactor(runtime): own inventory transaction state
Move the retail one-request-at-a-time gate, shared use busy references, external-container state, item mana, shortcuts, and desired-component snapshots into one Runtime-owned graph over J3's exact ClientObjectTable. Retained UI and session routing now borrow that owner; reset and shutdown preserve the existing order while failure/reentrancy tests protect the transaction boundary. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
595d5e6b01
commit
011efbeaa7
18 changed files with 1337 additions and 406 deletions
|
|
@ -31,6 +31,7 @@ public sealed class ItemInteractionControllerTests
|
|||
public readonly List<CombatMode> CombatModeRequests = new();
|
||||
public readonly CombatState Combat = new();
|
||||
public readonly StackSplitQuantityState SplitQuantity = new();
|
||||
public readonly InventoryTransactionState? SharedTransactions;
|
||||
public uint SelectedObject;
|
||||
public bool NonCombatMode;
|
||||
public bool DragOnPlayerOpensSecureTrade = true;
|
||||
|
|
@ -38,7 +39,8 @@ public sealed class ItemInteractionControllerTests
|
|||
public long Now = 1_000;
|
||||
|
||||
public Harness(
|
||||
Action<uint, ItemUseRequestReservation>? requestUse = null)
|
||||
Action<uint, ItemUseRequestReservation>? requestUse = null,
|
||||
bool sharedTransactions = false)
|
||||
{
|
||||
Objects.AddOrUpdate(new ClientObject
|
||||
{
|
||||
|
|
@ -54,6 +56,9 @@ public sealed class ItemInteractionControllerTests
|
|||
ItemsCapacity = 24,
|
||||
});
|
||||
Objects.MoveItem(Pack, Player, 0);
|
||||
SharedTransactions = sharedTransactions
|
||||
? new InventoryTransactionState(Objects)
|
||||
: null;
|
||||
|
||||
Controller = new ItemInteractionController(
|
||||
Objects,
|
||||
|
|
@ -86,7 +91,8 @@ public sealed class ItemInteractionControllerTests
|
|||
},
|
||||
combatState: Combat,
|
||||
sendChangeCombatMode: CombatModeRequests.Add,
|
||||
requestUse: requestUse);
|
||||
requestUse: requestUse,
|
||||
transactions: SharedTransactions);
|
||||
}
|
||||
|
||||
public ItemInteractionController Controller { get; }
|
||||
|
|
@ -312,6 +318,39 @@ public sealed class ItemInteractionControllerTests
|
|||
Assert.False(h.Controller.IsAnyTargetModeActive);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BorrowedTransactionOwnerIsExactAndOutlivesController()
|
||||
{
|
||||
var h = new Harness(sharedTransactions: true);
|
||||
|
||||
h.Controller.IncrementBusyCount();
|
||||
|
||||
Assert.Equal(1, h.SharedTransactions!.BusyCount);
|
||||
h.Controller.Dispose();
|
||||
Assert.False(h.SharedTransactions.IsDisposed);
|
||||
|
||||
h.SharedTransactions.ClearBusy();
|
||||
Assert.Equal(0, h.SharedTransactions.BusyCount);
|
||||
h.SharedTransactions.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BorrowedTransactionOwnerRejectsDifferentObjectTable()
|
||||
{
|
||||
var objects = new ClientObjectTable();
|
||||
using var transactions =
|
||||
new InventoryTransactionState(new ClientObjectTable());
|
||||
|
||||
Assert.Throws<ArgumentException>(() => new ItemInteractionController(
|
||||
objects,
|
||||
playerGuid: () => Player,
|
||||
sendUse: null,
|
||||
sendUseWithTarget: null,
|
||||
sendWield: null,
|
||||
sendDrop: null,
|
||||
transactions: transactions));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppraisalResponse_releasesOneBusyReferenceAndAcceptsCurrentRefresh()
|
||||
{
|
||||
|
|
@ -2019,6 +2058,67 @@ public sealed class ItemInteractionControllerTests
|
|||
Assert.False(h.Controller.TryGetPendingInventoryRequest(out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ThrowingReservedDispatchWithdrawsBothOwnersBeforeRethrow()
|
||||
{
|
||||
var h = new Harness();
|
||||
const uint item = 0x70000A27u;
|
||||
h.Objects.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = item,
|
||||
Name = "Loot",
|
||||
Type = ItemType.Misc,
|
||||
});
|
||||
var cancelled = new List<PendingBackpackPlacement>();
|
||||
h.Controller.PendingBackpackPlacementCancelled += cancelled.Add;
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
h.Controller.TryDispatchPendingBackpackPlacement(
|
||||
item,
|
||||
Player,
|
||||
0,
|
||||
InventoryRequestKind.Pickup,
|
||||
static () => throw new InvalidOperationException("transport")));
|
||||
|
||||
Assert.False(h.Controller.TryGetPendingBackpackPlacement(item, out _));
|
||||
Assert.False(h.Controller.TryGetPendingInventoryRequest(out _));
|
||||
Assert.Single(cancelled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ThrowingPendingProjectionObserverRollsBackEveryOwner()
|
||||
{
|
||||
var h = new Harness();
|
||||
const uint item = 0x70000A28u;
|
||||
h.Objects.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = item,
|
||||
Name = "Loot",
|
||||
Type = ItemType.Misc,
|
||||
});
|
||||
bool firstObserved = false;
|
||||
bool cancelled = false;
|
||||
h.Controller.PendingBackpackPlacementRequested += _ =>
|
||||
firstObserved = true;
|
||||
h.Controller.PendingBackpackPlacementRequested += _ =>
|
||||
throw new InvalidOperationException("projection");
|
||||
h.Controller.PendingBackpackPlacementCancelled += _ =>
|
||||
cancelled = true;
|
||||
|
||||
Assert.Throws<AggregateException>(() =>
|
||||
h.Controller.TryDispatchPendingBackpackPlacement(
|
||||
item,
|
||||
Player,
|
||||
0,
|
||||
InventoryRequestKind.Pickup,
|
||||
static () => true));
|
||||
|
||||
Assert.True(firstObserved);
|
||||
Assert.True(cancelled);
|
||||
Assert.False(h.Controller.TryGetPendingBackpackPlacement(item, out _));
|
||||
Assert.False(h.Controller.TryGetPendingInventoryRequest(out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResetSession_ClearsTargetBusyThrottleAndConsumedClickState()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue