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
|
|
@ -96,7 +96,8 @@ public sealed class HostInputCameraCompositionTests
|
|||
ViewportAspect = new ViewportAspectState();
|
||||
Framebuffer = new FramebufferResizeController(ViewportAspect);
|
||||
Capture = new CaptureSource();
|
||||
Movement = new DispatcherMovementInputSource(Capture);
|
||||
MovementState = new RuntimeLocalPlayerMovementState();
|
||||
Movement = new DispatcherMovementInputSource(MovementState, Capture);
|
||||
CameraInput = new DispatcherCameraInputSource();
|
||||
PlayerMode = new LocalPlayerModeState();
|
||||
Chase = new ChaseCameraInputState();
|
||||
|
|
@ -111,6 +112,7 @@ public sealed class HostInputCameraCompositionTests
|
|||
public ViewportAspectState ViewportAspect { get; }
|
||||
public FramebufferResizeController Framebuffer { get; }
|
||||
public CaptureSource Capture { get; }
|
||||
public RuntimeLocalPlayerMovementState MovementState { get; }
|
||||
public DispatcherMovementInputSource Movement { get; }
|
||||
public DispatcherCameraInputSource CameraInput { get; }
|
||||
public LocalPlayerModeState PlayerMode { get; }
|
||||
|
|
@ -141,7 +143,11 @@ public sealed class HostInputCameraCompositionTests
|
|||
throw new InvalidOperationException($"fault at {point}");
|
||||
});
|
||||
|
||||
public void Dispose() => Publication.Dispose();
|
||||
public void Dispose()
|
||||
{
|
||||
Publication.Dispose();
|
||||
MovementState.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class Publication :
|
||||
|
|
|
|||
7
tests/AcDream.App.Tests/GlobalUsings.cs
Normal file
7
tests/AcDream.App.Tests/GlobalUsings.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
global using AcDream.Runtime.Gameplay;
|
||||
global using LocalPlayerControllerSlot =
|
||||
AcDream.Runtime.Gameplay.RuntimeLocalPlayerMovementState;
|
||||
global using ILocalPlayerControllerSource =
|
||||
AcDream.Runtime.Gameplay.IRuntimeLocalPlayerControllerSource;
|
||||
global using ILocalPlayerMotionSource =
|
||||
AcDream.Runtime.Gameplay.IRuntimeLocalPlayerMotionSource;
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,215 +0,0 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Input;
|
||||
using AcDream.Core.Physics;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.App.Tests.Physics;
|
||||
|
||||
public sealed class PlayerOutboundPositionTests
|
||||
{
|
||||
private static readonly Plane GroundPlane =
|
||||
new(Vector3.UnitZ, 0f);
|
||||
|
||||
[Fact]
|
||||
public void CrossLandblockWorldFrame_SendsCarriedCellLocalPosition()
|
||||
{
|
||||
var controller = new PlayerMovementController(new PhysicsEngine());
|
||||
|
||||
// The render origin remains A9B1, so a point 22.56 m into A9B2 is
|
||||
// represented at world Y=214.56. The wire frame must remain the
|
||||
// carried A9B2-local 22.56, never the render-space 214.56.
|
||||
controller.SetPosition(
|
||||
new Vector3(116.07f, 214.56f, 83.76f),
|
||||
0xA9B20021u,
|
||||
new Vector3(116.07f, 22.56f, 83.76f));
|
||||
|
||||
Assert.True(controller.TryGetOutboundPosition(out Position outbound));
|
||||
Assert.Equal(0xA9B20021u, outbound.ObjCellId);
|
||||
Assert.Equal(
|
||||
new Vector3(116.07f, 22.56f, 83.76f),
|
||||
outbound.Frame.Origin);
|
||||
Assert.NotEqual(controller.Position, outbound.Frame.Origin);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnplacedBody_DoesNotProduceWirePosition()
|
||||
{
|
||||
var controller = new PlayerMovementController(new PhysicsEngine());
|
||||
|
||||
Assert.False(controller.TryGetOutboundPosition(out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OutboundPosition_PreservesCompleteAuthoritativeQuaternion()
|
||||
{
|
||||
var controller = new PlayerMovementController(new PhysicsEngine());
|
||||
controller.SetPosition(
|
||||
new Vector3(116.07f, 214.56f, 83.76f),
|
||||
0xA9B20021u,
|
||||
new Vector3(116.07f, 22.56f, 83.76f));
|
||||
Quaternion complete = Quaternion.Normalize(
|
||||
Quaternion.CreateFromAxisAngle(Vector3.UnitX, 0.7f)
|
||||
* Quaternion.CreateFromAxisAngle(Vector3.UnitY, -0.4f));
|
||||
controller.SetBodyOrientation(complete);
|
||||
|
||||
Assert.True(controller.TryGetOutboundPosition(out Position outbound));
|
||||
|
||||
Assert.InRange(
|
||||
MathF.Abs(Quaternion.Dot(
|
||||
complete,
|
||||
Quaternion.Normalize(outbound.Frame.Orientation))),
|
||||
0.99999f,
|
||||
1.00001f);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PositionEvent_FirstFrameIsDue()
|
||||
{
|
||||
var controller = new PlayerMovementController(new PhysicsEngine());
|
||||
var current = PositionAt(Quaternion.Identity);
|
||||
|
||||
Assert.True(controller.ShouldSendPositionEvent(current, GroundPlane, 0f));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PositionEvent_StationaryHeadingChangeIsDueAfterRetailInterval()
|
||||
{
|
||||
var controller = new PlayerMovementController(new PhysicsEngine());
|
||||
var sent = PositionAt(Quaternion.Identity);
|
||||
var turned = PositionAt(Quaternion.CreateFromAxisAngle(
|
||||
Vector3.UnitZ,
|
||||
MathF.PI / 2f));
|
||||
controller.NotePositionSent(sent, GroundPlane, nowSeconds: 0f);
|
||||
|
||||
Assert.False(controller.ShouldSendPositionEvent(
|
||||
turned,
|
||||
GroundPlane,
|
||||
PlayerMovementController.HeartbeatInterval));
|
||||
Assert.True(controller.ShouldSendPositionEvent(
|
||||
turned,
|
||||
GroundPlane,
|
||||
PlayerMovementController.HeartbeatInterval + 0.01f));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PositionEvent_UnchangedCompleteFrameIsNotIdleHeartbeat()
|
||||
{
|
||||
var controller = new PlayerMovementController(new PhysicsEngine());
|
||||
var sent = PositionAt(Quaternion.Identity);
|
||||
controller.NotePositionSent(sent, GroundPlane, nowSeconds: 0f);
|
||||
|
||||
Assert.False(controller.ShouldSendPositionEvent(
|
||||
sent,
|
||||
GroundPlane,
|
||||
PlayerMovementController.HeartbeatInterval + 0.01f));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MovementEvent_StampsOnlyTime_NotTheRememberedFrame()
|
||||
{
|
||||
var controller = new PlayerMovementController(new PhysicsEngine());
|
||||
var sent = PositionAt(Quaternion.Identity);
|
||||
var turned = PositionAt(Quaternion.CreateFromAxisAngle(
|
||||
Vector3.UnitZ,
|
||||
MathF.PI / 2f));
|
||||
controller.NotePositionSent(sent, GroundPlane, nowSeconds: 0f);
|
||||
controller.NoteMovementSent(nowSeconds: 0.9f);
|
||||
|
||||
Assert.False(controller.ShouldSendPositionEvent(
|
||||
turned,
|
||||
GroundPlane,
|
||||
nowSeconds: 1.1f));
|
||||
Assert.True(controller.ShouldSendPositionEvent(
|
||||
turned,
|
||||
GroundPlane,
|
||||
nowSeconds: 1.91f));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PositionEvent_CellOrContactPlaneChangeIsDueInsideInterval()
|
||||
{
|
||||
var controller = new PlayerMovementController(new PhysicsEngine());
|
||||
var sent = PositionAt(Quaternion.Identity);
|
||||
controller.NotePositionSent(sent, GroundPlane, nowSeconds: 0f);
|
||||
|
||||
var otherCell = sent with { ObjCellId = 0xA9B20022u };
|
||||
var otherPlane = new Plane(Vector3.Normalize(new Vector3(0.01f, 0f, 1f)), 0f);
|
||||
|
||||
Assert.True(controller.ShouldSendPositionEvent(otherCell, GroundPlane, 0.1f));
|
||||
Assert.True(controller.ShouldSendPositionEvent(sent, otherPlane, 0.1f));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PositionEvent_QuaternionSignIsStoredFrameDataLikeRetail()
|
||||
{
|
||||
var controller = new PlayerMovementController(new PhysicsEngine());
|
||||
var sent = PositionAt(Quaternion.Identity);
|
||||
var oppositeStoredQuaternion = PositionAt(new Quaternion(0f, 0f, 0f, -1f));
|
||||
controller.NotePositionSent(sent, GroundPlane, nowSeconds: 0f);
|
||||
|
||||
Assert.True(controller.ShouldSendPositionEvent(
|
||||
oppositeStoredQuaternion,
|
||||
GroundPlane,
|
||||
PlayerMovementController.HeartbeatInterval + 0.01f));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PositionEvent_RetailEpsilonUsesDifferentOriginAndQuaternionBoundaries()
|
||||
{
|
||||
const float epsilon = 0.000199999995f;
|
||||
var controller = new PlayerMovementController(new PhysicsEngine());
|
||||
var sent = new Position(0xA9B20021u, Vector3.Zero, Quaternion.Identity);
|
||||
controller.NotePositionSent(sent, GroundPlane, nowSeconds: 0f);
|
||||
|
||||
var exactOriginBoundary = sent with
|
||||
{
|
||||
Frame = sent.Frame with { Origin = new Vector3(epsilon, 0f, 0f) },
|
||||
};
|
||||
var exactQuaternionBoundary = sent with
|
||||
{
|
||||
Frame = sent.Frame with
|
||||
{
|
||||
Orientation = new Quaternion(epsilon, 0f, 0f, 1f),
|
||||
},
|
||||
};
|
||||
float elapsed = PlayerMovementController.HeartbeatInterval + 0.01f;
|
||||
|
||||
Assert.False(controller.ShouldSendPositionEvent(
|
||||
exactOriginBoundary,
|
||||
GroundPlane,
|
||||
elapsed));
|
||||
Assert.True(controller.ShouldSendPositionEvent(
|
||||
exactQuaternionBoundary,
|
||||
GroundPlane,
|
||||
elapsed));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PositionEvent_RetailPlaneDistanceBoundaryIsStrict()
|
||||
{
|
||||
const float epsilon = 0.000199999995f;
|
||||
var controller = new PlayerMovementController(new PhysicsEngine());
|
||||
var sent = PositionAt(Quaternion.Identity);
|
||||
controller.NotePositionSent(sent, GroundPlane, nowSeconds: 0f);
|
||||
|
||||
var exactNormalBoundary = new Plane(
|
||||
new Vector3(epsilon, 0f, 1f),
|
||||
0f);
|
||||
var exactDistanceBoundary = new Plane(Vector3.UnitZ, epsilon);
|
||||
|
||||
Assert.False(controller.ShouldSendPositionEvent(
|
||||
sent,
|
||||
exactNormalBoundary,
|
||||
nowSeconds: 0.1f));
|
||||
Assert.True(controller.ShouldSendPositionEvent(
|
||||
sent,
|
||||
exactDistanceBoundary,
|
||||
nowSeconds: 0.1f));
|
||||
}
|
||||
|
||||
private static Position PositionAt(Quaternion orientation)
|
||||
=> new(
|
||||
0xA9B20021u,
|
||||
new Vector3(116.07f, 22.56f, 83.76f),
|
||||
orientation);
|
||||
}
|
||||
|
|
@ -782,7 +782,8 @@ public sealed class CurrentGameRuntimeAdapterTests
|
|||
CombatMode = new RecordingCombatModeOperations();
|
||||
_combatModeBinding =
|
||||
CombatModeOperations.BindOwned(CombatMode);
|
||||
MovementInput = new DispatcherMovementInputSource();
|
||||
MovementState = new RuntimeLocalPlayerMovementState();
|
||||
MovementInput = new DispatcherMovementInputSource(MovementState);
|
||||
GameplayInput = new GameplayInputFrameController(
|
||||
dispatcher: null,
|
||||
MovementInput,
|
||||
|
|
@ -832,11 +833,10 @@ public sealed class CurrentGameRuntimeAdapterTests
|
|||
Character,
|
||||
Communication,
|
||||
Actions,
|
||||
new LocalPlayerControllerSlot(),
|
||||
MovementState,
|
||||
WorldReveal,
|
||||
Clock,
|
||||
selectionController,
|
||||
GameplayInput);
|
||||
selectionController);
|
||||
}
|
||||
|
||||
public RuntimeOptions Options { get; }
|
||||
|
|
@ -853,6 +853,7 @@ public sealed class CurrentGameRuntimeAdapterTests
|
|||
public RuntimeSpellCastOperationsSlot SpellCastOperations { get; }
|
||||
public RuntimeActionState Actions { get; }
|
||||
public SelectionState Selection => Actions.Selection;
|
||||
public RuntimeLocalPlayerMovementState MovementState { get; }
|
||||
public DispatcherMovementInputSource MovementInput { get; }
|
||||
public GameplayInputFrameController GameplayInput { get; }
|
||||
public RecordingCombatModeOperations CombatMode { get; }
|
||||
|
|
@ -874,6 +875,7 @@ public sealed class CurrentGameRuntimeAdapterTests
|
|||
Actions.Dispose();
|
||||
InventoryState.Dispose();
|
||||
Entities.Clear();
|
||||
MovementState.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
123
tests/AcDream.App.Tests/Runtime/RuntimeMovementOwnershipTests.cs
Normal file
123
tests/AcDream.App.Tests/Runtime/RuntimeMovementOwnershipTests.cs
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace AcDream.App.Tests.Runtime;
|
||||
|
||||
public sealed class RuntimeMovementOwnershipTests
|
||||
{
|
||||
[Fact]
|
||||
public void ProductionConstructsOneCanonicalRuntimeMovementOwner()
|
||||
{
|
||||
string root = FindRepositoryRoot();
|
||||
string appRoot = Path.Combine(root, "src", "AcDream.App");
|
||||
string runtimeRoot = Path.Combine(root, "src", "AcDream.Runtime");
|
||||
string app = string.Join(
|
||||
"\n",
|
||||
Directory.EnumerateFiles(appRoot, "*.cs", SearchOption.AllDirectories)
|
||||
.Select(File.ReadAllText));
|
||||
string runtime = string.Join(
|
||||
"\n",
|
||||
Directory.EnumerateFiles(runtimeRoot, "*.cs", SearchOption.AllDirectories)
|
||||
.Select(File.ReadAllText));
|
||||
|
||||
Assert.False(File.Exists(Path.Combine(
|
||||
appRoot,
|
||||
"Input",
|
||||
"PlayerMovementController.cs")));
|
||||
Assert.False(File.Exists(Path.Combine(
|
||||
appRoot,
|
||||
"Input",
|
||||
"LocalPlayerOutboundController.cs")));
|
||||
Assert.DoesNotContain(
|
||||
"class PlayerMovementController",
|
||||
app,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(
|
||||
"class LocalPlayerOutboundController",
|
||||
app,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Empty(Regex.Matches(app, @"\bbool\s+_autoRunActive\b"));
|
||||
Assert.Single(Regex.Matches(
|
||||
app,
|
||||
@"RuntimeLocalPlayerMovementState\s+\w+\s*=\s*new\s*\(")
|
||||
.Cast<Match>());
|
||||
Assert.DoesNotContain(
|
||||
"ACDREAM_RUN_SKILL",
|
||||
runtime,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(
|
||||
"ACDREAM_JUMP_SKILL",
|
||||
runtime,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GraphicalInputRuntimeViewsAndShutdownBorrowTheExactOwner()
|
||||
{
|
||||
string root = FindRepositoryRoot();
|
||||
string gameWindow = ReadAppSource(root, "Rendering", "GameWindow.cs");
|
||||
string input = ReadAppSource(
|
||||
root,
|
||||
"Input",
|
||||
"DispatcherMovementInputSource.cs");
|
||||
string view = ReadAppSource(
|
||||
root,
|
||||
"Runtime",
|
||||
"CurrentGameRuntimeViewAdapter.cs");
|
||||
string commands = ReadAppSource(
|
||||
root,
|
||||
"Runtime",
|
||||
"CurrentGameRuntimeCommandAdapter.cs");
|
||||
string lifetime = ReadAppSource(
|
||||
root,
|
||||
"Rendering",
|
||||
"GameWindowLifetime.cs");
|
||||
|
||||
Assert.Contains(
|
||||
"RuntimeLocalPlayerMovementState _playerControllerSlot = new();",
|
||||
gameWindow,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"new AcDream.App.Input.DispatcherMovementInputSource(",
|
||||
gameWindow,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"_playerControllerSlot,",
|
||||
gameWindow,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"RuntimeLocalPlayerMovementState movement,",
|
||||
input,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("_movement.AutoRunActive", input, StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"movement ?? throw new ArgumentNullException(nameof(movement))).View",
|
||||
view,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("_movement.Execute(command)", commands, StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"RuntimeLocalPlayerMovementState Movement",
|
||||
lifetime,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"\"runtime local movement state\"",
|
||||
lifetime,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static string ReadAppSource(string root, params string[] relative) =>
|
||||
File.ReadAllText(Path.Combine(
|
||||
[root, "src", "AcDream.App", .. relative]));
|
||||
|
||||
private static string FindRepositoryRoot()
|
||||
{
|
||||
var current = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (current is not null)
|
||||
{
|
||||
if (File.Exists(Path.Combine(current.FullName, "AcDream.slnx")))
|
||||
return current.FullName;
|
||||
current = current.Parent;
|
||||
}
|
||||
|
||||
throw new DirectoryNotFoundException("AcDream.slnx was not found.");
|
||||
}
|
||||
}
|
||||
|
|
@ -625,11 +625,11 @@ public sealed class UpdateFrameOrchestratorTests
|
|||
== typeof(AcDream.App.Interaction.SelectionInteractionController));
|
||||
|
||||
FieldInfo outboundDiagnostics = Assert.Single(
|
||||
typeof(AcDream.App.Input.LocalPlayerOutboundController).GetFields(
|
||||
typeof(LocalPlayerOutboundController).GetFields(
|
||||
BindingFlags.Instance | BindingFlags.NonPublic),
|
||||
field => field.Name == "_diagnostic");
|
||||
Assert.Equal(
|
||||
typeof(AcDream.App.Input.IMovementTruthDiagnosticSink),
|
||||
typeof(IMovementTruthDiagnosticSink),
|
||||
outboundDiagnostics.FieldType);
|
||||
Assert.DoesNotContain(
|
||||
typeof(AcDream.App.Input.MovementTruthDiagnosticController).GetFields(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue