using AcDream.Core.Physics;
using AcDream.Runtime.Gameplay;
namespace AcDream.Runtime.Tests.Gameplay;
///
/// #365 Step 4: must never
/// advance a DORMANT (constructed but not yet ActivateRuntimePublication
/// -ed) controller's live-movement operations, even if a host's own
/// CanAdvancePlayer is wrong (the exact headless defect — it only
/// checked Controller is not null, true for a dormant controller too).
/// These tests deliberately SABOTAGE the host's CanAdvancePlayer to
/// true to prove the controller's own
/// gate is the
/// one holding the line, not the host's contract.
///
public sealed class RuntimeLocalPlayerFrameControllerTests
{
[Fact]
public void AdvanceBeforeNetworkDoesNotThrowOnDormantControllerInSuspendDisposition()
{
PlayerMovementController dormant = CreateDormantController();
var host = new SabotagingHost(
dormant,
RetailObjectClockDisposition.Suspend);
var controller = new RuntimeLocalPlayerFrameController(
host,
new FixedMovementInputSource());
Exception? exception = Record.Exception(
() => controller.AdvanceBeforeNetwork(0.015f));
Assert.Null(exception);
Assert.Equal(0, host.ProjectCallCount);
}
[Fact]
public void AdvanceBeforeNetworkDoesNotThrowOnDormantControllerInAdvanceDisposition()
{
PlayerMovementController dormant = CreateDormantController();
var host = new SabotagingHost(
dormant,
RetailObjectClockDisposition.Advance);
var controller = new RuntimeLocalPlayerFrameController(
host,
new FixedMovementInputSource());
Exception? exception = Record.Exception(
() => controller.AdvanceBeforeNetwork(0.015f));
Assert.Null(exception);
Assert.Equal(0, host.ProjectCallCount);
}
[Fact]
public void RunPostNetworkCommandPhaseDoesNotThrowOnDormantController()
{
PlayerMovementController dormant = CreateDormantController();
var host = new SabotagingHost(
dormant,
RetailObjectClockDisposition.Advance);
var controller = new RuntimeLocalPlayerFrameController(
host,
new FixedMovementInputSource());
Exception? exception = Record.Exception(
controller.RunPostNetworkCommandPhase);
Assert.Null(exception);
Assert.Equal(0, host.SendPostNetworkCallCount);
}
[Fact]
public void TryGetPresentationAfterNetworkReturnsFalseOnDormantControllerWithoutThrowing()
{
PlayerMovementController dormant = CreateDormantController();
var host = new SabotagingHost(
dormant,
RetailObjectClockDisposition.Advance);
var controller = new RuntimeLocalPlayerFrameController(
host,
new FixedMovementInputSource());
bool result = false;
Exception? exception = Record.Exception(() =>
result = controller.TryGetPresentationAfterNetwork(
out RuntimeLocalPlayerPresentationFrame _));
Assert.Null(exception);
Assert.False(result);
}
private static PlayerMovementController CreateDormantController()
{
PlayerMovementController candidate =
PlayerMovementController.CreatePublicationCandidate(
new PhysicsEngine(),
PlayerMovementConstructionOptions.Fallback);
candidate.SealPublicationCandidate();
candidate.CommitRuntimeOwnership(new RetailObjectQuantumClock());
Assert.True(candidate.IsRuntimeOwnedDormant);
Assert.False(candidate.CanExecuteLiveMovement);
return candidate;
}
private sealed class FixedMovementInputSource : IRuntimeMovementInputSource
{
public MovementInput Capture() => default;
}
///
/// A host that reports CanAdvancePlayer: true unconditionally —
/// the exact pre-fix headless bug shape — regardless of the wired
/// controller's real publication state.
///
private sealed class SabotagingHost(
PlayerMovementController? controller,
RetailObjectClockDisposition disposition)
: IRuntimeLocalPlayerFrameHost
{
internal int ProjectCallCount { get; private set; }
internal int SendPostNetworkCallCount { get; private set; }
public bool CanAdvancePlayer => true;
public PlayerMovementController? Controller => controller;
public uint ResolveLocalEntityId() => 1u;
public void HandleTargeting()
{
}
public bool IsHidden => false;
public RetailObjectClockDisposition ObjectClockDisposition =>
disposition;
public void Project(
PlayerMovementController controller,
MovementResult movement,
bool hidden) =>
ProjectCallCount++;
public void SendPreNetwork(
PlayerMovementController controller,
MovementResult movement,
bool hidden)
{
}
public void SendPostNetwork(
PlayerMovementController controller,
bool hidden) =>
SendPostNetworkCallCount++;
}
}