refactor(runtime): extract local teleport and player mode
Move local teleport, player-mode, animation, shadow, and sealed-dungeon lifetimes out of GameWindow. Make player-mode entry transactional and isolate approach completions by controller lifetime and approach generation so stale callbacks cannot affect replacements. Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
parent
c557038353
commit
eeb0f6b45c
29 changed files with 3311 additions and 1073 deletions
141
src/AcDream.App/Interaction/PlayerApproachCompletionState.cs
Normal file
141
src/AcDream.App/Interaction/PlayerApproachCompletionState.cs
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
using AcDream.Core.Physics;
|
||||
|
||||
namespace AcDream.App.Interaction;
|
||||
|
||||
internal interface IPlayerApproachCompletionSink
|
||||
{
|
||||
void PublishNaturalCompletion();
|
||||
void PublishCancellation(WeenieError error);
|
||||
}
|
||||
|
||||
internal interface IPlayerApproachCompletionLifetimeOwner
|
||||
{
|
||||
IPlayerApproachCompletionSink BeginControllerLifetime();
|
||||
void RetireControllerLifetime(IPlayerApproachCompletionSink lifetime);
|
||||
}
|
||||
|
||||
internal interface IPlayerApproachTokenSource
|
||||
{
|
||||
bool TryBeginApproach(out PlayerApproachToken token);
|
||||
}
|
||||
|
||||
internal readonly record struct PlayerApproachToken(
|
||||
ulong ControllerLifetime,
|
||||
ulong ApproachGeneration);
|
||||
|
||||
internal readonly record struct PlayerApproachCompletion(
|
||||
PlayerApproachToken Token,
|
||||
bool IsNatural,
|
||||
WeenieError Error);
|
||||
|
||||
/// <summary>
|
||||
/// Same-thread mailbox between local MoveTo completion and the selection/
|
||||
/// item interaction phase. It deliberately owns no query, item, transport,
|
||||
/// window, or callback reference.
|
||||
/// </summary>
|
||||
internal sealed class PlayerApproachCompletionState
|
||||
: IPlayerApproachCompletionLifetimeOwner,
|
||||
IPlayerApproachTokenSource
|
||||
{
|
||||
private readonly Queue<PlayerApproachCompletion> _pending = new();
|
||||
private ControllerLifetime? _active;
|
||||
private ulong _nextLifetime;
|
||||
|
||||
public IPlayerApproachCompletionSink BeginControllerLifetime()
|
||||
{
|
||||
if (_active is not null)
|
||||
throw new InvalidOperationException(
|
||||
"A player approach-completion lifetime is already active.");
|
||||
|
||||
var lifetime = new ControllerLifetime(this, ++_nextLifetime);
|
||||
_active = lifetime;
|
||||
return lifetime;
|
||||
}
|
||||
|
||||
public void RetireControllerLifetime(IPlayerApproachCompletionSink lifetime)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(lifetime);
|
||||
if (!ReferenceEquals(_active, lifetime)
|
||||
|| lifetime is not ControllerLifetime retiring)
|
||||
return;
|
||||
|
||||
PurgeLifetime(retiring.LifetimeId);
|
||||
if (retiring.TryGetCurrentToken(out PlayerApproachToken token))
|
||||
{
|
||||
_pending.Enqueue(new PlayerApproachCompletion(
|
||||
token,
|
||||
IsNatural: false,
|
||||
WeenieError.ActionCancelled));
|
||||
}
|
||||
_active = null;
|
||||
}
|
||||
|
||||
public bool TryBeginApproach(out PlayerApproachToken token)
|
||||
{
|
||||
if (_active is null)
|
||||
{
|
||||
token = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
token = _active.BeginApproach();
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryTake(out PlayerApproachCompletion completion) =>
|
||||
_pending.TryDequeue(out completion);
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_active = null;
|
||||
_pending.Clear();
|
||||
}
|
||||
|
||||
private void Publish(
|
||||
ControllerLifetime lifetime,
|
||||
bool isNatural,
|
||||
WeenieError error)
|
||||
{
|
||||
if (!ReferenceEquals(_active, lifetime)
|
||||
|| !lifetime.TryGetCurrentToken(out PlayerApproachToken token))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_pending.Enqueue(new PlayerApproachCompletion(token, isNatural, error));
|
||||
}
|
||||
|
||||
private void PurgeLifetime(ulong lifetimeId)
|
||||
{
|
||||
int retained = _pending.Count;
|
||||
for (int i = 0; i < retained; i++)
|
||||
{
|
||||
PlayerApproachCompletion completion = _pending.Dequeue();
|
||||
if (completion.Token.ControllerLifetime != lifetimeId)
|
||||
_pending.Enqueue(completion);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ControllerLifetime(
|
||||
PlayerApproachCompletionState owner,
|
||||
ulong lifetimeId) : IPlayerApproachCompletionSink
|
||||
{
|
||||
private ulong _approachGeneration;
|
||||
public ulong LifetimeId => lifetimeId;
|
||||
|
||||
public PlayerApproachToken BeginApproach() =>
|
||||
new(lifetimeId, ++_approachGeneration);
|
||||
|
||||
public bool TryGetCurrentToken(out PlayerApproachToken token)
|
||||
{
|
||||
token = new PlayerApproachToken(lifetimeId, _approachGeneration);
|
||||
return _approachGeneration != 0;
|
||||
}
|
||||
|
||||
public void PublishNaturalCompletion() =>
|
||||
owner.Publish(this, isNatural: true, WeenieError.None);
|
||||
|
||||
public void PublishCancellation(WeenieError error) =>
|
||||
owner.Publish(this, isNatural: false, error);
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,9 @@ internal interface IPlayerInteractionMovementSink
|
|||
/// owner arm completion state before an already-facing TurnTo can finish
|
||||
/// synchronously, without letting the preceding cancellation clear it.
|
||||
/// </summary>
|
||||
bool BeginApproach(InteractionApproach approach, Action? armAfterCancel = null);
|
||||
bool BeginApproach(
|
||||
InteractionApproach approach,
|
||||
Action<PlayerApproachToken>? armAfterCancel = null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -20,13 +22,18 @@ internal interface IPlayerInteractionMovementSink
|
|||
/// the same MovementManager used by authoritative movement packets.
|
||||
/// </summary>
|
||||
internal sealed class PlayerInteractionMovementSink(
|
||||
Func<PlayerMovementController?> player)
|
||||
Func<PlayerMovementController?> player,
|
||||
IPlayerApproachTokenSource approachTokens)
|
||||
: IPlayerInteractionMovementSink
|
||||
{
|
||||
private readonly Func<PlayerMovementController?> _player = player
|
||||
?? throw new ArgumentNullException(nameof(player));
|
||||
private readonly IPlayerApproachTokenSource _approachTokens = approachTokens
|
||||
?? throw new ArgumentNullException(nameof(approachTokens));
|
||||
|
||||
public bool BeginApproach(InteractionApproach approach, Action? armAfterCancel = null)
|
||||
public bool BeginApproach(
|
||||
InteractionApproach approach,
|
||||
Action<PlayerApproachToken>? armAfterCancel = null)
|
||||
{
|
||||
PlayerMovementController? controller = _player();
|
||||
if (controller?.MoveTo is null)
|
||||
|
|
@ -57,7 +64,9 @@ internal sealed class PlayerInteractionMovementSink(
|
|||
// intent is armed so cancellation of the preceding move cannot clear
|
||||
// the new request; the internal second call is then a retail no-op.
|
||||
controller.Movement.CancelMoveTo(WeenieError.ActionCancelled);
|
||||
armAfterCancel?.Invoke();
|
||||
if (!_approachTokens.TryBeginApproach(out PlayerApproachToken token))
|
||||
return false;
|
||||
armAfterCancel?.Invoke(token);
|
||||
|
||||
// P1's wire MoveToObject store marks the command non-autonomous.
|
||||
// The speculative local install must do the same or the next raw-input
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ internal sealed class SelectionInteractionController
|
|||
private readonly ISelectionInteractionTransport _transport;
|
||||
private readonly IPlayerInteractionMovementSink _movement;
|
||||
private readonly OutboundInteractionQueue _outbound = new();
|
||||
private readonly PlayerApproachCompletionState _approachCompletions;
|
||||
private readonly Action<string>? _toast;
|
||||
private PendingPostArrivalAction? _pendingPostArrival;
|
||||
|
||||
|
|
@ -36,7 +37,8 @@ internal sealed class SelectionInteractionController
|
|||
bool IsPickup,
|
||||
uint DestinationContainerId,
|
||||
int Placement,
|
||||
ulong PendingPlacementToken);
|
||||
ulong PendingPlacementToken,
|
||||
PlayerApproachToken ApproachToken);
|
||||
|
||||
public SelectionInteractionController(
|
||||
SelectionState selection,
|
||||
|
|
@ -44,7 +46,8 @@ internal sealed class SelectionInteractionController
|
|||
ItemInteractionController items,
|
||||
ISelectionInteractionTransport transport,
|
||||
IPlayerInteractionMovementSink movement,
|
||||
Action<string>? toast = null)
|
||||
Action<string>? toast = null,
|
||||
PlayerApproachCompletionState? approachCompletions = null)
|
||||
{
|
||||
_selection = selection ?? throw new ArgumentNullException(nameof(selection));
|
||||
_query = query ?? throw new ArgumentNullException(nameof(query));
|
||||
|
|
@ -52,6 +55,8 @@ internal sealed class SelectionInteractionController
|
|||
_transport = transport ?? throw new ArgumentNullException(nameof(transport));
|
||||
_movement = movement ?? throw new ArgumentNullException(nameof(movement));
|
||||
_toast = toast;
|
||||
_approachCompletions = approachCompletions
|
||||
?? new PlayerApproachCompletionState();
|
||||
}
|
||||
|
||||
public bool HandleInputAction(InputAction action)
|
||||
|
|
@ -213,11 +218,12 @@ internal sealed class SelectionInteractionController
|
|||
IsPickup: false,
|
||||
DestinationContainerId: 0u,
|
||||
Placement: 0,
|
||||
PendingPlacementToken: 0u);
|
||||
PendingPlacementToken: 0u,
|
||||
ApproachToken: default);
|
||||
bool started = _movement.BeginApproach(
|
||||
approach,
|
||||
() => _pendingPostArrival = pending);
|
||||
if (!started && _pendingPostArrival == pending)
|
||||
token => _pendingPostArrival = pending with { ApproachToken = token });
|
||||
if (!started && _pendingPostArrival?.ServerGuid == pending.ServerGuid)
|
||||
CancelPendingApproach();
|
||||
return;
|
||||
}
|
||||
|
|
@ -265,11 +271,12 @@ internal sealed class SelectionInteractionController
|
|||
IsPickup: true,
|
||||
destinationContainerId,
|
||||
placement,
|
||||
pendingPlacementToken);
|
||||
pendingPlacementToken,
|
||||
ApproachToken: default);
|
||||
bool started = _movement.BeginApproach(
|
||||
approach,
|
||||
() => _pendingPostArrival = pending);
|
||||
if (!started && _pendingPostArrival == pending)
|
||||
token => _pendingPostArrival = pending with { ApproachToken = token });
|
||||
if (!started && _pendingPostArrival?.ServerGuid == pending.ServerGuid)
|
||||
CancelPendingApproach();
|
||||
else if (!started)
|
||||
CancelPickupPresentation(itemGuid, pendingPlacementToken);
|
||||
|
|
@ -307,6 +314,9 @@ 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)
|
||||
return;
|
||||
|
|
@ -351,7 +361,19 @@ internal sealed class SelectionInteractionController
|
|||
}
|
||||
}
|
||||
|
||||
public void DrainOutbound() => _outbound.Drain();
|
||||
public void DrainOutbound()
|
||||
{
|
||||
while (_approachCompletions.TryTake(out PlayerApproachCompletion completion))
|
||||
{
|
||||
if (_pendingPostArrival?.ApproachToken != completion.Token)
|
||||
continue;
|
||||
if (completion.IsNatural)
|
||||
HandleNaturalMoveToComplete();
|
||||
else
|
||||
CancelPendingApproach();
|
||||
}
|
||||
_outbound.Drain();
|
||||
}
|
||||
|
||||
public void OnMoveToCancelled(WeenieError _) => CancelPendingApproach();
|
||||
|
||||
|
|
@ -395,6 +417,8 @@ internal sealed class SelectionInteractionController
|
|||
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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue