refactor(runtime): own projectile simulation

Move projectile component identity, prediction invalidation, spatial worksets, authoritative corrections, and the retail physics step into AcDream.Runtime. Keep App as the DAT-shape and presentation adapter so ACE outcomes and visible behavior remain unchanged.
This commit is contained in:
Erik 2026-07-26 14:17:42 +02:00
parent fce8e7b18e
commit 2aee33569f
19 changed files with 1255 additions and 335 deletions

View file

@ -86,6 +86,201 @@ public sealed class RuntimePhysicsStateTests
Assert.Equal(0, lifetime.Physics.SpatialRootCount);
}
[Fact]
public void CanonicalRecordAndPhysicsOwnerOwnProjectileComponentAndWorkset()
{
using var lifetime = new RuntimeEntityObjectLifetime();
RuntimeEntityRecord record =
lifetime.Entities.AddActive(Spawn(0x70000031u, 1));
lifetime.Entities.SetFullCell(record, 0x01010001u, 0x0101FFFFu);
lifetime.Entities.SetFinalPhysicsState(
record,
PhysicsStateFlags.ReportCollisions
| PhysicsStateFlags.Missile
| PhysicsStateFlags.AlignPath
| PhysicsStateFlags.PathClipped);
var body = new PhysicsBody
{
Position = new Vector3(10f, 20f, 5f),
Orientation = Quaternion.Identity,
InWorld = true,
TransientState = TransientStateFlags.Active,
};
body.SnapToCell(
record.FullCellId,
body.Position,
body.Position);
lifetime.Entities.SetPhysicsBody(record, body);
var sphere = new ProjectileCollisionSphere(
new Vector3(0.1f, 0f, 0f),
0.25f);
IRuntimeProjectile projectile = lifetime.Physics.BindProjectile(
record,
body,
sphere);
lifetime.Physics.AcknowledgeSpatialProjection(
record,
spatial: true);
Assert.Same(projectile, record.Projectile);
Assert.Same(body, projectile.Body);
Assert.Equal(sphere, projectile.CollisionSphere);
Assert.True(
lifetime.Physics.IsSpatialProjectile(record, projectile));
Assert.Equal(1, lifetime.Physics.SpatialProjectileCount);
Assert.Equal(
1,
lifetime.Physics.CaptureOwnership().SpatialProjectileCount);
Assert.Same(
projectile,
lifetime.Physics.BindProjectile(record, body, sphere));
Assert.Throws<InvalidOperationException>(() =>
lifetime.Physics.BindProjectile(
record,
new PhysicsBody(),
sphere));
lifetime.Physics.RemoveSpatialProjection(record);
Assert.Equal(0, lifetime.Physics.SpatialProjectileCount);
Assert.Same(projectile, record.Projectile);
Assert.True(lifetime.Physics.ClearProjectile(record));
Assert.Null(record.Projectile);
}
[Fact]
public void ProjectileAuthoritativeMutationInvalidatesSplitPrediction()
{
using var lifetime = new RuntimeEntityObjectLifetime();
RuntimeEntityRecord record =
lifetime.Entities.AddActive(Spawn(0x70000032u, 1));
lifetime.Entities.SetFullCell(record, 0x01010001u, 0x0101FFFFu);
lifetime.Entities.SetFinalPhysicsState(
record,
PhysicsStateFlags.ReportCollisions
| PhysicsStateFlags.Missile
| PhysicsStateFlags.AlignPath
| PhysicsStateFlags.PathClipped);
var body = new PhysicsBody
{
Position = new Vector3(10f, 20f, 5f),
Orientation = Quaternion.Identity,
InWorld = true,
TransientState = TransientStateFlags.Active,
};
body.set_velocity(new Vector3(10f, 0f, 0f));
body.SnapToCell(
record.FullCellId,
body.Position,
body.Position);
lifetime.Entities.SetPhysicsBody(record, body);
IRuntimeProjectile projectile = lifetime.Physics.BindProjectile(
record,
body,
new ProjectileCollisionSphere(Vector3.Zero, 0.25f));
lifetime.Physics.AcknowledgeSpatialProjection(
record,
spatial: true);
var updater =
new RuntimeProjectilePhysicsUpdater(lifetime.Physics);
Assert.True(updater.TryBegin(
record,
quantum: 0.05f,
record.ObjectClockEpoch,
externalOwnerValid: null,
out RuntimeProjectilePhysicsCommit commit));
lifetime.Entities.AdvanceVectorAuthority(record);
Vector3 correction = new(7f, 8f, 9f);
Assert.True(updater.ApplyAuthoritativeVector(
record,
record.VectorAuthorityVersion,
record.VelocityAuthorityVersion,
correction,
Vector3.UnitZ,
currentTime: 1.0));
Assert.True(
projectile.PredictionAuthorityVersion
> commit.PredictionAuthorityVersion);
Assert.False(updater.Complete(
commit,
liveCenterX: 1,
liveCenterY: 1,
_ => true));
Assert.Equal(correction, body.Velocity);
Assert.Equal(Vector3.UnitZ, body.Omega);
}
[Fact]
public void ProjectileQuantumPublishesOneImmutableRuntimeFrame()
{
using var lifetime = new RuntimeEntityObjectLifetime();
RuntimeEntityRecord record =
lifetime.Entities.AddActive(Spawn(0x70000033u, 1));
lifetime.Entities.SetFullCell(record, 0x01010001u, 0x0101FFFFu);
lifetime.Entities.SetFinalPhysicsState(
record,
PhysicsStateFlags.ReportCollisions
| PhysicsStateFlags.Missile
| PhysicsStateFlags.PathClipped);
var body = new PhysicsBody
{
Position = new Vector3(10f, 20f, 5f),
Orientation = Quaternion.Identity,
InWorld = true,
TransientState = TransientStateFlags.Active,
};
body.SnapToCell(
record.FullCellId,
body.Position,
body.Position);
lifetime.Entities.SetPhysicsBody(record, body);
lifetime.Physics.BindProjectile(
record,
body,
new ProjectileCollisionSphere(Vector3.Zero, 0.25f));
lifetime.Physics.AcknowledgeSpatialProjection(
record,
spatial: true);
var updater =
new RuntimeProjectilePhysicsUpdater(lifetime.Physics);
Assert.True(updater.TryBegin(
record,
quantum: 0.05f,
record.ObjectClockEpoch,
externalOwnerValid: null,
out RuntimeProjectilePhysicsCommit commit));
int acknowledgements = 0;
RuntimePhysicsFrameSnapshot published = default;
Assert.True(updater.Complete(
commit,
liveCenterX: 1,
liveCenterY: 1,
snapshot =>
{
published = snapshot;
acknowledgements++;
return true;
}));
Assert.Equal(1, acknowledgements);
Assert.Equal(body.Position, published.Position);
Assert.Equal(body.Orientation, published.Orientation);
Assert.Equal(record.FullCellId, published.FullCellId);
Assert.Throws<InvalidOperationException>(() =>
updater.Complete(
commit,
liveCenterX: 1,
liveCenterY: 1,
_ => true));
}
[Fact]
public void EqualLocalKeysInConcurrentRuntimesDoNotShareWorksets()
{