refactor(runtime): own local movement and outbound cadence
Move the canonical local movement controller, body/motion managers, object clock, movement wire data, and MTS/jump/AP sender into AcDream.Runtime. Replace process skill defaults with typed Runtime character options, make graphical and direct commands borrow one autorun owner, retain the construction-time PartArray seam, and include movement in terminal ownership convergence. Preserve the accepted pre-inbound movement/jump and post-inbound autonomous-position order while moving the exact packet/cadence fixtures into Runtime tests. Add graphical/direct parity, two-instance isolation, teardown, allocation, architecture, and divergence-path coverage. Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
parent
3456dff038
commit
aa3f4a60f8
36 changed files with 878 additions and 276 deletions
|
|
@ -76,7 +76,7 @@ public sealed class CameraPointerInputControllerTests
|
|||
var mouseLook = new MouseLook(active: true);
|
||||
var frame = new GameplayInputFrameController(
|
||||
dispatcher: null,
|
||||
new DispatcherMovementInputSource(),
|
||||
new DispatcherMovementInputSource(new RuntimeLocalPlayerMovementState()),
|
||||
mouseLook,
|
||||
new Combat());
|
||||
|
||||
|
|
@ -113,7 +113,7 @@ public sealed class CameraPointerInputControllerTests
|
|||
var mouseLook = new MouseLook(active: true);
|
||||
var frame = new GameplayInputFrameController(
|
||||
dispatcher: null,
|
||||
new DispatcherMovementInputSource(),
|
||||
new DispatcherMovementInputSource(new RuntimeLocalPlayerMovementState()),
|
||||
mouseLook,
|
||||
new Combat());
|
||||
fixture.Owner.BindGameplayFrame(frame);
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ public sealed class DispatcherMovementInputSourceTests
|
|||
[Fact]
|
||||
public void UnboundSourceCapturesNeutralInput()
|
||||
{
|
||||
var source = new DispatcherMovementInputSource();
|
||||
var source = CreateSource();
|
||||
|
||||
Assert.Equal(default, source.Capture());
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ public sealed class DispatcherMovementInputSourceTests
|
|||
public void CapturesDispatcherHeldStateAndRetailWalkModifier()
|
||||
{
|
||||
var (dispatcher, _, _) = CreateDispatcher();
|
||||
var source = new DispatcherMovementInputSource();
|
||||
var source = CreateSource();
|
||||
source.Bind(dispatcher);
|
||||
|
||||
Assert.True(dispatcher.TrySetAutomationActionHeld(
|
||||
|
|
@ -38,7 +38,7 @@ public sealed class DispatcherMovementInputSourceTests
|
|||
public void RetainedKeyboardCaptureSilencesHeldKeysButDoesNotCancelAutorun()
|
||||
{
|
||||
var (dispatcher, _, mouse) = CreateDispatcher();
|
||||
var source = new DispatcherMovementInputSource();
|
||||
var source = CreateSource();
|
||||
source.Bind(dispatcher);
|
||||
dispatcher.TrySetAutomationActionHeld(InputAction.MovementForward, held: true);
|
||||
Assert.True(source.HandlePressedAction(InputAction.MovementRunLock));
|
||||
|
|
@ -59,7 +59,7 @@ public sealed class DispatcherMovementInputSourceTests
|
|||
{
|
||||
var capture = new FakeCapture();
|
||||
var (dispatcher, _, _) = CreateDispatcher();
|
||||
var source = new DispatcherMovementInputSource(capture);
|
||||
var source = CreateSource(capture);
|
||||
source.Bind(dispatcher);
|
||||
source.HandlePressedAction(InputAction.MovementRunLock);
|
||||
|
||||
|
|
@ -78,7 +78,7 @@ public sealed class DispatcherMovementInputSourceTests
|
|||
[InlineData(InputAction.MovementStrafeRight)]
|
||||
public void RetailCancelActionsClearAutorun(InputAction action)
|
||||
{
|
||||
var source = new DispatcherMovementInputSource();
|
||||
var source = CreateSource();
|
||||
source.HandlePressedAction(InputAction.MovementRunLock);
|
||||
|
||||
Assert.False(source.HandlePressedAction(action));
|
||||
|
|
@ -89,7 +89,7 @@ public sealed class DispatcherMovementInputSourceTests
|
|||
[Fact]
|
||||
public void ForwardDoesNotCancelAutorunAndResetDoes()
|
||||
{
|
||||
var source = new DispatcherMovementInputSource();
|
||||
var source = CreateSource();
|
||||
source.HandlePressedAction(InputAction.MovementRunLock);
|
||||
|
||||
Assert.False(source.HandlePressedAction(InputAction.MovementForward));
|
||||
|
|
@ -102,7 +102,7 @@ public sealed class DispatcherMovementInputSourceTests
|
|||
[Fact]
|
||||
public void BindingIsIdempotentOnlyForTheSameDispatcher()
|
||||
{
|
||||
var source = new DispatcherMovementInputSource();
|
||||
var source = CreateSource();
|
||||
var (first, _, _) = CreateDispatcher();
|
||||
var (second, _, _) = CreateDispatcher();
|
||||
|
||||
|
|
@ -115,7 +115,7 @@ public sealed class DispatcherMovementInputSourceTests
|
|||
[Fact]
|
||||
public void UnbindRequiresExactDispatcherAndRestoresNeutralCapture()
|
||||
{
|
||||
var source = new DispatcherMovementInputSource();
|
||||
var source = CreateSource();
|
||||
var (first, _, _) = CreateDispatcher();
|
||||
var (other, _, _) = CreateDispatcher();
|
||||
source.Bind(first);
|
||||
|
|
@ -142,6 +142,10 @@ public sealed class DispatcherMovementInputSourceTests
|
|||
return (dispatcher, keyboard, mouse);
|
||||
}
|
||||
|
||||
private static DispatcherMovementInputSource CreateSource(
|
||||
IInputCaptureSource? capture = null) =>
|
||||
new(new RuntimeLocalPlayerMovementState(), capture);
|
||||
|
||||
private sealed class FakeKeyboard : IKeyboardSource
|
||||
{
|
||||
#pragma warning disable CS0067
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ public sealed class GameplayInputFrameControllerTests
|
|||
var combat = new FakeCombat(calls);
|
||||
var controller = new GameplayInputFrameController(
|
||||
dispatcher,
|
||||
new DispatcherMovementInputSource(),
|
||||
new DispatcherMovementInputSource(new RuntimeLocalPlayerMovementState()),
|
||||
mouseLook,
|
||||
combat);
|
||||
|
||||
|
|
@ -45,7 +45,7 @@ public sealed class GameplayInputFrameControllerTests
|
|||
var calls = new List<string>();
|
||||
var controller = new GameplayInputFrameController(
|
||||
dispatcher: null,
|
||||
new DispatcherMovementInputSource(),
|
||||
new DispatcherMovementInputSource(new RuntimeLocalPlayerMovementState()),
|
||||
mouseLook: null,
|
||||
new FakeCombat(calls));
|
||||
|
||||
|
|
@ -60,7 +60,7 @@ public sealed class GameplayInputFrameControllerTests
|
|||
var calls = new List<string>();
|
||||
var controller = new GameplayInputFrameController(
|
||||
dispatcher: null,
|
||||
new DispatcherMovementInputSource(),
|
||||
new DispatcherMovementInputSource(new RuntimeLocalPlayerMovementState()),
|
||||
mouseLook: null,
|
||||
new FakeCombat(calls, consumes: true));
|
||||
|
||||
|
|
@ -76,7 +76,8 @@ public sealed class GameplayInputFrameControllerTests
|
|||
public void ResetSessionReleasesMouseLookAndAutorun()
|
||||
{
|
||||
var calls = new List<string>();
|
||||
var movement = new DispatcherMovementInputSource();
|
||||
var movement = new DispatcherMovementInputSource(
|
||||
new RuntimeLocalPlayerMovementState());
|
||||
movement.HandlePressedAction(InputAction.MovementRunLock);
|
||||
var mouseLook = new FakeMouseLook(calls);
|
||||
var controller = new GameplayInputFrameController(
|
||||
|
|
@ -98,7 +99,7 @@ public sealed class GameplayInputFrameControllerTests
|
|||
var mouseLook = new FakeMouseLook(calls, active: true, consumes: true);
|
||||
var controller = new GameplayInputFrameController(
|
||||
dispatcher: null,
|
||||
new DispatcherMovementInputSource(),
|
||||
new DispatcherMovementInputSource(new RuntimeLocalPlayerMovementState()),
|
||||
mouseLook,
|
||||
new FakeCombat(calls));
|
||||
|
||||
|
|
|
|||
|
|
@ -1,114 +0,0 @@
|
|||
using System.Buffers.Binary;
|
||||
using System.Net;
|
||||
using System.Numerics;
|
||||
using AcDream.App.Input;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Physics;
|
||||
|
||||
namespace AcDream.App.Tests.Input;
|
||||
|
||||
public sealed class LocalPlayerImmediatePositionTests
|
||||
{
|
||||
[Fact]
|
||||
public void ForcePositionAcknowledgement_SendsOneExactAutonomousPositionAndStampsIt()
|
||||
{
|
||||
using var session = new WorldSession(
|
||||
new IPEndPoint(IPAddress.Loopback, 9000));
|
||||
session.PublishAcceptedLocalPhysicsTimestamps(
|
||||
instance: 11,
|
||||
serverControlledMove: 12,
|
||||
teleport: 13,
|
||||
forcePosition: 14);
|
||||
var sent = new List<byte[]>();
|
||||
session.GameActionCapture = sent.Add;
|
||||
|
||||
PlayerMovementController player = GroundedPlayer();
|
||||
Position preTurnCanonical = player.CellPosition;
|
||||
Quaternion rotation = Quaternion.Normalize(
|
||||
Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 0.7f));
|
||||
player.SetBodyOrientation(rotation);
|
||||
Assert.NotEqual(rotation, preTurnCanonical.Frame.Orientation);
|
||||
Assert.True(player.TryGetOutboundPosition(out Position outboundPosition));
|
||||
var outbound = new LocalPlayerOutboundController((_, _, _, _, _, _) => { });
|
||||
|
||||
outbound.SendImmediatePosition(session, player);
|
||||
|
||||
byte[] body = Assert.Single(sent);
|
||||
Assert.Equal(56, body.Length);
|
||||
Assert.Equal(AutonomousPosition.GameActionOpcode, U32(body, 0));
|
||||
Assert.Equal(1u, U32(body, 4));
|
||||
Assert.Equal(AutonomousPosition.AutonomousPositionAction, U32(body, 8));
|
||||
Assert.Equal(outboundPosition.ObjCellId, U32(body, 12));
|
||||
Assert.Equal(outboundPosition.Frame.Origin.X, F32(body, 16));
|
||||
Assert.Equal(outboundPosition.Frame.Origin.Y, F32(body, 20));
|
||||
Assert.Equal(outboundPosition.Frame.Origin.Z, F32(body, 24));
|
||||
Assert.Equal(rotation.W, F32(body, 28), precision: 6);
|
||||
Assert.Equal(rotation.X, F32(body, 32), precision: 6);
|
||||
Assert.Equal(rotation.Y, F32(body, 36), precision: 6);
|
||||
Assert.Equal(rotation.Z, F32(body, 40), precision: 6);
|
||||
Assert.Equal((ushort)11, U16(body, 44));
|
||||
Assert.Equal((ushort)12, U16(body, 46));
|
||||
Assert.Equal((ushort)13, U16(body, 48));
|
||||
Assert.Equal((ushort)14, U16(body, 50));
|
||||
Assert.Equal((byte)1, body[52]);
|
||||
Assert.True(player.TryGetOutboundPosition(out Position currentOutbound));
|
||||
Assert.False(player.ShouldSendPositionEvent(
|
||||
currentOutbound,
|
||||
player.ContactPlane,
|
||||
player.SimTimeSeconds));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImmediateAcknowledgement_RequiresSessionPlayerAndGroundContact()
|
||||
{
|
||||
using var session = new WorldSession(
|
||||
new IPEndPoint(IPAddress.Loopback, 9000));
|
||||
var sent = new List<byte[]>();
|
||||
session.GameActionCapture = sent.Add;
|
||||
var outbound = new LocalPlayerOutboundController((_, _, _, _, _, _) => { });
|
||||
PlayerMovementController grounded = GroundedPlayer();
|
||||
|
||||
outbound.SendImmediatePosition(null, grounded);
|
||||
outbound.SendImmediatePosition(session, null);
|
||||
outbound.SendImmediatePosition(
|
||||
session,
|
||||
new PlayerMovementController(new PhysicsEngine()));
|
||||
|
||||
Assert.Empty(sent);
|
||||
}
|
||||
|
||||
private static PlayerMovementController GroundedPlayer()
|
||||
{
|
||||
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 player = new PlayerMovementController(engine);
|
||||
player.SetPosition(
|
||||
new Vector3(96f, 97f, 50f),
|
||||
0xA9B40001u,
|
||||
new Vector3(96f, 97f, 50f));
|
||||
Assert.True(player.CanSendPositionEvent);
|
||||
return player;
|
||||
}
|
||||
|
||||
private static uint U32(byte[] body, int offset) =>
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(offset, 4));
|
||||
|
||||
private static ushort U16(byte[] body, int offset) =>
|
||||
BinaryPrimitives.ReadUInt16LittleEndian(body.AsSpan(offset, 2));
|
||||
|
||||
private static float F32(byte[] body, int offset) =>
|
||||
BitConverter.Int32BitsToSingle(
|
||||
BinaryPrimitives.ReadInt32LittleEndian(body.AsSpan(offset, 4)));
|
||||
}
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
using System.Buffers.Binary;
|
||||
using AcDream.App.Input;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Net.Packets;
|
||||
using AcDream.Core.Physics;
|
||||
|
||||
namespace AcDream.App.Tests.Input;
|
||||
|
||||
public sealed class LocalPlayerOutboundCombatStyleTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(0x8000003Cu)] // HandCombat
|
||||
[InlineData(0x8000003Eu)] // Magic
|
||||
[InlineData(0x8000003Fu)] // BowCombat
|
||||
[InlineData(0x80000041u)] // CrossbowCombat
|
||||
public void CaptureAndBuild_PreservesCanonicalRawCombatStyle(uint combatStyle)
|
||||
{
|
||||
var controller = new PlayerMovementController(new PhysicsEngine());
|
||||
controller.Motion.RawState.CurrentStyle = combatStyle;
|
||||
|
||||
MovementResult movement = controller.CaptureMovementResult(
|
||||
mouseLookEvent: false);
|
||||
RawMotionState outbound = LocalPlayerOutboundController.BuildRawMotionState(
|
||||
movement);
|
||||
|
||||
Assert.Equal(combatStyle, movement.CurrentStyle);
|
||||
Assert.Equal(combatStyle, outbound.CurrentStyle);
|
||||
|
||||
var writer = new PacketWriter(16);
|
||||
RawMotionStatePacker.Pack(writer, outbound);
|
||||
byte[] bytes = writer.ToArray();
|
||||
|
||||
uint flags = BinaryPrimitives.ReadUInt32LittleEndian(bytes);
|
||||
Assert.NotEqual(0u, flags & 0x2u);
|
||||
Assert.Equal(
|
||||
combatStyle,
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(bytes.AsSpan(4)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CaptureAndBuild_NonCombatRetainsRetailDefault()
|
||||
{
|
||||
var controller = new PlayerMovementController(new PhysicsEngine());
|
||||
|
||||
RawMotionState outbound = LocalPlayerOutboundController.BuildRawMotionState(
|
||||
controller.CaptureMovementResult(mouseLookEvent: false));
|
||||
|
||||
Assert.Equal(RawMotionState.Default.CurrentStyle, outbound.CurrentStyle);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,456 +0,0 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Input;
|
||||
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 = 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.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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue