Restore the named-retail object update order across local, remote, static, projectile, animation, shadow, teleport, and effect lifetimes. Separate authoritative root commits from spatial rebucketing, preserve per-owner hook/FIFO ordering, and remove update-path allocations with exact lifecycle and residency gates. Add deterministic conformance, adversarial lifetime, GUID-reuse, pending-cell, quaternion, timestamp, and allocation coverage. Release build is warning-free and all 6,446 tests pass with five intentional skips; retail, architecture, and adversarial reviews are clean. Co-authored-by: OpenAI Codex <codex@openai.com>
457 lines
17 KiB
C#
457 lines
17 KiB
C#
using System.Numerics;
|
|
using AcDream.App.Input;
|
|
using AcDream.App.Rendering;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.Physics.Motion;
|
|
using Xunit;
|
|
|
|
namespace AcDream.App.Tests.Input;
|
|
|
|
public sealed class PlayerMouseLookMovementTests
|
|
{
|
|
[Fact]
|
|
public void KeyboardMotionEdges_StillRequestImmediateMovementEvents()
|
|
{
|
|
var controller = CreateController();
|
|
|
|
MovementResult pressed = controller.Update(
|
|
1f / 60f,
|
|
new MovementInput(Forward: true));
|
|
MovementResult held = controller.Update(
|
|
1f / 60f,
|
|
new MovementInput(Forward: true));
|
|
MovementResult released = controller.Update(
|
|
1f / 60f,
|
|
new MovementInput());
|
|
|
|
Assert.True(pressed.ShouldSendMovementEvent);
|
|
Assert.False(held.ShouldSendMovementEvent);
|
|
Assert.True(released.ShouldSendMovementEvent);
|
|
}
|
|
|
|
[Fact]
|
|
public void MouseAdjustment_UsesTurnMotionInsteadOfDirectYawMutation()
|
|
{
|
|
var controller = CreateController();
|
|
Assert.True(controller.BeginMouseLook(new MovementInput()));
|
|
controller.NoteMovementSent(controller.SimTimeSeconds, mouseLookEvent: false);
|
|
|
|
float yawBefore = controller.Yaw;
|
|
controller.SubmitMouseTurnAdjustment(-0.4f, new MovementInput());
|
|
// Rotation is authored/applied on an admitted retail object quantum,
|
|
// not directly by the mouse sample. Stay below the outbound mouse
|
|
// cadence while crossing the strict physics minimum.
|
|
MovementResult turning = controller.Update(
|
|
PhysicsBody.MinQuantum + 0.001f,
|
|
new MovementInput());
|
|
|
|
Assert.Equal(MotionCommand.TurnRight, turning.TurnCommand);
|
|
Assert.Equal(0.8f, turning.TurnSpeed);
|
|
Assert.True(controller.Yaw < yawBefore);
|
|
Assert.False(turning.ShouldSendMovementEvent);
|
|
Assert.True(turning.TurnUsesRunHold);
|
|
|
|
MovementResult held = controller.Update(1f / 60f, new MovementInput());
|
|
Assert.Equal(MotionCommand.TurnRight, held.TurnCommand);
|
|
|
|
controller.StopMouseDrift(new MovementInput());
|
|
MovementResult stopped = controller.Update(1f / 60f, new MovementInput());
|
|
Assert.Null(stopped.TurnCommand);
|
|
Assert.False(stopped.ShouldSendMovementEvent);
|
|
}
|
|
|
|
[Fact]
|
|
public void MouseLook_ReportsStartCadenceAndStopWithoutPerSampleFlooding()
|
|
{
|
|
var controller = CreateController();
|
|
Assert.True(controller.BeginMouseLook(new MovementInput()));
|
|
MovementResult begin = controller.CaptureMovementResult(mouseLookEvent: false);
|
|
Assert.True(begin.ShouldSendMovementEvent);
|
|
Assert.False(begin.IsMouseLookMovementEvent);
|
|
controller.NoteMovementSent(controller.SimTimeSeconds, mouseLookEvent: false);
|
|
|
|
controller.SubmitMouseTurnAdjustment(0.5f, new MovementInput());
|
|
MovementResult sample = controller.Update(0.01f, new MovementInput());
|
|
Assert.Equal(MotionCommand.TurnLeft, sample.TurnCommand);
|
|
Assert.False(sample.ShouldSendMovementEvent);
|
|
|
|
controller.Update(
|
|
PlayerMovementController.MouseMovementEventInterval,
|
|
new MovementInput());
|
|
controller.SubmitMouseTurnAdjustment(0.5f, new MovementInput());
|
|
MovementResult cadence = controller.Update(
|
|
0.001f,
|
|
new MovementInput());
|
|
Assert.True(cadence.ShouldSendMovementEvent);
|
|
Assert.True(cadence.IsMouseLookMovementEvent);
|
|
controller.NoteMovementSent(
|
|
controller.SimTimeSeconds,
|
|
mouseLookEvent: true);
|
|
|
|
Assert.True(controller.EndMouseLook(new MovementInput()));
|
|
MovementResult ended = controller.CaptureMovementResult(mouseLookEvent: false);
|
|
Assert.True(ended.ShouldSendMovementEvent);
|
|
Assert.False(ended.IsMouseLookMovementEvent);
|
|
Assert.Null(ended.TurnCommand);
|
|
}
|
|
|
|
[Fact]
|
|
public void MouseMovementEvent_IsRetriedUntilSuccessfulSendIsAcknowledged()
|
|
{
|
|
var controller = CreateController();
|
|
controller.BeginMouseLook(new MovementInput());
|
|
controller.Update(
|
|
PlayerMovementController.MouseMovementEventInterval + 0.001f,
|
|
new MovementInput());
|
|
controller.SubmitMouseTurnAdjustment(0.4f, new MovementInput());
|
|
|
|
MovementResult first = controller.Update(0.01f, new MovementInput(Run: true));
|
|
MovementResult retry = controller.Update(0.01f, new MovementInput(Run: true));
|
|
|
|
Assert.True(first.ShouldSendMovementEvent);
|
|
Assert.True(first.IsMouseLookMovementEvent);
|
|
Assert.True(retry.ShouldSendMovementEvent);
|
|
|
|
controller.NoteMovementSent(
|
|
controller.SimTimeSeconds,
|
|
mouseLookEvent: true);
|
|
MovementResult acknowledged = controller.Update(
|
|
0.01f,
|
|
new MovementInput(Run: true));
|
|
Assert.False(acknowledged.ShouldSendMovementEvent);
|
|
}
|
|
|
|
[Fact]
|
|
public void MouseLook_RapidPressReleaseCanBeCapturedSynchronously()
|
|
{
|
|
var controller = CreateController();
|
|
var input = new MovementInput(Run: true);
|
|
|
|
Assert.True(controller.BeginMouseLook(input));
|
|
MovementResult press = controller.CaptureMovementResult(mouseLookEvent: false);
|
|
Assert.True(controller.EndMouseLook(input));
|
|
MovementResult release = controller.CaptureMovementResult(mouseLookEvent: false);
|
|
|
|
Assert.True(press.ShouldSendMovementEvent);
|
|
Assert.False(press.IsMouseLookMovementEvent);
|
|
Assert.True(release.ShouldSendMovementEvent);
|
|
Assert.False(release.IsMouseLookMovementEvent);
|
|
}
|
|
|
|
[Fact]
|
|
public void MouseLook_CadenceUsesStrictHalfSecondBoundary()
|
|
{
|
|
var controller = CreateController();
|
|
controller.BeginMouseLook(new MovementInput());
|
|
controller.NoteMovementSent(nowSeconds: 0f, mouseLookEvent: true);
|
|
|
|
controller.SubmitMouseTurnAdjustment(0.4f, new MovementInput());
|
|
MovementResult boundary = controller.Update(
|
|
PlayerMovementController.MouseMovementEventInterval,
|
|
new MovementInput());
|
|
Assert.False(boundary.ShouldSendMovementEvent);
|
|
|
|
controller.SubmitMouseTurnAdjustment(0.4f, new MovementInput());
|
|
MovementResult justOver = controller.Update(
|
|
0.0001f,
|
|
new MovementInput());
|
|
Assert.True(justOver.ShouldSendMovementEvent);
|
|
Assert.True(justOver.IsMouseLookMovementEvent);
|
|
}
|
|
|
|
[Fact]
|
|
public void MouseTurn_UsesPerAxisRunHoldEvenInWalkMode()
|
|
{
|
|
var controller = CreateController();
|
|
controller.BeginMouseLook(new MovementInput());
|
|
controller.NoteMovementSent(nowSeconds: 0f, mouseLookEvent: true);
|
|
controller.SubmitMouseTurnAdjustment(-0.4f, new MovementInput());
|
|
|
|
MovementResult turning = controller.Update(
|
|
0.01f,
|
|
new MovementInput(Run: false));
|
|
|
|
Assert.Equal(MotionCommand.TurnRight, turning.TurnCommand);
|
|
Assert.False(turning.IsRunning);
|
|
Assert.True(turning.TurnUsesRunHold);
|
|
Assert.Equal(HoldKey.Run, controller.Motion.RawState.TurnHoldKey);
|
|
}
|
|
|
|
[Fact]
|
|
public void MouseLookRelease_SynchronouslyRestoresHeldKeyboardTurn()
|
|
{
|
|
var controller = CreateController();
|
|
controller.Update(
|
|
0.01f,
|
|
new MovementInput(TurnLeft: true, Run: true));
|
|
var held = new MovementInput(TurnLeft: true, Run: true);
|
|
controller.BeginMouseLook(held);
|
|
controller.NoteMovementSent(controller.SimTimeSeconds, mouseLookEvent: false);
|
|
controller.SubmitMouseTurnAdjustment(-0.4f, held);
|
|
controller.Update(0.01f, held);
|
|
|
|
controller.EndMouseLook(held);
|
|
MovementResult release = controller.CaptureMovementResult(mouseLookEvent: false);
|
|
|
|
Assert.Equal(MotionCommand.TurnLeft, release.TurnCommand);
|
|
Assert.Equal(1f, release.TurnSpeed);
|
|
Assert.False(release.TurnUsesRunHold);
|
|
}
|
|
|
|
[Fact]
|
|
public void MouseLook_RemapMovesHeldKeyboardTurnToSidestepAndBack()
|
|
{
|
|
var controller = CreateController();
|
|
var held = new MovementInput(TurnLeft: true, Run: true);
|
|
controller.Update(0.01f, held);
|
|
|
|
Assert.True(controller.BeginMouseLook(held));
|
|
MovementResult press = controller.CaptureMovementResult(mouseLookEvent: false);
|
|
|
|
Assert.Null(press.TurnCommand);
|
|
Assert.Equal(MotionCommand.SideStepLeft, press.SidestepCommand);
|
|
Assert.Equal(HoldKey.Run, controller.Motion.RawState.SidestepHoldKey);
|
|
|
|
Assert.True(controller.EndMouseLook(held));
|
|
MovementResult release = controller.CaptureMovementResult(mouseLookEvent: false);
|
|
|
|
Assert.Null(release.SidestepCommand);
|
|
Assert.Equal(MotionCommand.TurnLeft, release.TurnCommand);
|
|
Assert.False(release.TurnUsesRunHold);
|
|
}
|
|
|
|
[Fact]
|
|
public void MouseLookEntry_PreservesHeldForwardAndRunAfterTakingControl()
|
|
{
|
|
var controller = CreateController();
|
|
var held = new MovementInput(Forward: true, Run: true);
|
|
|
|
Assert.True(controller.BeginMouseLook(held));
|
|
MovementResult press = controller.CaptureMovementResult(mouseLookEvent: false);
|
|
|
|
Assert.Equal(MotionCommand.WalkForward, press.ForwardCommand);
|
|
Assert.True(press.IsRunning);
|
|
Assert.Equal(HoldKey.Run, controller.Motion.RawState.CurrentHoldKey);
|
|
}
|
|
|
|
[Fact]
|
|
public void MouseLookEntry_AlreadyAutonomousSameFrameForwardEdgeIsNotLost()
|
|
{
|
|
var controller = CreateController();
|
|
controller.Update(0.01f, new MovementInput(Forward: true));
|
|
controller.Update(0.01f, new MovementInput());
|
|
var justPressed = new MovementInput(Forward: true, Run: true);
|
|
|
|
controller.BeginMouseLook(justPressed);
|
|
MovementResult press = controller.CaptureMovementResult(mouseLookEvent: false);
|
|
|
|
Assert.Equal(MotionCommand.WalkForward, press.ForwardCommand);
|
|
Assert.True(press.IsRunning);
|
|
Assert.Equal(HoldKey.Run, controller.Motion.RawState.CurrentHoldKey);
|
|
}
|
|
|
|
[Fact]
|
|
public void MouseLookEntry_AlreadyAutonomousSameFrameRunChangeIsNotLost()
|
|
{
|
|
var controller = CreateController();
|
|
controller.Update(0.01f, new MovementInput(Forward: true));
|
|
controller.Update(0.01f, new MovementInput());
|
|
|
|
controller.BeginMouseLook(new MovementInput(Run: true));
|
|
MovementResult press = controller.CaptureMovementResult(mouseLookEvent: false);
|
|
RawMotionState wire = GameWindow.BuildOutboundRawMotionState(press);
|
|
|
|
Assert.Equal(HoldKey.Run, controller.Motion.RawState.CurrentHoldKey);
|
|
Assert.True(press.IsRunning);
|
|
Assert.Equal(HoldKey.Run, wire.CurrentHoldKey);
|
|
Assert.Equal(HoldKey.Invalid, wire.ForwardHoldKey);
|
|
Assert.Equal(HoldKey.Invalid, wire.SidestepHoldKey);
|
|
Assert.Equal(HoldKey.Invalid, wire.TurnHoldKey);
|
|
}
|
|
|
|
[Fact]
|
|
public void MouseLookEntry_ServerControlledHeldTurnNeverQueuesWrongAxis()
|
|
{
|
|
var controller = CreateController();
|
|
|
|
controller.BeginMouseLook(new MovementInput(TurnLeft: true));
|
|
|
|
Assert.DoesNotContain(
|
|
controller.Motion.PendingMotions,
|
|
node => node.Motion == MotionCommand.TurnLeft);
|
|
Assert.Equal(MotionCommand.SideStepLeft,
|
|
controller.Motion.RawState.SidestepCommand);
|
|
}
|
|
|
|
[Fact]
|
|
public void MouseLookExit_ServerControlledHeldTurnNeverQueuesRemappedAxis()
|
|
{
|
|
var controller = CreateController();
|
|
var held = new MovementInput(TurnLeft: true);
|
|
controller.BeginMouseLook(held);
|
|
DrainPendingMotions(controller);
|
|
controller.SetLastMoveWasAutonomous(false);
|
|
|
|
controller.EndMouseLook(held);
|
|
|
|
Assert.DoesNotContain(
|
|
controller.Motion.PendingMotions,
|
|
node => node.Motion == MotionCommand.SideStepLeft);
|
|
Assert.Equal(MotionCommand.TurnLeft, controller.Motion.RawState.TurnCommand);
|
|
}
|
|
|
|
[Fact]
|
|
public void MouseLook_RemapPressedAfterEntryAppearsInOutboundResult()
|
|
{
|
|
var controller = CreateController();
|
|
controller.BeginMouseLook(new MovementInput());
|
|
|
|
MovementResult result = controller.Update(
|
|
0.01f,
|
|
new MovementInput(TurnLeft: true));
|
|
|
|
Assert.Equal(MotionCommand.SideStepLeft, result.SidestepCommand);
|
|
Assert.Null(result.TurnCommand);
|
|
Assert.True(result.SidestepUsesRunHold);
|
|
Assert.False(result.IsRunning);
|
|
Assert.True(result.ShouldSendMovementEvent);
|
|
}
|
|
|
|
[Fact]
|
|
public void MouseLook_WalkModeRemapCarriesPerAxisRunHold()
|
|
{
|
|
var controller = CreateController();
|
|
var held = new MovementInput(TurnRight: true, Run: false);
|
|
|
|
controller.BeginMouseLook(held);
|
|
MovementResult press = controller.CaptureMovementResult(mouseLookEvent: false);
|
|
RawMotionState wire = GameWindow.BuildOutboundRawMotionState(press);
|
|
|
|
Assert.Equal(MotionCommand.SideStepRight, press.SidestepCommand);
|
|
Assert.True(press.SidestepUsesRunHold);
|
|
Assert.False(press.IsRunning);
|
|
Assert.Equal(HoldKey.Run, controller.Motion.RawState.SidestepHoldKey);
|
|
Assert.Equal(HoldKey.None, wire.CurrentHoldKey);
|
|
Assert.Equal(HoldKey.Run, wire.SidestepHoldKey);
|
|
}
|
|
|
|
[Fact]
|
|
public void MouseLook_MouseHandlerRetakesServerControlWithoutDroppingHeldInput()
|
|
{
|
|
var controller = CreateController();
|
|
var held = new MovementInput(Forward: true, TurnLeft: true, Run: true);
|
|
controller.BeginMouseLook(held);
|
|
controller.SetLastMoveWasAutonomous(false);
|
|
|
|
controller.SubmitMouseTurnAdjustment(-0.4f, held);
|
|
MovementResult result = controller.Update(0.01f, held);
|
|
|
|
Assert.Equal(MotionCommand.WalkForward, result.ForwardCommand);
|
|
Assert.Equal(MotionCommand.SideStepLeft, result.SidestepCommand);
|
|
Assert.Equal(MotionCommand.TurnRight, result.TurnCommand);
|
|
Assert.True(result.IsRunning);
|
|
Assert.True(result.SidestepUsesRunHold);
|
|
Assert.True(result.TurnUsesRunHold);
|
|
}
|
|
|
|
[Fact]
|
|
public void MouseLookRelease_UsesCurrentInputSnapshotInsteadOfPreviousFrame()
|
|
{
|
|
var controller = CreateController();
|
|
var held = new MovementInput(Forward: true, TurnLeft: true, Run: true);
|
|
controller.Update(0.01f, held);
|
|
controller.BeginMouseLook(held);
|
|
|
|
Assert.True(controller.EndMouseLook(new MovementInput()));
|
|
MovementResult release = controller.CaptureMovementResult(mouseLookEvent: false);
|
|
|
|
Assert.Null(release.ForwardCommand);
|
|
Assert.Null(release.SidestepCommand);
|
|
Assert.Null(release.TurnCommand);
|
|
Assert.False(release.IsRunning);
|
|
}
|
|
|
|
[Fact]
|
|
public void MouseLook_CannotBeginInPortalSpace()
|
|
{
|
|
var controller = CreateController();
|
|
controller.State = PlayerState.PortalSpace;
|
|
|
|
Assert.False(controller.BeginMouseLook(new MovementInput(Forward: true)));
|
|
|
|
MovementResult result = controller.CaptureMovementResult(mouseLookEvent: false);
|
|
Assert.Null(result.ForwardCommand);
|
|
Assert.Null(result.SidestepCommand);
|
|
Assert.Null(result.TurnCommand);
|
|
}
|
|
|
|
[Fact]
|
|
public void AttackPreparation_StopsMouseTurnAndPublishesFinalHeading()
|
|
{
|
|
var controller = CreateController();
|
|
controller.BeginMouseLook(new MovementInput());
|
|
controller.Update(0.01f, new MovementInput());
|
|
controller.SubmitMouseTurnAdjustment(-0.5f, new MovementInput());
|
|
MovementResult turning = controller.Update(1f / 60f, new MovementInput());
|
|
Assert.Equal(MotionCommand.TurnRight, turning.TurnCommand);
|
|
|
|
Assert.True(controller.PrepareForAttackRequest());
|
|
float finalYaw = controller.Yaw;
|
|
MovementResult stopped = controller.CaptureMovementResult(mouseLookEvent: false);
|
|
|
|
Assert.Equal(finalYaw, controller.Yaw);
|
|
Assert.Null(stopped.TurnCommand);
|
|
Assert.True(stopped.ShouldSendMovementEvent);
|
|
}
|
|
|
|
[Fact]
|
|
public void AttackPreparation_DoesNotInterruptServerControlledMovement()
|
|
{
|
|
var controller = CreateController();
|
|
controller.SetLastMoveWasAutonomous(false);
|
|
|
|
Assert.False(controller.PrepareForAttackRequest());
|
|
MovementResult result = controller.Update(
|
|
0.01f,
|
|
new MovementInput(Run: true));
|
|
|
|
Assert.False(result.ShouldSendMovementEvent);
|
|
}
|
|
|
|
[Fact]
|
|
public void AttackPreparation_DefaultServerControlledStateIsNoOp()
|
|
{
|
|
var controller = CreateController();
|
|
|
|
Assert.False(controller.PrepareForAttackRequest());
|
|
}
|
|
|
|
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<CellSurface>(),
|
|
Array.Empty<PortalPlane>(),
|
|
worldOffsetX: 0f,
|
|
worldOffsetY: 0f);
|
|
|
|
var controller = new PlayerMovementController(engine);
|
|
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001u);
|
|
return controller;
|
|
}
|
|
|
|
private static void DrainPendingMotions(PlayerMovementController controller)
|
|
{
|
|
int remaining = controller.Motion.PendingMotions.Count();
|
|
for (int i = 0; i < remaining; i++)
|
|
controller.Motion.MotionDone(0u, success: true);
|
|
}
|
|
}
|