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:
parent
0dc3bfdeff
commit
7e6033d0ad
39 changed files with 3685 additions and 1722 deletions
415
tests/AcDream.Runtime.Tests/Physics/RuntimePhysicsStateTests.cs
Normal file
415
tests/AcDream.Runtime.Tests/Physics/RuntimePhysicsStateTests.cs
Normal file
|
|
@ -0,0 +1,415 @@
|
|||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Runtime.Entities;
|
||||
using AcDream.Runtime.Physics;
|
||||
using AcDream.Core.Physics.Motion;
|
||||
using System.Numerics;
|
||||
|
||||
namespace AcDream.Runtime.Tests.Physics;
|
||||
|
||||
public sealed class RuntimePhysicsStateTests
|
||||
{
|
||||
[Fact]
|
||||
public void EntityLifetimeConstructsOneCanonicalProductionPhysicsWorld()
|
||||
{
|
||||
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||
|
||||
Assert.Same(
|
||||
lifetime.Physics.DataCache,
|
||||
lifetime.Physics.Engine.DataCache);
|
||||
Assert.True(lifetime.Physics.CaptureOwnership().OwnsProductionDataCache);
|
||||
Assert.Equal(0, lifetime.Physics.CaptureOwnership().LandblockCount);
|
||||
Assert.False(lifetime.Physics.CaptureOwnership().IsDisposed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConcurrentRuntimeLifetimesDoNotShareMutablePhysicsState()
|
||||
{
|
||||
using var first = new RuntimeEntityObjectLifetime();
|
||||
using var second = new RuntimeEntityObjectLifetime();
|
||||
|
||||
Assert.NotSame(first.Physics, second.Physics);
|
||||
Assert.NotSame(first.Physics.Engine, second.Physics.Engine);
|
||||
Assert.NotSame(first.Physics.DataCache, second.Physics.DataCache);
|
||||
Assert.NotSame(
|
||||
first.Physics.Engine.ShadowObjects,
|
||||
second.Physics.Engine.ShadowObjects);
|
||||
Assert.NotSame(
|
||||
first.Physics.DataCache.CellGraph,
|
||||
second.Physics.DataCache.CellGraph);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhysicsOwnerDisposesWithItsEntityLifetime()
|
||||
{
|
||||
var lifetime = new RuntimeEntityObjectLifetime();
|
||||
var physics = lifetime.Physics;
|
||||
|
||||
lifetime.Dispose();
|
||||
|
||||
Assert.True(physics.CaptureOwnership().IsDisposed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanonicalRecordAndPhysicsOwnerOwnRemoteComponentAndWorksets()
|
||||
{
|
||||
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||
RuntimeEntityRecord record =
|
||||
lifetime.Entities.AddActive(Spawn(0x70000001u, 1));
|
||||
var remote = new TestRemoteMotion();
|
||||
lifetime.Entities.SetPhysicsBody(record, remote.Body);
|
||||
lifetime.Entities.SetRemoteMotion(record, remote);
|
||||
|
||||
lifetime.Physics.AcknowledgeSpatialProjection(record, spatial: true);
|
||||
|
||||
Assert.Same(remote, record.RemoteMotion);
|
||||
Assert.Same(remote.Body, record.PhysicsBody);
|
||||
Assert.True(lifetime.Physics.IsSpatialRoot(record));
|
||||
Assert.True(lifetime.Physics.IsSpatialRemote(record, remote));
|
||||
Assert.Equal(1, lifetime.Physics.SpatialRootCount);
|
||||
Assert.Equal(1, lifetime.Physics.SpatialRemoteCount);
|
||||
|
||||
var roots = new List<RuntimeEntityRecord>();
|
||||
var remotes = new List<RuntimeEntityRecord>();
|
||||
lifetime.Physics.CopySpatialRootsTo(roots);
|
||||
lifetime.Physics.CopySpatialRemotesTo(remotes);
|
||||
Assert.Same(record, Assert.Single(roots));
|
||||
Assert.Same(record, Assert.Single(remotes));
|
||||
|
||||
lifetime.Entities.SetRemoteMotion(record, null);
|
||||
lifetime.Physics.RefreshRemoteComponent(record);
|
||||
Assert.True(lifetime.Physics.IsSpatialRoot(record));
|
||||
Assert.Equal(0, lifetime.Physics.SpatialRemoteCount);
|
||||
|
||||
lifetime.Physics.RemoveSpatialProjection(record);
|
||||
Assert.Equal(0, lifetime.Physics.SpatialRootCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EqualLocalKeysInConcurrentRuntimesDoNotShareWorksets()
|
||||
{
|
||||
using var first = new RuntimeEntityObjectLifetime();
|
||||
using var second = new RuntimeEntityObjectLifetime();
|
||||
RuntimeEntityRecord firstRecord =
|
||||
first.Entities.AddActive(Spawn(0x70000002u, 1));
|
||||
RuntimeEntityRecord secondRecord =
|
||||
second.Entities.AddActive(Spawn(0x70000003u, 1));
|
||||
Assert.Equal(firstRecord.Key, secondRecord.Key);
|
||||
|
||||
first.Physics.AcknowledgeSpatialProjection(firstRecord, spatial: true);
|
||||
|
||||
Assert.True(first.Physics.IsSpatialRoot(firstRecord));
|
||||
Assert.False(second.Physics.IsSpatialRoot(secondRecord));
|
||||
Assert.Equal(1, first.Physics.SpatialRootCount);
|
||||
Assert.Equal(0, second.Physics.SpatialRootCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CollisionAdmissionIsExactRuntimeScopedAndWithdrawalIsTyped()
|
||||
{
|
||||
using var first = new RuntimeEntityObjectLifetime();
|
||||
using var second = new RuntimeEntityObjectLifetime();
|
||||
RuntimeCollisionAdmission admission =
|
||||
first.Physics.BeginCollisionAdmission(0xA9B4FFFFu);
|
||||
RuntimeCollisionAdmission newer =
|
||||
first.Physics.BeginCollisionAdmission(0xA9B4FFFFu);
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
first.Physics.AdmitCollisionAssets(
|
||||
admission,
|
||||
CollisionAssets(0xA9B4FFFFu)));
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
second.Physics.AdmitCollisionAssets(
|
||||
newer,
|
||||
CollisionAssets(0xA9B4FFFFu)));
|
||||
|
||||
first.Physics.AdmitCollisionAssets(
|
||||
newer,
|
||||
CollisionAssets(0xA9B4FFFFu));
|
||||
RuntimeCollisionAcknowledgement completed =
|
||||
first.Physics.CompleteCollisionAdmission(newer);
|
||||
|
||||
Assert.True(completed.WasResident);
|
||||
Assert.Equal(1, first.Physics.Engine.LandblockCount);
|
||||
Assert.Equal(
|
||||
0,
|
||||
first.Physics.CaptureOwnership().CollisionAdmissionCount);
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
first.Physics.CompleteCollisionAdmission(newer));
|
||||
|
||||
RuntimeCollisionAcknowledgement withdrawn =
|
||||
first.Physics.WithdrawCollision(0xA9B4FFFFu);
|
||||
Assert.True(withdrawn.WasResident);
|
||||
Assert.True(withdrawn.Generation > completed.Generation);
|
||||
Assert.Equal(0, first.Physics.Engine.LandblockCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoteBindingOwnsCanonicalBodyCellAndTypedCellPublication()
|
||||
{
|
||||
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||
RuntimeEntityRecord record =
|
||||
lifetime.Entities.AddActive(Spawn(0x70000021u, 1));
|
||||
var remote = new RemoteMotion();
|
||||
var commits = new List<RuntimePhysicsCellCommit>();
|
||||
lifetime.Physics.CellCommitted += commits.Add;
|
||||
lifetime.Physics.SetRemoteMotion(record, remote);
|
||||
lifetime.Physics.AcknowledgeSpatialProjection(record, spatial: true);
|
||||
|
||||
remote.CellId = 0x02020001u;
|
||||
|
||||
Assert.Same(remote, record.RemoteMotion);
|
||||
Assert.Same(remote.Body, record.PhysicsBody);
|
||||
Assert.Equal(0x02020001u, record.FullCellId);
|
||||
RuntimePhysicsCellCommit commit = Assert.Single(commits);
|
||||
Assert.Same(record, commit.Record);
|
||||
Assert.Equal(0x0101FFFFu, commit.PreviousFullCellId);
|
||||
Assert.Equal(0x02020001u, commit.FullCellId);
|
||||
Assert.True(lifetime.Physics.IsSpatialRemote(record, remote));
|
||||
|
||||
Assert.True(lifetime.Physics.ClearRemoteMotion(record));
|
||||
Assert.Null(record.RemoteMotion);
|
||||
Assert.Equal(0, lifetime.Physics.SpatialRemoteCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemotePhysicsTickUsesCanonicalRuntimeAndPublishesPoseSnapshot()
|
||||
{
|
||||
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||
RuntimeEntityRecord record =
|
||||
lifetime.Entities.AddActive(Spawn(0x70000022u, 1));
|
||||
var remote = new RemoteMotion();
|
||||
remote.Body.Position = new Vector3(10f, 20f, 5f);
|
||||
remote.Body.Orientation = Quaternion.Identity;
|
||||
remote.Body.TransientState = TransientStateFlags.Active
|
||||
| TransientStateFlags.Contact
|
||||
| TransientStateFlags.OnWalkable;
|
||||
lifetime.Physics.SetRemoteMotion(record, remote);
|
||||
lifetime.Physics.AcknowledgeSpatialProjection(record, spatial: true);
|
||||
var updater = new RuntimeRemotePhysicsUpdater(lifetime.Physics);
|
||||
RuntimeRemotePhysicsSnapshot snapshot = default;
|
||||
int publishes = 0;
|
||||
|
||||
Assert.True(updater.Tick(
|
||||
record,
|
||||
remote,
|
||||
objectScale: 1f,
|
||||
sequencer: null,
|
||||
dt: 0.1f,
|
||||
objectClockEpoch: record.ObjectClockEpoch,
|
||||
new MotionDeltaFrame
|
||||
{
|
||||
Origin = Vector3.UnitX,
|
||||
Orientation = Quaternion.Identity,
|
||||
},
|
||||
radius: 0.48f,
|
||||
height: 1.835f,
|
||||
liveCenterX: 1,
|
||||
liveCenterY: 1,
|
||||
acknowledgeProjection: value =>
|
||||
{
|
||||
snapshot = value;
|
||||
publishes++;
|
||||
return true;
|
||||
}));
|
||||
|
||||
Assert.Equal(1, publishes);
|
||||
Assert.True(snapshot.Position.X > 10f);
|
||||
Assert.Equal(remote.Body.Position, snapshot.Position);
|
||||
Assert.Equal(record.FullCellId, snapshot.FullCellId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhysicsBodyAcquisitionIsCanonicalAndRejectsGuidReuse()
|
||||
{
|
||||
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||
RuntimeEntityRecord first =
|
||||
lifetime.Entities.AddActive(Spawn(0x70000023u, 1));
|
||||
var body = new PhysicsBody();
|
||||
|
||||
Assert.Same(
|
||||
body,
|
||||
lifetime.Physics.GetOrCreatePhysicsBody(first, _ => body));
|
||||
Assert.Same(
|
||||
body,
|
||||
lifetime.Physics.GetOrCreatePhysicsBody(
|
||||
first,
|
||||
_ => throw new InvalidOperationException(
|
||||
"The factory must not replay.")));
|
||||
|
||||
RuntimeEntityRecord stale =
|
||||
lifetime.Entities.AddActive(Spawn(0x70000024u, 1));
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
lifetime.Physics.GetOrCreatePhysicsBody(
|
||||
stale,
|
||||
_ =>
|
||||
{
|
||||
Assert.True(lifetime.Entities.RemoveActive(stale));
|
||||
lifetime.Entities.AddActive(Spawn(0x70000024u, 2));
|
||||
return new PhysicsBody();
|
||||
}));
|
||||
Assert.Null(stale.PhysicsBody);
|
||||
Assert.False(stale.PhysicsBodyAcquisitionInProgress);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhysicsHostIdentityAndLookupBelongToExactRuntimeIncarnation()
|
||||
{
|
||||
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||
RuntimeEntityRecord record =
|
||||
lifetime.Entities.AddActive(Spawn(0x70000025u, 1));
|
||||
EntityPhysicsHost initial = Host(record.ServerGuid, 1f);
|
||||
EntityPhysicsHost replacement = Host(record.ServerGuid, 2f);
|
||||
|
||||
Assert.Same(
|
||||
initial,
|
||||
lifetime.Physics.InstallOrRebindPhysicsHost(record, initial));
|
||||
Assert.True(
|
||||
lifetime.Physics.TryGetPhysicsHost(
|
||||
record.ServerGuid,
|
||||
out IPhysicsObjHost resolved));
|
||||
Assert.Same(initial, resolved);
|
||||
Assert.Same(
|
||||
initial,
|
||||
lifetime.Physics.InstallOrRebindPhysicsHost(record, replacement));
|
||||
Assert.Equal(2f, initial.Position.Frame.Origin.X);
|
||||
|
||||
Assert.True(lifetime.Entities.RemoveActive(record));
|
||||
RuntimeEntityRecord next =
|
||||
lifetime.Entities.AddActive(Spawn(record.ServerGuid, 2));
|
||||
Assert.False(
|
||||
lifetime.Physics.TryGetPhysicsHost(
|
||||
record.ServerGuid,
|
||||
out _));
|
||||
Assert.Null(next.PhysicsHost);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OrdinaryCellCommitIsCanonicalBeforePublication()
|
||||
{
|
||||
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||
RuntimeEntityRecord record =
|
||||
lifetime.Entities.AddActive(Spawn(0x70000026u, 1));
|
||||
var body = new PhysicsBody();
|
||||
lifetime.Entities.SetPhysicsBody(record, body);
|
||||
lifetime.Physics.AcknowledgeSpatialProjection(record, spatial: true);
|
||||
RuntimePhysicsCellCommit observed = default;
|
||||
lifetime.Physics.CellCommitted += value =>
|
||||
{
|
||||
Assert.Equal(value.FullCellId, value.Record.FullCellId);
|
||||
observed = value;
|
||||
};
|
||||
|
||||
Assert.True(lifetime.Physics.CommitOrdinaryCell(
|
||||
record,
|
||||
body,
|
||||
record.ObjectClockEpoch,
|
||||
0x02020001u,
|
||||
externalOwnerValid: null));
|
||||
|
||||
Assert.Equal(0x02020001u, record.FullCellId);
|
||||
Assert.Same(record, observed.Record);
|
||||
Assert.Equal(record.SpatialAuthorityVersion, observed.SpatialAuthorityVersion);
|
||||
}
|
||||
|
||||
private static RuntimeLandblockCollisionAssets CollisionAssets(
|
||||
uint landblockId)
|
||||
{
|
||||
var heights = new byte[81];
|
||||
var table = new float[256];
|
||||
return new RuntimeLandblockCollisionAssets(
|
||||
landblockId,
|
||||
new TerrainSurface(heights, table),
|
||||
Array.Empty<CellSurface>(),
|
||||
Array.Empty<PortalPlane>(),
|
||||
0f,
|
||||
0f,
|
||||
0u);
|
||||
}
|
||||
|
||||
private static WorldSession.EntitySpawn Spawn(uint guid, ushort instance)
|
||||
{
|
||||
var position = new CreateObject.ServerPosition(
|
||||
0x0101FFFFu,
|
||||
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: 0x408u,
|
||||
Position: position,
|
||||
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,
|
||||
position,
|
||||
0x02000001u,
|
||||
Array.Empty<CreateObject.AnimPartChange>(),
|
||||
Array.Empty<CreateObject.TextureChange>(),
|
||||
Array.Empty<CreateObject.SubPaletteSwap>(),
|
||||
null,
|
||||
null,
|
||||
"fixture",
|
||||
null,
|
||||
null,
|
||||
0x09000001u,
|
||||
PhysicsState: 0x408u,
|
||||
InstanceSequence: instance,
|
||||
MovementSequence: 1,
|
||||
ServerControlSequence: 1,
|
||||
PositionSequence: 1,
|
||||
Physics: physics);
|
||||
}
|
||||
|
||||
private static EntityPhysicsHost Host(uint guid, float x) =>
|
||||
new(
|
||||
guid,
|
||||
getPosition: () => new Position(
|
||||
0x0101FFFFu,
|
||||
new Vector3(x, 0f, 0f),
|
||||
Quaternion.Identity),
|
||||
getVelocity: () => Vector3.Zero,
|
||||
getRadius: () => 0.48f,
|
||||
inContact: () => true,
|
||||
minterpMaxSpeed: () => null,
|
||||
curTime: () => 0d,
|
||||
physicsTimerTime: () => 0d,
|
||||
getObjectA: _ => null,
|
||||
handleUpdateTarget: _ => { },
|
||||
interruptCurrentMovement: () => { });
|
||||
|
||||
private sealed class TestRemoteMotion : IRuntimeRemoteMotion
|
||||
{
|
||||
public PhysicsBody Body { get; } = new();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue