acdream/tests/AcDream.Core.Tests/Input/AutoEnterPlayerModeTests.cs
Erik eeb0f6b45c 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>
2026-07-22 02:50:15 +02:00

206 lines
6.6 KiB
C#

using AcDream.App.Input;
namespace AcDream.Core.Tests.Input;
/// <summary>
/// Phase K.2 — guard logic for auto-entering player mode at login.
/// The trigger fires exactly once when:
/// <list type="number">
/// <item>It has been <see cref="PlayerModeAutoEntry.Arm"/>'d (login
/// succeeded).</item>
/// <item>The live session is in <c>InWorld</c>.</item>
/// <item>The player entity has been streamed into the local
/// dictionary.</item>
/// <item>The player movement controller is ready to attach.</item>
/// </list>
/// All four predicates are passed as <see cref="Func{bool}"/>; tests
/// flip plain boolean fields and assert against an entry-counter that
/// the entry callback bumps.
/// </summary>
public sealed class AutoEnterPlayerModeTests
{
private sealed class State
{
public bool LiveInWorld;
public bool PlayerEntityPresent;
public bool PlayerControllerReady;
// Defaults TRUE: the world-reveal hold is not under test in the
// original K.2 cases (see WorldNotReady test for it).
public bool WorldReady = true;
public bool PlayerModeActive;
public int EnteredCount;
public PlayerModeAutoEntry Build() =>
new(
isLiveInWorld: () => LiveInWorld,
isPlayerEntityPresent: () => PlayerEntityPresent,
isPlayerControllerReady: () => PlayerControllerReady,
isWorldReady: () => WorldReady,
enterPlayerMode: () => EnteredCount++,
isPlayerModeActive: () => PlayerModeActive);
}
[Fact]
public void TryEnter_Armed_WorldNotReady_DoesNotFire()
{
// #229 extends #106's ground-only hold to retail's complete blocking
// cell-load edge. Player mode must not expose the viewport until the
// render, composite-texture, and collision domains have all converged.
var s = new State
{
LiveInWorld = true, PlayerEntityPresent = true,
PlayerControllerReady = true, WorldReady = false,
};
var guard = s.Build();
guard.Arm();
Assert.False(guard.TryEnter());
Assert.Equal(0, s.EnteredCount);
Assert.True(guard.IsArmed);
// Render, texture, and collision domains converge → next tick fires.
s.WorldReady = true;
Assert.True(guard.TryEnter());
Assert.Equal(1, s.EnteredCount);
}
[Fact]
public void TryEnter_NotArmed_DoesNotFire()
{
var s = new State { LiveInWorld = true, PlayerEntityPresent = true, PlayerControllerReady = true };
var guard = s.Build();
// Not armed → must NOT fire even though every precondition is true.
Assert.False(guard.TryEnter());
Assert.Equal(0, s.EnteredCount);
Assert.False(guard.IsArmed);
}
[Fact]
public void TryEnter_Armed_LiveNotInWorld_DoesNotFire()
{
var s = new State { LiveInWorld = false, PlayerEntityPresent = true, PlayerControllerReady = true };
var guard = s.Build();
guard.Arm();
Assert.False(guard.TryEnter());
Assert.Equal(0, s.EnteredCount);
Assert.True(guard.IsArmed);
}
[Fact]
public void TryEnter_Armed_PlayerEntityNotPresent_DoesNotFire()
{
var s = new State { LiveInWorld = true, PlayerEntityPresent = false, PlayerControllerReady = true };
var guard = s.Build();
guard.Arm();
Assert.False(guard.TryEnter());
Assert.Equal(0, s.EnteredCount);
Assert.True(guard.IsArmed);
}
[Fact]
public void TryEnter_Armed_PlayerControllerNotReady_DoesNotFire()
{
var s = new State { LiveInWorld = true, PlayerEntityPresent = true, PlayerControllerReady = false };
var guard = s.Build();
guard.Arm();
Assert.False(guard.TryEnter());
Assert.Equal(0, s.EnteredCount);
Assert.True(guard.IsArmed);
}
[Fact]
public void TryEnter_AllConditionsSatisfied_FiresExactlyOnce()
{
var s = new State { LiveInWorld = true, PlayerEntityPresent = true, PlayerControllerReady = true };
var guard = s.Build();
guard.Arm();
Assert.True(guard.TryEnter());
Assert.Equal(1, s.EnteredCount);
Assert.False(guard.IsArmed);
// Subsequent tick must not re-fire — one-shot semantics.
Assert.False(guard.TryEnter());
Assert.Equal(1, s.EnteredCount);
}
[Fact]
public void TryEnter_FiresOnLaterTickWhenPreconditionsBecomeTrue()
{
var s = new State();
var guard = s.Build();
guard.Arm();
// Tick 1: only LiveInWorld true.
s.LiveInWorld = true;
Assert.False(guard.TryEnter());
// Tick 2: + PlayerEntityPresent.
s.PlayerEntityPresent = true;
Assert.False(guard.TryEnter());
// Tick 3: + PlayerControllerReady → fires.
s.PlayerControllerReady = true;
Assert.True(guard.TryEnter());
Assert.Equal(1, s.EnteredCount);
}
[Fact]
public void Cancel_BeforeFiring_SuppressesAutoEntry()
{
// Manual fly-mode toggle BEFORE the auto-entry fires must
// disarm the trigger; the user's choice wins.
var s = new State();
var guard = s.Build();
guard.Arm();
// User opts out before any precondition is true.
guard.Cancel();
Assert.False(guard.IsArmed);
// Even when every precondition flips true, the guard stays
// silent — the user's manual fly-mode choice wins.
s.LiveInWorld = true;
s.PlayerEntityPresent = true;
s.PlayerControllerReady = true;
Assert.False(guard.TryEnter());
Assert.Equal(0, s.EnteredCount);
}
[Fact]
public void Arm_WhileAlreadyArmed_IsIdempotent()
{
var s = new State { LiveInWorld = true, PlayerEntityPresent = true, PlayerControllerReady = true };
var guard = s.Build();
guard.Arm();
guard.Arm(); // second Arm() — no-op.
Assert.True(guard.TryEnter());
Assert.Equal(1, s.EnteredCount);
}
[Fact]
public void PortalEntryThatAlreadyActivatedPlayerMode_RetiresArmedGuard()
{
var s = new State
{
LiveInWorld = true,
PlayerEntityPresent = true,
PlayerControllerReady = true,
};
var guard = s.Build();
guard.Arm();
// F751/player-mode activation occurs before the auto-entry phase in
// the update graph. The guard must not rebuild that PortalSpace owner.
s.PlayerModeActive = true;
Assert.False(guard.TryEnter());
Assert.False(guard.IsArmed);
Assert.Equal(0, s.EnteredCount);
}
}