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
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue