using System.Numerics; using AcDream.App.Input; using AcDream.App.World; using AcDream.Core.Physics; namespace AcDream.App.Tests.Input; public sealed class RetailLocalPlayerFrameControllerTests { [Fact] public void F751BetweenPhases_CannotRelocateOrReplayCapturedOutboundMovement() { PlayerMovementController controller = CreateController(); var calls = new List(); var preNetwork = new List<(PlayerState State, MovementResult Movement)>(); var postNetwork = new List(); var local = new RetailLocalPlayerFrameController( canPresentPlayer: () => true, getController: () => controller, captureInput: () => new MovementInput(Forward: true), resolveLocalEntityId: () => 7u, handleTargeting: () => calls.Add("target"), isHidden: () => false, project: (_, _, _) => calls.Add("project"), sendPreNetwork: (owner, movement, _) => { calls.Add("pre-outbound"); preNetwork.Add((owner.State, movement)); }, sendPostNetwork: (owner, _) => { calls.Add("post-position"); postNetwork.Add(owner.State); }); var live = new RetailLiveFrameCoordinator( dt => { calls.Add("objects"); local.AdvanceBeforeNetwork(dt); }, () => { calls.Add("f751"); controller.State = PlayerState.PortalSpace; }, () => { calls.Add("commands"); local.RunPostNetworkCommandPhase(); }, () => calls.Add("spatial")); // CPhysicsObj::update_object admits manager time only after the strict // minimum quantum. Use one admitted object tick; the test's concern is // the network barrier, not render-fragment accumulation. live.Tick(PhysicsBody.MaxQuantum); Assert.True(local.TryGetPresentationAfterNetwork(out var presentation)); Assert.Equal( [ "objects", "target", "project", "pre-outbound", "f751", "commands", "project", "post-position", "spatial", ], calls); Assert.Single(preNetwork); Assert.Equal(PlayerState.InWorld, preNetwork[0].State); Assert.True(preNetwork[0].Movement.ShouldSendMovementEvent); Assert.Equal([PlayerState.PortalSpace], postNetwork); Assert.Equal(PlayerState.PortalSpace, controller.State); Assert.True(presentation.AdvancedBeforeNetwork); } [Fact] public void OwnerCreatedByInboundPass_IsProjectedWithoutPhysicsOrOutboundTick() { PlayerMovementController? controller = null; int projections = 0; int preNetwork = 0; int postNetwork = 0; var local = new RetailLocalPlayerFrameController( canPresentPlayer: () => controller is not null, getController: () => controller, captureInput: () => new MovementInput(Forward: true), resolveLocalEntityId: () => 9u, handleTargeting: () => { }, isHidden: () => false, project: (_, _, _) => projections++, sendPreNetwork: (_, _, _) => preNetwork++, sendPostNetwork: (_, _) => postNetwork++); local.AdvanceBeforeNetwork(1f / 60f); controller = CreateController(); float timeBefore = controller.SimTimeSeconds; Assert.True(local.TryGetPresentationAfterNetwork(out var presentation)); Assert.False(presentation.AdvancedBeforeNetwork); Assert.Equal(timeBefore, controller.SimTimeSeconds); Assert.Equal(1, projections); Assert.Equal(0, preNetwork); Assert.Equal(0, postNetwork); Assert.False(presentation.Movement.ShouldSendMovementEvent); } [Fact] public void OrdinaryInboundRootMutation_IsReconciledWithoutSecondPhysicsTick() { PlayerMovementController controller = CreateController(); Vector3 projectedRoot = default; var local = new RetailLocalPlayerFrameController( canPresentPlayer: () => true, getController: () => controller, captureInput: () => new MovementInput(), resolveLocalEntityId: () => 7u, handleTargeting: () => { }, isHidden: () => false, project: (_, movement, _) => projectedRoot = movement.RenderPosition, sendPreNetwork: (_, _, _) => { }, sendPostNetwork: (_, _) => { }); var live = new RetailLiveFrameCoordinator( local.AdvanceBeforeNetwork, () => projectedRoot = new Vector3(999f, 999f, 999f), local.RunPostNetworkCommandPhase, () => { }); live.Tick(1f / 60f); float timeAfterOneTick = controller.SimTimeSeconds; Assert.Equal(controller.RenderPosition, projectedRoot); Assert.Equal(1f / 60f, timeAfterOneTick); } [Fact] public void TargetManager_ObservesPostTransitionPlayerPosition() { PlayerMovementController controller = CreateController(); Vector3 initial = controller.Position; Vector3 positionSeenByTargetManager = new(float.NaN); var local = new RetailLocalPlayerFrameController( canPresentPlayer: () => true, getController: () => controller, captureInput: () => new MovementInput(Forward: true), resolveLocalEntityId: () => 7u, handleTargeting: () => positionSeenByTargetManager = controller.Position, isHidden: () => false, project: (_, _, _) => { }, sendPreNetwork: (_, _, _) => { }, sendPostNetwork: (_, _) => { }); local.AdvanceBeforeNetwork(PhysicsBody.MaxQuantum); Assert.NotEqual(initial, controller.Position); Assert.Equal(controller.Position, positionSeenByTargetManager); } [Fact] public void FrozenObject_IsPresentedWithoutPhysicsInputOrOutboundState() { PlayerMovementController controller = CreateController(); Vector3 initial = controller.Position; int inputCaptures = 0; int projections = 0; int preNetwork = 0; int postNetwork = 0; var local = new RetailLocalPlayerFrameController( canPresentPlayer: () => true, getController: () => controller, captureInput: () => { inputCaptures++; return new MovementInput(Forward: true); }, resolveLocalEntityId: () => 7u, handleTargeting: () => throw new InvalidOperationException( "Frozen object advanced its manager tail."), isHidden: () => false, project: (_, _, _) => projections++, sendPreNetwork: (_, _, _) => preNetwork++, sendPostNetwork: (_, _) => postNetwork++, objectClockDisposition: () => RetailObjectClockDisposition.Suspend); local.AdvanceBeforeNetwork(PhysicsBody.MaxQuantum); local.RunPostNetworkCommandPhase(); Assert.True(local.TryGetPresentationAfterNetwork(out var presentation)); Assert.Equal(initial, controller.Position); Assert.Equal(PhysicsBody.MaxQuantum, controller.SimTimeSeconds); Assert.Equal(0, inputCaptures); Assert.Equal(2, projections); Assert.Equal(0, preNetwork); Assert.Equal(0, postNetwork); Assert.False(presentation.AdvancedBeforeNetwork); } [Fact] public void HiddenPoseIsDirtyOnlyWhenACompleteObjectQuantumRuns() { PlayerMovementController controller = CreateController(); var local = new RetailLocalPlayerFrameController( canPresentPlayer: () => true, getController: () => controller, captureInput: () => new MovementInput(), resolveLocalEntityId: () => 7u, handleTargeting: () => { }, isHidden: () => true, project: (_, _, _) => { }, sendPreNetwork: (_, _, _) => { }, sendPostNetwork: (_, _) => { }); local.AdvanceBeforeNetwork(PhysicsBody.MaxQuantum); Assert.True(local.HiddenPartPoseDirty); local.AdvanceBeforeNetwork(PhysicsBody.MinQuantum / 2f); Assert.False(local.HiddenPartPoseDirty); } [Fact] public void InvalidElapsed_PublishesSnapshotWithoutInputOrNetworkCallbacks() { PlayerMovementController controller = CreateController(); int inputCaptures = 0; int targeting = 0; int projections = 0; int preNetwork = 0; int postNetwork = 0; var local = new RetailLocalPlayerFrameController( canPresentPlayer: () => true, getController: () => controller, captureInput: () => { inputCaptures++; return new MovementInput(Forward: true); }, resolveLocalEntityId: () => 7u, handleTargeting: () => targeting++, isHidden: () => false, project: (_, _, _) => projections++, sendPreNetwork: (_, _, _) => preNetwork++, sendPostNetwork: (_, _) => postNetwork++); float initialTime = controller.SimTimeSeconds; Vector3 initialPosition = controller.Position; foreach (float elapsed in new[] { float.NaN, float.PositiveInfinity, float.NegativeInfinity, -1f, 0f, }) { local.AdvanceBeforeNetwork(elapsed); local.RunPostNetworkCommandPhase(); Assert.True(local.TryGetPresentationAfterNetwork(out var frame)); Assert.False(frame.AdvancedBeforeNetwork); Assert.False(local.HiddenPartPoseDirty); } Assert.Equal(initialTime, controller.SimTimeSeconds); Assert.Equal(initialPosition, controller.Position); Assert.Equal(0, inputCaptures); Assert.Equal(0, targeting); Assert.Equal(0, preNetwork); Assert.Equal(0, postNetwork); Assert.Equal(10, projections); } private static PlayerMovementController CreateController() { var engine = new PhysicsEngine(); var heights = new byte[81]; Array.Fill(heights, (byte)50); var heightTable = new float[256]; for (int i = 0; i < heightTable.Length; i++) heightTable[i] = i; engine.AddLandblock( 0xA9B4FFFFu, new TerrainSurface(heights, heightTable), Array.Empty(), Array.Empty(), worldOffsetX: 0f, worldOffsetY: 0f); var clock = new RetailObjectQuantumClock(); var controller = new PlayerMovementController(engine, clock); controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001u); Assert.True(clock.IsActive); return controller; } }