refactor(runtime): close simulation ownership

Move remote-motion construction, CreateObject vector initialization, final simulation-component retirement, and the combined J5 ownership ledger into Runtime. Delete App compatibility views and moved-state reconstruction while preserving the existing graphical projection and retail update order.
This commit is contained in:
Erik 2026-07-26 15:53:31 +02:00
parent c30a3efeb0
commit cdee7a4b49
57 changed files with 1013 additions and 410 deletions

View file

@ -96,7 +96,7 @@ public sealed class LiveCombatAttackOperationsTests
var combat = new CombatState();
var targets = new FakeTargets();
var settings = new FakeSettings();
var player = new LocalPlayerControllerSlot();
var player = new RuntimeLocalPlayerMovementState();
var live = new FakeLiveSource { IsInWorld = inWorld };
var feedback = new FakeFeedback();
var outbound = new LocalPlayerOutboundController(

View file

@ -1,18 +1,4 @@
global using AcDream.Runtime.Gameplay;
global using AcDream.Runtime.Physics;
global using ILiveEntityRemoteMotionRuntime =
AcDream.Runtime.Physics.IRuntimeRemoteMotion;
global using ILiveEntityPhysicsHostConsumer =
AcDream.Runtime.Physics.IRuntimePhysicsHostConsumer;
global using ILiveEntityCanonicalRuntimeConsumer =
AcDream.Runtime.Physics.IRuntimeCanonicalPhysicsConsumer;
global using ILiveEntityCanonicalCellConsumer =
AcDream.Runtime.Physics.IRuntimeCanonicalCellConsumer;
global using ILiveEntityRemotePlacementRuntime =
AcDream.Runtime.Physics.IRuntimeRemotePlacement;
global using LocalPlayerControllerSlot =
AcDream.Runtime.Gameplay.RuntimeLocalPlayerMovementState;
global using ILocalPlayerControllerSource =
AcDream.Runtime.Gameplay.IRuntimeLocalPlayerControllerSource;
global using ILocalPlayerMotionSource =
AcDream.Runtime.Gameplay.IRuntimeLocalPlayerMotionSource;

View file

@ -201,7 +201,7 @@ public sealed class MouseLookControllerTests
{
PlayerMovementController player = CreatePlayer();
var mode = new LocalPlayerModeState();
var slot = new LocalPlayerControllerSlot { Controller = player };
var slot = new RuntimeLocalPlayerMovementState { Controller = player };
var camera = new CameraController(new OrbitCamera(), new FlyCamera());
var chase = new ChaseCameraInputState();
var mouse = new FakeMouse();

View file

@ -3,12 +3,12 @@ using AcDream.Core.Physics;
namespace AcDream.App.Tests.Input;
public sealed class LocalPlayerControllerSlotTests
public sealed class RuntimeLocalPlayerMovementStateTests
{
[Fact]
public void MotionPreparation_RoutesCompletionWithoutPublishingController()
{
var slot = new LocalPlayerControllerSlot();
var slot = new RuntimeLocalPlayerMovementState();
ILocalPlayerMotionSource source = slot;
var controller = new PlayerMovementController(new PhysicsEngine());
controller.Motion.AddToQueue(0, MotionCommand.Ready, 0);
@ -29,7 +29,7 @@ public sealed class LocalPlayerControllerSlotTests
[Fact]
public void MotionPreparation_HandsOffToCommittedControllerOnDispose()
{
var slot = new LocalPlayerControllerSlot();
var slot = new RuntimeLocalPlayerMovementState();
ILocalPlayerMotionSource source = slot;
var controller = new PlayerMovementController(new PhysicsEngine());
@ -45,7 +45,7 @@ public sealed class LocalPlayerControllerSlotTests
[Fact]
public void MotionPreparation_DrainsPriorAnimationBeforePublishingCandidate()
{
var slot = new LocalPlayerControllerSlot();
var slot = new RuntimeLocalPlayerMovementState();
ILocalPlayerMotionSource source = slot;
var prior = new PlayerMovementController(new PhysicsEngine());
var candidate = new PlayerMovementController(new PhysicsEngine());
@ -71,7 +71,7 @@ public sealed class LocalPlayerControllerSlotTests
[Fact]
public void MotionPreparation_RejectsConcurrentCandidate()
{
var slot = new LocalPlayerControllerSlot();
var slot = new RuntimeLocalPlayerMovementState();
var first = new PlayerMovementController(new PhysicsEngine());
var second = new PlayerMovementController(new PhysicsEngine());

View file

@ -105,11 +105,10 @@ public sealed class ProjectileControllerTests
LiveEntityRecord record = fixture.Spawn(instance: 1);
WorldEntity entity = record.WorldEntity!;
Assert.Null(record.AnimationRuntime);
var remote = new AcDream.Runtime.Physics.RemoteMotion();
RemotePhysicsBodyInitializer.Initialize(remote.Body, record);
var remote =
fixture.Live.GetOrCreateRemoteMotionRuntime(Guid);
remote.Body.Position = entity.Position;
remote.Body.Orientation = entity.Rotation;
fixture.Live.SetRemoteMotionRuntime(Guid, remote);
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0, 1, 1));
Assert.Same(remote.Body, record.ProjectileRuntime!.Body);
@ -686,7 +685,9 @@ public sealed class ProjectileControllerTests
LiveEntityRecord record = fixture.Spawn(instance: 1);
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0));
PhysicsBody body = record.PhysicsBody!;
fixture.Live.SetRemoteMotionRuntime(Guid, new AcDream.Runtime.Physics.RemoteMotion(body));
Assert.Same(
body,
fixture.Live.GetOrCreateRemoteMotionRuntime(Guid).Body);
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
record, record.FinalPhysicsState, 2.0, 1, 1));
@ -710,7 +711,9 @@ public sealed class ProjectileControllerTests
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0));
PhysicsBody body = record.PhysicsBody!;
Vector3 position = body.Position;
fixture.Live.SetRemoteMotionRuntime(Guid, new AcDream.Runtime.Physics.RemoteMotion(body));
Assert.Same(
body,
fixture.Live.GetOrCreateRemoteMotionRuntime(Guid).Body);
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
@ -949,9 +952,8 @@ public sealed class ProjectileControllerTests
var fixture = new Fixture();
LiveEntityRecord record = fixture.Spawn(instance: 1);
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
var remote = new AcDream.Runtime.Physics.RemoteMotion();
RemotePhysicsBodyInitializer.Initialize(remote.Body, record);
fixture.Live.SetRemoteMotionRuntime(Guid, remote);
var remote =
fixture.Live.GetOrCreateRemoteMotionRuntime(Guid);
PhysicsStateFlags ordinaryState =
PhysicsStateFlags.Gravity | PhysicsStateFlags.IgnoreCollisions;
@ -1001,9 +1003,8 @@ public sealed class ProjectileControllerTests
var fixture = new Fixture();
LiveEntityRecord record = fixture.Spawn(instance: 1);
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
var remote = new AcDream.Runtime.Physics.RemoteMotion();
RemotePhysicsBodyInitializer.Initialize(remote.Body, record);
fixture.Live.SetRemoteMotionRuntime(Guid, remote);
var remote =
fixture.Live.GetOrCreateRemoteMotionRuntime(Guid);
record.FinalPhysicsState = MissileState;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
@ -1073,15 +1074,14 @@ public sealed class ProjectileControllerTests
seedCellId: startCell,
isStatic: false);
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
var remote = new AcDream.Runtime.Physics.RemoteMotion();
RemotePhysicsBodyInitializer.Initialize(remote.Body, record);
var remote =
fixture.Live.GetOrCreateRemoteMotionRuntime(Guid);
Assert.Equal(new Vector3(40f, 0f, 0f), remote.Body.Velocity);
Assert.Equal(new Vector3(0f, 0f, 2f), remote.Body.Omega);
remote.Body.set_velocity(new Vector3(8f, 0f, 0f));
remote.Body.Omega = new Vector3(0f, 0f, 3f);
remote.Body.Position = entity.Position;
remote.Body.State = record.FinalPhysicsState;
fixture.Live.SetRemoteMotionRuntime(Guid, remote);
PhysicsBody body = remote.Body;
record.FinalPhysicsState = MissileState;
@ -1139,11 +1139,10 @@ public sealed class ProjectileControllerTests
velocity: new Vector3(float.NaN, 0f, 0f));
WorldEntity entity = record.WorldEntity!;
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
var remote = new AcDream.Runtime.Physics.RemoteMotion();
RemotePhysicsBodyInitializer.Initialize(remote.Body, record);
var remote =
fixture.Live.GetOrCreateRemoteMotionRuntime(Guid);
remote.Body.Position = entity.Position;
remote.Body.Orientation = entity.Rotation;
fixture.Live.SetRemoteMotionRuntime(Guid, remote);
Assert.True(float.IsNaN(record.Snapshot.Physics!.Value.Velocity!.Value.X));
Assert.Equal(Vector3.Zero, remote.Body.Velocity);
@ -1165,12 +1164,11 @@ public sealed class ProjectileControllerTests
LiveEntityRecord record = fixture.Spawn(instance: 1);
WorldEntity entity = record.WorldEntity!;
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
var remote = new AcDream.Runtime.Physics.RemoteMotion();
RemotePhysicsBodyInitializer.Initialize(remote.Body, record);
var remote =
fixture.Live.GetOrCreateRemoteMotionRuntime(Guid);
remote.Body.Position = entity.Position;
remote.Body.Orientation = entity.Rotation;
remote.Body.Velocity = new Vector3(float.PositiveInfinity, 0f, 0f);
fixture.Live.SetRemoteMotionRuntime(Guid, remote);
record.FinalPhysicsState = MissileState;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
@ -1189,8 +1187,9 @@ public sealed class ProjectileControllerTests
LiveEntityRecord record = fixture.Spawn(instance: 1);
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0));
PhysicsBody body = record.PhysicsBody!;
var remote = new AcDream.Runtime.Physics.RemoteMotion(body);
fixture.Live.SetRemoteMotionRuntime(Guid, remote);
var remote =
fixture.Live.GetOrCreateRemoteMotionRuntime(Guid);
Assert.Same(body, remote.Body);
fixture.Live.RebucketLiveEntity(Guid, CellB);
Assert.Equal(CellB, remote.CellId);

View file

@ -19,12 +19,12 @@ public sealed class RemoteTeleportControllerTests
public void Unregister(WorldEntity entity) { }
}
private sealed class BodyOnlyRemote(PhysicsBody body) : ILiveEntityRemoteMotionRuntime
private sealed class BodyOnlyRemote(PhysicsBody body) : IRuntimeRemoteMotion
{
public PhysicsBody Body { get; } = body;
}
private sealed class MutablePlacementRemote(PhysicsBody body) : ILiveEntityRemotePlacementRuntime
private sealed class MutablePlacementRemote(PhysicsBody body) : IRuntimeRemotePlacement
{
private uint _cellId;
private Func<uint>? _readCell;
@ -56,7 +56,7 @@ public sealed class RemoteTeleportControllerTests
private sealed class AlternatingBodyRemote(
PhysicsBody first,
PhysicsBody later) : ILiveEntityRemotePlacementRuntime
PhysicsBody later) : IRuntimeRemotePlacement
{
private int _reads;
public PhysicsBody Body => _reads++ == 0 ? first : later;

View file

@ -190,7 +190,7 @@ public sealed class WorldRenderFrameBuilderTests
cells,
mode,
chase,
new LocalPlayerControllerSlot(),
new RuntimeLocalPlayerMovementState(),
origin);
var camera = new FlyCamera { Position = new Vector3(5f, 6f, 7f) };
WorldCameraFrame cameraFrame = CameraFrame(camera);
@ -227,7 +227,7 @@ public sealed class WorldRenderFrameBuilderTests
new CellVisibility(),
new LocalPlayerModeState(),
new ChaseCameraInputState(),
new LocalPlayerControllerSlot(),
new RuntimeLocalPlayerMovementState(),
new LiveWorldOriginState());
WorldCameraFrame camera = CameraFrame(new FlyCamera());

View file

@ -827,7 +827,6 @@ public sealed class CurrentGameRuntimeAdapterTests
Host,
Commands,
Identity,
Entities,
EntityObjects,
InventoryState,
Character,

View file

@ -69,6 +69,13 @@ public sealed class RuntimePhysicsOwnershipTests
runtimeRoot,
"Physics",
"RuntimePhysicsState.cs"));
string app = string.Join(
"\n",
Directory.EnumerateFiles(
appRoot,
"*.cs",
SearchOption.AllDirectories)
.Select(File.ReadAllText));
Assert.DoesNotContain(
"ResolveWithTransition(",
@ -131,6 +138,67 @@ public sealed class RuntimePhysicsOwnershipTests
"public bool TryGetPhysicsHost(",
runtimePhysics,
StringComparison.Ordinal);
Assert.Contains(
"GetOrCreateRemoteMotion(",
runtimePhysics,
StringComparison.Ordinal);
Assert.Contains(
"_physics.TryCommitAuthoritativeVector(",
liveRuntime,
StringComparison.Ordinal);
Assert.DoesNotContain(
"TryActivateOrdinaryObject(",
liveRuntime,
StringComparison.Ordinal);
Assert.Contains(
"_entityObjects.CompleteProjectionRetirement(",
liveRuntime,
StringComparison.Ordinal);
Assert.DoesNotContain(
"record.RemoteMotionRuntime =",
liveRuntime,
StringComparison.Ordinal);
Assert.DoesNotContain(
"record.PhysicsBody =",
liveRuntime,
StringComparison.Ordinal);
Assert.DoesNotContain(
"new RemoteMotion(",
app,
StringComparison.Ordinal);
Assert.DoesNotContain(
"LiveEntityRemoteMotionRuntimeView",
app,
StringComparison.Ordinal);
Assert.DoesNotContain(
"ILiveEntityRemoteMotionRuntime",
app,
StringComparison.Ordinal);
Assert.DoesNotContain(
"ILiveEntityRemotePlacementRuntime",
app,
StringComparison.Ordinal);
Assert.DoesNotContain(
"LocalPlayerControllerSlot",
app,
StringComparison.Ordinal);
Assert.DoesNotContain(
"ILocalPlayerControllerSource",
app,
StringComparison.Ordinal);
Assert.False(File.Exists(Path.Combine(
appRoot,
"Physics",
"RemotePhysicsBodyInitializer.cs")));
Assert.False(typeof(AcDream.App.World.LiveEntityRecord)
.GetProperty(nameof(AcDream.App.World.LiveEntityRecord.PhysicsBody))!
.CanWrite);
Assert.False(typeof(AcDream.App.World.LiveEntityRecord)
.GetProperty(nameof(AcDream.App.World.LiveEntityRecord.RemoteMotionRuntime))!
.CanWrite);
Assert.False(typeof(AcDream.App.World.LiveEntityRecord)
.GetProperty(nameof(AcDream.App.World.LiveEntityRecord.PhysicsHost))!
.CanWrite);
}
private static string FindRepositoryRoot()

View file

@ -411,7 +411,7 @@ public sealed class LocalPlayerTeleportControllerTests
MeshRefs = Array.Empty<MeshRef>(),
})!;
var identity = new LocalPlayerIdentityState { ServerGuid = guid };
var controllerSlot = new LocalPlayerControllerSlot
var controllerSlot = new RuntimeLocalPlayerMovementState
{
Controller = new PlayerMovementController(new PhysicsEngine()),
};
@ -494,7 +494,7 @@ public sealed class LocalPlayerTeleportControllerTests
MeshRefs = Array.Empty<MeshRef>(),
})!;
var identity = new LocalPlayerIdentityState { ServerGuid = guid };
var controllerSlot = new LocalPlayerControllerSlot
var controllerSlot = new RuntimeLocalPlayerMovementState
{
Controller = new PlayerMovementController(new PhysicsEngine()),
};

View file

@ -422,7 +422,7 @@ public sealed class StreamingFrameControllerTests
}
internal LocalPlayerModeState Mode { get; } = new();
internal LocalPlayerControllerSlot Player { get; } = new();
internal RuntimeLocalPlayerMovementState Player { get; } = new();
internal SessionSource Session { get; } = new();
internal LiveWorldOriginState Origin { get; } = new();
internal PlayerLandblockSource PlayerLandblock { get; } = new();

View file

@ -144,12 +144,7 @@ public sealed class GameWindowLiveEntityCompositionTests
typeof(LiveEntityAnimationRuntimeView<LiveEntityAnimationState>)
.GetFields(BindingFlags.Instance | BindingFlags.NonPublic),
field => field.Name == "_runtime");
FieldInfo remoteRuntime = Assert.Single(
typeof(LiveEntityRemoteMotionRuntimeView<RemoteMotion>)
.GetFields(BindingFlags.Instance | BindingFlags.NonPublic),
field => field.Name == "_runtime");
Assert.Equal(typeof(ILiveEntityRuntimeSource), animationRuntime.FieldType);
Assert.Equal(typeof(ILiveEntityRuntimeSource), remoteRuntime.FieldType);
FieldInfo[] projectileFields = typeof(ProjectileController).GetFields(
BindingFlags.Instance | BindingFlags.NonPublic);
@ -192,7 +187,7 @@ public sealed class GameWindowLiveEntityCompositionTests
source,
StringComparison.Ordinal);
Assert.DoesNotContain(
"new LiveEntityRemoteMotionRuntimeView<RemoteMotion>(() =>",
"LiveEntityRemoteMotionRuntimeView",
source,
StringComparison.Ordinal);
Assert.Contains("new DatProjectileSetupResolver", livePresentation);

View file

@ -371,7 +371,11 @@ public sealed class LiveEntityHydrationControllerTests
LiveEntityRecord record = fixture.Record;
WorldEntity entity = record.WorldEntity!;
var body = new PhysicsBody();
record.PhysicsBody = body;
Assert.Same(
body,
fixture.Runtime.GetOrCreatePhysicsBody(
record.ServerGuid,
_ => body));
fixture.Relationships.OnUnparentAction = _ =>
fixture.Runtime.WithdrawLiveEntityProjection(record)
? ChildUnparentDisposition.Completed

View file

@ -874,7 +874,7 @@ public sealed class LiveEntityLifecycleStressTests
_animations.TryGetValue(id, out Animation? animation) ? animation : null;
}
private sealed class TestRemoteMotion : ILiveEntityRemotePlacementRuntime
private sealed class TestRemoteMotion : IRuntimeRemotePlacement
{
private uint _cellId;
private Func<uint>? _readCell;

View file

@ -710,8 +710,8 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
interruptCurrentMovement);
private sealed class HostConsumerMotion :
ILiveEntityRemoteMotionRuntime,
ILiveEntityPhysicsHostConsumer
IRuntimeRemoteMotion,
IRuntimePhysicsHostConsumer
{
private Func<IPhysicsObjHost?>? _read;
public PhysicsBody Body { get; } = new();
@ -725,8 +725,8 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
}
private sealed class ThrowingCellConsumerMotion :
ILiveEntityRemoteMotionRuntime,
ILiveEntityCanonicalRuntimeConsumer
IRuntimeRemoteMotion,
IRuntimeCanonicalPhysicsConsumer
{
private bool _failOnce = true;
public PhysicsBody Body { get; } = new();
@ -748,14 +748,14 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
private sealed class AlternatingBodyMotion(
PhysicsBody first,
PhysicsBody second) : ILiveEntityRemoteMotionRuntime
PhysicsBody second) : IRuntimeRemoteMotion
{
private int _reads;
public PhysicsBody Body => _reads++ == 0 ? first : second;
}
private sealed class SequenceBodyMotion(params PhysicsBody[] bodies) :
ILiveEntityRemoteMotionRuntime
IRuntimeRemoteMotion
{
public int ReadCount { get; private set; }
@ -771,8 +771,8 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
}
private sealed class CallbackCanonicalMotion(Action callback) :
ILiveEntityRemoteMotionRuntime,
ILiveEntityCanonicalRuntimeConsumer
IRuntimeRemoteMotion,
IRuntimeCanonicalPhysicsConsumer
{
public PhysicsBody Body { get; } = new();

View file

@ -48,7 +48,7 @@ public sealed class LiveEntityRuntimeTests
private sealed class EffectProfile : ILiveEntityEffectProfile { }
private sealed class RemoteMotionRuntime : ILiveEntityRemoteMotionRuntime
private sealed class RemoteMotionRuntime : IRuntimeRemoteMotion
{
public PhysicsBody Body { get; } = new();
}
@ -1156,43 +1156,6 @@ public sealed class LiveEntityRuntimeTests
Assert.False(remote.Body.TransientState.HasFlag(TransientStateFlags.Active));
}
[Fact]
public void MovementActivation_ReactivatesExactIncarnation_ButStaticIsNoOp()
{
const uint ordinaryGuid = 0x70000050u;
const uint staticGuid = 0x70000051u;
var spatial = new GpuWorldState();
spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
var runtime = LiveEntityRuntimeFixture.Create(spatial, new RecordingResources());
LiveEntityRecord ordinary = RegisterProjection(
runtime,
Spawn(ordinaryGuid, 1, 1, 0x01010001u));
var ordinaryRemote = new RemoteMotionRuntime();
runtime.SetRemoteMotionRuntime(ordinaryGuid, ordinaryRemote);
ordinary.ObjectClock.Deactivate();
ordinaryRemote.Body.TransientState &= ~TransientStateFlags.Active;
Assert.True(runtime.TryActivateOrdinaryObject(ordinaryGuid, ordinaryRemote));
Assert.True(ordinary.ObjectClock.IsActive);
Assert.True(ordinaryRemote.Body.TransientState.HasFlag(TransientStateFlags.Active));
LiveEntityRecord staticRecord = RegisterProjection(
runtime,
Spawn(
staticGuid,
1,
1,
0x01010001u,
PhysicsStateFlags.Static));
var staticRemote = new RemoteMotionRuntime();
runtime.SetRemoteMotionRuntime(staticGuid, staticRemote);
Assert.False(runtime.TryActivateOrdinaryObject(staticGuid, staticRemote));
Assert.False(staticRecord.ObjectClock.IsActive);
Assert.False(staticRemote.Body.TransientState.HasFlag(TransientStateFlags.Active));
}
[Fact]
public void AuthoritativeVector_WakesOrdinaryClockButPreservesStaticActivity()
{

View file

@ -244,6 +244,10 @@ public sealed class RuntimeEntityOwnershipTests
Assert.Contains(
viewFields,
field => field.FieldType == typeof(IRuntimeInventoryView));
Assert.DoesNotContain(
viewFields,
field => field.FieldType == typeof(LiveEntityRuntime)
|| field.FieldType == typeof(ClientObjectTable));
Assert.DoesNotContain(
typeof(CurrentGameRuntimeViewAdapter).GetNestedTypes(
BindingFlags.NonPublic),
@ -271,6 +275,21 @@ public sealed class RuntimeEntityOwnershipTests
Assert.DoesNotContain("_entityObjects.PublishEntity", liveSource);
Assert.DoesNotContain("_directory.TryApply", liveSource);
Assert.DoesNotContain("_directory.RemoveActive", liveSource);
string viewSource = File.ReadAllText(Path.Combine(
root,
"src",
"AcDream.App",
"Runtime",
"CurrentGameRuntimeViewAdapter.cs"));
Assert.Contains(
"_entityView.MaterializedCount",
viewSource,
StringComparison.Ordinal);
Assert.DoesNotContain(
"_entities.MaterializedCount",
viewSource,
StringComparison.Ordinal);
}
private static bool IsPresentationType(Type type)

View file

@ -1123,7 +1123,10 @@ public sealed class RuntimeEntityObjectLifetimeTests
lifetime.Dispose();
Assert.Equal(
default(RuntimeEntityObjectOwnershipSnapshot),
default(RuntimeEntityObjectOwnershipSnapshot) with
{
IsDisposed = true,
},
lifetime.CaptureOwnership());
}
@ -1158,7 +1161,10 @@ public sealed class RuntimeEntityObjectLifetimeTests
lifetime.Dispose();
Assert.Equal(
default(RuntimeEntityObjectOwnershipSnapshot),
default(RuntimeEntityObjectOwnershipSnapshot) with
{
IsDisposed = true,
},
lifetime.CaptureOwnership());
}

View file

@ -3,11 +3,60 @@ using AcDream.Core.Items;
using AcDream.Core.Social;
using AcDream.Runtime.Entities;
using AcDream.Runtime.Gameplay;
using AcDream.Runtime;
namespace AcDream.Runtime.Tests.Gameplay;
public sealed class RuntimeGameplayOwnershipTests
{
[Fact]
public void J5LedgerConvergesEntityPhysicsAndGameplayAsOneLifetime()
{
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.Transactions.IncrementBusyCount();
actions.Selection.Select(
0x70000001u,
AcDream.Core.Selection.SelectionChangeSource.System);
character.Spellbook.OnSpellLearned(42u);
communication.Chat.OnSystemMessage("runtime-only", 0u);
RuntimeSimulationOwnershipSnapshot populated =
RuntimeSimulationOwnership.Capture(
entities,
inventory,
character,
communication,
actions,
movement);
Assert.False(populated.IsConverged);
movement.Dispose();
actions.Dispose();
communication.Dispose();
character.Dispose();
inventory.Dispose();
entities.Dispose();
RuntimeSimulationOwnershipSnapshot retired =
RuntimeSimulationOwnership.Capture(
entities,
inventory,
character,
communication,
actions,
movement);
Assert.True(retired.IsConverged);
Assert.True(retired.EntityObjects.IsDisposed);
Assert.True(retired.Physics.IsDisposed);
}
[Fact]
public void CombinedLedgerConvergesEveryGameplayOwnerAndSubscription()
{

View file

@ -340,6 +340,49 @@ public sealed class RuntimePhysicsStateTests
Assert.Equal(0, first.Physics.Engine.LandblockCount);
}
[Fact]
public void TerminalDisposalClearsLandblocksShadowsAndWorksets()
{
var lifetime = new RuntimeEntityObjectLifetime();
RuntimeEntityRecord record =
lifetime.Entities.AddActive(Spawn(0x70000041u, 1));
lifetime.Physics.AcknowledgeSpatialProjection(
record,
spatial: true);
RuntimeCollisionAdmission admission =
lifetime.Physics.BeginCollisionAdmission(0x0101FFFFu);
lifetime.Physics.AdmitCollisionAssets(
admission,
CollisionAssets(0x0101FFFFu));
_ = lifetime.Physics.CompleteCollisionAdmission(admission);
lifetime.Physics.Engine.ShadowObjects.Register(
entityId: record.LocalEntityId!.Value,
gfxObjId: 0x01000001u,
worldPos: new Vector3(10f, 20f, 5f),
rotation: Quaternion.Identity,
radius: 0.5f,
worldOffsetX: 0f,
worldOffsetY: 0f,
landblockId: 0x0101FFFFu,
seedCellId: 0x01010001u,
isStatic: false);
Assert.False(lifetime.Physics.CaptureOwnership().IsConverged);
Assert.True(
lifetime.Physics.Engine.ShadowObjects
.RetainedRegistrationCount > 0);
lifetime.Dispose();
RuntimePhysicsOwnershipSnapshot retired =
lifetime.Physics.CaptureOwnership();
Assert.True(retired.IsConverged);
Assert.Equal(0, retired.LandblockCount);
Assert.Equal(0, retired.RetainedShadowRegistrationCount);
Assert.Equal(0, retired.SpatialRootCount);
}
[Fact]
public void RemoteBindingOwnsCanonicalBodyCellAndTypedCellPublication()
{
@ -368,6 +411,66 @@ public sealed class RuntimePhysicsStateTests
Assert.Equal(0, lifetime.Physics.SpatialRemoteCount);
}
[Fact]
public void BuiltInRemoteConstructionInstallsCreateVectorsOnlyOnce()
{
using var lifetime = new RuntimeEntityObjectLifetime();
RuntimeEntityRecord record = lifetime.Entities.AddActive(
Spawn(
0x70000042u,
1,
velocity: new Vector3(40f, 0f, 0f),
angularVelocity: new Vector3(0f, 0f, 2f)));
RemoteMotion remote =
lifetime.Physics.GetOrCreateRemoteMotion(record);
Assert.Equal(new Vector3(40f, 0f, 0f), remote.Body.Velocity);
Assert.Equal(new Vector3(0f, 0f, 2f), remote.Body.Omega);
remote.Body.set_velocity(new Vector3(8f, 0f, 0f));
remote.Body.Omega = new Vector3(0f, 0f, 3f);
Assert.Same(
remote,
lifetime.Physics.GetOrCreateRemoteMotion(record));
Assert.Equal(new Vector3(8f, 0f, 0f), remote.Body.Velocity);
Assert.Equal(new Vector3(0f, 0f, 3f), remote.Body.Omega);
}
[Fact]
public void BuiltInMovementActivationUsesExactRuntimeIncarnation()
{
using var lifetime = new RuntimeEntityObjectLifetime();
RuntimeEntityRecord ordinary =
lifetime.Entities.AddActive(Spawn(0x70000043u, 1));
RemoteMotion ordinaryRemote =
lifetime.Physics.GetOrCreateRemoteMotion(ordinary);
ordinary.ObjectClock.Deactivate();
ordinaryRemote.Body.TransientState &=
~TransientStateFlags.Active;
ordinaryRemote.Movement.ActivatePhysicsObject!();
Assert.True(ordinary.ObjectClock.IsActive);
Assert.True(ordinaryRemote.Body.IsActive);
RuntimeEntityRecord staticRecord =
lifetime.Entities.AddActive(Spawn(0x70000044u, 1));
lifetime.Entities.SetFinalPhysicsState(
staticRecord,
PhysicsStateFlags.Static);
RemoteMotion staticRemote =
lifetime.Physics.GetOrCreateRemoteMotion(staticRecord);
staticRecord.ObjectClock.Deactivate();
staticRemote.Body.TransientState &=
~TransientStateFlags.Active;
staticRemote.Movement.ActivatePhysicsObject!();
Assert.False(staticRecord.ObjectClock.IsActive);
Assert.False(staticRemote.Body.IsActive);
}
[Fact]
public void RemotePhysicsTickUsesCanonicalRuntimeAndPublishesPoseSnapshot()
{
@ -523,7 +626,11 @@ public sealed class RuntimePhysicsStateTests
0u);
}
private static WorldSession.EntitySpawn Spawn(uint guid, ushort instance)
private static WorldSession.EntitySpawn Spawn(
uint guid,
ushort instance,
Vector3? velocity = null,
Vector3? angularVelocity = null)
{
var position = new CreateObject.ServerPosition(
0x0101FFFFu,
@ -559,9 +666,9 @@ public sealed class RuntimePhysicsStateTests
Friction: null,
Elasticity: null,
Translucency: null,
Velocity: null,
Velocity: velocity,
Acceleration: null,
AngularVelocity: null,
AngularVelocity: angularVelocity,
DefaultScriptType: null,
DefaultScriptIntensity: null,
Timestamps: timestamps);

View file

@ -0,0 +1,384 @@
using System.Numerics;
using AcDream.Core.Combat;
using AcDream.Core.Items;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Core.Spells;
using AcDream.Runtime.Entities;
using AcDream.Runtime.Gameplay;
using AcDream.Runtime.Physics;
namespace AcDream.Runtime.Tests;
public sealed class RuntimeSimulationFixtureHostTests
{
[Fact]
public void RuntimeOnlyHostDrivesCompleteJ5SimulationWithoutPresentation()
{
var host = new RuntimeOnlySimulationFixtureHost();
host.Move();
host.Use(0x70000011u);
host.Attack();
host.Cast();
host.AdvanceProjectile(failProjection: false);
Assert.Equal(
[
"movement:run",
"use:70000011",
"attack:prepare",
"attack:Medium:0.5",
"cast:stop",
"cast:1",
"cast:busy",
"projectile:ack",
],
host.Trace);
string[] loaded = AppDomain.CurrentDomain.GetAssemblies()
.Select(static assembly =>
assembly.GetName().Name ?? string.Empty)
.ToArray();
Assert.DoesNotContain(loaded, static name =>
name.StartsWith(
"AcDream.App",
StringComparison.OrdinalIgnoreCase)
|| name.StartsWith(
"AcDream.UI.",
StringComparison.OrdinalIgnoreCase)
|| name.StartsWith(
"Silk.NET",
StringComparison.OrdinalIgnoreCase)
|| name.StartsWith(
"OpenAL",
StringComparison.OrdinalIgnoreCase)
|| name.StartsWith(
"ImGui",
StringComparison.OrdinalIgnoreCase));
host.Dispose();
Assert.True(host.CaptureOwnership().IsConverged);
}
[Fact]
public void ProjectionAcknowledgementFailureStillAllowsTerminalConvergence()
{
var host = new RuntimeOnlySimulationFixtureHost();
Assert.Throws<InvalidOperationException>(() =>
host.AdvanceProjectile(failProjection: true));
host.Dispose();
RuntimeSimulationOwnershipSnapshot ownership =
host.CaptureOwnership();
Assert.True(ownership.IsConverged);
Assert.Equal(0, ownership.Physics.SpatialProjectileCount);
Assert.Equal(0, ownership.EntityObjects.ActiveEntityCount);
}
private sealed class RuntimeOnlySimulationFixtureHost :
IRuntimeInteractionTransport,
IRuntimeCombatAttackOperations,
IRuntimeCombatTargetOperations,
IRuntimeCombatModeOperations,
IRuntimeSpellCastOperations,
IDisposable
{
private const uint ProjectileGuid = 0x70000021u;
private double _now = 10d;
private uint _sequence;
private bool _disposed;
public RuntimeOnlySimulationFixtureHost()
{
EntityObjects = new RuntimeEntityObjectLifetime();
Inventory = new RuntimeInventoryState(EntityObjects);
Character = new RuntimeCharacterState();
Character.Spellbook.InstallMetadata(SpellMetadata());
Character.Spellbook.OnSpellLearned(1u);
Communication = new RuntimeCommunicationState();
Actions = new RuntimeActionState(
Inventory.Transactions,
Character.Spellbook,
this,
this,
this,
this,
now: () => _now);
Movement = new RuntimeLocalPlayerMovementState();
}
public RuntimeEntityObjectLifetime EntityObjects { get; }
public RuntimeInventoryState Inventory { get; }
public RuntimeCharacterState Character { get; }
public RuntimeCommunicationState Communication { get; }
public RuntimeActionState Actions { get; }
public RuntimeLocalPlayerMovementState Movement { get; }
public List<string> Trace { get; } = [];
public void Move()
{
Assert.True(Movement.Execute(
RuntimeMovementCommand.ToggleRunLock));
Assert.True(Movement.AutoRunActive);
Trace.Add("movement:run");
}
public void Use(uint serverGuid)
{
ItemUseRequestReservation reservation =
Actions.Transactions.BeginUseRequestReservation();
RuntimeInteractionDispatchResult result =
Actions.Transactions.TryDispatchUse(
serverGuid,
ownedByPlayer: false,
useable: true,
reservation,
this,
out _);
Assert.Equal(
RuntimeInteractionDispatchResult.Dispatched,
result);
Actions.Transactions.CompleteUse(0u);
}
public void Attack()
{
Actions.Combat.SetCombatMode(CombatMode.Melee);
Actions.CombatAttack.SetDesiredPower(0.5f);
Actions.CombatAttack.PressAttack(AttackHeight.Medium);
_now += 0.5d;
Actions.CombatAttack.ReleaseAttack();
}
public void Cast() =>
Assert.Equal(
CastRequestResult.Sent,
Actions.SpellCast.Cast(1u));
public void AdvanceProjectile(bool failProjection)
{
RuntimeEntityRecord record =
EntityObjects.RegisterEntity(
Spawn(ProjectileGuid, 1)).Canonical!;
PhysicsBody body =
EntityObjects.Physics.GetOrCreatePhysicsBody(
record,
static canonical =>
{
var created = new PhysicsBody
{
Position = new Vector3(10f, 20f, 5f),
Orientation = Quaternion.Identity,
InWorld = true,
TransientState =
TransientStateFlags.Active,
};
created.SnapToCell(
canonical.FullCellId,
created.Position,
created.Position);
return created;
});
_ = EntityObjects.Physics.BindProjectile(
record,
body,
new ProjectileCollisionSphere(
Vector3.Zero,
0.25f));
EntityObjects.Physics.AcknowledgeSpatialProjection(
record,
spatial: true);
var updater =
new RuntimeProjectilePhysicsUpdater(
EntityObjects.Physics);
Assert.True(updater.TryBegin(
record,
quantum: 0.05f,
record.ObjectClockEpoch,
externalOwnerValid: null,
out RuntimeProjectilePhysicsCommit commit));
_ = updater.Complete(
commit,
liveCenterX: 1,
liveCenterY: 1,
_ =>
{
if (failProjection)
{
throw new InvalidOperationException(
"projection acknowledgement");
}
Trace.Add("projectile:ack");
return true;
});
}
public RuntimeSimulationOwnershipSnapshot CaptureOwnership() =>
RuntimeSimulationOwnership.Capture(
EntityObjects,
Inventory,
Character,
Communication,
Actions,
Movement);
public bool IsInWorld => true;
public uint LocalPlayerId => 0x50000001u;
public bool CanSend => true;
public bool IsDualWield => false;
public bool PlayerReadyForAttack => true;
public bool AutoRepeatAttack => false;
public bool AutoTarget => false;
public bool TrySendUse(uint serverGuid, out uint sequence)
{
sequence = ++_sequence;
Trace.Add($"use:{serverGuid:X8}");
return true;
}
public bool TrySendPickup(
uint itemGuid,
uint destinationContainerId,
int placement,
out uint sequence)
{
sequence = ++_sequence;
return true;
}
public bool CanStartAttack() => true;
public void PrepareAttackRequest() =>
Trace.Add("attack:prepare");
public bool SendAttack(AttackHeight height, float power)
{
Trace.Add($"attack:{height}:{power:0.0}");
return true;
}
public void SendCancelAttack() { }
public uint? SelectClosestTarget() => null;
public IReadOnlyList<ClientObject> GetOrderedEquipment() => [];
public void NotifyExplicitCombatModeRequest() { }
public void SendChangeCombatMode(CombatMode mode) { }
public bool HasRequiredComponents(uint spellId) => true;
public bool IsTargetCompatible(
uint targetId,
SpellMetadata spell,
bool showMessage) => true;
public void StopCompletely() =>
Trace.Add("cast:stop");
public void SendUntargeted(uint spellId) =>
Trace.Add($"cast:{spellId}");
public void SendTargeted(uint targetId, uint spellId) =>
Trace.Add($"cast:{targetId}:{spellId}");
public void DisplayMessage(string message) =>
Trace.Add($"message:{message}");
public void IncrementBusy()
{
Inventory.Transactions.IncrementBusyCount();
Trace.Add("cast:busy");
}
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
Movement.Dispose();
Actions.Dispose();
Communication.Dispose();
Character.Dispose();
Inventory.Dispose();
EntityObjects.Dispose();
}
private static SpellTable SpellMetadata()
{
const string header =
"Spell ID,Name,Flags [Hex],IsUntargetted,TargetMask [Hex]";
const string row = "1,Runtime Fixture,0x0,true,0x0";
return SpellTable.LoadFromReader(
new System.IO.StringReader($"{header}\n{row}"));
}
private static WorldSession.EntitySpawn Spawn(
uint guid,
ushort instance)
{
var position = new CreateObject.ServerPosition(
0x01010001u,
10f,
20f,
5f,
1f,
0f,
0f,
0f);
var timestamps = new PhysicsTimestamps(
Position: 1,
Movement: 1,
State: 1,
Vector: 1,
Teleport: 0,
ServerControlledMove: 1,
ForcePosition: 0,
ObjDesc: 1,
Instance: instance);
var physics = new PhysicsSpawnData(
RawState: (uint)(
PhysicsStateFlags.ReportCollisions
| PhysicsStateFlags.Missile),
Position: position,
Movement: null,
AnimationFrame: null,
SetupTableId: 0x02000001u,
MotionTableId: null,
SoundTableId: null,
PhysicsScriptTableId: null,
Parent: null,
Children: null,
Scale: null,
Friction: null,
Elasticity: null,
Translucency: null,
Velocity: new Vector3(10f, 0f, 0f),
Acceleration: null,
AngularVelocity: null,
DefaultScriptType: null,
DefaultScriptIntensity: null,
Timestamps: timestamps);
return new WorldSession.EntitySpawn(
guid,
position,
0x02000001u,
Array.Empty<CreateObject.AnimPartChange>(),
Array.Empty<CreateObject.TextureChange>(),
Array.Empty<CreateObject.SubPaletteSwap>(),
null,
null,
"runtime projectile",
null,
null,
null,
PhysicsState: physics.RawState,
InstanceSequence: instance,
MovementSequence: 1,
ServerControlSequence: 1,
PositionSequence: 1,
Physics: physics);
}
}
}