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:
Erik 2026-07-22 02:50:15 +02:00
parent c557038353
commit eeb0f6b45c
29 changed files with 3311 additions and 1073 deletions

View file

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