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
|
|
@ -0,0 +1,79 @@
|
|||
using AcDream.App.Interaction;
|
||||
using AcDream.Core.Physics;
|
||||
|
||||
namespace AcDream.App.Tests.Interaction;
|
||||
|
||||
public sealed class PlayerApproachCompletionStateTests
|
||||
{
|
||||
[Fact]
|
||||
public void CompletionMailbox_PreservesPublicationOrder()
|
||||
{
|
||||
var state = new PlayerApproachCompletionState();
|
||||
IPlayerApproachCompletionSink lifetime = state.BeginControllerLifetime();
|
||||
|
||||
Assert.True(state.TryBeginApproach(out PlayerApproachToken token));
|
||||
lifetime.PublishNaturalCompletion();
|
||||
lifetime.PublishCancellation(WeenieError.NoObject);
|
||||
|
||||
Assert.True(state.TryTake(out PlayerApproachCompletion natural));
|
||||
Assert.Equal(token, natural.Token);
|
||||
Assert.True(natural.IsNatural);
|
||||
Assert.Equal(WeenieError.None, natural.Error);
|
||||
Assert.True(state.TryTake(out PlayerApproachCompletion cancellation));
|
||||
Assert.False(cancellation.IsNatural);
|
||||
Assert.Equal(WeenieError.NoObject, cancellation.Error);
|
||||
Assert.False(state.TryTake(out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clear_DiscardsEveryPendingCompletion()
|
||||
{
|
||||
var state = new PlayerApproachCompletionState();
|
||||
IPlayerApproachCompletionSink lifetime = state.BeginControllerLifetime();
|
||||
Assert.True(state.TryBeginApproach(out _));
|
||||
lifetime.PublishNaturalCompletion();
|
||||
lifetime.PublishCancellation(WeenieError.ActionCancelled);
|
||||
|
||||
state.Clear();
|
||||
|
||||
Assert.False(state.TryTake(out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RetiredLifetime_CannotPublishIntoReplacementLifetime()
|
||||
{
|
||||
var state = new PlayerApproachCompletionState();
|
||||
IPlayerApproachCompletionSink stale = state.BeginControllerLifetime();
|
||||
Assert.True(state.TryBeginApproach(out PlayerApproachToken staleToken));
|
||||
state.RetireControllerLifetime(stale);
|
||||
Assert.True(state.TryTake(out PlayerApproachCompletion retirement));
|
||||
Assert.Equal(staleToken, retirement.Token);
|
||||
Assert.False(retirement.IsNatural);
|
||||
IPlayerApproachCompletionSink current = state.BeginControllerLifetime();
|
||||
Assert.True(state.TryBeginApproach(out PlayerApproachToken currentToken));
|
||||
|
||||
stale.PublishNaturalCompletion();
|
||||
current.PublishNaturalCompletion();
|
||||
|
||||
Assert.True(state.TryTake(out PlayerApproachCompletion completion));
|
||||
Assert.Equal(currentToken, completion.Token);
|
||||
Assert.False(state.TryTake(out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Retirement_ReplacesQueuedSuccessWithMatchingCancellation()
|
||||
{
|
||||
var state = new PlayerApproachCompletionState();
|
||||
IPlayerApproachCompletionSink lifetime = state.BeginControllerLifetime();
|
||||
Assert.True(state.TryBeginApproach(out PlayerApproachToken token));
|
||||
lifetime.PublishNaturalCompletion();
|
||||
|
||||
state.RetireControllerLifetime(lifetime);
|
||||
|
||||
Assert.True(state.TryTake(out PlayerApproachCompletion completion));
|
||||
Assert.Equal(token, completion.Token);
|
||||
Assert.False(completion.IsNatural);
|
||||
Assert.Equal(WeenieError.ActionCancelled, completion.Error);
|
||||
Assert.False(state.TryTake(out _));
|
||||
}
|
||||
}
|
||||
|
|
@ -16,10 +16,11 @@ public sealed class PlayerInteractionMovementSinkTests
|
|||
[Fact]
|
||||
public void MissingPlayerDoesNotArmTheIntent()
|
||||
{
|
||||
var sink = new PlayerInteractionMovementSink(() => null);
|
||||
var completions = new PlayerApproachCompletionState();
|
||||
var sink = new PlayerInteractionMovementSink(() => null, completions);
|
||||
bool armed = false;
|
||||
|
||||
Assert.False(sink.BeginApproach(Approach(closeRange: true), () => armed = true));
|
||||
Assert.False(sink.BeginApproach(Approach(closeRange: true), _ => armed = true));
|
||||
Assert.False(armed);
|
||||
}
|
||||
|
||||
|
|
@ -66,11 +67,15 @@ public sealed class PlayerInteractionMovementSinkTests
|
|||
new Position(Cell, new Vector3(2f, 0f, 0f), Quaternion.Identity),
|
||||
new MovementParameters { UseSpheres = false });
|
||||
bool armedAfterCancellation = false;
|
||||
var sink = new PlayerInteractionMovementSink(() => controller);
|
||||
var completions = new PlayerApproachCompletionState();
|
||||
_ = completions.BeginControllerLifetime();
|
||||
var sink = new PlayerInteractionMovementSink(
|
||||
() => controller,
|
||||
completions);
|
||||
|
||||
Assert.True(sink.BeginApproach(
|
||||
Approach(closeRange),
|
||||
() => armedAfterCancellation = cancellations == 1 && !moveTo.IsMovingTo()));
|
||||
_ => armedAfterCancellation = cancellations == 1 && !moveTo.IsMovingTo()));
|
||||
|
||||
Assert.True(armedAfterCancellation);
|
||||
Assert.True(nonAutonomousAtTargetInstall);
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue