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>
206 lines
6.6 KiB
C#
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);
|
|
}
|
|
}
|