refactor(runtime): own interaction transactions
Move use throttling, appraisal identity, queued interactions, and exact post-arrival pickup state beneath RuntimeActionState. Keep App as the picker, movement, transport, and retained-presentation adapter while preserving retail send and UseDone ordering. Add reset, disposal, GUID-reuse, callback-reentrancy, and transport-failure coverage. Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
parent
be73bccf5a
commit
f5f7b4177f
31 changed files with 1365 additions and 432 deletions
|
|
@ -284,7 +284,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
|||
DeferredSelectionUiAuthority selection = late.Selection;
|
||||
return new ItemInteractionController(
|
||||
d.Inventory.Objects,
|
||||
d.Inventory.Transactions,
|
||||
d.Actions.Transactions,
|
||||
d.Actions.Interaction,
|
||||
playerGuid: () => d.PlayerIdentity.ServerGuid,
|
||||
sendUse: null,
|
||||
|
|
|
|||
|
|
@ -1,47 +0,0 @@
|
|||
namespace AcDream.App.Input;
|
||||
|
||||
/// <summary>
|
||||
/// Orders input-originated item interactions after the local player's movement
|
||||
/// edge has been serialized for the same frame, but before inbound network
|
||||
/// dispatch.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Silk input callbacks arrive before the retail object phase. A movement-key
|
||||
/// release and a later Use press can therefore be observed in one frame even
|
||||
/// though the movement controller has not yet emitted the release packet.
|
||||
/// Draining here preserves that event order on the wire: the release is sent
|
||||
/// first, then the one-shot interaction. ACE otherwise interprets the later
|
||||
/// MoveToState as cancellation of the Use-created MoveTo chain.
|
||||
/// </remarks>
|
||||
public sealed class OutboundInteractionQueue
|
||||
{
|
||||
private readonly Queue<Action> _pending = new();
|
||||
private uint _clearEpoch;
|
||||
|
||||
public int Count => _pending.Count;
|
||||
|
||||
public void Enqueue(Action action)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(action);
|
||||
_pending.Enqueue(action);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drain only the actions present at the frame boundary. An action that
|
||||
/// enqueues another action leaves it for the next frame instead of making
|
||||
/// the current dispatch recursively unbounded.
|
||||
/// </summary>
|
||||
public void Drain()
|
||||
{
|
||||
int count = _pending.Count;
|
||||
uint epoch = _clearEpoch;
|
||||
while (count-- > 0 && epoch == _clearEpoch && _pending.Count > 0)
|
||||
_pending.Dequeue().Invoke();
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_pending.Clear();
|
||||
_clearEpoch++;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
using AcDream.App.Input;
|
||||
using AcDream.App.UI;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Selection;
|
||||
using AcDream.Core.Ui;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
using AcDream.UI.Abstractions.Input;
|
||||
|
||||
namespace AcDream.App.Interaction;
|
||||
|
|
@ -19,31 +19,17 @@ internal sealed class SelectionInteractionController
|
|||
private readonly SelectionState _selection;
|
||||
private readonly IWorldSelectionQuery _query;
|
||||
private readonly ItemInteractionController _items;
|
||||
private readonly ISelectionInteractionTransport _transport;
|
||||
private readonly RuntimeInteractionTransactionState _transactions;
|
||||
private readonly IRuntimeInteractionTransport _transport;
|
||||
private readonly IPlayerInteractionMovementSink _movement;
|
||||
private readonly OutboundInteractionQueue _outbound = new();
|
||||
private readonly PlayerApproachCompletionState _approachCompletions;
|
||||
private readonly Action<string>? _toast;
|
||||
private PendingPostArrivalPickup? _pendingPostArrival;
|
||||
|
||||
private readonly record struct QueuedInteractionIdentity(
|
||||
uint ServerGuid,
|
||||
uint? LocalEntityId,
|
||||
ClientObject? ClientObject);
|
||||
|
||||
private readonly record struct PendingPostArrivalPickup(
|
||||
uint ServerGuid,
|
||||
uint LocalEntityId,
|
||||
uint DestinationContainerId,
|
||||
int Placement,
|
||||
ulong PendingPlacementToken,
|
||||
PlayerApproachToken ApproachToken);
|
||||
|
||||
public SelectionInteractionController(
|
||||
SelectionState selection,
|
||||
IWorldSelectionQuery query,
|
||||
ItemInteractionController items,
|
||||
ISelectionInteractionTransport transport,
|
||||
IRuntimeInteractionTransport transport,
|
||||
IPlayerInteractionMovementSink movement,
|
||||
Action<string>? toast = null,
|
||||
PlayerApproachCompletionState? approachCompletions = null)
|
||||
|
|
@ -51,6 +37,7 @@ internal sealed class SelectionInteractionController
|
|||
_selection = selection ?? throw new ArgumentNullException(nameof(selection));
|
||||
_query = query ?? throw new ArgumentNullException(nameof(query));
|
||||
_items = items ?? throw new ArgumentNullException(nameof(items));
|
||||
_transactions = _items.RuntimeTransactions;
|
||||
_transport = transport ?? throw new ArgumentNullException(nameof(transport));
|
||||
_movement = movement ?? throw new ArgumentNullException(nameof(movement));
|
||||
_toast = toast;
|
||||
|
|
@ -88,13 +75,9 @@ internal sealed class SelectionInteractionController
|
|||
if (_selection.SelectedObjectId is uint pickupTarget)
|
||||
{
|
||||
EnqueueIdentityBound(
|
||||
RuntimeQueuedInteractionKind.Pickup,
|
||||
pickupTarget,
|
||||
requireLiveEntity: true,
|
||||
guid =>
|
||||
{
|
||||
if (ValidatePickupTarget(guid, showToast: true))
|
||||
_items.PlaceWorldItemInBackpack(guid);
|
||||
});
|
||||
requireLiveEntity: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -178,12 +161,9 @@ internal sealed class SelectionInteractionController
|
|||
_toast?.Invoke($"Selected: {label}");
|
||||
if (useImmediately)
|
||||
EnqueueIdentityBound(
|
||||
RuntimeQueuedInteractionKind.Activate,
|
||||
guid,
|
||||
requireLiveEntity: true,
|
||||
selected =>
|
||||
{
|
||||
_items.ActivateItem(selected);
|
||||
});
|
||||
requireLiveEntity: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -213,12 +193,9 @@ internal sealed class SelectionInteractionController
|
|||
return;
|
||||
}
|
||||
EnqueueIdentityBound(
|
||||
RuntimeQueuedInteractionKind.Use,
|
||||
selected,
|
||||
requireLiveEntity: false,
|
||||
guid =>
|
||||
{
|
||||
_items.UseSelectedOrEnterMode(guid);
|
||||
});
|
||||
requireLiveEntity: false);
|
||||
}
|
||||
|
||||
public void SendUse(uint serverGuid)
|
||||
|
|
@ -229,45 +206,19 @@ internal sealed class SelectionInteractionController
|
|||
ItemUseRequestReservation? reservation)
|
||||
{
|
||||
CancelPendingApproach();
|
||||
if (!_transport.IsInWorld)
|
||||
{
|
||||
bool ownedByPlayer = _items.IsOwnedByPlayer(serverGuid);
|
||||
RuntimeInteractionDispatchResult result =
|
||||
_transactions.TryDispatchUse(
|
||||
serverGuid,
|
||||
ownedByPlayer,
|
||||
ownedByPlayer || _query.IsUseable(serverGuid),
|
||||
reservation,
|
||||
_transport,
|
||||
out uint sequence);
|
||||
if (result == RuntimeInteractionDispatchResult.NotInWorld)
|
||||
_toast?.Invoke("Not in world");
|
||||
reservation?.CancelBeforeDispatch();
|
||||
return;
|
||||
}
|
||||
|
||||
// ItemHolder::UseObject @ 0x00588A80 sends Event_UseEvent before
|
||||
// CPlayerSystem::UsingItem for every accepted ordinary Use. The latter
|
||||
// is presentation/local inventory policy; it never delays the wire
|
||||
// request behind a client-side TurnTo/MoveTo completion. ACE owns the
|
||||
// authoritative approach chain for spatial targets.
|
||||
if (_items.IsOwnedByPlayer(serverGuid))
|
||||
{
|
||||
SendUseNow(serverGuid, reservation);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_query.IsUseable(serverGuid))
|
||||
{
|
||||
reservation?.CancelBeforeDispatch();
|
||||
return;
|
||||
}
|
||||
|
||||
SendUseNow(serverGuid, reservation);
|
||||
}
|
||||
|
||||
private void SendUseNow(
|
||||
uint serverGuid,
|
||||
ItemUseRequestReservation? reservation)
|
||||
{
|
||||
if (_transport.TrySendUse(serverGuid, out uint sequence))
|
||||
{
|
||||
reservation?.MarkDispatched();
|
||||
if (result == RuntimeInteractionDispatchResult.Dispatched)
|
||||
Console.WriteLine($"[B.4b] use guid=0x{serverGuid:X8} seq={sequence}");
|
||||
return;
|
||||
}
|
||||
|
||||
reservation?.CancelBeforeDispatch();
|
||||
}
|
||||
|
||||
public void SendPickup(uint itemGuid, uint destinationContainerId, int placement)
|
||||
|
|
@ -302,20 +253,38 @@ internal sealed class SelectionInteractionController
|
|||
|
||||
if (approach.IsCloseRange)
|
||||
{
|
||||
var pending = new PendingPostArrivalPickup(
|
||||
itemGuid,
|
||||
approach.Target.LocalEntityId,
|
||||
destinationContainerId,
|
||||
placement,
|
||||
pendingPlacementToken,
|
||||
ApproachToken: default);
|
||||
bool armed = false;
|
||||
bool started = _movement.BeginApproach(
|
||||
approach,
|
||||
token => _pendingPostArrival = pending with { ApproachToken = token });
|
||||
if (!started && _pendingPostArrival?.ServerGuid == pending.ServerGuid)
|
||||
CancelPendingApproach();
|
||||
else if (!started)
|
||||
CancelPickupPresentation(itemGuid, pendingPlacementToken);
|
||||
token =>
|
||||
{
|
||||
armed = _transactions.TryArmPostArrivalPickup(
|
||||
itemGuid,
|
||||
approach.Target.LocalEntityId,
|
||||
destinationContainerId,
|
||||
placement,
|
||||
pendingPlacementToken,
|
||||
new RuntimeInteractionApproachToken(
|
||||
token.ControllerLifetime,
|
||||
token.ApproachGeneration),
|
||||
out _);
|
||||
});
|
||||
if (!started || !armed)
|
||||
{
|
||||
if (_transactions.TryCancelPendingPickup(
|
||||
itemGuid,
|
||||
approach.Target.LocalEntityId,
|
||||
out RuntimePendingPickup cancelled))
|
||||
{
|
||||
CancelPickupPresentation(
|
||||
cancelled.ServerGuid,
|
||||
cancelled.PendingPlacementToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
CancelPickupPresentation(itemGuid, pendingPlacementToken);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -328,16 +297,18 @@ internal sealed class SelectionInteractionController
|
|||
{
|
||||
return;
|
||||
}
|
||||
uint sequence = 0u;
|
||||
if (_items.TryDispatchInventoryRequest(
|
||||
InventoryRequestKind.Pickup,
|
||||
itemGuid,
|
||||
() => _transport.TrySendPickup(
|
||||
itemGuid,
|
||||
destinationContainerId,
|
||||
placement,
|
||||
out sequence),
|
||||
pendingPlacementToken))
|
||||
var immediate = new RuntimePendingPickup(
|
||||
Token: 0u,
|
||||
itemGuid,
|
||||
approach.Target.LocalEntityId,
|
||||
destinationContainerId,
|
||||
placement,
|
||||
pendingPlacementToken,
|
||||
ApproachToken: default);
|
||||
if (_transactions.TryDispatchPickup(
|
||||
immediate,
|
||||
_transport,
|
||||
out uint sequence))
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"[B.5] pickup item=0x{itemGuid:X8} container=0x{destinationContainerId:X8} placement={placement} seq={sequence}");
|
||||
|
|
@ -350,13 +321,28 @@ internal sealed class SelectionInteractionController
|
|||
|
||||
/// <summary>Fires only after natural MoveToComplete(None), never cancellation.</summary>
|
||||
public void OnNaturalMoveToComplete()
|
||||
=> HandleNaturalMoveToComplete();
|
||||
|
||||
private void HandleNaturalMoveToComplete()
|
||||
{
|
||||
if (_pendingPostArrival is not { } pending)
|
||||
if (_transactions.TryGetPendingPickup(out RuntimePendingPickup pending))
|
||||
HandleApproachCompletion(pending.ApproachToken, natural: true);
|
||||
}
|
||||
|
||||
private void HandleApproachCompletion(
|
||||
RuntimeInteractionApproachToken approachToken,
|
||||
bool natural)
|
||||
{
|
||||
bool accepted = _transactions.TryResolveApproachCompletion(
|
||||
approachToken,
|
||||
natural,
|
||||
out RuntimePendingPickup pending);
|
||||
if (pending.Token == 0u)
|
||||
return;
|
||||
_pendingPostArrival = null;
|
||||
if (!accepted)
|
||||
{
|
||||
CancelPickupPresentation(
|
||||
pending.ServerGuid,
|
||||
pending.PendingPlacementToken);
|
||||
return;
|
||||
}
|
||||
if (!_query.IsCurrent(pending.ServerGuid, pending.LocalEntityId))
|
||||
{
|
||||
CancelPickupPresentation(
|
||||
|
|
@ -373,15 +359,10 @@ internal sealed class SelectionInteractionController
|
|||
{
|
||||
return;
|
||||
}
|
||||
if (!_items.TryDispatchInventoryRequest(
|
||||
InventoryRequestKind.Pickup,
|
||||
pending.ServerGuid,
|
||||
() => _transport.TrySendPickup(
|
||||
pending.ServerGuid,
|
||||
pending.DestinationContainerId,
|
||||
pending.Placement,
|
||||
out _),
|
||||
pending.PendingPlacementToken))
|
||||
if (!_transactions.TryDispatchPickup(
|
||||
pending,
|
||||
_transport,
|
||||
out _))
|
||||
{
|
||||
CancelPickupPresentation(
|
||||
pending.ServerGuid,
|
||||
|
|
@ -393,22 +374,29 @@ internal sealed class SelectionInteractionController
|
|||
{
|
||||
while (_approachCompletions.TryTake(out PlayerApproachCompletion completion))
|
||||
{
|
||||
if (_pendingPostArrival?.ApproachToken != completion.Token)
|
||||
continue;
|
||||
if (completion.IsNatural)
|
||||
HandleNaturalMoveToComplete();
|
||||
else
|
||||
CancelPendingApproach();
|
||||
HandleApproachCompletion(
|
||||
new RuntimeInteractionApproachToken(
|
||||
completion.Token.ControllerLifetime,
|
||||
completion.Token.ApproachGeneration),
|
||||
completion.IsNatural);
|
||||
}
|
||||
_outbound.Drain();
|
||||
_transactions.DrainOutbound(DispatchQueuedInteraction);
|
||||
}
|
||||
|
||||
public void OnMoveToCancelled(WeenieError _) => CancelPendingApproach();
|
||||
|
||||
public void OnEntityHidden(uint serverGuid)
|
||||
{
|
||||
if (_pendingPostArrival?.ServerGuid == serverGuid)
|
||||
CancelPendingApproach();
|
||||
_transactions.CancelQueuedInteractions(serverGuid);
|
||||
if (_transactions.TryCancelPendingPickup(
|
||||
serverGuid,
|
||||
localEntityId: null,
|
||||
out RuntimePendingPickup cancelled))
|
||||
{
|
||||
CancelPickupPresentation(
|
||||
cancelled.ServerGuid,
|
||||
cancelled.PendingPlacementToken);
|
||||
}
|
||||
if (_selection.SelectedObjectId == serverGuid)
|
||||
{
|
||||
_selection.Clear(
|
||||
|
|
@ -420,11 +408,17 @@ internal sealed class SelectionInteractionController
|
|||
public void OnEntityRemoved(LiveEntityRecord record, bool replacementExists)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
if (_pendingPostArrival is { } pending
|
||||
&& pending.ServerGuid == record.ServerGuid
|
||||
&& pending.LocalEntityId == record.LocalEntityId)
|
||||
_transactions.CancelQueuedInteractions(
|
||||
record.ServerGuid,
|
||||
record.LocalEntityId);
|
||||
if (_transactions.TryCancelPendingPickup(
|
||||
record.ServerGuid,
|
||||
record.LocalEntityId,
|
||||
out RuntimePendingPickup cancelled))
|
||||
{
|
||||
CancelPendingApproach();
|
||||
CancelPickupPresentation(
|
||||
cancelled.ServerGuid,
|
||||
cancelled.PendingPlacementToken);
|
||||
}
|
||||
if (!replacementExists && _selection.SelectedObjectId == record.ServerGuid)
|
||||
{
|
||||
|
|
@ -443,11 +437,8 @@ internal sealed class SelectionInteractionController
|
|||
catch (Exception error) { failures.Add(error); }
|
||||
try { _selection.Reset(); }
|
||||
catch (Exception error) { failures.Add(error); }
|
||||
try { _outbound.Clear(); }
|
||||
catch (Exception error) { failures.Add(error); }
|
||||
try { _approachCompletions.Clear(); }
|
||||
catch (Exception error) { failures.Add(error); }
|
||||
_pendingPostArrival = null;
|
||||
|
||||
if (failures.Count != 0)
|
||||
throw new AggregateException(
|
||||
|
|
@ -471,11 +462,10 @@ internal sealed class SelectionInteractionController
|
|||
}
|
||||
|
||||
private bool EnqueueIdentityBound(
|
||||
RuntimeQueuedInteractionKind kind,
|
||||
uint serverGuid,
|
||||
bool requireLiveEntity,
|
||||
Action<uint> action)
|
||||
bool requireLiveEntity)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(action);
|
||||
uint? localEntityId = _query.TryCaptureIdentity(serverGuid, out uint localId)
|
||||
? localId
|
||||
: null;
|
||||
|
|
@ -488,16 +478,15 @@ internal sealed class SelectionInteractionController
|
|||
return false;
|
||||
}
|
||||
|
||||
var identity = new QueuedInteractionIdentity(serverGuid, localEntityId, item);
|
||||
_outbound.Enqueue(() =>
|
||||
{
|
||||
if (IsCurrent(identity))
|
||||
action(identity.ServerGuid);
|
||||
});
|
||||
var identity = new RuntimeInteractionIdentity(
|
||||
serverGuid,
|
||||
localEntityId,
|
||||
item);
|
||||
_transactions.Enqueue(new RuntimeQueuedInteraction(kind, identity));
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool IsCurrent(QueuedInteractionIdentity identity)
|
||||
private bool IsCurrent(RuntimeInteractionIdentity identity)
|
||||
=> (identity.LocalEntityId is not uint localId
|
||||
|| _query.IsCurrent(identity.ServerGuid, localId))
|
||||
&& (identity.ClientObject is not { } item
|
||||
|
|
@ -505,14 +494,39 @@ internal sealed class SelectionInteractionController
|
|||
|
||||
private void CancelPendingApproach()
|
||||
{
|
||||
if (_pendingPostArrival is not { } pending)
|
||||
if (!_transactions.TryCancelPendingPickup(
|
||||
out RuntimePendingPickup pending))
|
||||
return;
|
||||
_pendingPostArrival = null;
|
||||
CancelPickupPresentation(
|
||||
pending.ServerGuid,
|
||||
pending.PendingPlacementToken);
|
||||
}
|
||||
|
||||
private void DispatchQueuedInteraction(
|
||||
RuntimeQueuedInteraction interaction)
|
||||
{
|
||||
RuntimeInteractionIdentity identity = interaction.Identity;
|
||||
if (!IsCurrent(identity))
|
||||
return;
|
||||
|
||||
switch (interaction.Kind)
|
||||
{
|
||||
case RuntimeQueuedInteractionKind.Activate:
|
||||
_items.ActivateItem(identity.ServerGuid);
|
||||
break;
|
||||
case RuntimeQueuedInteractionKind.Use:
|
||||
_items.UseSelectedOrEnterMode(identity.ServerGuid);
|
||||
break;
|
||||
case RuntimeQueuedInteractionKind.Pickup:
|
||||
if (ValidatePickupTarget(identity.ServerGuid, showToast: true))
|
||||
_items.PlaceWorldItemInBackpack(identity.ServerGuid);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException(
|
||||
$"Unknown queued interaction kind {interaction.Kind}.");
|
||||
}
|
||||
}
|
||||
|
||||
private void CancelPickupPresentation(uint itemGuid, ulong token = 0u)
|
||||
=> _items.CancelPendingBackpackPlacement(itemGuid, token);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,22 +1,12 @@
|
|||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
|
||||
namespace AcDream.App.Interaction;
|
||||
|
||||
internal interface ISelectionInteractionTransport
|
||||
{
|
||||
bool IsInWorld { get; }
|
||||
bool TrySendUse(uint serverGuid, out uint sequence);
|
||||
bool TrySendPickup(
|
||||
uint itemGuid,
|
||||
uint destinationContainerId,
|
||||
int placement,
|
||||
out uint sequence);
|
||||
}
|
||||
|
||||
internal sealed class WorldSessionSelectionInteractionTransport(
|
||||
Func<WorldSession?> session)
|
||||
: ISelectionInteractionTransport
|
||||
: IRuntimeInteractionTransport
|
||||
{
|
||||
private readonly Func<WorldSession?> _session = session
|
||||
?? throw new ArgumentNullException(nameof(session));
|
||||
|
|
|
|||
|
|
@ -258,7 +258,7 @@ internal sealed class LiveSessionRuntimeFactory
|
|||
OnUseDone: error =>
|
||||
{
|
||||
_domain.Inventory.ExternalContainers.ApplyUseDone(error);
|
||||
_domain.Inventory.Transactions.CompleteUse(error);
|
||||
_domain.Actions.Transactions.CompleteUse(error);
|
||||
},
|
||||
_domain.Inventory.ItemMana,
|
||||
ExternalContainers: _domain.Inventory.ExternalContainers,
|
||||
|
|
|
|||
|
|
@ -326,7 +326,7 @@ public sealed class GameWindow :
|
|||
// J4/J5.1: Runtime owns communication, selection, combat, and target-mode
|
||||
// state. App, UI, plugins, and live-session routing borrow exact children.
|
||||
private readonly RuntimeCommunicationState _runtimeCommunication = new();
|
||||
private readonly RuntimeActionState _runtimeActions = new();
|
||||
private readonly RuntimeActionState _runtimeActions;
|
||||
public AcDream.Core.Selection.SelectionState Selection =>
|
||||
_runtimeActions.Selection;
|
||||
public AcDream.Core.Chat.ChatLog Chat => _runtimeCommunication.Chat;
|
||||
|
|
@ -561,6 +561,7 @@ public sealed class GameWindow :
|
|||
{
|
||||
_options = options ?? throw new System.ArgumentNullException(nameof(options));
|
||||
_runtimeInventory = new RuntimeInventoryState(_runtimeEntityObjects);
|
||||
_runtimeActions = new RuntimeActionState(_runtimeInventory.Transactions);
|
||||
_runtimeCharacter = new RuntimeCharacterState();
|
||||
var alphaScratchBudgets =
|
||||
AcDream.App.Rendering.Residency.AlphaScratchBudgetProfile.Create(
|
||||
|
|
|
|||
|
|
@ -396,12 +396,12 @@ internal static class GameWindowShutdownManifest
|
|||
Hard(
|
||||
"runtime character state",
|
||||
live.Character.Dispose),
|
||||
Hard(
|
||||
"runtime inventory state",
|
||||
live.Inventory.Dispose),
|
||||
Hard(
|
||||
"runtime action state",
|
||||
live.Actions.Dispose),
|
||||
Hard(
|
||||
"runtime inventory state",
|
||||
live.Inventory.Dispose),
|
||||
Hard(
|
||||
"runtime entity/object lifetime",
|
||||
live.EntityObjects.Dispose),
|
||||
|
|
|
|||
|
|
@ -141,7 +141,8 @@ public static class FixtureProvider
|
|||
var selection = new AcDream.Core.Selection.SelectionState();
|
||||
var itemInteraction = new ItemInteractionController(
|
||||
objects,
|
||||
new InventoryTransactionState(objects),
|
||||
new AcDream.Runtime.Gameplay.RuntimeInteractionTransactionState(
|
||||
new InventoryTransactionState(objects)),
|
||||
new AcDream.Runtime.Gameplay.InteractionState(),
|
||||
() => SampleData.PlayerGuid,
|
||||
sendUse: null,
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ public readonly record struct PendingBackpackPlacement(
|
|||
/// </summary>
|
||||
public sealed class ItemInteractionController : IDisposable
|
||||
{
|
||||
private const long RetailUseThrottleMs = 200;
|
||||
internal const string InventoryRequestBusyMessage =
|
||||
"You can only move or use one item at a time";
|
||||
private const long RetailDoubleClickMs = 500;
|
||||
|
|
@ -62,19 +61,17 @@ public sealed class ItemInteractionController : IDisposable
|
|||
private readonly Action<string>? _systemMessage;
|
||||
private readonly AutoWieldController _autoWield;
|
||||
private readonly Action<uint, ItemUseRequestReservation>? _requestUse;
|
||||
private readonly RuntimeInteractionTransactionState _runtimeTransactions;
|
||||
private readonly InventoryTransactionState _transactions;
|
||||
|
||||
private long _lastUseMs = long.MinValue / 2;
|
||||
private uint _consumedPrimaryClickTarget;
|
||||
private long _consumedPrimaryClickMs = long.MinValue / 2;
|
||||
private uint _awaitingAppraisalId;
|
||||
private uint _currentAppraisalId;
|
||||
private PendingBackpackPlacement? _pendingBackpackPlacement;
|
||||
private bool _disposed;
|
||||
|
||||
public ItemInteractionController(
|
||||
ClientObjectTable objects,
|
||||
InventoryTransactionState transactions,
|
||||
RuntimeInteractionTransactionState runtimeTransactions,
|
||||
InteractionState interactionState,
|
||||
Func<uint> playerGuid,
|
||||
Action<uint>? sendUse,
|
||||
|
|
@ -136,13 +133,14 @@ public sealed class ItemInteractionController : IDisposable
|
|||
_requestUse = requestUse;
|
||||
_interactionState = interactionState
|
||||
?? throw new ArgumentNullException(nameof(interactionState));
|
||||
_transactions = transactions
|
||||
?? throw new ArgumentNullException(nameof(transactions));
|
||||
_runtimeTransactions = runtimeTransactions
|
||||
?? throw new ArgumentNullException(nameof(runtimeTransactions));
|
||||
_transactions = _runtimeTransactions.Inventory;
|
||||
if (!ReferenceEquals(_transactions.Objects, _objects))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"The inventory transaction owner must borrow the controller's exact object table.",
|
||||
nameof(transactions));
|
||||
nameof(runtimeTransactions));
|
||||
}
|
||||
_autoWield = new AutoWieldController(
|
||||
_objects,
|
||||
|
|
@ -184,9 +182,12 @@ public sealed class ItemInteractionController : IDisposable
|
|||
public uint PlayerGuid => _playerGuid();
|
||||
|
||||
public InteractionState InteractionState => _interactionState;
|
||||
public RuntimeInteractionTransactionState RuntimeTransactions =>
|
||||
_runtimeTransactions;
|
||||
|
||||
public int BusyCount => _transactions.BusyCount;
|
||||
public uint CurrentAppraisalId => _currentAppraisalId;
|
||||
public uint CurrentAppraisalId =>
|
||||
_runtimeTransactions.CurrentAppraisalId;
|
||||
|
||||
public bool CanMakeInventoryRequest =>
|
||||
BaseCanMakeInventoryRequest && !_transactions.HasPendingRequest;
|
||||
|
|
@ -277,7 +278,7 @@ public sealed class ItemInteractionController : IDisposable
|
|||
/// Item__UseDone too; AttackDone belongs only to the combat attack owner.
|
||||
/// </summary>
|
||||
public void IncrementBusyCount()
|
||||
=> _transactions.IncrementBusyCount();
|
||||
=> _runtimeTransactions.IncrementBusyCount();
|
||||
|
||||
/// <summary>
|
||||
/// Raised for retail confirmation/auxiliary actions whose retained panel is
|
||||
|
|
@ -403,11 +404,7 @@ public sealed class ItemInteractionController : IDisposable
|
|||
{
|
||||
if (objectId == 0 || _sendExamine is null)
|
||||
return;
|
||||
|
||||
if (_awaitingAppraisalId == 0)
|
||||
IncrementBusyCount();
|
||||
_awaitingAppraisalId = objectId;
|
||||
_sendExamine(objectId);
|
||||
_runtimeTransactions.TryRequestAppraisal(objectId, _sendExamine);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -416,25 +413,14 @@ public sealed class ItemInteractionController : IDisposable
|
|||
/// </summary>
|
||||
public AppraisalResponseAcceptance AcceptAppraisalResponse(uint objectId)
|
||||
{
|
||||
if (objectId == 0
|
||||
|| (objectId != _awaitingAppraisalId
|
||||
&& objectId != _currentAppraisalId))
|
||||
return default;
|
||||
|
||||
bool firstResponse = objectId == _awaitingAppraisalId;
|
||||
if (firstResponse)
|
||||
{
|
||||
_awaitingAppraisalId = 0;
|
||||
_currentAppraisalId = objectId;
|
||||
bool ownedBusyReference = _transactions.BusyCount > 0;
|
||||
_transactions.CompleteUse(0u);
|
||||
if (!ownedBusyReference)
|
||||
StateChanged?.Invoke();
|
||||
}
|
||||
|
||||
bool ownedBusyReference = _transactions.BusyCount > 0;
|
||||
RuntimeAppraisalResponseAcceptance acceptance =
|
||||
_runtimeTransactions.AcceptAppraisalResponse(objectId);
|
||||
if (acceptance.FirstResponse && !ownedBusyReference)
|
||||
StateChanged?.Invoke();
|
||||
return new AppraisalResponseAcceptance(
|
||||
Accepted: true,
|
||||
FirstResponse: firstResponse);
|
||||
acceptance.Accepted,
|
||||
acceptance.FirstResponse);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -443,10 +429,9 @@ public sealed class ItemInteractionController : IDisposable
|
|||
/// </summary>
|
||||
public bool RefreshCurrentAppraisal()
|
||||
{
|
||||
if (_currentAppraisalId == 0 || _sendExamine is null)
|
||||
if (_sendExamine is null)
|
||||
return false;
|
||||
_sendExamine(_currentAppraisalId);
|
||||
return true;
|
||||
return _runtimeTransactions.RefreshCurrentAppraisal(_sendExamine);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -460,24 +445,14 @@ public sealed class ItemInteractionController : IDisposable
|
|||
/// </summary>
|
||||
public void CancelObjectAppraisalForSpell()
|
||||
{
|
||||
if (_awaitingAppraisalId == 0u && _currentAppraisalId == 0u)
|
||||
if (_sendExamine is null)
|
||||
return;
|
||||
|
||||
if (_awaitingAppraisalId != 0u)
|
||||
bool ownedBusyReference = _transactions.BusyCount > 0;
|
||||
if (_runtimeTransactions.CancelObjectAppraisalForSpell(_sendExamine)
|
||||
&& !ownedBusyReference)
|
||||
{
|
||||
_awaitingAppraisalId = 0u;
|
||||
_currentAppraisalId = 0u;
|
||||
bool ownedBusyReference = _transactions.BusyCount > 0;
|
||||
_transactions.CompleteUse(0u);
|
||||
if (!ownedBusyReference)
|
||||
StateChanged?.Invoke();
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentAppraisalId = 0u;
|
||||
StateChanged?.Invoke();
|
||||
}
|
||||
_sendExamine?.Invoke(0u);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -791,8 +766,11 @@ public sealed class ItemInteractionController : IDisposable
|
|||
|
||||
if (!EnsureInventoryRequestReady())
|
||||
return false;
|
||||
_sendUseWithTarget?.Invoke(sourceGuid, targetGuid);
|
||||
_transactions.IncrementBusyCount();
|
||||
_runtimeTransactions.TryDispatchTargetedUse(
|
||||
sourceGuid,
|
||||
targetGuid,
|
||||
_sendUseWithTarget,
|
||||
incrementBusy: true);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -827,7 +805,7 @@ public sealed class ItemInteractionController : IDisposable
|
|||
else
|
||||
{
|
||||
_sendUse!(objectId);
|
||||
_transactions.IncrementBusyCount();
|
||||
_runtimeTransactions.IncrementBusyCount();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
@ -926,8 +904,11 @@ public sealed class ItemInteractionController : IDisposable
|
|||
}
|
||||
break;
|
||||
case ItemPolicyActionKind.SendUseWithTarget:
|
||||
_sendUseWithTarget?.Invoke(action.ObjectId, action.TargetId);
|
||||
acted |= _sendUseWithTarget is not null;
|
||||
acted |= _runtimeTransactions.TryDispatchTargetedUse(
|
||||
action.ObjectId,
|
||||
action.TargetId,
|
||||
_sendUseWithTarget,
|
||||
incrementBusy: false);
|
||||
break;
|
||||
case ItemPolicyActionKind.SetGroundObject:
|
||||
_requestExternalContainer?.Invoke(action.ObjectId);
|
||||
|
|
@ -944,7 +925,7 @@ public sealed class ItemInteractionController : IDisposable
|
|||
}
|
||||
else
|
||||
{
|
||||
_transactions.IncrementBusyCount();
|
||||
_runtimeTransactions.IncrementBusyCount();
|
||||
}
|
||||
break;
|
||||
case ItemPolicyActionKind.Reject:
|
||||
|
|
@ -965,7 +946,7 @@ public sealed class ItemInteractionController : IDisposable
|
|||
}
|
||||
|
||||
private ItemUseRequestReservation BeginUseRequestReservation()
|
||||
=> _transactions.BeginUseRequestReservation();
|
||||
=> _runtimeTransactions.BeginUseRequestReservation();
|
||||
|
||||
private void ExecutePlacementActions(System.Collections.Generic.IReadOnlyList<ItemPolicyAction> actions)
|
||||
{
|
||||
|
|
@ -1160,8 +1141,8 @@ public sealed class ItemInteractionController : IDisposable
|
|||
}
|
||||
|
||||
/// <summary>Retail UseDone (0x01C7) releases one UI busy reference.</summary>
|
||||
public void CompleteUse(uint _)
|
||||
=> _transactions.CompleteUse(0u);
|
||||
public void CompleteUse(uint error)
|
||||
=> _runtimeTransactions.CompleteUse(error);
|
||||
|
||||
/// <summary>Session teardown drops every outstanding client UI busy reference.</summary>
|
||||
public void ClearBusy()
|
||||
|
|
@ -1176,22 +1157,18 @@ public sealed class ItemInteractionController : IDisposable
|
|||
{
|
||||
PendingBackpackPlacement? pendingPlacement = _pendingBackpackPlacement;
|
||||
_pendingBackpackPlacement = null;
|
||||
// The canonical owner publishes its reset to Runtime observers, while
|
||||
// this controller preserves the pre-J4 single UI notification emitted
|
||||
// by InteractionState.ResetSession below.
|
||||
// Runtime owns the transaction reset. This controller preserves the
|
||||
// pre-J4 single UI notification emitted by InteractionState below.
|
||||
_transactions.StateChanged -= OnTransactionStateChanged;
|
||||
try
|
||||
{
|
||||
_transactions.ResetSession();
|
||||
_runtimeTransactions.ResetSession();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!_disposed)
|
||||
_transactions.StateChanged += OnTransactionStateChanged;
|
||||
}
|
||||
_awaitingAppraisalId = 0;
|
||||
_currentAppraisalId = 0;
|
||||
_lastUseMs = long.MinValue / 2;
|
||||
_consumedPrimaryClickTarget = 0u;
|
||||
_consumedPrimaryClickMs = long.MinValue / 2;
|
||||
|
||||
|
|
@ -1238,13 +1215,7 @@ public sealed class ItemInteractionController : IDisposable
|
|||
}
|
||||
|
||||
private bool ConsumeUseThrottle()
|
||||
{
|
||||
long now = _nowMs();
|
||||
if (now - _lastUseMs < RetailUseThrottleMs)
|
||||
return false;
|
||||
_lastUseMs = now;
|
||||
return true;
|
||||
}
|
||||
=> _runtimeTransactions.TryConsumeUseThrottle(_nowMs());
|
||||
|
||||
private static bool IsContainer(ClientObject item)
|
||||
=> item.ContainerTypeHint != 0
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ public readonly record struct RuntimeActionSnapshot(
|
|||
int TrackedTargetHealthCount,
|
||||
long InteractionRevision,
|
||||
InteractionModeKind InteractionMode,
|
||||
uint InteractionSourceObjectId);
|
||||
uint InteractionSourceObjectId,
|
||||
RuntimeInteractionTransactionSnapshot InteractionTransactions);
|
||||
|
||||
public interface IRuntimeActionView
|
||||
{
|
||||
|
|
|
|||
|
|
@ -169,7 +169,14 @@ public sealed class RuntimeTraceRecorder : IRuntimeEventObserver
|
|||
$"{checkpoint.Actions.TrackedTargetHealthCount}:" +
|
||||
$"{checkpoint.Actions.InteractionRevision}:" +
|
||||
$"{(int)checkpoint.Actions.InteractionMode}:" +
|
||||
$"{checkpoint.Actions.InteractionSourceObjectId:X8};" +
|
||||
$"{checkpoint.Actions.InteractionSourceObjectId:X8}:" +
|
||||
$"{checkpoint.Actions.InteractionTransactions.Revision}:" +
|
||||
$"{checkpoint.Actions.InteractionTransactions.LastUseSourceId:X8}:" +
|
||||
$"{checkpoint.Actions.InteractionTransactions.LastUseTargetId:X8}:" +
|
||||
$"{checkpoint.Actions.InteractionTransactions.AwaitingAppraisalId:X8}:" +
|
||||
$"{checkpoint.Actions.InteractionTransactions.CurrentAppraisalId:X8}:" +
|
||||
$"{checkpoint.Actions.InteractionTransactions.OutboundCount}:" +
|
||||
$"{checkpoint.Actions.InteractionTransactions.PendingPickupToken};" +
|
||||
$"portal={checkpoint.Portal.Generation}:{(int)checkpoint.Portal.Kind}:" +
|
||||
$"{checkpoint.Portal.DestinationCell:X8}:{checkpoint.Portal.IsReady}")));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Selection;
|
||||
|
||||
namespace AcDream.Runtime.Gameplay;
|
||||
|
|
@ -6,12 +7,14 @@ namespace AcDream.Runtime.Gameplay;
|
|||
public readonly record struct RuntimeActionOwnershipSnapshot(
|
||||
bool IsDisposed,
|
||||
bool InternalSubscriptionsAttached,
|
||||
bool InteractionTransactionsDisposed,
|
||||
uint SelectedObjectId,
|
||||
uint PreviousObjectId,
|
||||
uint PreviousValidObjectId,
|
||||
CombatMode CombatMode,
|
||||
int TrackedTargetHealthCount,
|
||||
InteractionMode InteractionMode,
|
||||
RuntimeInteractionTransactionSnapshot InteractionTransactions,
|
||||
long SelectionRevision,
|
||||
long CombatRevision,
|
||||
long InteractionRevision)
|
||||
|
|
@ -19,12 +22,14 @@ public readonly record struct RuntimeActionOwnershipSnapshot(
|
|||
public bool IsConverged =>
|
||||
IsDisposed
|
||||
&& !InternalSubscriptionsAttached
|
||||
&& InteractionTransactionsDisposed
|
||||
&& SelectedObjectId == 0u
|
||||
&& PreviousObjectId == 0u
|
||||
&& PreviousValidObjectId == 0u
|
||||
&& CombatMode == CombatMode.NonCombat
|
||||
&& TrackedTargetHealthCount == 0
|
||||
&& InteractionMode == InteractionMode.None;
|
||||
&& InteractionMode == InteractionMode.None
|
||||
&& InteractionTransactions.IsConverged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -40,11 +45,14 @@ public sealed class RuntimeActionState : IDisposable
|
|||
private long _combatRevision;
|
||||
private long _interactionRevision;
|
||||
|
||||
public RuntimeActionState()
|
||||
public RuntimeActionState(InventoryTransactionState inventoryTransactions)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(inventoryTransactions);
|
||||
Selection = new SelectionState();
|
||||
Combat = new CombatState();
|
||||
Interaction = new InteractionState();
|
||||
Transactions = new RuntimeInteractionTransactionState(
|
||||
inventoryTransactions);
|
||||
View = new ActionView(this);
|
||||
|
||||
Selection.Changed += OnSelectionChanged;
|
||||
|
|
@ -57,18 +65,21 @@ public sealed class RuntimeActionState : IDisposable
|
|||
public SelectionState Selection { get; }
|
||||
public CombatState Combat { get; }
|
||||
public InteractionState Interaction { get; }
|
||||
public RuntimeInteractionTransactionState Transactions { get; }
|
||||
public IRuntimeActionView View { get; }
|
||||
public bool IsDisposed => _disposed;
|
||||
|
||||
public RuntimeActionOwnershipSnapshot CaptureOwnership() => new(
|
||||
_disposed,
|
||||
_internalSubscriptionsAttached,
|
||||
Transactions.IsDisposed,
|
||||
Selection.SelectedObjectId ?? 0u,
|
||||
Selection.PreviousObjectId ?? 0u,
|
||||
Selection.PreviousValidObjectId ?? 0u,
|
||||
Combat.CurrentMode,
|
||||
Combat.TrackedTargetCount,
|
||||
Interaction.Current,
|
||||
Transactions.CaptureOwnership(),
|
||||
Interlocked.Read(ref _selectionRevision),
|
||||
Interlocked.Read(ref _combatRevision),
|
||||
Interlocked.Read(ref _interactionRevision));
|
||||
|
|
@ -82,6 +93,7 @@ public sealed class RuntimeActionState : IDisposable
|
|||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
List<Exception>? failures = null;
|
||||
Try(Transactions.ResetSession, ref failures);
|
||||
Try(Interaction.ResetSession, ref failures);
|
||||
Try(() => Selection.Reset(), ref failures);
|
||||
Try(Combat.Clear, ref failures);
|
||||
|
|
@ -101,6 +113,7 @@ public sealed class RuntimeActionState : IDisposable
|
|||
List<Exception>? failures = null;
|
||||
try
|
||||
{
|
||||
Try(Transactions.Dispose, ref failures);
|
||||
Try(Interaction.ResetSession, ref failures);
|
||||
Try(() => Selection.Reset(), ref failures);
|
||||
Try(Combat.Clear, ref failures);
|
||||
|
|
@ -160,7 +173,8 @@ public sealed class RuntimeActionState : IDisposable
|
|||
owner.Combat.TrackedTargetCount,
|
||||
Interlocked.Read(ref owner._interactionRevision),
|
||||
owner.Interaction.Current.Kind,
|
||||
owner.Interaction.Current.SourceObjectId);
|
||||
owner.Interaction.Current.SourceObjectId,
|
||||
owner.Transactions.CaptureOwnership());
|
||||
|
||||
public bool TryGetHealth(uint objectId, out float healthPercent)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,572 @@
|
|||
using AcDream.Core.Items;
|
||||
|
||||
namespace AcDream.Runtime.Gameplay;
|
||||
|
||||
public enum RuntimeQueuedInteractionKind
|
||||
{
|
||||
Activate,
|
||||
Use,
|
||||
Pickup,
|
||||
}
|
||||
|
||||
public readonly record struct RuntimeInteractionIdentity(
|
||||
uint ServerGuid,
|
||||
uint? LocalEntityId,
|
||||
ClientObject? ClientObject);
|
||||
|
||||
public readonly record struct RuntimeQueuedInteraction(
|
||||
RuntimeQueuedInteractionKind Kind,
|
||||
RuntimeInteractionIdentity Identity);
|
||||
|
||||
public readonly record struct RuntimeInteractionApproachToken(
|
||||
ulong ControllerLifetime,
|
||||
ulong ApproachGeneration);
|
||||
|
||||
public readonly record struct RuntimePendingPickup(
|
||||
ulong Token,
|
||||
uint ServerGuid,
|
||||
uint LocalEntityId,
|
||||
uint DestinationContainerId,
|
||||
int Placement,
|
||||
ulong PendingPlacementToken,
|
||||
RuntimeInteractionApproachToken ApproachToken);
|
||||
|
||||
public readonly record struct RuntimeAppraisalResponseAcceptance(
|
||||
bool Accepted,
|
||||
bool FirstResponse);
|
||||
|
||||
public enum RuntimeInteractionDispatchResult
|
||||
{
|
||||
Rejected,
|
||||
NotInWorld,
|
||||
NotUseable,
|
||||
Dispatched,
|
||||
}
|
||||
|
||||
public readonly record struct RuntimeInteractionTransactionSnapshot(
|
||||
bool IsDisposed,
|
||||
long Revision,
|
||||
uint LastUseSourceId,
|
||||
uint LastUseTargetId,
|
||||
uint AwaitingAppraisalId,
|
||||
uint CurrentAppraisalId,
|
||||
int OutboundCount,
|
||||
bool HasPendingPickup,
|
||||
ulong PendingPickupToken,
|
||||
long DispatchFailureCount)
|
||||
{
|
||||
public bool IsConverged =>
|
||||
IsDisposed
|
||||
&& LastUseSourceId == 0u
|
||||
&& LastUseTargetId == 0u
|
||||
&& AwaitingAppraisalId == 0u
|
||||
&& CurrentAppraisalId == 0u
|
||||
&& OutboundCount == 0
|
||||
&& !HasPendingPickup;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Canonical presentation-independent owner for retail interaction
|
||||
/// transactions. The shared inventory gate remains owned by
|
||||
/// <see cref="InventoryTransactionState"/> and is borrowed exactly.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Ordinary Use follows <c>ItemHolder::UseObject @ 0x00588A80</c>: consume the
|
||||
/// strict 0.2-second gate, send immediately, then transfer the busy reference
|
||||
/// to <c>ClientUISystem::Handle_Item__UseDone @ 0x00564900</c>. Pickup keeps
|
||||
/// the existing local approach transaction and exact post-arrival token.
|
||||
/// </remarks>
|
||||
public sealed class RuntimeInteractionTransactionState : IDisposable
|
||||
{
|
||||
public const long RetailUseThrottleMs = 200;
|
||||
|
||||
private readonly InventoryTransactionState _inventory;
|
||||
private readonly Queue<RuntimeQueuedInteraction> _outbound = new();
|
||||
private long _lastUseMs = long.MinValue / 2;
|
||||
private uint _lastUseSourceId;
|
||||
private uint _lastUseTargetId;
|
||||
private uint _awaitingAppraisalId;
|
||||
private uint _currentAppraisalId;
|
||||
private RuntimePendingPickup? _pendingPickup;
|
||||
private ulong _nextPickupToken;
|
||||
private uint _clearEpoch;
|
||||
private long _revision;
|
||||
private long _dispatchFailureCount;
|
||||
private bool _disposed;
|
||||
|
||||
public RuntimeInteractionTransactionState(
|
||||
InventoryTransactionState inventory)
|
||||
{
|
||||
_inventory = inventory
|
||||
?? throw new ArgumentNullException(nameof(inventory));
|
||||
}
|
||||
|
||||
public InventoryTransactionState Inventory => _inventory;
|
||||
public uint AwaitingAppraisalId => _awaitingAppraisalId;
|
||||
public uint CurrentAppraisalId => _currentAppraisalId;
|
||||
public int OutboundCount => _outbound.Count;
|
||||
public bool HasPendingPickup => _pendingPickup is not null;
|
||||
public bool IsDisposed => _disposed;
|
||||
public long Revision => Interlocked.Read(ref _revision);
|
||||
public long DispatchFailureCount =>
|
||||
Interlocked.Read(ref _dispatchFailureCount);
|
||||
public Exception? LastDispatchFailure { get; private set; }
|
||||
|
||||
public RuntimeInteractionTransactionSnapshot CaptureOwnership() => new(
|
||||
_disposed,
|
||||
Revision,
|
||||
_lastUseSourceId,
|
||||
_lastUseTargetId,
|
||||
_awaitingAppraisalId,
|
||||
_currentAppraisalId,
|
||||
_outbound.Count,
|
||||
_pendingPickup is not null,
|
||||
_pendingPickup?.Token ?? 0u,
|
||||
DispatchFailureCount);
|
||||
|
||||
public bool TryConsumeUseThrottle(long nowMs)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (nowMs - _lastUseMs < RetailUseThrottleMs)
|
||||
return false;
|
||||
_lastUseMs = nowMs;
|
||||
IncrementRevision();
|
||||
return true;
|
||||
}
|
||||
|
||||
public ItemUseRequestReservation BeginUseRequestReservation()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
return _inventory.BeginUseRequestReservation();
|
||||
}
|
||||
|
||||
public RuntimeInteractionDispatchResult TryDispatchUse(
|
||||
uint serverGuid,
|
||||
bool ownedByPlayer,
|
||||
bool useable,
|
||||
ItemUseRequestReservation? reservation,
|
||||
IRuntimeInteractionTransport transport,
|
||||
out uint sequence)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
ArgumentNullException.ThrowIfNull(transport);
|
||||
sequence = 0u;
|
||||
|
||||
if (serverGuid == 0u)
|
||||
{
|
||||
reservation?.CancelBeforeDispatch();
|
||||
return RuntimeInteractionDispatchResult.Rejected;
|
||||
}
|
||||
if (!transport.IsInWorld)
|
||||
{
|
||||
reservation?.CancelBeforeDispatch();
|
||||
return RuntimeInteractionDispatchResult.NotInWorld;
|
||||
}
|
||||
if (!ownedByPlayer && !useable)
|
||||
{
|
||||
reservation?.CancelBeforeDispatch();
|
||||
return RuntimeInteractionDispatchResult.NotUseable;
|
||||
}
|
||||
if (!transport.TrySendUse(serverGuid, out sequence))
|
||||
{
|
||||
reservation?.CancelBeforeDispatch();
|
||||
return RuntimeInteractionDispatchResult.Rejected;
|
||||
}
|
||||
|
||||
reservation?.MarkDispatched();
|
||||
_lastUseSourceId = serverGuid;
|
||||
_lastUseTargetId = 0u;
|
||||
IncrementRevision();
|
||||
return RuntimeInteractionDispatchResult.Dispatched;
|
||||
}
|
||||
|
||||
public bool TryDispatchTargetedUse(
|
||||
uint sourceObjectId,
|
||||
uint targetObjectId,
|
||||
Action<uint, uint>? dispatch,
|
||||
bool incrementBusy)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (sourceObjectId == 0u
|
||||
|| targetObjectId == 0u
|
||||
|| dispatch is null)
|
||||
return false;
|
||||
|
||||
uint epoch = _clearEpoch;
|
||||
dispatch(sourceObjectId, targetObjectId);
|
||||
if (_disposed || epoch != _clearEpoch)
|
||||
return false;
|
||||
|
||||
_lastUseSourceId = sourceObjectId;
|
||||
_lastUseTargetId = targetObjectId;
|
||||
if (incrementBusy)
|
||||
_inventory.IncrementBusyCount();
|
||||
IncrementRevision();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void IncrementBusyCount()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
_inventory.IncrementBusyCount();
|
||||
IncrementRevision();
|
||||
}
|
||||
|
||||
public void CompleteUse(uint error)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
int before = _inventory.BusyCount;
|
||||
_inventory.CompleteUse(error);
|
||||
if (_inventory.BusyCount != before)
|
||||
IncrementRevision();
|
||||
}
|
||||
|
||||
public bool TryRequestAppraisal(
|
||||
uint objectId,
|
||||
Action<uint> sendAppraisal)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
ArgumentNullException.ThrowIfNull(sendAppraisal);
|
||||
if (objectId == 0u)
|
||||
return false;
|
||||
|
||||
uint epoch = _clearEpoch;
|
||||
bool acquiredBusy = _awaitingAppraisalId == 0u;
|
||||
if (acquiredBusy)
|
||||
{
|
||||
_inventory.IncrementBusyCount();
|
||||
if (_disposed || epoch != _clearEpoch)
|
||||
return false;
|
||||
}
|
||||
|
||||
uint previousAwaiting = _awaitingAppraisalId;
|
||||
_awaitingAppraisalId = objectId;
|
||||
IncrementRevision();
|
||||
try
|
||||
{
|
||||
sendAppraisal(objectId);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// A synchronous reset/disposal or newer request owns the result.
|
||||
// Roll back only the exact request installed by this call.
|
||||
if (!_disposed
|
||||
&& epoch == _clearEpoch
|
||||
&& _awaitingAppraisalId == objectId)
|
||||
{
|
||||
_awaitingAppraisalId = previousAwaiting;
|
||||
if (acquiredBusy)
|
||||
_inventory.CompleteUse(0u);
|
||||
IncrementRevision();
|
||||
}
|
||||
throw;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public RuntimeAppraisalResponseAcceptance AcceptAppraisalResponse(
|
||||
uint objectId)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (objectId == 0u
|
||||
|| (objectId != _awaitingAppraisalId
|
||||
&& objectId != _currentAppraisalId))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
bool firstResponse = objectId == _awaitingAppraisalId;
|
||||
if (firstResponse)
|
||||
{
|
||||
_awaitingAppraisalId = 0u;
|
||||
_currentAppraisalId = objectId;
|
||||
_inventory.CompleteUse(0u);
|
||||
IncrementRevision();
|
||||
}
|
||||
|
||||
return new RuntimeAppraisalResponseAcceptance(
|
||||
Accepted: true,
|
||||
FirstResponse: firstResponse);
|
||||
}
|
||||
|
||||
public bool RefreshCurrentAppraisal(Action<uint> sendAppraisal)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
ArgumentNullException.ThrowIfNull(sendAppraisal);
|
||||
if (_currentAppraisalId == 0u)
|
||||
return false;
|
||||
sendAppraisal(_currentAppraisalId);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool CancelObjectAppraisalForSpell(Action<uint> sendAppraisal)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
ArgumentNullException.ThrowIfNull(sendAppraisal);
|
||||
if (_awaitingAppraisalId == 0u && _currentAppraisalId == 0u)
|
||||
return false;
|
||||
|
||||
if (_awaitingAppraisalId != 0u)
|
||||
_inventory.CompleteUse(0u);
|
||||
_awaitingAppraisalId = 0u;
|
||||
_currentAppraisalId = 0u;
|
||||
IncrementRevision();
|
||||
sendAppraisal(0u);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Enqueue(RuntimeQueuedInteraction interaction)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (interaction.Identity.ServerGuid == 0u)
|
||||
return;
|
||||
_outbound.Enqueue(interaction);
|
||||
IncrementRevision();
|
||||
}
|
||||
|
||||
public int CancelQueuedInteractions(
|
||||
uint serverGuid,
|
||||
uint? localEntityId = null)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (serverGuid == 0u || _outbound.Count == 0)
|
||||
return 0;
|
||||
|
||||
int original = _outbound.Count;
|
||||
int removed = 0;
|
||||
for (int i = 0; i < original; i++)
|
||||
{
|
||||
RuntimeQueuedInteraction interaction = _outbound.Dequeue();
|
||||
RuntimeInteractionIdentity identity = interaction.Identity;
|
||||
bool matches =
|
||||
identity.ServerGuid == serverGuid
|
||||
&& (localEntityId is not uint exact
|
||||
|| identity.LocalEntityId == exact);
|
||||
if (matches)
|
||||
removed++;
|
||||
else
|
||||
_outbound.Enqueue(interaction);
|
||||
}
|
||||
if (removed != 0)
|
||||
IncrementRevision();
|
||||
return removed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drains only work present at the frame boundary. Re-entrant additions
|
||||
/// remain for the next frame, matching the pre-J5 ordered interaction
|
||||
/// queue without retaining App delegates.
|
||||
/// </summary>
|
||||
public void DrainOutbound(Action<RuntimeQueuedInteraction> dispatch)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
ArgumentNullException.ThrowIfNull(dispatch);
|
||||
|
||||
int count = _outbound.Count;
|
||||
uint epoch = _clearEpoch;
|
||||
bool drained = false;
|
||||
while (count-- > 0 && epoch == _clearEpoch && _outbound.Count > 0)
|
||||
{
|
||||
RuntimeQueuedInteraction interaction = _outbound.Dequeue();
|
||||
drained = true;
|
||||
try
|
||||
{
|
||||
dispatch(interaction);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
Interlocked.Increment(ref _dispatchFailureCount);
|
||||
LastDispatchFailure = error;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
if (drained)
|
||||
IncrementRevision();
|
||||
}
|
||||
|
||||
public bool TryArmPostArrivalPickup(
|
||||
uint serverGuid,
|
||||
uint localEntityId,
|
||||
uint destinationContainerId,
|
||||
int placement,
|
||||
ulong pendingPlacementToken,
|
||||
RuntimeInteractionApproachToken approachToken,
|
||||
out RuntimePendingPickup pending)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (serverGuid == 0u
|
||||
|| localEntityId == 0u
|
||||
|| destinationContainerId == 0u
|
||||
|| pendingPlacementToken == 0u
|
||||
|| approachToken.ControllerLifetime == 0u
|
||||
|| approachToken.ApproachGeneration == 0u)
|
||||
{
|
||||
pending = default;
|
||||
return false;
|
||||
}
|
||||
if (_pendingPickup is not null)
|
||||
{
|
||||
pending = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
ulong token = ++_nextPickupToken;
|
||||
if (token == 0u)
|
||||
token = ++_nextPickupToken;
|
||||
pending = new RuntimePendingPickup(
|
||||
token,
|
||||
serverGuid,
|
||||
localEntityId,
|
||||
destinationContainerId,
|
||||
placement,
|
||||
pendingPlacementToken,
|
||||
approachToken);
|
||||
_pendingPickup = pending;
|
||||
IncrementRevision();
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryResolveApproachCompletion(
|
||||
RuntimeInteractionApproachToken approachToken,
|
||||
bool natural,
|
||||
out RuntimePendingPickup pending)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (_pendingPickup is not { } current
|
||||
|| current.ApproachToken != approachToken)
|
||||
{
|
||||
pending = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
_pendingPickup = null;
|
||||
pending = current;
|
||||
IncrementRevision();
|
||||
return natural;
|
||||
}
|
||||
|
||||
public bool TryGetPendingPickup(out RuntimePendingPickup pending)
|
||||
{
|
||||
if (_pendingPickup is { } current)
|
||||
{
|
||||
pending = current;
|
||||
return true;
|
||||
}
|
||||
pending = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryCancelPendingPickup(out RuntimePendingPickup pending)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (_pendingPickup is not { } current)
|
||||
{
|
||||
pending = default;
|
||||
return false;
|
||||
}
|
||||
_pendingPickup = null;
|
||||
pending = current;
|
||||
IncrementRevision();
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryCancelPendingPickup(
|
||||
uint serverGuid,
|
||||
uint? localEntityId,
|
||||
out RuntimePendingPickup pending)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (_pendingPickup is not { } current
|
||||
|| current.ServerGuid != serverGuid
|
||||
|| (localEntityId is uint exact
|
||||
&& current.LocalEntityId != exact))
|
||||
{
|
||||
pending = default;
|
||||
return false;
|
||||
}
|
||||
_pendingPickup = null;
|
||||
pending = current;
|
||||
IncrementRevision();
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryDispatchPickup(
|
||||
RuntimePendingPickup pickup,
|
||||
IRuntimeInteractionTransport transport,
|
||||
out uint sequence)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
ArgumentNullException.ThrowIfNull(transport);
|
||||
sequence = 0u;
|
||||
if (!transport.IsInWorld)
|
||||
return false;
|
||||
|
||||
uint sentSequence = 0u;
|
||||
bool dispatched = _inventory.TryDispatch(
|
||||
InventoryRequestKind.Pickup,
|
||||
pickup.ServerGuid,
|
||||
() => transport.TrySendPickup(
|
||||
pickup.ServerGuid,
|
||||
pickup.DestinationContainerId,
|
||||
pickup.Placement,
|
||||
out sentSequence),
|
||||
pickup.PendingPlacementToken);
|
||||
sequence = sentSequence;
|
||||
if (dispatched)
|
||||
IncrementRevision();
|
||||
return dispatched;
|
||||
}
|
||||
|
||||
public void ResetSession()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
ResetCore(resetInventory: true);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
ResetCore(resetInventory: true);
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
private void ResetCore(bool resetInventory)
|
||||
{
|
||||
bool changed =
|
||||
_awaitingAppraisalId != 0u
|
||||
|| _currentAppraisalId != 0u
|
||||
|| _outbound.Count != 0
|
||||
|| _pendingPickup is not null
|
||||
|| _lastUseSourceId != 0u
|
||||
|| _lastUseTargetId != 0u
|
||||
|| _lastUseMs != long.MinValue / 2;
|
||||
|
||||
_lastUseSourceId = 0u;
|
||||
_lastUseTargetId = 0u;
|
||||
_awaitingAppraisalId = 0u;
|
||||
_currentAppraisalId = 0u;
|
||||
_outbound.Clear();
|
||||
_pendingPickup = null;
|
||||
_lastUseMs = long.MinValue / 2;
|
||||
_clearEpoch++;
|
||||
if (resetInventory)
|
||||
_inventory.ResetSession();
|
||||
if (changed)
|
||||
IncrementRevision();
|
||||
}
|
||||
|
||||
private void IncrementRevision() =>
|
||||
Interlocked.Increment(ref _revision);
|
||||
}
|
||||
|
||||
public interface IRuntimeInteractionTransport
|
||||
{
|
||||
bool IsInWorld { get; }
|
||||
bool TrySendUse(uint serverGuid, out uint sequence);
|
||||
bool TrySendPickup(
|
||||
uint itemGuid,
|
||||
uint destinationContainerId,
|
||||
int placement,
|
||||
out uint sequence);
|
||||
}
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
using AcDream.App.Input;
|
||||
|
||||
namespace AcDream.App.Tests.Input;
|
||||
|
||||
public sealed class OutboundInteractionQueueTests
|
||||
{
|
||||
[Fact]
|
||||
public void Drain_PreservesInputOrder()
|
||||
{
|
||||
var queue = new OutboundInteractionQueue();
|
||||
var actual = new List<string>();
|
||||
|
||||
queue.Enqueue(() => actual.Add("use-corpse"));
|
||||
queue.Enqueue(() => actual.Add("pickup-item"));
|
||||
|
||||
queue.Drain();
|
||||
|
||||
Assert.Equal(new[] { "use-corpse", "pickup-item" }, actual);
|
||||
Assert.Equal(0, queue.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Drain_ReentrantEnqueueWaitsForNextFrame()
|
||||
{
|
||||
var queue = new OutboundInteractionQueue();
|
||||
var actual = new List<string>();
|
||||
queue.Enqueue(() =>
|
||||
{
|
||||
actual.Add("first");
|
||||
queue.Enqueue(() => actual.Add("next-frame"));
|
||||
});
|
||||
|
||||
queue.Drain();
|
||||
|
||||
Assert.Equal(new[] { "first" }, actual);
|
||||
Assert.Equal(1, queue.Count);
|
||||
|
||||
queue.Drain();
|
||||
|
||||
Assert.Equal(new[] { "first", "next-frame" }, actual);
|
||||
Assert.Equal(0, queue.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clear_DropsPendingSessionActions()
|
||||
{
|
||||
var queue = new OutboundInteractionQueue();
|
||||
bool invoked = false;
|
||||
queue.Enqueue(() => invoked = true);
|
||||
|
||||
queue.Clear();
|
||||
queue.Drain();
|
||||
|
||||
Assert.False(invoked);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Drain_ReentrantClearStopsTheCapturedBatch()
|
||||
{
|
||||
var queue = new OutboundInteractionQueue();
|
||||
var actual = new List<string>();
|
||||
queue.Enqueue(() =>
|
||||
{
|
||||
actual.Add("first");
|
||||
queue.Clear();
|
||||
});
|
||||
queue.Enqueue(() => actual.Add("stale"));
|
||||
|
||||
queue.Drain();
|
||||
|
||||
Assert.Equal(new[] { "first" }, actual);
|
||||
Assert.Equal(0, queue.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Drain_ReentrantClearAndEnqueueDefersTheNewEpoch()
|
||||
{
|
||||
var queue = new OutboundInteractionQueue();
|
||||
var actual = new List<string>();
|
||||
queue.Enqueue(() =>
|
||||
{
|
||||
actual.Add("first");
|
||||
queue.Clear();
|
||||
queue.Enqueue(() => actual.Add("new-session"));
|
||||
});
|
||||
queue.Enqueue(() => actual.Add("stale"));
|
||||
|
||||
queue.Drain();
|
||||
Assert.Equal(new[] { "first" }, actual);
|
||||
Assert.Equal(1, queue.Count);
|
||||
|
||||
queue.Drain();
|
||||
Assert.Equal(new[] { "first", "new-session" }, actual);
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ using AcDream.Core.Net.Messages;
|
|||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Selection;
|
||||
using AcDream.Core.World;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
using AcDream.UI.Abstractions.Input;
|
||||
|
||||
namespace AcDream.App.Tests.Interaction;
|
||||
|
|
@ -69,7 +70,7 @@ public sealed class SelectionInteractionControllerTests
|
|||
}
|
||||
}
|
||||
|
||||
private sealed class Transport : ISelectionInteractionTransport
|
||||
private sealed class Transport : IRuntimeInteractionTransport
|
||||
{
|
||||
private uint _sequence;
|
||||
public bool IsInWorld { get; set; } = true;
|
||||
|
|
@ -160,7 +161,7 @@ public sealed class SelectionInteractionControllerTests
|
|||
});
|
||||
Items = new ItemInteractionController(
|
||||
Objects,
|
||||
new InventoryTransactionState(Objects),
|
||||
new AcDream.Runtime.Gameplay.RuntimeInteractionTransactionState(new InventoryTransactionState(Objects)),
|
||||
new InteractionState(),
|
||||
() => Player,
|
||||
sendUse: null,
|
||||
|
|
@ -464,6 +465,21 @@ public sealed class SelectionInteractionControllerTests
|
|||
Assert.Equal(new[] { Target }, stale.Transport.Uses);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HiddenTargetCancelsQueuedUseBeforeFrameDrain()
|
||||
{
|
||||
var h = new Harness();
|
||||
h.Selection.Select(Target, SelectionChangeSource.World);
|
||||
h.Controller.HandleInputAction(InputAction.UseSelected);
|
||||
|
||||
h.Controller.OnEntityHidden(Target);
|
||||
h.Controller.DrainOutbound();
|
||||
|
||||
Assert.Null(h.Selection.SelectedObjectId);
|
||||
Assert.Empty(h.Transport.Uses);
|
||||
Assert.Equal(0, h.Items.BusyCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PickupInputWaitsForFrameDrainThenUsesPendingDestination()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -681,7 +681,7 @@ public sealed class CurrentGameRuntimeAdapterTests
|
|||
EntityObjects);
|
||||
Objects = EntityObjects.Objects;
|
||||
Communication = new RuntimeCommunicationState();
|
||||
Actions = new RuntimeActionState();
|
||||
Actions = new RuntimeActionState(InventoryState.Transactions);
|
||||
MovementInput = new DispatcherMovementInputSource();
|
||||
GameplayInput = new GameplayInputFrameController(
|
||||
dispatcher: null,
|
||||
|
|
@ -701,7 +701,7 @@ public sealed class CurrentGameRuntimeAdapterTests
|
|||
|
||||
_items = new ItemInteractionController(
|
||||
Objects,
|
||||
InventoryState.Transactions,
|
||||
Actions.Transactions,
|
||||
Actions.Interaction,
|
||||
() => PlayerGuid,
|
||||
sendUse: null,
|
||||
|
|
@ -766,11 +766,11 @@ public sealed class CurrentGameRuntimeAdapterTests
|
|||
{
|
||||
Runtime.Dispose();
|
||||
Character.Dispose();
|
||||
InventoryState.Dispose();
|
||||
Communication.Dispose();
|
||||
_session.Dispose();
|
||||
_items.Dispose();
|
||||
Actions.Dispose();
|
||||
InventoryState.Dispose();
|
||||
Entities.Clear();
|
||||
}
|
||||
}
|
||||
|
|
@ -1149,7 +1149,7 @@ public sealed class CurrentGameRuntimeAdapterTests
|
|||
}
|
||||
|
||||
private sealed class SelectionTransport(Func<bool> isInWorld)
|
||||
: ISelectionInteractionTransport
|
||||
: IRuntimeInteractionTransport
|
||||
{
|
||||
public bool IsInWorld => isInWorld();
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,11 @@ public sealed class RuntimeActionOwnershipTests
|
|||
string program = ReadAppSource(root, "Program.cs");
|
||||
|
||||
Assert.Contains(
|
||||
"private readonly RuntimeActionState _runtimeActions = new();",
|
||||
"private readonly RuntimeActionState _runtimeActions;",
|
||||
gameWindow,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"_runtimeActions = new RuntimeActionState(_runtimeInventory.Transactions);",
|
||||
gameWindow,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
|
|
@ -54,6 +58,9 @@ public sealed class RuntimeActionOwnershipTests
|
|||
Assert.Empty(Regex.Matches(
|
||||
production,
|
||||
@"\bnew\s+(?:AcDream\.Runtime\.Gameplay\.)?InteractionState\s*\("));
|
||||
Assert.Empty(Regex.Matches(
|
||||
production,
|
||||
@"\bnew\s+(?:AcDream\.Runtime\.Gameplay\.)?RuntimeInteractionTransactionState\s*\("));
|
||||
Assert.Equal(
|
||||
1,
|
||||
Regex.Matches(
|
||||
|
|
@ -94,14 +101,19 @@ public sealed class RuntimeActionOwnershipTests
|
|||
"GameWindowLifetime.cs");
|
||||
|
||||
Assert.Contains("d.Actions.Interaction,", ui, StringComparison.Ordinal);
|
||||
Assert.Contains("d.Actions.Transactions,", ui, StringComparison.Ordinal);
|
||||
Assert.Contains("d.Actions.Selection", ui, StringComparison.Ordinal);
|
||||
Assert.Contains("d.Actions.Combat", ui, StringComparison.Ordinal);
|
||||
Assert.Contains("d.Actions.Selection", session, StringComparison.Ordinal);
|
||||
Assert.Contains("d.Actions.Combat", session, StringComparison.Ordinal);
|
||||
Assert.Contains("_domain.Actions.Combat", liveSession, StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"_domain.Actions.Transactions.CompleteUse(error)",
|
||||
liveSession,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("_actions.Selection", commands, StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"InteractionState interactionState,",
|
||||
"RuntimeInteractionTransactionState runtimeTransactions,",
|
||||
itemInteraction,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(
|
||||
|
|
@ -114,8 +126,8 @@ public sealed class RuntimeActionOwnershipTests
|
|||
StringComparison.Ordinal);
|
||||
AssertAppearsInOrder(
|
||||
shutdown,
|
||||
"\"runtime inventory state\"",
|
||||
"\"runtime action state\"",
|
||||
"\"runtime inventory state\"",
|
||||
"\"runtime entity/object lifetime\"");
|
||||
Assert.False(File.Exists(Path.Combine(
|
||||
root,
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ public sealed class RuntimeInventoryOwnershipTests
|
|||
"ItemInteractionController.cs");
|
||||
|
||||
Assert.Contains(
|
||||
"d.Inventory.Transactions",
|
||||
"d.Actions.Transactions",
|
||||
ui,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(
|
||||
|
|
@ -62,7 +62,7 @@ public sealed class RuntimeInventoryOwnershipTests
|
|||
itemInteraction,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"InventoryTransactionState transactions,",
|
||||
"RuntimeInteractionTransactionState runtimeTransactions,",
|
||||
itemInteraction,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
|
|
@ -70,7 +70,7 @@ public sealed class RuntimeInventoryOwnershipTests
|
|||
session,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"_domain.Inventory.Transactions.CompleteUse(error)",
|
||||
"_domain.Actions.Transactions.CompleteUse(error)",
|
||||
session,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(
|
||||
|
|
@ -79,6 +79,7 @@ public sealed class RuntimeInventoryOwnershipTests
|
|||
StringComparison.Ordinal);
|
||||
AssertAppearsInOrder(
|
||||
shutdown,
|
||||
"\"runtime action state\"",
|
||||
"\"runtime inventory state\"",
|
||||
"\"runtime entity/object lifetime\"");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ public sealed class CursorFeedbackControllerTests
|
|||
var objects = SeedTargetObjects();
|
||||
var interaction = new ItemInteractionController(
|
||||
objects,
|
||||
new InventoryTransactionState(objects),
|
||||
new AcDream.Runtime.Gameplay.RuntimeInteractionTransactionState(new InventoryTransactionState(objects)),
|
||||
new InteractionState(),
|
||||
playerGuid: () => Player,
|
||||
sendUse: null,
|
||||
|
|
@ -190,7 +190,7 @@ public sealed class CursorFeedbackControllerTests
|
|||
objects.Get(Source)!.TargetType = (uint)ItemType.Misc; // a tool that targets items
|
||||
var interaction = new ItemInteractionController(
|
||||
objects,
|
||||
new InventoryTransactionState(objects),
|
||||
new AcDream.Runtime.Gameplay.RuntimeInteractionTransactionState(new InventoryTransactionState(objects)),
|
||||
new InteractionState(),
|
||||
playerGuid: () => Player,
|
||||
sendUse: null,
|
||||
|
|
@ -230,7 +230,7 @@ public sealed class CursorFeedbackControllerTests
|
|||
var objects = SeedTargetObjects();
|
||||
var interaction = new ItemInteractionController(
|
||||
objects,
|
||||
new InventoryTransactionState(objects),
|
||||
new AcDream.Runtime.Gameplay.RuntimeInteractionTransactionState(new InventoryTransactionState(objects)),
|
||||
new InteractionState(),
|
||||
playerGuid: () => Player,
|
||||
sendUse: null,
|
||||
|
|
@ -260,7 +260,7 @@ public sealed class CursorFeedbackControllerTests
|
|||
var objects = SeedTargetObjects();
|
||||
var interaction = new ItemInteractionController(
|
||||
objects,
|
||||
new InventoryTransactionState(objects),
|
||||
new AcDream.Runtime.Gameplay.RuntimeInteractionTransactionState(new InventoryTransactionState(objects)),
|
||||
new InteractionState(),
|
||||
playerGuid: () => Player,
|
||||
sendUse: null,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using AcDream.App.UI;
|
||||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
|
||||
namespace AcDream.App.Tests.UI;
|
||||
|
||||
|
|
@ -32,6 +33,7 @@ public sealed class ItemInteractionControllerTests
|
|||
public readonly CombatState Combat = new();
|
||||
public readonly StackSplitQuantityState SplitQuantity = new();
|
||||
public readonly InventoryTransactionState SharedTransactions;
|
||||
public readonly RuntimeInteractionTransactionState RuntimeTransactions;
|
||||
public uint SelectedObject;
|
||||
public bool NonCombatMode;
|
||||
public bool DragOnPlayerOpensSecureTrade = true;
|
||||
|
|
@ -56,10 +58,12 @@ public sealed class ItemInteractionControllerTests
|
|||
});
|
||||
Objects.MoveItem(Pack, Player, 0);
|
||||
SharedTransactions = new InventoryTransactionState(Objects);
|
||||
RuntimeTransactions = new RuntimeInteractionTransactionState(
|
||||
SharedTransactions);
|
||||
|
||||
Controller = new ItemInteractionController(
|
||||
Objects,
|
||||
SharedTransactions,
|
||||
RuntimeTransactions,
|
||||
new InteractionState(),
|
||||
playerGuid: () => Player,
|
||||
sendUse: requestUse is null ? Uses.Add : null,
|
||||
|
|
@ -338,10 +342,12 @@ public sealed class ItemInteractionControllerTests
|
|||
var objects = new ClientObjectTable();
|
||||
using var transactions =
|
||||
new InventoryTransactionState(new ClientObjectTable());
|
||||
using var runtimeTransactions =
|
||||
new RuntimeInteractionTransactionState(transactions);
|
||||
|
||||
Assert.Throws<ArgumentException>(() => new ItemInteractionController(
|
||||
objects,
|
||||
transactions,
|
||||
runtimeTransactions,
|
||||
new InteractionState(),
|
||||
playerGuid: () => Player,
|
||||
sendUse: null,
|
||||
|
|
|
|||
|
|
@ -858,7 +858,7 @@ public sealed class AppraisalUiControllerTests
|
|||
List<uint> sent)
|
||||
=> new(
|
||||
objects,
|
||||
new InventoryTransactionState(objects),
|
||||
new AcDream.Runtime.Gameplay.RuntimeInteractionTransactionState(new InventoryTransactionState(objects)),
|
||||
new InteractionState(),
|
||||
playerGuid: () => 0x50000002u,
|
||||
sendUse: null,
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ public sealed class ExternalContainerControllerTests
|
|||
|
||||
Interaction = new ItemInteractionController(
|
||||
Objects,
|
||||
new InventoryTransactionState(Objects),
|
||||
new AcDream.Runtime.Gameplay.RuntimeInteractionTransactionState(new InventoryTransactionState(Objects)),
|
||||
new InteractionState(),
|
||||
playerGuid: () => Player,
|
||||
sendUse: Uses.Add,
|
||||
|
|
|
|||
|
|
@ -508,7 +508,7 @@ public class InventoryControllerTests
|
|||
var appraisals = new List<uint>();
|
||||
using var interaction = new ItemInteractionController(
|
||||
objects,
|
||||
new InventoryTransactionState(objects),
|
||||
new AcDream.Runtime.Gameplay.RuntimeInteractionTransactionState(new InventoryTransactionState(objects)),
|
||||
new InteractionState(),
|
||||
playerGuid: () => Player,
|
||||
sendUse: null,
|
||||
|
|
@ -546,7 +546,7 @@ public class InventoryControllerTests
|
|||
objects.Get(0xA)!.Useability = 0x000A0008u;
|
||||
var interaction = new ItemInteractionController(
|
||||
objects,
|
||||
new InventoryTransactionState(objects),
|
||||
new AcDream.Runtime.Gameplay.RuntimeInteractionTransactionState(new InventoryTransactionState(objects)),
|
||||
new InteractionState(),
|
||||
playerGuid: () => Player,
|
||||
sendUse: null,
|
||||
|
|
@ -716,7 +716,7 @@ public class InventoryControllerTests
|
|||
var pickups = new List<(uint item, uint container, int placement)>();
|
||||
using var interaction = new ItemInteractionController(
|
||||
objects,
|
||||
new InventoryTransactionState(objects),
|
||||
new AcDream.Runtime.Gameplay.RuntimeInteractionTransactionState(new InventoryTransactionState(objects)),
|
||||
new InteractionState(),
|
||||
playerGuid: () => Player,
|
||||
sendUse: null,
|
||||
|
|
@ -762,7 +762,7 @@ public class InventoryControllerTests
|
|||
var eventOrder = new List<(string Kind, uint Item, ulong Token)>();
|
||||
using var interaction = new ItemInteractionController(
|
||||
objects,
|
||||
new InventoryTransactionState(objects),
|
||||
new AcDream.Runtime.Gameplay.RuntimeInteractionTransactionState(new InventoryTransactionState(objects)),
|
||||
new InteractionState(),
|
||||
playerGuid: () => Player,
|
||||
sendUse: null,
|
||||
|
|
@ -815,7 +815,7 @@ public class InventoryControllerTests
|
|||
var eventOrder = new List<(string Kind, uint Item)>();
|
||||
using var interaction = new ItemInteractionController(
|
||||
objects,
|
||||
new InventoryTransactionState(objects),
|
||||
new AcDream.Runtime.Gameplay.RuntimeInteractionTransactionState(new InventoryTransactionState(objects)),
|
||||
new InteractionState(),
|
||||
playerGuid: () => Player,
|
||||
sendUse: null,
|
||||
|
|
@ -874,7 +874,7 @@ public class InventoryControllerTests
|
|||
var messages = new List<string>();
|
||||
using var interaction = new ItemInteractionController(
|
||||
objects,
|
||||
new InventoryTransactionState(objects),
|
||||
new AcDream.Runtime.Gameplay.RuntimeInteractionTransactionState(new InventoryTransactionState(objects)),
|
||||
new InteractionState(),
|
||||
playerGuid: () => Player,
|
||||
sendUse: null,
|
||||
|
|
@ -944,7 +944,7 @@ public class InventoryControllerTests
|
|||
var messages = new List<string>();
|
||||
using var interaction = new ItemInteractionController(
|
||||
objects,
|
||||
new InventoryTransactionState(objects),
|
||||
new AcDream.Runtime.Gameplay.RuntimeInteractionTransactionState(new InventoryTransactionState(objects)),
|
||||
new InteractionState(),
|
||||
playerGuid: () => Player,
|
||||
sendUse: null,
|
||||
|
|
@ -1008,7 +1008,7 @@ public class InventoryControllerTests
|
|||
var merges = new List<(uint Source, uint Target, uint Amount)>();
|
||||
using var interaction = new ItemInteractionController(
|
||||
objects,
|
||||
new InventoryTransactionState(objects),
|
||||
new AcDream.Runtime.Gameplay.RuntimeInteractionTransactionState(new InventoryTransactionState(objects)),
|
||||
new InteractionState(),
|
||||
playerGuid: () => Player,
|
||||
sendUse: null,
|
||||
|
|
@ -1069,7 +1069,7 @@ public class InventoryControllerTests
|
|||
var splits = new List<(uint Item, uint Container, uint Placement, uint Amount)>();
|
||||
using var interaction = new ItemInteractionController(
|
||||
objects,
|
||||
new InventoryTransactionState(objects),
|
||||
new AcDream.Runtime.Gameplay.RuntimeInteractionTransactionState(new InventoryTransactionState(objects)),
|
||||
new InteractionState(),
|
||||
playerGuid: () => Player,
|
||||
sendUse: null,
|
||||
|
|
@ -1117,7 +1117,7 @@ public class InventoryControllerTests
|
|||
var puts = new List<(uint Item, uint Container, int Placement)>();
|
||||
using var interaction = new ItemInteractionController(
|
||||
objects,
|
||||
new InventoryTransactionState(objects),
|
||||
new AcDream.Runtime.Gameplay.RuntimeInteractionTransactionState(new InventoryTransactionState(objects)),
|
||||
new InteractionState(),
|
||||
playerGuid: () => Player,
|
||||
sendUse: null,
|
||||
|
|
@ -1158,7 +1158,7 @@ public class InventoryControllerTests
|
|||
SeedContained(objects, loot, chest, slot: 0, type: ItemType.Misc);
|
||||
using var interaction = new ItemInteractionController(
|
||||
objects,
|
||||
new InventoryTransactionState(objects),
|
||||
new AcDream.Runtime.Gameplay.RuntimeInteractionTransactionState(new InventoryTransactionState(objects)),
|
||||
new InteractionState(),
|
||||
playerGuid: () => Player,
|
||||
sendUse: null,
|
||||
|
|
@ -1198,7 +1198,7 @@ public class InventoryControllerTests
|
|||
SeedContained(objects, loot, chest, slot: 0, type: ItemType.Misc);
|
||||
using var interaction = new ItemInteractionController(
|
||||
objects,
|
||||
new InventoryTransactionState(objects),
|
||||
new AcDream.Runtime.Gameplay.RuntimeInteractionTransactionState(new InventoryTransactionState(objects)),
|
||||
new InteractionState(),
|
||||
playerGuid: () => Player,
|
||||
sendUse: null,
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ public class PaperdollControllerTests
|
|||
{
|
||||
var itemInteraction = new ItemInteractionController(
|
||||
objects,
|
||||
new InventoryTransactionState(objects),
|
||||
new AcDream.Runtime.Gameplay.RuntimeInteractionTransactionState(new InventoryTransactionState(objects)),
|
||||
new InteractionState(),
|
||||
() => Player,
|
||||
sendUse: null,
|
||||
|
|
|
|||
|
|
@ -265,7 +265,7 @@ public class ToolbarControllerTests
|
|||
var useWithTarget = new List<(uint Source, uint Target)>();
|
||||
var interaction = new ItemInteractionController(
|
||||
repo,
|
||||
new InventoryTransactionState(repo),
|
||||
new AcDream.Runtime.Gameplay.RuntimeInteractionTransactionState(new InventoryTransactionState(repo)),
|
||||
new InteractionState(),
|
||||
playerGuid: () => player,
|
||||
sendUse: null,
|
||||
|
|
@ -332,7 +332,7 @@ public class ToolbarControllerTests
|
|||
var directPuts = new List<(uint Item, uint Container, int Placement)>();
|
||||
using var interaction = new ItemInteractionController(
|
||||
repo,
|
||||
new InventoryTransactionState(repo),
|
||||
new AcDream.Runtime.Gameplay.RuntimeInteractionTransactionState(new InventoryTransactionState(repo)),
|
||||
new InteractionState(),
|
||||
playerGuid: () => player,
|
||||
sendUse: null,
|
||||
|
|
@ -382,7 +382,7 @@ public class ToolbarControllerTests
|
|||
var messages = new List<string>();
|
||||
using var interaction = new ItemInteractionController(
|
||||
repo,
|
||||
new InventoryTransactionState(repo),
|
||||
new AcDream.Runtime.Gameplay.RuntimeInteractionTransactionState(new InventoryTransactionState(repo)),
|
||||
new InteractionState(),
|
||||
playerGuid: () => player,
|
||||
sendUse: null,
|
||||
|
|
@ -512,7 +512,7 @@ public class ToolbarControllerTests
|
|||
uint selected = item;
|
||||
var interaction = new ItemInteractionController(
|
||||
repo,
|
||||
new InventoryTransactionState(repo),
|
||||
new AcDream.Runtime.Gameplay.RuntimeInteractionTransactionState(new InventoryTransactionState(repo)),
|
||||
new InteractionState(),
|
||||
() => player,
|
||||
sendUse: uses.Add,
|
||||
|
|
@ -571,7 +571,7 @@ public class ToolbarControllerTests
|
|||
var selection = new SelectionState();
|
||||
using var interaction = new ItemInteractionController(
|
||||
repo,
|
||||
new InventoryTransactionState(repo),
|
||||
new AcDream.Runtime.Gameplay.RuntimeInteractionTransactionState(new InventoryTransactionState(repo)),
|
||||
new InteractionState(),
|
||||
() => player,
|
||||
sendUse: null,
|
||||
|
|
@ -637,7 +637,7 @@ public class ToolbarControllerTests
|
|||
var wields = new List<(uint Item, uint Location)>();
|
||||
using var interaction = new ItemInteractionController(
|
||||
repo,
|
||||
new InventoryTransactionState(repo),
|
||||
new AcDream.Runtime.Gameplay.RuntimeInteractionTransactionState(new InventoryTransactionState(repo)),
|
||||
new InteractionState(),
|
||||
() => player,
|
||||
sendUse: null,
|
||||
|
|
@ -804,7 +804,7 @@ public class ToolbarControllerTests
|
|||
var appraisals = new List<uint>();
|
||||
using var interaction = new ItemInteractionController(
|
||||
repo,
|
||||
new InventoryTransactionState(repo),
|
||||
new AcDream.Runtime.Gameplay.RuntimeInteractionTransactionState(new InventoryTransactionState(repo)),
|
||||
new InteractionState(),
|
||||
playerGuid: () => player,
|
||||
sendUse: null,
|
||||
|
|
@ -854,7 +854,7 @@ public class ToolbarControllerTests
|
|||
uint sentTarget = 0;
|
||||
var interaction = new ItemInteractionController(
|
||||
repo,
|
||||
new InventoryTransactionState(repo),
|
||||
new AcDream.Runtime.Gameplay.RuntimeInteractionTransactionState(new InventoryTransactionState(repo)),
|
||||
new InteractionState(),
|
||||
() => player,
|
||||
sendUse: null,
|
||||
|
|
@ -897,7 +897,7 @@ public class ToolbarControllerTests
|
|||
repo.MoveItem(item, pack, 0);
|
||||
var interaction = new ItemInteractionController(
|
||||
repo,
|
||||
new InventoryTransactionState(repo),
|
||||
new AcDream.Runtime.Gameplay.RuntimeInteractionTransactionState(new InventoryTransactionState(repo)),
|
||||
new InteractionState(),
|
||||
() => player,
|
||||
sendUse: null,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ public sealed class RetailItemConfirmationControllerTests
|
|||
var uses = new List<uint>();
|
||||
var items = new ItemInteractionController(
|
||||
objects,
|
||||
new InventoryTransactionState(objects),
|
||||
new AcDream.Runtime.Gameplay.RuntimeInteractionTransactionState(new InventoryTransactionState(objects)),
|
||||
new InteractionState(),
|
||||
playerGuid: () => Player,
|
||||
sendUse: uses.Add,
|
||||
|
|
@ -54,7 +54,7 @@ public sealed class RetailItemConfirmationControllerTests
|
|||
var uses = new List<uint>();
|
||||
var items = new ItemInteractionController(
|
||||
objects,
|
||||
new InventoryTransactionState(objects),
|
||||
new AcDream.Runtime.Gameplay.RuntimeInteractionTransactionState(new InventoryTransactionState(objects)),
|
||||
new InteractionState(),
|
||||
playerGuid: () => Player,
|
||||
sendUse: uses.Add,
|
||||
|
|
@ -84,7 +84,7 @@ public sealed class RetailItemConfirmationControllerTests
|
|||
var messages = new List<string>();
|
||||
var items = new ItemInteractionController(
|
||||
objects,
|
||||
new InventoryTransactionState(objects),
|
||||
new AcDream.Runtime.Gameplay.RuntimeInteractionTransactionState(new InventoryTransactionState(objects)),
|
||||
new InteractionState(),
|
||||
playerGuid: () => Player,
|
||||
sendUse: uses.Add,
|
||||
|
|
|
|||
|
|
@ -163,7 +163,7 @@ public sealed class RetailUiInteractionFlowTests
|
|||
{
|
||||
var interaction = new ItemInteractionController(
|
||||
Objects,
|
||||
new InventoryTransactionState(Objects),
|
||||
new AcDream.Runtime.Gameplay.RuntimeInteractionTransactionState(new InventoryTransactionState(Objects)),
|
||||
new InteractionState(),
|
||||
playerGuid: () => Player,
|
||||
sendUse: Uses.Add,
|
||||
|
|
@ -199,7 +199,7 @@ public sealed class RetailUiInteractionFlowTests
|
|||
{
|
||||
itemInteraction ??= new ItemInteractionController(
|
||||
Objects,
|
||||
new InventoryTransactionState(Objects),
|
||||
new AcDream.Runtime.Gameplay.RuntimeInteractionTransactionState(new InventoryTransactionState(Objects)),
|
||||
new InteractionState(),
|
||||
() => Player,
|
||||
sendUse: null,
|
||||
|
|
|
|||
|
|
@ -179,7 +179,8 @@ public sealed class GameRuntimeContractTests
|
|||
TrackedTargetHealthCount: 1,
|
||||
InteractionRevision: 16,
|
||||
InteractionMode: InteractionModeKind.Use,
|
||||
InteractionSourceObjectId: 0u),
|
||||
InteractionSourceObjectId: 0u,
|
||||
InteractionTransactions: default),
|
||||
default,
|
||||
default);
|
||||
|
||||
|
|
@ -190,7 +191,8 @@ public sealed class GameRuntimeContractTests
|
|||
Assert.Contains("character=5:6:7:8:0:0", entry.Text);
|
||||
Assert.Contains("social=9:10:11", entry.Text);
|
||||
Assert.Contains(
|
||||
"actions=14:50000001:15:4:1:16:1:00000000",
|
||||
"actions=14:50000001:15:4:1:16:1:00000000:0:00000000:" +
|
||||
"00000000:00000000:00000000:0:0",
|
||||
entry.Text);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Selection;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
|
||||
|
|
@ -9,7 +10,8 @@ public sealed class RuntimeActionStateTests
|
|||
[Fact]
|
||||
public void ViewProjectsTheExactCanonicalChildrenAndRevisions()
|
||||
{
|
||||
using var actions = new RuntimeActionState();
|
||||
using var inventory = NewInventoryTransactions();
|
||||
using var actions = new RuntimeActionState(inventory);
|
||||
|
||||
actions.Selection.Select(
|
||||
0x50000001u,
|
||||
|
|
@ -38,7 +40,8 @@ public sealed class RuntimeActionStateTests
|
|||
[Fact]
|
||||
public void ResetSessionConvergesEveryChildAfterObserverFailures()
|
||||
{
|
||||
var actions = new RuntimeActionState();
|
||||
using var inventory = NewInventoryTransactions();
|
||||
var actions = new RuntimeActionState(inventory);
|
||||
actions.Selection.Select(
|
||||
0x50000001u,
|
||||
SelectionChangeSource.World);
|
||||
|
|
@ -69,7 +72,8 @@ public sealed class RuntimeActionStateTests
|
|||
[Fact]
|
||||
public void DisposeIsTerminalAndConvergedAfterObserverFailures()
|
||||
{
|
||||
var actions = new RuntimeActionState();
|
||||
using var inventory = NewInventoryTransactions();
|
||||
var actions = new RuntimeActionState(inventory);
|
||||
actions.Selection.Select(
|
||||
0x50000001u,
|
||||
SelectionChangeSource.World);
|
||||
|
|
@ -94,8 +98,10 @@ public sealed class RuntimeActionStateTests
|
|||
[Fact]
|
||||
public void InstancesAreFullyIsolated()
|
||||
{
|
||||
using var first = new RuntimeActionState();
|
||||
using var second = new RuntimeActionState();
|
||||
using var firstInventory = NewInventoryTransactions();
|
||||
using var secondInventory = NewInventoryTransactions();
|
||||
using var first = new RuntimeActionState(firstInventory);
|
||||
using var second = new RuntimeActionState(secondInventory);
|
||||
|
||||
first.Selection.Select(
|
||||
0x50000001u,
|
||||
|
|
@ -114,7 +120,21 @@ public sealed class RuntimeActionStateTests
|
|||
0,
|
||||
0,
|
||||
InteractionModeKind.None,
|
||||
0u),
|
||||
0u,
|
||||
new RuntimeInteractionTransactionSnapshot(
|
||||
IsDisposed: false,
|
||||
Revision: 0,
|
||||
LastUseSourceId: 0u,
|
||||
LastUseTargetId: 0u,
|
||||
AwaitingAppraisalId: 0u,
|
||||
CurrentAppraisalId: 0u,
|
||||
OutboundCount: 0,
|
||||
HasPendingPickup: false,
|
||||
PendingPickupToken: 0u,
|
||||
DispatchFailureCount: 0)),
|
||||
second.View.Snapshot);
|
||||
}
|
||||
|
||||
private static InventoryTransactionState NewInventoryTransactions() =>
|
||||
new(new ClientObjectTable());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ public sealed class RuntimeGameplayOwnershipTests
|
|||
var inventory = new RuntimeInventoryState(entities);
|
||||
var character = new RuntimeCharacterState();
|
||||
var communication = new RuntimeCommunicationState();
|
||||
var actions = new RuntimeActionState();
|
||||
var actions = new RuntimeActionState(inventory.Transactions);
|
||||
inventory.Shortcuts.Changed += static () => { };
|
||||
inventory.Shortcuts.Load([new ShortcutEntry(1, 2u, 3u)]);
|
||||
inventory.ItemMana.OnQueryItemManaResponse(2u, 0.5f, true);
|
||||
|
|
@ -114,7 +114,7 @@ public sealed class RuntimeGameplayOwnershipTests
|
|||
var inventory = new RuntimeInventoryState(entities);
|
||||
var character = new RuntimeCharacterState();
|
||||
var communication = new RuntimeCommunicationState();
|
||||
var actions = new RuntimeActionState();
|
||||
var actions = new RuntimeActionState(inventory.Transactions);
|
||||
|
||||
inventory.ExternalContainers.RequestOpen(0x70000001u);
|
||||
inventory.ExternalContainers.ApplyViewContents(0x70000001u);
|
||||
|
|
@ -132,10 +132,10 @@ public sealed class RuntimeGameplayOwnershipTests
|
|||
communication.Chat.OnSystemMessage("failure-isolated event", 0u);
|
||||
Assert.Equal(1, communication.DispatchFailureCount);
|
||||
|
||||
actions.Dispose();
|
||||
Assert.Throws<AggregateException>(inventory.Dispose);
|
||||
Assert.Throws<AggregateException>(character.Dispose);
|
||||
communication.Dispose();
|
||||
actions.Dispose();
|
||||
|
||||
RuntimeGameplayOwnershipSnapshot retired =
|
||||
RuntimeGameplayOwnership.Capture(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,447 @@
|
|||
using AcDream.Core.Items;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
|
||||
namespace AcDream.Runtime.Tests.Gameplay;
|
||||
|
||||
public sealed class RuntimeInteractionTransactionStateTests
|
||||
{
|
||||
private const uint Item = 0x70000001u;
|
||||
private const uint Container = 0x50000001u;
|
||||
|
||||
[Fact]
|
||||
public void UseThrottleMatchesRetailStrictBoundaryAndResets()
|
||||
{
|
||||
using var inventory = NewInventory(out _);
|
||||
using var state = new RuntimeInteractionTransactionState(inventory);
|
||||
|
||||
Assert.True(state.TryConsumeUseThrottle(1_000));
|
||||
Assert.False(state.TryConsumeUseThrottle(1_199));
|
||||
Assert.True(state.TryConsumeUseThrottle(1_200));
|
||||
|
||||
state.ResetSession();
|
||||
|
||||
Assert.True(state.TryConsumeUseThrottle(1_200));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OrdinaryUseTransfersBusyReferenceToAuthoritativeUseDone()
|
||||
{
|
||||
using var inventory = NewInventory(out _);
|
||||
using var state = new RuntimeInteractionTransactionState(inventory);
|
||||
var transport = new Transport();
|
||||
ItemUseRequestReservation reservation =
|
||||
state.BeginUseRequestReservation();
|
||||
|
||||
RuntimeInteractionDispatchResult result = state.TryDispatchUse(
|
||||
Item,
|
||||
ownedByPlayer: false,
|
||||
useable: true,
|
||||
reservation,
|
||||
transport,
|
||||
out uint sequence);
|
||||
|
||||
Assert.Equal(RuntimeInteractionDispatchResult.Dispatched, result);
|
||||
Assert.Equal(1u, sequence);
|
||||
Assert.Equal(1, inventory.BusyCount);
|
||||
Assert.Equal(new[] { Item }, transport.Uses);
|
||||
RuntimeInteractionTransactionSnapshot snapshot =
|
||||
state.CaptureOwnership();
|
||||
Assert.Equal(Item, snapshot.LastUseSourceId);
|
||||
Assert.Equal(0u, snapshot.LastUseTargetId);
|
||||
|
||||
reservation.CancelBeforeDispatch();
|
||||
Assert.Equal(1, inventory.BusyCount);
|
||||
|
||||
state.CompleteUse(0u);
|
||||
Assert.Equal(0, inventory.BusyCount);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, true, RuntimeInteractionDispatchResult.NotInWorld)]
|
||||
[InlineData(true, false, RuntimeInteractionDispatchResult.NotUseable)]
|
||||
public void RejectedUseReleasesOnlyItsReservation(
|
||||
bool inWorld,
|
||||
bool useable,
|
||||
RuntimeInteractionDispatchResult expected)
|
||||
{
|
||||
using var inventory = NewInventory(out _);
|
||||
using var state = new RuntimeInteractionTransactionState(inventory);
|
||||
var transport = new Transport { IsInWorld = inWorld };
|
||||
ItemUseRequestReservation reservation =
|
||||
state.BeginUseRequestReservation();
|
||||
|
||||
RuntimeInteractionDispatchResult result = state.TryDispatchUse(
|
||||
Item,
|
||||
ownedByPlayer: false,
|
||||
useable,
|
||||
reservation,
|
||||
transport,
|
||||
out _);
|
||||
|
||||
Assert.Equal(expected, result);
|
||||
Assert.Equal(0, inventory.BusyCount);
|
||||
Assert.Empty(transport.Uses);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TargetedUseRecordsExactSourceAndTarget()
|
||||
{
|
||||
using var inventory = NewInventory(out _);
|
||||
using var state = new RuntimeInteractionTransactionState(inventory);
|
||||
var sent = new List<(uint Source, uint Target)>();
|
||||
|
||||
Assert.True(state.TryDispatchTargetedUse(
|
||||
Item,
|
||||
Container,
|
||||
(source, target) => sent.Add((source, target)),
|
||||
incrementBusy: true));
|
||||
|
||||
Assert.Equal(new[] { (Item, Container) }, sent);
|
||||
Assert.Equal(1, inventory.BusyCount);
|
||||
RuntimeInteractionTransactionSnapshot snapshot =
|
||||
state.CaptureOwnership();
|
||||
Assert.Equal(Item, snapshot.LastUseSourceId);
|
||||
Assert.Equal(Container, snapshot.LastUseTargetId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReentrantResetPreventsTargetedUseStateResurrection()
|
||||
{
|
||||
using var inventory = NewInventory(out _);
|
||||
using var state = new RuntimeInteractionTransactionState(inventory);
|
||||
var sent = new List<(uint Source, uint Target)>();
|
||||
|
||||
Assert.False(state.TryDispatchTargetedUse(
|
||||
Item,
|
||||
Container,
|
||||
(source, target) =>
|
||||
{
|
||||
sent.Add((source, target));
|
||||
state.ResetSession();
|
||||
},
|
||||
incrementBusy: true));
|
||||
|
||||
Assert.Equal(new[] { (Item, Container) }, sent);
|
||||
Assert.Equal(0, inventory.BusyCount);
|
||||
RuntimeInteractionTransactionSnapshot snapshot =
|
||||
state.CaptureOwnership();
|
||||
Assert.Equal(0u, snapshot.LastUseSourceId);
|
||||
Assert.Equal(0u, snapshot.LastUseTargetId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppraisalReplacementKeepsOneBusyReferenceAndExactCurrentId()
|
||||
{
|
||||
using var inventory = NewInventory(out _);
|
||||
using var state = new RuntimeInteractionTransactionState(inventory);
|
||||
var sent = new List<uint>();
|
||||
|
||||
Assert.True(state.TryRequestAppraisal(Item, sent.Add));
|
||||
Assert.True(state.TryRequestAppraisal(Container, sent.Add));
|
||||
Assert.Equal(1, inventory.BusyCount);
|
||||
Assert.False(state.AcceptAppraisalResponse(Item).Accepted);
|
||||
|
||||
RuntimeAppraisalResponseAcceptance first =
|
||||
state.AcceptAppraisalResponse(Container);
|
||||
Assert.True(first.Accepted);
|
||||
Assert.True(first.FirstResponse);
|
||||
Assert.Equal(Container, state.CurrentAppraisalId);
|
||||
Assert.Equal(0, inventory.BusyCount);
|
||||
|
||||
Assert.True(state.RefreshCurrentAppraisal(sent.Add));
|
||||
Assert.Equal(new[] { Item, Container, Container }, sent);
|
||||
|
||||
Assert.True(state.CancelObjectAppraisalForSpell(sent.Add));
|
||||
Assert.Equal(0u, state.CurrentAppraisalId);
|
||||
Assert.Equal(0u, sent[^1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppraisalTransportFailureRollsBackOnlyItsBusyReference()
|
||||
{
|
||||
using var inventory = NewInventory(out _);
|
||||
using var state = new RuntimeInteractionTransactionState(inventory);
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
state.TryRequestAppraisal(
|
||||
Item,
|
||||
static _ => throw new InvalidOperationException("transport")));
|
||||
|
||||
RuntimeInteractionTransactionSnapshot snapshot =
|
||||
state.CaptureOwnership();
|
||||
Assert.Equal(0u, snapshot.AwaitingAppraisalId);
|
||||
Assert.Equal(0u, snapshot.CurrentAppraisalId);
|
||||
Assert.Equal(0, inventory.BusyCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OutboundQueueIsTypedFifoAndReentrantWorkWaitsForNextDrain()
|
||||
{
|
||||
using var inventory = NewInventory(out ClientObjectTable objects);
|
||||
using var state = new RuntimeInteractionTransactionState(inventory);
|
||||
var item = new ClientObject { ObjectId = Item };
|
||||
objects.AddOrUpdate(item);
|
||||
RuntimeInteractionIdentity identity = new(Item, 12u, item);
|
||||
state.Enqueue(new RuntimeQueuedInteraction(
|
||||
RuntimeQueuedInteractionKind.Activate,
|
||||
identity));
|
||||
state.Enqueue(new RuntimeQueuedInteraction(
|
||||
RuntimeQueuedInteractionKind.Use,
|
||||
identity));
|
||||
var seen = new List<RuntimeQueuedInteractionKind>();
|
||||
|
||||
state.DrainOutbound(value =>
|
||||
{
|
||||
seen.Add(value.Kind);
|
||||
if (value.Kind == RuntimeQueuedInteractionKind.Activate)
|
||||
{
|
||||
state.Enqueue(new RuntimeQueuedInteraction(
|
||||
RuntimeQueuedInteractionKind.Pickup,
|
||||
identity));
|
||||
}
|
||||
});
|
||||
|
||||
Assert.Equal(
|
||||
new[]
|
||||
{
|
||||
RuntimeQueuedInteractionKind.Activate,
|
||||
RuntimeQueuedInteractionKind.Use,
|
||||
},
|
||||
seen);
|
||||
Assert.Equal(1, state.OutboundCount);
|
||||
|
||||
state.DrainOutbound(value => seen.Add(value.Kind));
|
||||
Assert.Equal(RuntimeQueuedInteractionKind.Pickup, seen[^1]);
|
||||
Assert.Equal(0, state.OutboundCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReentrantSessionResetStopsTheCapturedFrameSuffix()
|
||||
{
|
||||
using var inventory = NewInventory(out _);
|
||||
using var state = new RuntimeInteractionTransactionState(inventory);
|
||||
RuntimeInteractionIdentity identity = new(Item, 12u, null);
|
||||
state.Enqueue(new RuntimeQueuedInteraction(
|
||||
RuntimeQueuedInteractionKind.Activate,
|
||||
identity));
|
||||
state.Enqueue(new RuntimeQueuedInteraction(
|
||||
RuntimeQueuedInteractionKind.Use,
|
||||
identity));
|
||||
var seen = new List<RuntimeQueuedInteractionKind>();
|
||||
|
||||
state.DrainOutbound(interaction =>
|
||||
{
|
||||
seen.Add(interaction.Kind);
|
||||
state.ResetSession();
|
||||
});
|
||||
|
||||
Assert.Equal(
|
||||
new[] { RuntimeQueuedInteractionKind.Activate },
|
||||
seen);
|
||||
Assert.Equal(0, state.OutboundCount);
|
||||
Assert.Equal(0, inventory.BusyCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DispatchFailureIsRecordedWithoutLosingLaterQueuedWork()
|
||||
{
|
||||
using var inventory = NewInventory(out _);
|
||||
using var state = new RuntimeInteractionTransactionState(inventory);
|
||||
RuntimeInteractionIdentity identity = new(Item, 12u, null);
|
||||
state.Enqueue(new RuntimeQueuedInteraction(
|
||||
RuntimeQueuedInteractionKind.Activate,
|
||||
identity));
|
||||
state.Enqueue(new RuntimeQueuedInteraction(
|
||||
RuntimeQueuedInteractionKind.Use,
|
||||
identity));
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
state.DrainOutbound(static _ =>
|
||||
throw new InvalidOperationException("observer")));
|
||||
|
||||
Assert.Equal(1, state.DispatchFailureCount);
|
||||
Assert.Equal(1, state.OutboundCount);
|
||||
var seen = new List<RuntimeQueuedInteractionKind>();
|
||||
state.DrainOutbound(value => seen.Add(value.Kind));
|
||||
Assert.Equal(new[] { RuntimeQueuedInteractionKind.Use }, seen);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HiddenAndDeletedCancellationPreservesOtherIdentityOrder()
|
||||
{
|
||||
using var inventory = NewInventory(out _);
|
||||
using var state = new RuntimeInteractionTransactionState(inventory);
|
||||
const uint reusedGuid = Item;
|
||||
const uint otherGuid = 0x70000002u;
|
||||
state.Enqueue(new RuntimeQueuedInteraction(
|
||||
RuntimeQueuedInteractionKind.Activate,
|
||||
new RuntimeInteractionIdentity(reusedGuid, 11u, null)));
|
||||
state.Enqueue(new RuntimeQueuedInteraction(
|
||||
RuntimeQueuedInteractionKind.Use,
|
||||
new RuntimeInteractionIdentity(otherGuid, 21u, null)));
|
||||
state.Enqueue(new RuntimeQueuedInteraction(
|
||||
RuntimeQueuedInteractionKind.Pickup,
|
||||
new RuntimeInteractionIdentity(reusedGuid, 12u, null)));
|
||||
state.Enqueue(new RuntimeQueuedInteraction(
|
||||
RuntimeQueuedInteractionKind.Activate,
|
||||
new RuntimeInteractionIdentity(otherGuid, 22u, null)));
|
||||
|
||||
Assert.Equal(1, state.CancelQueuedInteractions(reusedGuid, 11u));
|
||||
Assert.Equal(3, state.OutboundCount);
|
||||
Assert.Equal(1, state.CancelQueuedInteractions(reusedGuid));
|
||||
Assert.Equal(2, state.OutboundCount);
|
||||
|
||||
var seen = new List<(uint Guid, uint? LocalId)>();
|
||||
state.DrainOutbound(interaction => seen.Add(
|
||||
(interaction.Identity.ServerGuid,
|
||||
interaction.Identity.LocalEntityId)));
|
||||
|
||||
Assert.Equal(
|
||||
new[]
|
||||
{
|
||||
(otherGuid, (uint?)21u),
|
||||
(otherGuid, (uint?)22u),
|
||||
},
|
||||
seen);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PostArrivalPickupRequiresItsExactApproachAndReservationTokens()
|
||||
{
|
||||
using var inventory = NewInventory(out ClientObjectTable objects);
|
||||
using var state = new RuntimeInteractionTransactionState(inventory);
|
||||
objects.AddOrUpdate(new ClientObject { ObjectId = Item });
|
||||
Assert.True(inventory.TryReserve(
|
||||
InventoryRequestKind.Pickup,
|
||||
Item,
|
||||
out PendingInventoryRequest request));
|
||||
var approach = new RuntimeInteractionApproachToken(3u, 7u);
|
||||
|
||||
Assert.True(state.TryArmPostArrivalPickup(
|
||||
Item,
|
||||
localEntityId: 12u,
|
||||
Container,
|
||||
placement: 0,
|
||||
request.Token,
|
||||
approach,
|
||||
out RuntimePendingPickup pending));
|
||||
Assert.False(state.TryResolveApproachCompletion(
|
||||
new RuntimeInteractionApproachToken(3u, 8u),
|
||||
natural: true,
|
||||
out _));
|
||||
Assert.True(state.TryResolveApproachCompletion(
|
||||
approach,
|
||||
natural: true,
|
||||
out RuntimePendingPickup ready));
|
||||
Assert.Equal(pending.Token, ready.Token);
|
||||
|
||||
var transport = new Transport();
|
||||
Assert.True(state.TryDispatchPickup(ready, transport, out uint sequence));
|
||||
Assert.Equal(1u, sequence);
|
||||
Assert.Equal(new[] { (Item, Container, 0) }, transport.Pickups);
|
||||
Assert.True(inventory.TryGetPending(out PendingInventoryRequest sent));
|
||||
Assert.True(sent.Dispatched);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CancellationResetAndDisposalConvergeTheCompleteLedger()
|
||||
{
|
||||
using var inventory = NewInventory(out ClientObjectTable objects);
|
||||
var state = new RuntimeInteractionTransactionState(inventory);
|
||||
objects.AddOrUpdate(new ClientObject { ObjectId = Item });
|
||||
Assert.True(inventory.TryReserve(
|
||||
InventoryRequestKind.Pickup,
|
||||
Item,
|
||||
out PendingInventoryRequest request));
|
||||
Assert.True(state.TryArmPostArrivalPickup(
|
||||
Item,
|
||||
localEntityId: 12u,
|
||||
Container,
|
||||
placement: 0,
|
||||
request.Token,
|
||||
new RuntimeInteractionApproachToken(1u, 1u),
|
||||
out _));
|
||||
state.Enqueue(new RuntimeQueuedInteraction(
|
||||
RuntimeQueuedInteractionKind.Pickup,
|
||||
new RuntimeInteractionIdentity(Item, 12u, objects.Get(Item))));
|
||||
state.IncrementBusyCount();
|
||||
|
||||
state.ResetSession();
|
||||
|
||||
Assert.Equal(0, inventory.BusyCount);
|
||||
Assert.False(inventory.HasPendingRequest);
|
||||
Assert.Equal(0, state.OutboundCount);
|
||||
Assert.False(state.HasPendingPickup);
|
||||
|
||||
state.Dispose();
|
||||
Assert.True(state.CaptureOwnership().IsConverged);
|
||||
state.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InstancesDoNotShareThrottleQueueAppraisalOrPickupState()
|
||||
{
|
||||
using var firstInventory = NewInventory(out _);
|
||||
using var secondInventory = NewInventory(out _);
|
||||
using var first =
|
||||
new RuntimeInteractionTransactionState(firstInventory);
|
||||
using var second =
|
||||
new RuntimeInteractionTransactionState(secondInventory);
|
||||
|
||||
Assert.True(first.TryConsumeUseThrottle(100));
|
||||
first.TryRequestAppraisal(Item, static _ => { });
|
||||
first.Enqueue(new RuntimeQueuedInteraction(
|
||||
RuntimeQueuedInteractionKind.Use,
|
||||
new RuntimeInteractionIdentity(Item, 1u, null)));
|
||||
|
||||
RuntimeInteractionTransactionSnapshot snapshot =
|
||||
second.CaptureOwnership();
|
||||
Assert.Equal(0u, snapshot.AwaitingAppraisalId);
|
||||
Assert.Equal(0, snapshot.OutboundCount);
|
||||
Assert.Equal(0, secondInventory.BusyCount);
|
||||
Assert.True(second.TryConsumeUseThrottle(100));
|
||||
}
|
||||
|
||||
private static InventoryTransactionState NewInventory(
|
||||
out ClientObjectTable objects)
|
||||
{
|
||||
objects = new ClientObjectTable();
|
||||
return new InventoryTransactionState(objects);
|
||||
}
|
||||
|
||||
private sealed class Transport : IRuntimeInteractionTransport
|
||||
{
|
||||
private uint _sequence;
|
||||
public bool IsInWorld { get; set; } = true;
|
||||
public List<uint> Uses { get; } = [];
|
||||
public List<(uint Item, uint Container, int Placement)> Pickups { get; } = [];
|
||||
|
||||
public bool TrySendUse(uint serverGuid, out uint sequence)
|
||||
{
|
||||
if (!IsInWorld)
|
||||
{
|
||||
sequence = 0u;
|
||||
return false;
|
||||
}
|
||||
sequence = ++_sequence;
|
||||
Uses.Add(serverGuid);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TrySendPickup(
|
||||
uint itemGuid,
|
||||
uint destinationContainerId,
|
||||
int placement,
|
||||
out uint sequence)
|
||||
{
|
||||
if (!IsInWorld)
|
||||
{
|
||||
sequence = 0u;
|
||||
return false;
|
||||
}
|
||||
sequence = ++_sequence;
|
||||
Pickups.Add((itemGuid, destinationContainerId, placement));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue