refactor(runtime): own per-session physics simulation

Move the sole PhysicsEngine, production cache, collision admissions, canonical bodies and hosts, remote components, ordinary/remote worksets, simulation, cell commits, and shadow synchronization under RuntimeEntityObjectLifetime. Keep App as the prepared-asset, animation-input, and render-projection adapter while preserving the named-retail update and collision order.

Add exact-incarnation, object-clock, callback-reentrancy, GUID-reuse, two-runtime isolation, source ownership, collision publication, and graphical projection coverage. Release build and the complete 8,588-test solution pass.

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Erik 2026-07-26 13:39:57 +02:00
parent 0dc3bfdeff
commit 7e6033d0ad
39 changed files with 3685 additions and 1722 deletions

View file

@ -19,13 +19,13 @@ public sealed class LiveEntityOrdinaryPhysicsUpdaterTests
[Fact]
public void CompleteRootFrame_CrossesTransitionAndCommitsCanonicalCell()
{
PhysicsEngine physics = BuildBoundaryEngine();
var spatial = new GpuWorldState();
spatial.AddLandblock(EmptyLandblock(0xA9B4FFFFu));
spatial.AddLandblock(EmptyLandblock(0xAAB4FFFFu));
var live = LiveEntityRuntimeFixture.Create(
spatial,
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }));
PopulateBoundaryEngine(live.Physics.Engine);
LiveEntityRecord record = live.RegisterAndMaterializeProjection(
Spawn(),
id => new WorldEntity
@ -40,7 +40,7 @@ public sealed class LiveEntityOrdinaryPhysicsUpdaterTests
});
WorldEntity entity = Assert.IsType<WorldEntity>(record.WorldEntity);
var remote = new AcDream.App.Physics.RemoteMotion
var remote = new AcDream.Runtime.Physics.RemoteMotion
{
CellId = SourceCell,
};
@ -56,7 +56,7 @@ public sealed class LiveEntityOrdinaryPhysicsUpdaterTests
ulong epoch = record.ObjectClockEpoch;
var updater = new LiveEntityOrdinaryPhysicsUpdater(
physics,
live.Physics,
(_, _) => (0.48f, 1.835f));
var rootFrame = new Frame
{
@ -106,7 +106,7 @@ public sealed class LiveEntityOrdinaryPhysicsUpdaterTests
ParentCellId = SourceCell,
});
WorldEntity entity = Assert.IsType<WorldEntity>(record.WorldEntity);
var remote = new AcDream.App.Physics.RemoteMotion
var remote = new AcDream.Runtime.Physics.RemoteMotion
{
CellId = SourceCell,
};
@ -123,7 +123,7 @@ public sealed class LiveEntityOrdinaryPhysicsUpdaterTests
new NullAnimationLoader());
Vector3 retainedEntityPosition = entity.Position;
var updater = new LiveEntityOrdinaryPhysicsUpdater(
new PhysicsEngine(),
live.Physics,
(_, _) => (0.48f, 1.835f));
Assert.False(updater.Tick(
@ -149,7 +149,7 @@ public sealed class LiveEntityOrdinaryPhysicsUpdaterTests
Assert.Equal(retainedEntityPosition, entity.Position);
}
private static PhysicsEngine BuildBoundaryEngine()
private static void PopulateBoundaryEngine(PhysicsEngine engine)
{
static TerrainSurface FlatTerrain()
{
@ -159,7 +159,6 @@ public sealed class LiveEntityOrdinaryPhysicsUpdaterTests
return new TerrainSurface(heights, table);
}
var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() };
engine.AddLandblock(
0xA9B4FFFFu,
FlatTerrain(),
@ -174,7 +173,6 @@ public sealed class LiveEntityOrdinaryPhysicsUpdaterTests
Array.Empty<PortalPlane>(),
192f,
0f);
return engine;
}
private static LoadedLandblock EmptyLandblock(uint landblockId) =>

View file

@ -105,7 +105,7 @@ public sealed class ProjectileControllerTests
LiveEntityRecord record = fixture.Spawn(instance: 1);
WorldEntity entity = record.WorldEntity!;
Assert.Null(record.AnimationRuntime);
var remote = new AcDream.App.Physics.RemoteMotion();
var remote = new AcDream.Runtime.Physics.RemoteMotion();
RemotePhysicsBodyInitializer.Initialize(remote.Body, record);
remote.Body.Position = entity.Position;
remote.Body.Orientation = entity.Rotation;
@ -124,7 +124,7 @@ public sealed class ProjectileControllerTests
isMovingTo: false,
currentBodyPosition: remote.Body.Position);
var updater = new RemotePhysicsUpdater(
fixture.Engine,
fixture.Live.Physics,
(_, _) => (0.48f, 1.835f),
(_, _, _, _) => { });
@ -685,7 +685,7 @@ 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.App.Physics.RemoteMotion(body));
fixture.Live.SetRemoteMotionRuntime(Guid, new AcDream.Runtime.Physics.RemoteMotion(body));
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
record, record.FinalPhysicsState, 2.0, 1, 1));
@ -709,7 +709,7 @@ 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.App.Physics.RemoteMotion(body));
fixture.Live.SetRemoteMotionRuntime(Guid, new AcDream.Runtime.Physics.RemoteMotion(body));
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
@ -948,7 +948,7 @@ public sealed class ProjectileControllerTests
var fixture = new Fixture();
LiveEntityRecord record = fixture.Spawn(instance: 1);
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
var remote = new AcDream.App.Physics.RemoteMotion();
var remote = new AcDream.Runtime.Physics.RemoteMotion();
RemotePhysicsBodyInitializer.Initialize(remote.Body, record);
fixture.Live.SetRemoteMotionRuntime(Guid, remote);
@ -1000,7 +1000,7 @@ public sealed class ProjectileControllerTests
var fixture = new Fixture();
LiveEntityRecord record = fixture.Spawn(instance: 1);
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
var remote = new AcDream.App.Physics.RemoteMotion();
var remote = new AcDream.Runtime.Physics.RemoteMotion();
RemotePhysicsBodyInitializer.Initialize(remote.Body, record);
fixture.Live.SetRemoteMotionRuntime(Guid, remote);
@ -1072,7 +1072,7 @@ public sealed class ProjectileControllerTests
seedCellId: startCell,
isStatic: false);
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
var remote = new AcDream.App.Physics.RemoteMotion();
var remote = new AcDream.Runtime.Physics.RemoteMotion();
RemotePhysicsBodyInitializer.Initialize(remote.Body, record);
Assert.Equal(new Vector3(40f, 0f, 0f), remote.Body.Velocity);
Assert.Equal(new Vector3(0f, 0f, 2f), remote.Body.Omega);
@ -1138,7 +1138,7 @@ public sealed class ProjectileControllerTests
velocity: new Vector3(float.NaN, 0f, 0f));
WorldEntity entity = record.WorldEntity!;
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
var remote = new AcDream.App.Physics.RemoteMotion();
var remote = new AcDream.Runtime.Physics.RemoteMotion();
RemotePhysicsBodyInitializer.Initialize(remote.Body, record);
remote.Body.Position = entity.Position;
remote.Body.Orientation = entity.Rotation;
@ -1164,7 +1164,7 @@ public sealed class ProjectileControllerTests
LiveEntityRecord record = fixture.Spawn(instance: 1);
WorldEntity entity = record.WorldEntity!;
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
var remote = new AcDream.App.Physics.RemoteMotion();
var remote = new AcDream.Runtime.Physics.RemoteMotion();
RemotePhysicsBodyInitializer.Initialize(remote.Body, record);
remote.Body.Position = entity.Position;
remote.Body.Orientation = entity.Rotation;
@ -1188,7 +1188,7 @@ 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.App.Physics.RemoteMotion(body);
var remote = new AcDream.Runtime.Physics.RemoteMotion(body);
fixture.Live.SetRemoteMotionRuntime(Guid, remote);
fixture.Live.RebucketLiveEntity(Guid, CellB);
@ -1200,7 +1200,7 @@ public sealed class ProjectileControllerTests
Assert.Same(body, record.RemoteMotionRuntime!.Body);
Assert.Same(body, record.ProjectileRuntime!.Body);
var replacement = new AcDream.App.Physics.RemoteMotion(body);
var replacement = new AcDream.Runtime.Physics.RemoteMotion(body);
fixture.Live.SetRemoteMotionRuntime(Guid, replacement);
remote.CellId = CellB;
Assert.Equal(CellA, record.FullCellId);

View file

@ -57,10 +57,9 @@ public sealed class RemotePhysicsUpdaterTests
Quaternion turn = Quaternion.CreateFromAxisAngle(
Vector3.UnitZ,
MathF.PI / 2f);
var engine = new PhysicsEngine();
var entity = new WorldEntity
{
Id = 13u,
Id = LiveEntityRuntime.FirstLiveEntityId + 13u,
ServerGuid = 0x80000013u,
SourceGfxObjOrSetupId = 0x02000001u,
Position = Vector3.Zero,
@ -68,7 +67,7 @@ public sealed class RemotePhysicsUpdaterTests
MeshRefs = Array.Empty<MeshRef>(),
ParentCellId = cellId,
};
var motion = new AcDream.App.Physics.RemoteMotion
var motion = new AcDream.Runtime.Physics.RemoteMotion
{
LastShadowSyncPos = Vector3.Zero,
LastShadowSyncOrientation = Quaternion.Identity,
@ -76,7 +75,8 @@ public sealed class RemotePhysicsUpdaterTests
motion.Body.Position = Vector3.Zero;
motion.Body.Orientation = Quaternion.Identity;
motion.CellId = cellId;
engine.ShadowObjects.RegisterMultiPart(
RemoteBinding binding = BindRemote(entity, motion, cellId);
binding.Engine.ShadowObjects.RegisterMultiPart(
entity.Id,
entity.Position,
entity.Rotation,
@ -96,12 +96,7 @@ public sealed class RemotePhysicsUpdaterTests
worldOffsetY: 0f,
landblockId: 0x01010000u,
seedCellId: cellId);
var updater = new RemotePhysicsUpdater(
engine,
(_, _) => (0.48f, 1.835f),
(_, _, _, _) => { });
updater.Tick(
binding.Updater.Tick(
motion,
entity,
objectScale: 1f,
@ -110,10 +105,13 @@ public sealed class RemotePhysicsUpdaterTests
dt: 0.1f,
new MotionDeltaFrame { Orientation = turn },
liveCenterX: 1,
liveCenterY: 1);
liveCenterY: 1,
ownerRuntime: binding.Live,
ownerRecord: binding.Record,
ownerClockEpoch: binding.Record.ObjectClockEpoch);
ShadowEntry entry = Assert.Single(
engine.ShadowObjects.AllEntriesForDebug(),
binding.Engine.ShadowObjects.AllEntriesForDebug(),
candidate => candidate.EntityId == entity.Id);
Assert.Equal(0f, entry.Position.X, 3);
Assert.Equal(1f, entry.Position.Y, 3);
@ -130,7 +128,7 @@ public sealed class RemotePhysicsUpdaterTests
{
var entity = new WorldEntity
{
Id = 10u,
Id = LiveEntityRuntime.FirstLiveEntityId + 10u,
ServerGuid = 0x80000010u,
SourceGfxObjOrSetupId = 0x02000001u,
Position = new Vector3(10f, 20f, 30f),
@ -151,7 +149,7 @@ public sealed class RemotePhysicsUpdaterTests
PartTemplate = Array.Empty<LiveAnimationPartTemplate>(),
PartAvailability = Array.Empty<bool>(),
};
var motion = new AcDream.App.Physics.RemoteMotion();
var motion = new AcDream.Runtime.Physics.RemoteMotion();
motion.Body.Position = entity.Position;
motion.Body.Orientation = entity.Rotation;
motion.Body.TransientState = TransientStateFlags.Contact
@ -164,18 +162,17 @@ public sealed class RemotePhysicsUpdaterTests
Vector3.UnitZ,
MathF.PI / 2f),
};
var updater = new RemotePhysicsUpdater(
new PhysicsEngine(),
(_, _) => (0.48f, 1.835f),
(_, _, _, _) => { });
updater.Tick(
RemoteBinding binding = BindRemote(entity, motion);
binding.Updater.Tick(
motion,
animated,
dt: 0.1f,
sequenceFrame,
liveCenterX: 0,
liveCenterY: 0);
liveCenterY: 0,
ownerRuntime: binding.Live,
ownerRecord: binding.Record,
ownerClockEpoch: binding.Record.ObjectClockEpoch);
// Current body faces east, so the sequence's local +Y origin moves
// east before its quarter-turn is composed. No ObservedOmega/manual
@ -198,7 +195,7 @@ public sealed class RemotePhysicsUpdaterTests
Quaternion rootTurn = Quaternion.CreateFromAxisAngle(Vector3.UnitY, -0.9f);
var entity = new WorldEntity
{
Id = 11u,
Id = LiveEntityRuntime.FirstLiveEntityId + 11u,
ServerGuid = 0x80000011u,
SourceGfxObjOrSetupId = 0x02000001u,
Position = new Vector3(10f, 20f, 30f),
@ -217,7 +214,7 @@ public sealed class RemotePhysicsUpdaterTests
PartTemplate = Array.Empty<LiveAnimationPartTemplate>(),
PartAvailability = Array.Empty<bool>(),
};
var motion = new AcDream.App.Physics.RemoteMotion
var motion = new AcDream.Runtime.Physics.RemoteMotion
{
Airborne = true,
};
@ -229,12 +226,17 @@ public sealed class RemotePhysicsUpdaterTests
Origin = new Vector3(0f, 10f, 0f),
Orientation = rootTurn,
};
var updater = new RemotePhysicsUpdater(
new PhysicsEngine(),
(_, _) => (0.48f, 1.835f),
(_, _, _, _) => { });
updater.Tick(motion, animated, 0.1f, root, 0, 0);
RemoteBinding binding = BindRemote(entity, motion);
binding.Updater.Tick(
motion,
animated,
0.1f,
root,
0,
0,
ownerRuntime: binding.Live,
ownerRecord: binding.Record,
ownerClockEpoch: binding.Record.ObjectClockEpoch);
Assert.Equal(new Vector3(10f, 20f, 30f), motion.Body.Position);
Quaternion expected = Quaternion.Normalize(initial * rootTurn);
@ -253,7 +255,7 @@ public sealed class RemotePhysicsUpdaterTests
Quaternion target = Quaternion.CreateFromAxisAngle(Vector3.UnitY, -0.9f);
var entity = new WorldEntity
{
Id = 12u,
Id = LiveEntityRuntime.FirstLiveEntityId + 12u,
ServerGuid = 0x80000012u,
SourceGfxObjOrSetupId = 0x02000001u,
Position = new Vector3(10f, 20f, 30f),
@ -272,7 +274,7 @@ public sealed class RemotePhysicsUpdaterTests
PartTemplate = Array.Empty<LiveAnimationPartTemplate>(),
PartAvailability = Array.Empty<bool>(),
};
var motion = new AcDream.App.Physics.RemoteMotion();
var motion = new AcDream.Runtime.Physics.RemoteMotion();
motion.Body.Position = entity.Position;
motion.Body.Orientation = initial;
motion.Interp.Enqueue(
@ -285,12 +287,17 @@ public sealed class RemotePhysicsUpdaterTests
Origin = new Vector3(0f, 10f, 0f),
Orientation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 0.7f),
};
var updater = new RemotePhysicsUpdater(
new PhysicsEngine(),
(_, _) => (0.48f, 1.835f),
(_, _, _, _) => { });
updater.Tick(motion, animated, 0.1f, root, 0, 0);
RemoteBinding binding = BindRemote(entity, motion);
binding.Updater.Tick(
motion,
animated,
0.1f,
root,
0,
0,
ownerRuntime: binding.Live,
ownerRecord: binding.Record,
ownerClockEpoch: binding.Record.ObjectClockEpoch);
// InterpolationManager::adjust_offset clamps the 1 m correction to
// maxSpeed (8 m/s) * 0.1 s = 0.8 m and replaces the authored 10 m
@ -308,7 +315,7 @@ public sealed class RemotePhysicsUpdaterTests
[Fact]
public void TickHidden_AppliesPositionManagerOffsetWithoutAdvancingPhysics()
{
var motion = new AcDream.App.Physics.RemoteMotion();
var motion = new AcDream.Runtime.Physics.RemoteMotion();
motion.Body.Position = Vector3.Zero;
motion.Body.Orientation = Quaternion.Identity;
motion.Body.Velocity = new Vector3(4f, 0f, 5f);
@ -321,7 +328,7 @@ public sealed class RemotePhysicsUpdaterTests
currentBodyPosition: Vector3.Zero);
var entity = new WorldEntity
{
Id = 1u,
Id = LiveEntityRuntime.FirstLiveEntityId + 1u,
ServerGuid = 0x70000001u,
SourceGfxObjOrSetupId = 0x02000001u,
Position = Vector3.Zero,
@ -329,13 +336,19 @@ public sealed class RemotePhysicsUpdaterTests
MeshRefs = Array.Empty<MeshRef>(),
};
var updater = new RemotePhysicsUpdater(
new PhysicsEngine(),
(_, _) => (0.48f, 1.835f),
(_, _, _, _) => { });
RemoteBinding binding = BindRemote(
entity,
motion,
motion.CellId);
var poses = new EntityEffectPoseRegistry();
poses.Publish(entity, Array.Empty<Matrix4x4>());
updater.TickHidden(motion, entity, 0.1f);
binding.Updater.TickHidden(
motion,
entity,
0.1f,
ownerRuntime: binding.Live,
ownerRecord: binding.Record,
ownerClockEpoch: binding.Record.ObjectClockEpoch);
Assert.True(poses.UpdateRoot(entity));
Assert.InRange(motion.Body.Position.X, 0.01f, 1f);
@ -350,29 +363,29 @@ public sealed class RemotePhysicsUpdaterTests
[Fact]
public void TickHidden_RunsPartArrayManagerTailWithoutAdvancingSequence()
{
var updater = new RemotePhysicsUpdater(
new PhysicsEngine(),
(_, _) => (0.48f, 1.835f),
(_, _, _, _) => { });
var motion = new AcDream.App.Physics.RemoteMotion();
var motion = new AcDream.Runtime.Physics.RemoteMotion();
motion.Body.Orientation = Quaternion.Identity;
var entity = new WorldEntity
{
Id = 2u,
Id = LiveEntityRuntime.FirstLiveEntityId + 2u,
ServerGuid = 0x70000002u,
SourceGfxObjOrSetupId = 0x02000001u,
Position = Vector3.Zero,
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
};
RemoteBinding binding = BindRemote(entity, motion);
AnimationSequencer sequencer = CreateSequencer();
sequencer.Manager.AddToQueue(MotionTableManager.ReadySentinel, 0);
updater.TickHidden(
binding.Updater.TickHidden(
motion,
entity,
0.1f,
sequencer.Manager);
sequencer.Manager,
ownerRuntime: binding.Live,
ownerRecord: binding.Record,
ownerClockEpoch: binding.Record.ObjectClockEpoch);
Assert.Empty(sequencer.Manager.PendingAnimations);
}
@ -380,26 +393,23 @@ public sealed class RemotePhysicsUpdaterTests
[Fact]
public void TickHidden_ProcessesPendingHooksBeforeManagerTail()
{
var updater = new RemotePhysicsUpdater(
new PhysicsEngine(),
(_, _) => (0.48f, 1.835f),
(_, _, _, _) => { });
var motion = new AcDream.App.Physics.RemoteMotion();
var motion = new AcDream.Runtime.Physics.RemoteMotion();
motion.Body.Orientation = Quaternion.Identity;
var entity = new WorldEntity
{
Id = 3u,
Id = LiveEntityRuntime.FirstLiveEntityId + 3u,
ServerGuid = 0x70000003u,
SourceGfxObjOrSetupId = 0x02000001u,
Position = Vector3.Zero,
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
};
RemoteBinding binding = BindRemote(entity, motion);
AnimationSequencer sequencer = CreateSequencer();
sequencer.Manager.AddToQueue(MotionTableManager.ReadySentinel, 0);
var order = new List<string>();
updater.TickHidden(
binding.Updater.TickHidden(
motion,
entity,
0.1f,
@ -410,7 +420,10 @@ public sealed class RemotePhysicsUpdaterTests
Assert.NotEmpty(sequencer.Manager.PendingAnimations);
order.Add("hooks");
},
sequencer);
sequencer,
binding.Live,
binding.Record,
binding.Record.ObjectClockEpoch);
Assert.Equal(["hooks"], order);
Assert.Empty(sequencer.Manager.PendingAnimations);
@ -419,27 +432,13 @@ public sealed class RemotePhysicsUpdaterTests
[Fact]
public void TickHidden_CrossCellCommitUpdatesCanonicalCellBeforeUnhide()
{
var engine = BuildBoundaryEngine();
var updater = new RemotePhysicsUpdater(
engine,
(_, _) => (0.48f, 1.835f),
(_, _, _, _) => { });
var motion = new AcDream.App.Physics.RemoteMotion();
var motion = new AcDream.Runtime.Physics.RemoteMotion();
motion.Body.Position = new Vector3(191f, 10f, 50f);
motion.Body.Orientation = Quaternion.Identity;
motion.Body.TransientState = TransientStateFlags.Contact
| TransientStateFlags.OnWalkable;
uint canonicalCell = 0xA9B40039u;
int canonicalWrites = 0;
motion.BindCanonicalCell(
() => canonicalCell,
value =>
{
canonicalCell = value;
canonicalWrites++;
});
motion.CellId = canonicalCell;
canonicalWrites = 0;
motion.Interp.Enqueue(
new Vector3(193f, 10f, 50f),
heading: 0f,
@ -447,31 +446,41 @@ public sealed class RemotePhysicsUpdaterTests
currentBodyPosition: motion.Body.Position);
var entity = new WorldEntity
{
Id = 1u,
Id = LiveEntityRuntime.FirstLiveEntityId + 1u,
ServerGuid = 0x70000002u,
SourceGfxObjOrSetupId = 0x02000001u,
Position = motion.Body.Position,
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
};
RemoteBinding binding = BindRemote(
entity,
motion,
canonicalCell);
PopulateBoundaryEngine(binding.Engine);
ulong spatialAuthority = binding.Record.Canonical
.SpatialAuthorityVersion;
updater.TickHidden(motion, entity, 2f);
binding.Updater.TickHidden(
motion,
entity,
2f,
ownerRuntime: binding.Live,
ownerRecord: binding.Record,
ownerClockEpoch: binding.Record.ObjectClockEpoch);
Assert.Equal(0xAAB40001u, canonicalCell);
Assert.Equal(canonicalCell, entity.ParentCellId);
Assert.True(canonicalWrites > 0);
Assert.Equal(0xAAB40001u, binding.Record.FullCellId);
Assert.Equal(binding.Record.FullCellId, entity.ParentCellId);
Assert.True(
binding.Record.Canonical.SpatialAuthorityVersion
> spatialAuthority);
Assert.True(entity.Position.X > 192f);
}
[Fact]
public void TickHidden_OutOfContactDoesNotInterpolateOrInventLanding()
{
var engine = BuildBoundaryEngine();
var updater = new RemotePhysicsUpdater(
engine,
(_, _) => (0.48f, 1.835f),
(_, _, _, _) => { });
var motion = new AcDream.App.Physics.RemoteMotion
var motion = new AcDream.Runtime.Physics.RemoteMotion
{
Airborne = true,
};
@ -486,15 +495,26 @@ public sealed class RemotePhysicsUpdaterTests
currentBodyPosition: motion.Body.Position);
var entity = new WorldEntity
{
Id = 3u,
Id = LiveEntityRuntime.FirstLiveEntityId + 3u,
ServerGuid = 0x70000003u,
SourceGfxObjOrSetupId = 0x02000001u,
Position = motion.Body.Position,
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
};
RemoteBinding binding = BindRemote(
entity,
motion,
motion.CellId);
PopulateBoundaryEngine(binding.Engine);
updater.TickHidden(motion, entity, 0.1f);
binding.Updater.TickHidden(
motion,
entity,
0.1f,
ownerRuntime: binding.Live,
ownerRecord: binding.Record,
ownerClockEpoch: binding.Record.ObjectClockEpoch);
Assert.False(motion.Body.InContact);
Assert.False(motion.Body.OnWalkable);
@ -591,7 +611,7 @@ public sealed class RemotePhysicsUpdaterTests
using var fixture = new BoundaryRemoteFixture();
LiveEntityRecord oldRecord = fixture.Record;
WorldEntity oldEntity = fixture.Entity;
AcDream.App.Physics.RemoteMotion oldRemote = fixture.Remote;
AcDream.Runtime.Physics.RemoteMotion oldRemote = fixture.Remote;
ulong oldAuthority = oldRecord.PositionAuthorityVersion;
int publications = 0;
@ -628,9 +648,9 @@ public sealed class RemotePhysicsUpdaterTests
using var fixture = new BoundaryRemoteFixture();
LiveEntityRecord oldRecord = fixture.Record;
WorldEntity oldEntity = fixture.Entity;
AcDream.App.Physics.RemoteMotion oldRemote = fixture.Remote;
AcDream.Runtime.Physics.RemoteMotion oldRemote = fixture.Remote;
WorldEntity? replacementEntity = null;
AcDream.App.Physics.RemoteMotion? replacementRemote = null;
AcDream.Runtime.Physics.RemoteMotion? replacementRemote = null;
ulong clockEpoch = oldRecord.ObjectClockEpoch;
fixture.Live.ProjectionVisibilityChanged += (record, visible) =>
@ -700,7 +720,7 @@ public sealed class RemotePhysicsUpdaterTests
Assert.Equal(2, live.SpatialRemoteMotionRuntimeCount);
var updater = new RemotePhysicsUpdater(
new PhysicsEngine(),
live.Physics,
(_, _) => (0.48f, 1.835f),
(_, _, _, _) => { });
var published = new List<uint>();
@ -724,7 +744,7 @@ public sealed class RemotePhysicsUpdaterTests
Assert.Equal(1, live.SpatialRemoteMotionRuntimeCount);
}
private static PhysicsEngine BuildBoundaryEngine()
private static void PopulateBoundaryEngine(PhysicsEngine engine)
{
static TerrainSurface FlatTerrain()
{
@ -734,7 +754,6 @@ public sealed class RemotePhysicsUpdaterTests
return new TerrainSurface(heights, table);
}
var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() };
engine.AddLandblock(
0xA9B4FFFFu,
FlatTerrain(),
@ -749,9 +768,119 @@ public sealed class RemotePhysicsUpdaterTests
Array.Empty<PortalPlane>(),
192f,
0f);
return engine;
}
private static RemoteBinding BindRemote(
WorldEntity entity,
AcDream.Runtime.Physics.RemoteMotion remote,
uint fullCellId = 0x01010001u)
{
entity.ParentCellId = fullCellId;
var spatial = new GpuWorldState();
spatial.AddLandblock(new LoadedLandblock(
(fullCellId & 0xFFFF0000u) | 0xFFFFu,
new LandBlock(),
Array.Empty<WorldEntity>()));
LiveEntityRuntime live = LiveEntityRuntimeFixture.Create(
spatial,
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }),
firstLocalEntityId: entity.Id);
LiveEntityRecord record = live.RegisterAndMaterializeProjection(
SpawnRemote(
entity.ServerGuid,
fullCellId,
entity.Position),
localId =>
{
Assert.Equal(entity.Id, localId);
return entity;
});
remote.CellId = fullCellId;
live.SetRemoteMotionRuntime(entity.ServerGuid, remote);
return new RemoteBinding(
live,
record,
live.Physics.Engine,
new RemotePhysicsUpdater(
live.Physics,
(_, _) => (0.48f, 1.835f),
(_, _, _, _) => { }));
}
private static WorldSession.EntitySpawn SpawnRemote(
uint guid,
uint fullCellId,
Vector3 position)
{
const PhysicsStateFlags state =
PhysicsStateFlags.ReportCollisions;
var serverPosition = new CreateObject.ServerPosition(
fullCellId,
position.X,
position.Y,
position.Z,
1f,
0f,
0f,
0f);
var timestamps = new PhysicsTimestamps(
1,
1,
1,
1,
0,
1,
0,
1,
1);
var physics = new PhysicsSpawnData(
RawState: (uint)state,
Position: serverPosition,
Movement: null,
AnimationFrame: null,
SetupTableId: 0x02000001u,
MotionTableId: 0x09000001u,
SoundTableId: null,
PhysicsScriptTableId: null,
Parent: null,
Children: null,
Scale: null,
Friction: null,
Elasticity: null,
Translucency: null,
Velocity: null,
Acceleration: null,
AngularVelocity: null,
DefaultScriptType: null,
DefaultScriptIntensity: null,
Timestamps: timestamps);
return new WorldSession.EntitySpawn(
guid,
serverPosition,
0x02000001u,
Array.Empty<CreateObject.AnimPartChange>(),
Array.Empty<CreateObject.TextureChange>(),
Array.Empty<CreateObject.SubPaletteSwap>(),
null,
null,
"remote physics fixture",
null,
null,
0x09000001u,
PhysicsState: (uint)state,
InstanceSequence: 1,
MovementSequence: 1,
ServerControlSequence: 1,
PositionSequence: 1,
Physics: physics);
}
private sealed record RemoteBinding(
LiveEntityRuntime Live,
LiveEntityRecord Record,
PhysicsEngine Engine,
RemotePhysicsUpdater Updater);
private static void BindHiddenRemote(LiveEntityRuntime live, uint guid)
{
const uint cellId = 0x01010001u;
@ -813,7 +942,7 @@ public sealed class RemotePhysicsUpdaterTests
MeshRefs = Array.Empty<MeshRef>(),
ParentCellId = cellId,
})!;
var remote = new AcDream.App.Physics.RemoteMotion();
var remote = new AcDream.Runtime.Physics.RemoteMotion();
remote.Body.Position = entity.Position;
remote.Body.Orientation = entity.Rotation;
remote.CellId = cellId;
@ -835,7 +964,7 @@ public sealed class RemotePhysicsUpdaterTests
public BoundaryRemoteFixture()
{
Engine = BuildBoundaryEngine();
Engine = null!;
Spatial.AddLandblock(new LoadedLandblock(
0xA9B4FFFFu,
new LandBlock(),
@ -849,6 +978,8 @@ public sealed class RemotePhysicsUpdaterTests
Engine.ShadowObjects.Deregister(entity.Id);
_presentation?.Forget(record);
});
Engine = Live.Physics.Engine;
PopulateBoundaryEngine(Engine);
(Record, Entity, Remote) = SpawnAndBind(
instanceSequence: 1,
new Vector3(191f, 10f, 50f),
@ -862,7 +993,7 @@ public sealed class RemotePhysicsUpdaterTests
RegisterShadow(Entity, Remote);
Assert.True(_presentation.OnLiveEntityReady(Guid));
Updater = new RemotePhysicsUpdater(
Engine,
Live.Physics,
(_, _) => (0.48f, 1.835f),
(_, _, _, _) => { });
}
@ -873,7 +1004,7 @@ public sealed class RemotePhysicsUpdaterTests
public RemotePhysicsUpdater Updater { get; }
public LiveEntityRecord Record { get; }
public WorldEntity Entity { get; }
public AcDream.App.Physics.RemoteMotion Remote { get; }
public AcDream.Runtime.Physics.RemoteMotion Remote { get; }
public void HydrateDestination() =>
Spatial.AddLandblock(new LoadedLandblock(
@ -882,7 +1013,7 @@ public sealed class RemotePhysicsUpdaterTests
Array.Empty<WorldEntity>()));
public (LiveEntityRecord Record, WorldEntity Entity,
AcDream.App.Physics.RemoteMotion Remote) ReplaceAtSource()
AcDream.Runtime.Physics.RemoteMotion Remote) ReplaceAtSource()
{
Assert.True(Live.UnregisterLiveEntity(
new DeleteObject.Parsed(Guid, InstanceSequence: 1),
@ -903,7 +1034,7 @@ public sealed class RemotePhysicsUpdaterTests
}
private (LiveEntityRecord Record, WorldEntity Entity,
AcDream.App.Physics.RemoteMotion Remote) SpawnAndBind(
AcDream.Runtime.Physics.RemoteMotion Remote) SpawnAndBind(
ushort instanceSequence,
Vector3 position,
bool enqueueDestination)
@ -921,7 +1052,7 @@ public sealed class RemotePhysicsUpdaterTests
ParentCellId = SourceCell,
});
WorldEntity entity = Assert.IsType<WorldEntity>(record.WorldEntity);
var remote = new AcDream.App.Physics.RemoteMotion();
var remote = new AcDream.Runtime.Physics.RemoteMotion();
remote.Body.Position = position;
remote.Body.Orientation = Quaternion.Identity;
remote.CellId = SourceCell;
@ -941,7 +1072,7 @@ public sealed class RemotePhysicsUpdaterTests
private void RegisterShadow(
WorldEntity entity,
AcDream.App.Physics.RemoteMotion remote)
AcDream.Runtime.Physics.RemoteMotion remote)
{
Engine.ShadowObjects.Register(
entity.Id,

View file

@ -101,7 +101,7 @@ public sealed class RemoteTeleportControllerTests
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
})!;
var remote = new AcDream.App.Physics.RemoteMotion();
var remote = new AcDream.Runtime.Physics.RemoteMotion();
remote.Body.SnapToCell(
sourceCell,
entity.Position,
@ -242,7 +242,7 @@ public sealed class RemoteTeleportControllerTests
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
})!;
var remote = new AcDream.App.Physics.RemoteMotion();
var remote = new AcDream.Runtime.Physics.RemoteMotion();
remote.Body.SnapToCell(sourceCell, entity.Position, entity.Position);
live.SetRemoteMotionRuntime(guid, remote);
var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() };
@ -333,7 +333,7 @@ public sealed class RemoteTeleportControllerTests
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
})!;
var remote = new AcDream.App.Physics.RemoteMotion();
var remote = new AcDream.Runtime.Physics.RemoteMotion();
remote.Body.SnapToCell(sourceCell, sourcePosition, sourcePosition);
remote.Body.TransientState = TransientStateFlags.Contact
| TransientStateFlags.OnWalkable;
@ -774,13 +774,13 @@ public sealed class RemoteTeleportControllerTests
Assert.Throws<InvalidOperationException>(() =>
fixture.Live.SetRemoteMotionRuntime(
RollbackFixture.Guid,
new AcDream.App.Physics.RemoteMotion()));
new AcDream.Runtime.Physics.RemoteMotion()));
Assert.Throws<InvalidOperationException>(() =>
fixture.Live.SetRemoteMotionRuntime(
RollbackFixture.Guid,
new BodyOnlyRemote(fixture.Remote.Body)));
var replacement = new AcDream.App.Physics.RemoteMotion(fixture.Remote.Body);
var replacement = new AcDream.Runtime.Physics.RemoteMotion(fixture.Remote.Body);
fixture.Live.SetRemoteMotionRuntime(RollbackFixture.Guid, replacement);
fixture.Spatial.AddLandblock(EmptyLandblock(RollbackFixture.DestinationLandblock));
@ -811,7 +811,7 @@ public sealed class RemoteTeleportControllerTests
Assert.Throws<InvalidOperationException>(() =>
fixture.Live.SetRemoteMotionRuntime(
RollbackFixture.Guid,
new AcDream.App.Physics.RemoteMotion()));
new AcDream.Runtime.Physics.RemoteMotion()));
Assert.Throws<InvalidOperationException>(() =>
fixture.Live.SetRemoteMotionRuntime(
RollbackFixture.Guid,
@ -1115,7 +1115,7 @@ public sealed class RemoteTeleportControllerTests
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
})!;
var remote = new AcDream.App.Physics.RemoteMotion();
var remote = new AcDream.Runtime.Physics.RemoteMotion();
remote.Body.SnapToCell(sourceCell, entity.Position, entity.Position);
live.SetRemoteMotionRuntime(guid, remote);
Assert.True(live.TryGetRecord(guid, out owner));
@ -1193,7 +1193,7 @@ public sealed class RemoteTeleportControllerTests
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
})!;
var remote = new AcDream.App.Physics.RemoteMotion();
var remote = new AcDream.Runtime.Physics.RemoteMotion();
remote.Body.SnapToCell(sourceCell, entity.Position, entity.Position);
live.SetRemoteMotionRuntime(guid, remote);
return new LoadedRemoteFixture(
@ -1238,7 +1238,7 @@ public sealed class RemoteTeleportControllerTests
private readonly record struct LoadedRemoteFixture(
LiveEntityRuntime Live,
WorldEntity Entity,
AcDream.App.Physics.RemoteMotion Remote,
AcDream.Runtime.Physics.RemoteMotion Remote,
PhysicsEngine Engine);
private static void AddDestination(PhysicsEngine engine) =>
@ -1397,7 +1397,7 @@ public sealed class RemoteTeleportControllerTests
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
})!;
Remote = new AcDream.App.Physics.RemoteMotion();
Remote = new AcDream.Runtime.Physics.RemoteMotion();
Remote.Body.SnapToCell(
celllessSource ? 0u : SourceCell,
SourcePosition,
@ -1479,7 +1479,7 @@ public sealed class RemoteTeleportControllerTests
internal GpuWorldState Spatial { get; } = new();
internal LiveEntityRuntime Live { get; }
internal WorldEntity Entity { get; }
internal AcDream.App.Physics.RemoteMotion Remote { get; }
internal AcDream.Runtime.Physics.RemoteMotion Remote { get; }
internal PhysicsEngine Engine { get; } = new() { DataCache = new PhysicsDataCache() };
internal RemoteTeleportController Controller { get; }
internal LiveEntityPresentationController Presentation { get; }

View file

@ -19,7 +19,7 @@ public sealed class RemoteTeleportPlacementTests
0x01010001u,
new Vector3(3f, 4f, 5f),
new Vector3(3f, 4f, 5f));
var remote = new AcDream.App.Physics.RemoteMotion(body);
var remote = new AcDream.Runtime.Physics.RemoteMotion(body);
Quaternion orientation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 0.75f);
RemoteTeleportPlacement.Apply(
@ -54,7 +54,7 @@ public sealed class RemoteTeleportPlacementTests
State = PhysicsStateFlags.Gravity | PhysicsStateFlags.ReportCollisions,
};
var contactPlane = new Plane(Vector3.UnitZ, 0f);
var remote = new AcDream.App.Physics.RemoteMotion(body)
var remote = new AcDream.Runtime.Physics.RemoteMotion(body)
{
Airborne = true,
};
@ -97,7 +97,7 @@ public sealed class RemoteTeleportPlacementTests
// landblock is absent. SetPositionInternal must still receive the
// grounded source edge captured before parking.
body.TransientState &= ~(TransientStateFlags.Contact | TransientStateFlags.OnWalkable);
var remote = new AcDream.App.Physics.RemoteMotion(body)
var remote = new AcDream.Runtime.Physics.RemoteMotion(body)
{
Airborne = true,
};
@ -141,7 +141,7 @@ public sealed class RemoteTeleportPlacementTests
// calc_acceleration, which clears grounded angular drift, before
// set_on_walkable installs the steep destination's false value.
body.TransientState &= ~(TransientStateFlags.Contact | TransientStateFlags.OnWalkable);
var remote = new AcDream.App.Physics.RemoteMotion(body);
var remote = new AcDream.Runtime.Physics.RemoteMotion(body);
RemoteTeleportPlacement.Apply(
remote,
@ -171,7 +171,7 @@ public sealed class RemoteTeleportPlacementTests
{
var original = new Vector3(7f, 8f, 9f);
var body = new PhysicsBody { Position = original };
var remote = new AcDream.App.Physics.RemoteMotion(body);
var remote = new AcDream.Runtime.Physics.RemoteMotion(body);
Assert.Throws<ArgumentOutOfRangeException>(() =>
RemoteTeleportPlacement.Apply(