Make initial login and portal arrival consume one WorldRevealReadinessBarrier that joins near-tier mesh publication, destination composite uploads, and collision residency before normal world geometry becomes visible. This ports retail SmartBox's blocking-cell completion edge into the asynchronous client instead of exposing a ground-only login. Add focused outdoor, indoor, texture, and invalid-claim tests; update the retail pseudocode, architecture, divergence record, issue ledger, roadmap baseline, and synchronized agent guidance. Co-authored-by: OpenAI Codex <codex@openai.com>
183 lines
5.9 KiB
C#
183 lines
5.9 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 int EnteredCount;
|
|
|
|
public PlayerModeAutoEntry Build() =>
|
|
new(
|
|
isLiveInWorld: () => LiveInWorld,
|
|
isPlayerEntityPresent: () => PlayerEntityPresent,
|
|
isPlayerControllerReady: () => PlayerControllerReady,
|
|
isWorldReady: () => WorldReady,
|
|
enterPlayerMode: () => EnteredCount++);
|
|
}
|
|
|
|
[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);
|
|
}
|
|
}
|