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:
Erik 2026-07-26 12:33:53 +02:00
parent 3456dff038
commit aa3f4a60f8
36 changed files with 878 additions and 276 deletions

View file

@ -0,0 +1,174 @@
using System.Numerics;
using AcDream.Core.Physics;
using AcDream.Runtime.Gameplay;
namespace AcDream.Runtime.Tests.Gameplay;
public sealed class RuntimeLocalPlayerMovementStateTests
{
[Fact]
public void ViewProjectsTheExactCanonicalControllerAndAutorunOwner()
{
var controller = new PlayerMovementController(new PhysicsEngine())
{
LocalEntityId = 0x50000001u,
};
controller.SetPosition(
new Vector3(11f, 12f, 13f),
0xA9B40001u,
new Vector3(11f, 12f, 13f));
using var movement = new RuntimeLocalPlayerMovementState
{
Controller = controller,
};
Assert.Same(movement, movement.View);
Assert.True(movement.Execute(RuntimeMovementCommand.ToggleRunLock));
RuntimeMovementSnapshot snapshot = movement.View.Snapshot;
Assert.True(snapshot.HasController);
Assert.Equal(0x50000001u, snapshot.LocalEntityId);
Assert.Equal(controller.CellPosition, snapshot.Position);
Assert.Equal(controller.BodyVelocity, snapshot.Velocity);
Assert.Equal(controller.IsAirborne, snapshot.IsAirborne);
Assert.Equal(controller.SimTimeSeconds, snapshot.SimulationTimeSeconds);
Assert.True(snapshot.AutoRunActive);
Assert.Equal(movement.Revision, snapshot.Revision);
}
[Fact]
public void GraphicalAndDirectCommandsMutateOneAutorunLatch()
{
using var movement = new RuntimeLocalPlayerMovementState();
Assert.True(movement.Execute(RuntimeMovementCommand.ToggleRunLock));
Assert.True(movement.AutoRunActive);
Assert.True(movement.View.Snapshot.AutoRunActive);
Assert.True(movement.Execute(RuntimeMovementCommand.Stop));
Assert.False(movement.AutoRunActive);
Assert.False(movement.View.Snapshot.AutoRunActive);
}
[Fact]
public void ConcurrentRuntimeInstancesHaveIndependentMovementState()
{
using var first = new RuntimeLocalPlayerMovementState();
using var second = new RuntimeLocalPlayerMovementState();
var firstController = new PlayerMovementController(new PhysicsEngine())
{
LocalEntityId = 1u,
};
var secondController = new PlayerMovementController(new PhysicsEngine())
{
LocalEntityId = 2u,
};
first.Controller = firstController;
second.Controller = secondController;
first.Execute(RuntimeMovementCommand.ToggleRunLock);
firstController.SetPosition(Vector3.One, 0xA9B40001u, Vector3.One);
secondController.SetPosition(
new Vector3(2f),
0xA9B50001u,
new Vector3(2f));
Assert.True(first.Snapshot.AutoRunActive);
Assert.False(second.Snapshot.AutoRunActive);
Assert.Equal(1u, first.Snapshot.LocalEntityId);
Assert.Equal(2u, second.Snapshot.LocalEntityId);
Assert.NotEqual(first.Snapshot.Position, second.Snapshot.Position);
}
[Fact]
public void MotionPreparationPublishesOnlyTheConstructionSeam()
{
using var movement = new RuntimeLocalPlayerMovementState();
IRuntimeLocalPlayerMotionSource source = movement;
var committed = new PlayerMovementController(new PhysicsEngine());
var candidate = new PlayerMovementController(new PhysicsEngine());
movement.Controller = committed;
bool drained = false;
using (movement.BeginMotionPreparation(
candidate,
() =>
{
drained = true;
Assert.Same(committed.Motion, source.Motion);
}))
{
Assert.True(drained);
Assert.Same(committed, movement.Controller);
Assert.Same(candidate.Motion, source.Motion);
}
Assert.Same(committed.Motion, source.Motion);
}
[Fact]
public void TerminalDisposalConvergesWhenPreparationLeaseUnwindsLater()
{
var movement = new RuntimeLocalPlayerMovementState();
var controller = new PlayerMovementController(new PhysicsEngine());
IDisposable preparation = movement.BeginMotionPreparation(controller);
movement.Controller = controller;
movement.Execute(RuntimeMovementCommand.ToggleRunLock);
movement.Dispose();
preparation.Dispose();
RuntimeLocalMovementOwnershipSnapshot ownership =
movement.CaptureOwnership();
Assert.True(ownership.IsConverged);
Assert.Throws<ObjectDisposedException>(
() => movement.Execute(RuntimeMovementCommand.ToggleRunLock));
}
[Fact]
public void TypedSkillOptionsPreserveCompleteValuesAndFallbackPerField()
{
Assert.Equal(
new PlayerMovementConstructionOptions(321, 654),
PlayerMovementConstructionOptions.From(
new RuntimeMovementSkillSnapshot(321, 654, 1)));
Assert.Equal(
new PlayerMovementConstructionOptions(
PlayerMovementConstructionOptions.FallbackRunSkill,
654),
PlayerMovementConstructionOptions.From(
new RuntimeMovementSkillSnapshot(-1, 654, 1)));
Assert.Equal(
new PlayerMovementConstructionOptions(
321,
PlayerMovementConstructionOptions.FallbackJumpSkill),
PlayerMovementConstructionOptions.From(
new RuntimeMovementSkillSnapshot(321, -1, 1)));
}
[Fact]
public void WarmMovementSnapshotsAndOwnershipCaptureAllocateNothing()
{
using var movement = new RuntimeLocalPlayerMovementState();
movement.Controller = new PlayerMovementController(new PhysicsEngine())
{
LocalEntityId = 7u,
};
for (int i = 0; i < 1_000; i++)
{
_ = movement.Snapshot;
_ = movement.CaptureOwnership();
}
long before = GC.GetAllocatedBytesForCurrentThread();
for (int i = 0; i < 100_000; i++)
{
_ = movement.Snapshot;
_ = movement.CaptureOwnership();
}
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
Assert.Equal(0L, allocated);
}
}