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
|
|
@ -189,13 +189,11 @@ public sealed class InteractionRetainedUiCompositionTests
|
|||
Combat: null!,
|
||||
CombatAttackOperations: null!,
|
||||
Selection: null!,
|
||||
ExternalContainers: null!,
|
||||
Objects: null!,
|
||||
Inventory: null!,
|
||||
MagicCatalog: null!,
|
||||
Spellbook: null!,
|
||||
Communication: null!,
|
||||
LocalPlayer: null!,
|
||||
ItemMana: null!,
|
||||
StackSplitQuantity: null!,
|
||||
UiRegistry: null,
|
||||
CombatModeCommands: null!,
|
||||
|
|
@ -203,7 +201,6 @@ public sealed class InteractionRetainedUiCompositionTests
|
|||
PlayerController: null!,
|
||||
PlayerMode: null!,
|
||||
CharacterOptions: null!,
|
||||
Shortcuts: null!,
|
||||
SelectionCameraFactory: static _ => Stub<SelectionCameraSource>(),
|
||||
FrameDiagnostics: new DeferredRenderFrameDiagnosticsSource(),
|
||||
ExistingVitals: null,
|
||||
|
|
|
|||
|
|
@ -16,23 +16,6 @@ namespace AcDream.App.Tests.Composition;
|
|||
|
||||
public sealed class InteractionUiRuntimeSourcesTests
|
||||
{
|
||||
[Fact]
|
||||
public void DesiredComponentSnapshotNormalizesNullAndClearsExactly()
|
||||
{
|
||||
var state = new DesiredComponentSnapshotState();
|
||||
IReadOnlyList<(uint Id, uint Amount)> items =
|
||||
[(0x01020304u, 7u)];
|
||||
|
||||
Assert.Empty(state.Items);
|
||||
state.Items = items;
|
||||
Assert.Same(items, state.Items);
|
||||
|
||||
state.Clear();
|
||||
Assert.Empty(state.Items);
|
||||
state.Items = null!;
|
||||
Assert.Empty(state.Items);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SessionAuthorityDefaultsBindUnbindRebindAndDeactivateExactly()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -281,6 +281,7 @@ public sealed class LiveSessionResetPlanTests
|
|||
InteractionAndSelection = Stage(
|
||||
"interaction and selection",
|
||||
() => selection.Reset()),
|
||||
InventoryTransactions = Stage("inventory transactions"),
|
||||
SelectionPresentation = Stage("selection presentation"),
|
||||
ObjectTable = Stage("object table", objects.Clear),
|
||||
Spellbook = Stage("spellbook", spells.Clear),
|
||||
|
|
@ -364,6 +365,7 @@ public sealed class LiveSessionResetPlanTests
|
|||
"equipped children",
|
||||
"external container",
|
||||
"interaction and selection",
|
||||
"inventory transactions",
|
||||
"selection presentation",
|
||||
"object table",
|
||||
"spellbook",
|
||||
|
|
|
|||
|
|
@ -511,6 +511,7 @@ public sealed class CurrentGameRuntimeAdapterTests
|
|||
EquippedChildren = noop,
|
||||
ExternalContainer = noop,
|
||||
InteractionAndSelection = noop,
|
||||
InventoryTransactions = noop,
|
||||
SelectionPresentation = noop,
|
||||
ObjectTable = noop,
|
||||
Spellbook = noop,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,97 @@
|
|||
namespace AcDream.App.Tests.Runtime;
|
||||
|
||||
public sealed class RuntimeInventoryOwnershipTests
|
||||
{
|
||||
[Fact]
|
||||
public void ProductionConstructsOnlyTheRuntimeInventoryOwner()
|
||||
{
|
||||
string source = ReadSource("Rendering", "GameWindow.cs");
|
||||
|
||||
Assert.Contains(
|
||||
"_runtimeInventory = new RuntimeInventoryState(_runtimeEntityObjects);",
|
||||
source,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(
|
||||
"new AcDream.Core.Items.ItemManaState",
|
||||
source,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(
|
||||
"new DesiredComponentSnapshotState",
|
||||
source,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(
|
||||
"new ShortcutSnapshotState",
|
||||
source,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(
|
||||
"new AcDream.Core.Items.ExternalContainerState",
|
||||
source,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UiSessionAndShutdownBorrowTheExactRuntimeOwner()
|
||||
{
|
||||
string ui = ReadSource(
|
||||
"Composition",
|
||||
"InteractionRetainedUiComposition.cs");
|
||||
string session = ReadSource("Net", "LiveSessionRuntimeFactory.cs");
|
||||
string shutdown = ReadSource("Rendering", "GameWindowLifetime.cs");
|
||||
|
||||
Assert.Contains(
|
||||
"transactions: d.Inventory.Transactions",
|
||||
ui,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"OnShortcuts: _domain.Inventory.Shortcuts.Replace",
|
||||
session,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"_domain.Inventory.Transactions.CompleteUse(error)",
|
||||
session,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"OnDesiredComponents: _domain.Inventory.DesiredComponents.Replace",
|
||||
session,
|
||||
StringComparison.Ordinal);
|
||||
AssertAppearsInOrder(
|
||||
shutdown,
|
||||
"\"runtime inventory state\"",
|
||||
"\"runtime entity/object lifetime\"");
|
||||
}
|
||||
|
||||
private static string ReadSource(params string[] relative)
|
||||
{
|
||||
string root = FindRepositoryRoot();
|
||||
return File.ReadAllText(Path.Combine(
|
||||
[root, "src", "AcDream.App", .. relative]));
|
||||
}
|
||||
|
||||
private static string FindRepositoryRoot()
|
||||
{
|
||||
var current = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (current is not null)
|
||||
{
|
||||
if (File.Exists(Path.Combine(current.FullName, "AcDream.slnx")))
|
||||
return current.FullName;
|
||||
current = current.Parent;
|
||||
}
|
||||
throw new DirectoryNotFoundException("AcDream.slnx was not found.");
|
||||
}
|
||||
|
||||
private static void AssertAppearsInOrder(
|
||||
string source,
|
||||
params string[] fragments)
|
||||
{
|
||||
int cursor = 0;
|
||||
foreach (string fragment in fragments)
|
||||
{
|
||||
int index = source.IndexOf(
|
||||
fragment,
|
||||
cursor,
|
||||
StringComparison.Ordinal);
|
||||
Assert.True(index >= 0, $"Missing source fragment: {fragment}");
|
||||
cursor = index + fragment.Length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
262
tests/AcDream.Core.Tests/Items/InventoryTransactionStateTests.cs
Normal file
262
tests/AcDream.Core.Tests/Items/InventoryTransactionStateTests.cs
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
using AcDream.Core.Items;
|
||||
|
||||
namespace AcDream.Core.Tests.Items;
|
||||
|
||||
public sealed class InventoryTransactionStateTests
|
||||
{
|
||||
private const uint Player = 0x50000001u;
|
||||
private const uint Pack = 0x50000002u;
|
||||
private const uint First = 0x60000001u;
|
||||
private const uint Second = 0x60000002u;
|
||||
|
||||
[Fact]
|
||||
public void ReserveClosesReentrantDispatchWindowAndSuccessfulSendCommits()
|
||||
{
|
||||
var objects = CreateTable();
|
||||
using var state = new InventoryTransactionState(objects);
|
||||
bool secondDispatched = true;
|
||||
|
||||
Assert.True(state.TryDispatch(
|
||||
InventoryRequestKind.PutInContainer,
|
||||
First,
|
||||
() =>
|
||||
{
|
||||
secondDispatched = state.TryDispatch(
|
||||
InventoryRequestKind.PutInContainer,
|
||||
Second,
|
||||
static () => true);
|
||||
return true;
|
||||
}));
|
||||
|
||||
Assert.False(secondDispatched);
|
||||
Assert.True(state.TryGetPending(out PendingInventoryRequest pending));
|
||||
Assert.Equal(First, pending.ItemId);
|
||||
Assert.True(pending.Dispatched);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DispatchFailureOrExceptionReleasesExactProvisionalOwner()
|
||||
{
|
||||
var objects = CreateTable();
|
||||
using var state = new InventoryTransactionState(objects);
|
||||
|
||||
Assert.False(state.TryDispatch(
|
||||
InventoryRequestKind.PutInContainer,
|
||||
First,
|
||||
static () => false));
|
||||
Assert.False(state.HasPendingRequest);
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => state.TryDispatch(
|
||||
InventoryRequestKind.PutInContainer,
|
||||
First,
|
||||
static () => throw new InvalidOperationException("transport")));
|
||||
Assert.False(state.HasPendingRequest);
|
||||
Assert.True(state.CanBeginRequest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FailedPrePublicationCallbackRollsBackOnlyItsReservation()
|
||||
{
|
||||
var objects = CreateTable();
|
||||
using var state = new InventoryTransactionState(objects);
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
state.TryReserve(
|
||||
InventoryRequestKind.Pickup,
|
||||
First,
|
||||
out _,
|
||||
static _ => throw new InvalidOperationException("projection")));
|
||||
|
||||
Assert.False(state.HasPendingRequest);
|
||||
Assert.True(state.CanBeginRequest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AuthoritativeResponseClearsBeforeReentrantCompletionObserver()
|
||||
{
|
||||
var objects = CreateTable();
|
||||
using var state = new InventoryTransactionState(objects);
|
||||
bool replacementDispatched = false;
|
||||
state.RequestCompleted += request =>
|
||||
{
|
||||
Assert.Equal(First, request.ItemId);
|
||||
Assert.False(state.HasPendingRequest);
|
||||
replacementDispatched = state.TryDispatch(
|
||||
InventoryRequestKind.PutInContainer,
|
||||
Second,
|
||||
static () => true);
|
||||
};
|
||||
|
||||
Assert.True(state.TryDispatch(
|
||||
InventoryRequestKind.PutInContainer,
|
||||
First,
|
||||
static () => true));
|
||||
Assert.True(objects.ApplyConfirmedServerMove(First, Player, 0u, 0));
|
||||
|
||||
Assert.True(replacementDispatched);
|
||||
Assert.True(state.TryGetPending(out PendingInventoryRequest pending));
|
||||
Assert.Equal(Second, pending.ItemId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OptimisticMoveAndUnrelatedResponsesDoNotCompleteRequest()
|
||||
{
|
||||
var objects = CreateTable();
|
||||
using var state = new InventoryTransactionState(objects);
|
||||
|
||||
Assert.True(state.TryDispatch(
|
||||
InventoryRequestKind.PutInContainer,
|
||||
First,
|
||||
() => objects.MoveItemOptimistic(First, Player, 0)));
|
||||
Assert.True(objects.ApplyConfirmedServerMove(Second, Player, 1u, 1));
|
||||
Assert.True(state.HasPendingRequest);
|
||||
|
||||
Assert.True(objects.ApplyConfirmedServerMove(First, Player, 0u, 0));
|
||||
Assert.False(state.HasPendingRequest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReusedGuidResponseCannotCompletePriorObjectIdentity()
|
||||
{
|
||||
var objects = CreateTable();
|
||||
using var state = new InventoryTransactionState(objects);
|
||||
ClientObject original = objects.Get(First)!;
|
||||
|
||||
Assert.True(state.TryDispatch(
|
||||
InventoryRequestKind.PutInContainer,
|
||||
First,
|
||||
static () => true));
|
||||
|
||||
var replacement = new ClientObject
|
||||
{
|
||||
ObjectId = First,
|
||||
Name = "replacement",
|
||||
Type = ItemType.Misc,
|
||||
};
|
||||
objects.AddOrUpdate(replacement);
|
||||
objects.MoveItem(First, Pack, 0);
|
||||
Assert.NotSame(original, replacement);
|
||||
|
||||
Assert.True(objects.ApplyConfirmedServerMove(First, Player, 0u, 0));
|
||||
Assert.True(state.HasPendingRequest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UseReservationTransfersToUseDoneOrCancelsBeforeDispatch()
|
||||
{
|
||||
var objects = CreateTable();
|
||||
using var state = new InventoryTransactionState(objects);
|
||||
|
||||
ItemUseRequestReservation cancelled = state.BeginUseRequestReservation();
|
||||
Assert.Equal(1, state.BusyCount);
|
||||
cancelled.CancelBeforeDispatch();
|
||||
cancelled.CancelBeforeDispatch();
|
||||
Assert.Equal(0, state.BusyCount);
|
||||
|
||||
ItemUseRequestReservation dispatched = state.BeginUseRequestReservation();
|
||||
dispatched.MarkDispatched();
|
||||
dispatched.CancelBeforeDispatch();
|
||||
Assert.Equal(1, state.BusyCount);
|
||||
state.CompleteUse(0u);
|
||||
Assert.Equal(0, state.BusyCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResetInvalidatesLateUseReservationAndClearsPendingRequest()
|
||||
{
|
||||
var objects = CreateTable();
|
||||
using var state = new InventoryTransactionState(objects);
|
||||
Assert.True(state.TryReserve(
|
||||
InventoryRequestKind.Pickup,
|
||||
First,
|
||||
out _));
|
||||
ItemUseRequestReservation stale = state.BeginUseRequestReservation();
|
||||
|
||||
state.ResetSession();
|
||||
stale.CancelBeforeDispatch();
|
||||
|
||||
Assert.Equal(0, state.BusyCount);
|
||||
Assert.False(state.HasPendingRequest);
|
||||
Assert.True(state.CanBeginRequest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ObserverFailureIsIsolatedAndRecorded()
|
||||
{
|
||||
var objects = CreateTable();
|
||||
using var state = new InventoryTransactionState(objects);
|
||||
int delivered = 0;
|
||||
state.StateChanged += () =>
|
||||
throw new InvalidOperationException("observer");
|
||||
state.StateChanged += () => delivered++;
|
||||
|
||||
state.IncrementBusyCount();
|
||||
|
||||
Assert.Equal(1, delivered);
|
||||
Assert.Equal(1, state.DispatchFailureCount);
|
||||
Assert.IsType<InvalidOperationException>(state.LastDispatchFailure);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ObjectTableClearReleasesRequestAndNotifiesProjectionOwner()
|
||||
{
|
||||
var objects = CreateTable();
|
||||
using var state = new InventoryTransactionState(objects);
|
||||
int cleared = 0;
|
||||
state.ObjectTableCleared += () => cleared++;
|
||||
Assert.True(state.TryDispatch(
|
||||
InventoryRequestKind.PutInContainer,
|
||||
First,
|
||||
static () => true));
|
||||
|
||||
objects.Clear();
|
||||
|
||||
Assert.Equal(1, cleared);
|
||||
Assert.False(state.HasPendingRequest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DisposeDetachesFromBorrowedObjectTable()
|
||||
{
|
||||
var objects = CreateTable();
|
||||
var state = new InventoryTransactionState(objects);
|
||||
int completed = 0;
|
||||
state.RequestCompleted += _ => completed++;
|
||||
Assert.True(state.TryDispatch(
|
||||
InventoryRequestKind.PutInContainer,
|
||||
First,
|
||||
static () => true));
|
||||
|
||||
state.Dispose();
|
||||
state.Dispose();
|
||||
objects.ApplyConfirmedServerMove(First, Player, 0u, 0);
|
||||
|
||||
Assert.True(state.IsDisposed);
|
||||
Assert.Equal(0, completed);
|
||||
Assert.Throws<ObjectDisposedException>(() =>
|
||||
state.TryReserve(
|
||||
InventoryRequestKind.Pickup,
|
||||
First,
|
||||
out _));
|
||||
}
|
||||
|
||||
private static ClientObjectTable CreateTable()
|
||||
{
|
||||
var objects = new ClientObjectTable();
|
||||
objects.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = First,
|
||||
Name = "first",
|
||||
Type = ItemType.Misc,
|
||||
});
|
||||
objects.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = Second,
|
||||
Name = "second",
|
||||
Type = ItemType.Misc,
|
||||
});
|
||||
objects.MoveItem(First, Pack, 0);
|
||||
objects.MoveItem(Second, Pack, 1);
|
||||
return objects;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
using AcDream.Core.Items;
|
||||
using AcDream.Runtime.Entities;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
|
||||
namespace AcDream.Runtime.Tests.Gameplay;
|
||||
|
||||
public sealed class RuntimeInventoryStateTests
|
||||
{
|
||||
[Fact]
|
||||
public void OwnsOneGameplayGraphOverExactEntityObjectTable()
|
||||
{
|
||||
using var entities = new RuntimeEntityObjectLifetime();
|
||||
using var inventory = new RuntimeInventoryState(entities);
|
||||
IReadOnlyList<ShortcutEntry> shortcuts =
|
||||
[new ShortcutEntry(2, 0x50000001u, 0u)];
|
||||
IReadOnlyList<(uint Id, uint Amount)> components =
|
||||
[(0x68000001u, 12u)];
|
||||
|
||||
inventory.ExternalContainers.RequestOpen(0x70000001u);
|
||||
inventory.ExternalContainers.ApplyViewContents(0x70000001u);
|
||||
inventory.ItemMana.OnQueryItemManaResponse(0x50000001u, 0.75f, true);
|
||||
inventory.Shortcuts.Replace(shortcuts);
|
||||
inventory.DesiredComponents.Replace(components);
|
||||
|
||||
Assert.Same(entities.Objects, inventory.Objects);
|
||||
Assert.Equal(0x70000001u, inventory.ExternalContainers.CurrentContainerId);
|
||||
Assert.Equal(0.75f, inventory.ItemMana.GetManaPercent(0x50000001u));
|
||||
Assert.Same(shortcuts, inventory.Shortcuts.Items);
|
||||
Assert.Same(components, inventory.DesiredComponents.Items);
|
||||
Assert.Equal(1, inventory.Shortcuts.Revision);
|
||||
Assert.Equal(1, inventory.DesiredComponents.Revision);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResetOperationsClearOnlyTheirOwnedLifetimeGroup()
|
||||
{
|
||||
using var entities = new RuntimeEntityObjectLifetime();
|
||||
using var inventory = new RuntimeInventoryState(entities);
|
||||
inventory.ExternalContainers.RequestOpen(0x70000001u);
|
||||
inventory.ExternalContainers.ApplyViewContents(0x70000001u);
|
||||
inventory.ItemMana.OnQueryItemManaResponse(0x50000001u, 0.75f, true);
|
||||
inventory.Shortcuts.Replace([new ShortcutEntry(1, 2u, 3u)]);
|
||||
inventory.DesiredComponents.Replace([(4u, 5u)]);
|
||||
inventory.Transactions.IncrementBusyCount();
|
||||
|
||||
inventory.ResetExternalContainer();
|
||||
Assert.Equal(0u, inventory.ExternalContainers.CurrentContainerId);
|
||||
Assert.True(inventory.ItemMana.HasMana(0x50000001u));
|
||||
Assert.NotEmpty(inventory.Shortcuts.Items);
|
||||
Assert.Equal(1, inventory.Transactions.BusyCount);
|
||||
|
||||
inventory.ResetTransactions();
|
||||
inventory.ResetItemMana();
|
||||
inventory.ResetPlayerSnapshots();
|
||||
|
||||
Assert.Equal(0, inventory.Transactions.BusyCount);
|
||||
Assert.False(inventory.ItemMana.HasMana(0x50000001u));
|
||||
Assert.Empty(inventory.Shortcuts.Items);
|
||||
Assert.Empty(inventory.DesiredComponents.Items);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IndependentRuntimeInstancesNeverShareState()
|
||||
{
|
||||
using var firstEntities = new RuntimeEntityObjectLifetime();
|
||||
using var secondEntities = new RuntimeEntityObjectLifetime();
|
||||
using var first = new RuntimeInventoryState(firstEntities);
|
||||
using var second = new RuntimeInventoryState(secondEntities);
|
||||
|
||||
first.Shortcuts.Replace([new ShortcutEntry(1, 2u, 3u)]);
|
||||
first.Transactions.IncrementBusyCount();
|
||||
|
||||
Assert.NotSame(first.Objects, second.Objects);
|
||||
Assert.Single(first.Shortcuts.Items);
|
||||
Assert.Empty(second.Shortcuts.Items);
|
||||
Assert.Equal(1, first.Transactions.BusyCount);
|
||||
Assert.Equal(0, second.Transactions.BusyCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DisposeDetachesTransactionsAndClearsOwnedState()
|
||||
{
|
||||
using var entities = new RuntimeEntityObjectLifetime();
|
||||
var inventory = new RuntimeInventoryState(entities);
|
||||
inventory.ExternalContainers.RequestOpen(0x70000001u);
|
||||
inventory.ExternalContainers.ApplyViewContents(0x70000001u);
|
||||
inventory.ItemMana.OnQueryItemManaResponse(0x50000001u, 0.5f, true);
|
||||
inventory.Shortcuts.Replace([new ShortcutEntry(1, 2u, 3u)]);
|
||||
inventory.DesiredComponents.Replace([(4u, 5u)]);
|
||||
|
||||
inventory.Dispose();
|
||||
inventory.Dispose();
|
||||
|
||||
Assert.True(inventory.IsDisposed);
|
||||
Assert.True(inventory.Transactions.IsDisposed);
|
||||
Assert.Equal(0u, inventory.ExternalContainers.CurrentContainerId);
|
||||
Assert.False(inventory.ItemMana.HasMana(0x50000001u));
|
||||
Assert.Empty(inventory.Shortcuts.Items);
|
||||
Assert.Empty(inventory.DesiredComponents.Items);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DisposalRetriesATransientOwnerObserverFailureToConvergence()
|
||||
{
|
||||
using var entities = new RuntimeEntityObjectLifetime();
|
||||
var inventory = new RuntimeInventoryState(entities);
|
||||
inventory.ExternalContainers.RequestOpen(0x70000001u);
|
||||
inventory.ExternalContainers.ApplyViewContents(0x70000001u);
|
||||
bool fail = true;
|
||||
inventory.ExternalContainers.Changed += _ =>
|
||||
{
|
||||
if (fail)
|
||||
{
|
||||
fail = false;
|
||||
throw new InvalidOperationException("transient");
|
||||
}
|
||||
};
|
||||
|
||||
Assert.Throws<AggregateException>(inventory.Dispose);
|
||||
Assert.False(inventory.IsDisposed);
|
||||
Assert.True(inventory.Transactions.IsDisposed);
|
||||
|
||||
inventory.Dispose();
|
||||
|
||||
Assert.True(inventory.IsDisposed);
|
||||
Assert.Equal(0u, inventory.ExternalContainers.CurrentContainerId);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue