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));
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
1
tests/AcDream.Core.Tests/GlobalUsings.cs
Normal file
1
tests/AcDream.Core.Tests/GlobalUsings.cs
Normal file
|
|
@ -0,0 +1 @@
|
|||
global using AcDream.Runtime.Gameplay;
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue