Member-wise deletion of the three legacy resolver members named in docs/research/2026-08-05-c5a-contract.md: PhysicsEngine.Resolve, PhysicsEngine.HasCellSurface, and PhysicsEngine.ResolvePlacement. An exhaustive receiver census over src/ found zero production callers of any of the three — every production placement writer already reaches the canonical PhysicsEngine.SetPosition transaction exclusively through RuntimeSetPositionState (three call sites total). The deletion is purely member-wise: IsSpawnCellReady and AdjustPosition, which shared the same source region as the deleted members, are preserved byte-identical — every remaining production caller of either (including PhysicsCameraCollisionProbe, AdjustPosition's sole surviving production caller) is unaffected. Companion changes: - PlayerMovementController's 3-argument SetPosition test overload is renamed to SeedPlacementForTest (internal) and CommitPreparedPosition is deleted; 83 call sites across 19 test files were mechanically renamed to match. - Seven pinned test dispositions from the contract are executed: 3.1 (PhysicsEngineTests.cs: 11 legacy-resolver tests deleted, 6 ResolveWithTransition tests kept), 3.2/3.3/3.4 (re-point to canonical SetPosition, with TransitionScratchDifferentialTests.cs additionally gaining positive IsCommitted assertions after each bitwise comparison so the differential proves a placement actually committed, not just that two possibly-uncommitted results match), 3.5 (Runtime rename), and 3.6 (PlayerMovementPlacementTransactionTests.cs rewritten — its xmldoc now states plainly that the render-root publish moved to RuntimeSetPositionState.cs, but the sticky-release relocation claim was false and is retracted; this disposition's coverage loss is the sticky release path, not silently absorbed elsewhere). - Stale `PhysicsEngine.Resolve`/`Resolve` doc citations in CellTransit.cs, PlayerMovementController.cs, and HeadlessSessionWorldProjection.cs are corrected to name the surviving canonical entry points by symbol (SetPosition, AdjustSetPosition/AdjustPosition, ResolveWithTransition) rather than fragile line numbers. Retires AP-1 and AD-1 in docs/architecture/retail-divergence-register.md: both rows described production zero-delta placement routing remaining on the legacy resolver pending the Slice 4B2/4B route cutover; that resolver no longer exists, so the condition each row tracked is now structurally false rather than merely narrowed. AP-145 (routed through the prior commit) and this commit's AP-1/AD-1 together bring the section counts to 101 AP / 47 AD active rows. Builds on the AP-145 fix (previous commit) — this commit's staged tree was independently rebuilt and its four suites independently rerun on top of that commit before this commit was created, in addition to the combined rebuild/rerun below. Full-solution build: 0 errors (21 pre-existing warnings, all unrelated). Suite results (combined tree): Core 4270/4271 passed (1 skip; the single DatSoundCacheTests concurrent-decode-dedup failure is a known load-sensitive race, confirmed passing standalone and unrelated to this change), Runtime 1176/1176, Headless 86/86, App 4132/4135 (3 skips). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
455 lines
17 KiB
C#
455 lines
17 KiB
C#
using System.Numerics;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.Physics.Motion;
|
|
using AcDream.Runtime.Gameplay;
|
|
|
|
namespace AcDream.Runtime.Tests.Gameplay;
|
|
|
|
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 = LocalPlayerOutboundController.BuildRawMotionState(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 = LocalPlayerOutboundController.BuildRawMotionState(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.SeedPlacementForTest(new Vector3(96f, 96f, 50f), 0x0001u, new Vector3(96f, 96f, 50f));
|
|
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);
|
|
}
|
|
}
|