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:
Erik 2026-07-26 08:20:32 +02:00
parent 595d5e6b01
commit 011efbeaa7
18 changed files with 1337 additions and 406 deletions

View file

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