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

@ -104,17 +104,20 @@ public sealed class SelectionInteractionControllerTests
}
}
private sealed class Movement : IPlayerInteractionMovementSink
private sealed class Movement(IPlayerApproachTokenSource approachTokens)
: IPlayerInteractionMovementSink
{
public List<InteractionApproach> Approaches { get; } = new();
public bool Starts { get; set; } = true;
public Action? AfterArm { get; set; }
public bool BeginApproach(InteractionApproach approach, Action? armAfterCancel = null)
public bool BeginApproach(
InteractionApproach approach,
Action<PlayerApproachToken>? armAfterCancel = null)
{
Approaches.Add(approach);
if (!Starts)
if (!Starts || !approachTokens.TryBeginApproach(out PlayerApproachToken token))
return false;
armAfterCancel?.Invoke();
armAfterCancel?.Invoke(token);
AfterArm?.Invoke();
return true;
}
@ -124,7 +127,9 @@ public sealed class SelectionInteractionControllerTests
{
public readonly Query Query = new();
public readonly Transport Transport = new();
public readonly Movement Movement = new();
public readonly PlayerApproachCompletionState Completions = new();
public readonly IPlayerApproachCompletionSink CompletionLifetime;
public readonly Movement Movement;
public readonly SelectionState Selection = new();
public readonly ClientObjectTable Objects = new();
public readonly List<string> Toasts = new();
@ -136,6 +141,8 @@ public sealed class SelectionInteractionControllerTests
public Harness()
{
CompletionLifetime = Completions.BeginControllerLifetime();
Movement = new Movement(Completions);
SelectionInteractionController? controller = null;
Objects.AddOrUpdate(new ClientObject
{
@ -168,7 +175,8 @@ public sealed class SelectionInteractionControllerTests
Items,
Transport,
Movement,
Toasts.Add);
Toasts.Add,
Completions);
Items.PendingBackpackPlacementRequested += PendingPlacements.Add;
Items.PendingBackpackPlacementCancelled += CancelledPlacements.Add;
}
@ -386,6 +394,47 @@ public sealed class SelectionInteractionControllerTests
Assert.Empty(h.Transport.Uses);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void DeferredCompletionFromPriorApproachCannotAffectReplacement(
bool priorCompletedNaturally)
{
var h = new Harness();
h.SetApproach(closeRange: true);
h.Controller.SendUse(Target);
if (priorCompletedNaturally)
h.CompletionLifetime.PublishNaturalCompletion();
else
h.CompletionLifetime.PublishCancellation(WeenieError.ActionCancelled);
h.Controller.SendUse(Target);
h.Controller.DrainOutbound();
Assert.Empty(h.Transport.Uses);
h.CompletionLifetime.PublishNaturalCompletion();
h.Controller.DrainOutbound();
Assert.Equal(new[] { Target }, h.Transport.Uses);
}
[Fact]
public void RetiringControllerLifetimeInvalidatesQueuedArrival()
{
var h = new Harness();
h.SetApproach(closeRange: true);
h.Controller.SendUse(Target);
h.CompletionLifetime.PublishNaturalCompletion();
h.Completions.RetireControllerLifetime(h.CompletionLifetime);
h.Controller.DrainOutbound();
Assert.Empty(h.Transport.Uses);
h.Controller.OnNaturalMoveToComplete();
Assert.Empty(h.Transport.Uses);
}
[Fact]
public void FailedClosePickupStartWithdrawsPresentation()
{