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

@ -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 :

View 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;

View file

@ -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);

View file

@ -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

View file

@ -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));

View file

@ -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();
}
}

View 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.");
}
}

View file

@ -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(

View file

@ -0,0 +1 @@
global using AcDream.Runtime.Gameplay;

View file

@ -1,12 +1,12 @@
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;
using AcDream.Runtime.Gameplay;
namespace AcDream.App.Tests.Input;
namespace AcDream.Runtime.Tests.Gameplay;
public sealed class LocalPlayerImmediatePositionTests
{

View file

@ -1,10 +1,10 @@
using System.Buffers.Binary;
using AcDream.App.Input;
using AcDream.Core.Net.Messages;
using AcDream.Core.Net.Packets;
using AcDream.Core.Physics;
using AcDream.Runtime.Gameplay;
namespace AcDream.App.Tests.Input;
namespace AcDream.Runtime.Tests.Gameplay;
public sealed class LocalPlayerOutboundCombatStyleTests
{

View file

@ -1,10 +1,9 @@
using System.Numerics;
using AcDream.App.Input;
using AcDream.Core.Physics;
using AcDream.Core.Physics.Motion;
using Xunit;
using AcDream.Runtime.Gameplay;
namespace AcDream.App.Tests.Input;
namespace AcDream.Runtime.Tests.Gameplay;
public sealed class PlayerMouseLookMovementTests
{

View file

@ -1,10 +1,9 @@
using System;
using System.Numerics;
using AcDream.App.Input;
using AcDream.Core.Physics;
using Xunit;
using AcDream.Runtime.Gameplay;
namespace AcDream.Core.Tests.Input;
namespace AcDream.Runtime.Tests.Gameplay;
public class PlayerMovementControllerTests
{

View file

@ -1,9 +1,8 @@
using System.Numerics;
using AcDream.App.Input;
using AcDream.Core.Physics;
using Xunit;
using AcDream.Runtime.Gameplay;
namespace AcDream.App.Tests.Physics;
namespace AcDream.Runtime.Tests.Gameplay;
public sealed class PlayerOutboundPositionTests
{

View file

@ -9,13 +9,15 @@ namespace AcDream.Runtime.Tests.Gameplay;
public sealed class RuntimeGameplayOwnershipTests
{
[Fact]
public void CombinedLedgerConvergesEveryJ4OwnerAndSubscription()
public void CombinedLedgerConvergesEveryGameplayOwnerAndSubscription()
{
using var entities = new RuntimeEntityObjectLifetime();
var inventory = new RuntimeInventoryState(entities);
var character = new RuntimeCharacterState();
var communication = new RuntimeCommunicationState();
var actions = RuntimeActionTestFactory.Create(inventory.Transactions);
var movement = new RuntimeLocalPlayerMovementState();
movement.Execute(RuntimeMovementCommand.ToggleRunLock);
inventory.Shortcuts.Changed += static () => { };
inventory.Shortcuts.Load([new ShortcutEntry(1, 2u, 3u)]);
inventory.ItemMana.OnQueryItemManaResponse(2u, 0.5f, true);
@ -73,7 +75,8 @@ public sealed class RuntimeGameplayOwnershipTests
inventory,
character,
communication,
actions);
actions,
movement);
Assert.False(populated.IsConverged);
Assert.Equal(1, populated.Inventory.ShortcutCount);
@ -86,7 +89,9 @@ public sealed class RuntimeGameplayOwnershipTests
AcDream.Core.Combat.CombatMode.Melee,
populated.Actions.CombatMode);
Assert.Equal(1, populated.Actions.TrackedTargetHealthCount);
Assert.True(populated.Movement.AutoRunActive);
movement.Dispose();
actions.Dispose();
communication.Dispose();
character.Dispose();
@ -98,7 +103,8 @@ public sealed class RuntimeGameplayOwnershipTests
inventory,
character,
communication,
actions);
actions,
movement);
Assert.True(retired.IsConverged);
Assert.Equal(0, retired.Inventory.ShortcutSubscriberCount);
@ -108,13 +114,14 @@ public sealed class RuntimeGameplayOwnershipTests
}
[Fact]
public void BorrowerFailuresCannotStrandAnyJ4OwnerDuringTerminalDisposal()
public void BorrowerFailuresCannotStrandAnyGameplayOwnerDuringTerminalDisposal()
{
using var entities = new RuntimeEntityObjectLifetime();
var inventory = new RuntimeInventoryState(entities);
var character = new RuntimeCharacterState();
var communication = new RuntimeCommunicationState();
var actions = RuntimeActionTestFactory.Create(inventory.Transactions);
var movement = new RuntimeLocalPlayerMovementState();
inventory.ExternalContainers.RequestOpen(0x70000001u);
inventory.ExternalContainers.ApplyViewContents(0x70000001u);
@ -132,6 +139,7 @@ public sealed class RuntimeGameplayOwnershipTests
communication.Chat.OnSystemMessage("failure-isolated event", 0u);
Assert.Equal(1, communication.DispatchFailureCount);
movement.Dispose();
actions.Dispose();
Assert.Throws<AggregateException>(inventory.Dispose);
Assert.Throws<AggregateException>(character.Dispose);
@ -142,7 +150,8 @@ public sealed class RuntimeGameplayOwnershipTests
inventory,
character,
communication,
actions);
actions,
movement);
Assert.True(retired.IsConverged);
Assert.Equal(1, retired.Communication.DispatchFailureCount);

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);
}
}