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

@ -16,89 +16,6 @@ internal sealed class LocalPlayerIdentityState : ILocalPlayerIdentitySource
public uint ServerGuid { get; set; }
}
internal interface ILocalPlayerControllerSource
{
PlayerMovementController? Controller { get; }
}
internal interface ILocalPlayerMotionSource
{
MotionInterpreter? Motion { get; }
}
/// <summary>
/// The one mutable local movement-controller slot. Player-mode lifecycle owns
/// assignment; update and presentation owners receive the read-only seam.
/// </summary>
internal sealed class LocalPlayerControllerSlot
: ILocalPlayerControllerSource,
ILocalPlayerMotionSource
{
private PlayerMovementController? _preparingMotionOwner;
public PlayerMovementController? Controller { get; set; }
/// <summary>
/// The motion owner visible to the local PartArray completion relay.
/// During player-mode construction this is the fully animation-bound
/// candidate controller; all other consumers continue to see only the
/// committed <see cref="Controller"/>.
/// </summary>
MotionInterpreter? ILocalPlayerMotionSource.Motion =>
_preparingMotionOwner?.Motion ?? Controller?.Motion;
/// <summary>
/// Opens the narrow construction-time ownership seam required by retail's
/// CPhysicsObj/PartArray lifetime. Initial placement synchronously queues
/// and completes StopCompletely, so its MotionDone relay must reach the
/// candidate interpreter before the complete controller is published.
/// </summary>
public IDisposable BeginMotionPreparation(
PlayerMovementController controller,
Action? drainPriorAnimationQueue = null)
{
ArgumentNullException.ThrowIfNull(controller);
if (_preparingMotionOwner is not null)
{
throw new InvalidOperationException(
"A local player motion owner is already being prepared.");
}
// The live PartArray exists before the local player controller. Drain
// any animation completions produced under that prior ownership while
// they still resolve to the prior (or null) interpreter. Publishing
// the fresh interpreter first would let those old callbacks pop its
// new pending_motions queue and leave an unmatched Ready sentinel.
drainPriorAnimationQueue?.Invoke();
_preparingMotionOwner = controller;
return new MotionPreparation(this, controller);
}
private void EndMotionPreparation(PlayerMovementController controller)
{
if (!ReferenceEquals(_preparingMotionOwner, controller))
{
throw new InvalidOperationException(
"The local player motion preparation owner changed unexpectedly.");
}
_preparingMotionOwner = null;
}
private sealed class MotionPreparation(
LocalPlayerControllerSlot owner,
PlayerMovementController controller) : IDisposable
{
private LocalPlayerControllerSlot? _owner = owner;
public void Dispose()
{
LocalPlayerControllerSlot? current = Interlocked.Exchange(ref _owner, null);
current?.EndMotionPreparation(controller);
}
}
}
internal interface ILocalPlayerPhysicsHostSource
{
EntityPhysicsHost? Host { get; }