feat(physics): port retail complete object frame pipeline

Restore the named-retail object update order across local, remote, static, projectile, animation, shadow, teleport, and effect lifetimes. Separate authoritative root commits from spatial rebucketing, preserve per-owner hook/FIFO ordering, and remove update-path allocations with exact lifecycle and residency gates.

Add deterministic conformance, adversarial lifetime, GUID-reuse, pending-cell, quaternion, timestamp, and allocation coverage. Release build is warning-free and all 6,446 tests pass with five intentional skips; retail, architecture, and adversarial reviews are clean.

Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-20 09:10:31 +02:00
parent 31a0889f08
commit f961d70023
77 changed files with 12513 additions and 1871 deletions

View file

@ -77,19 +77,19 @@ public sealed class ProjectileControllerTests
| PhysicsStateFlags.Hidden
| PhysicsStateFlags.IgnoreCollisions;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, record.FinalPhysicsState, 1.1, 1, 1));
record, record.FinalPhysicsState, 1.1, 1, 1));
Vector3 hiddenPosition = entity.Position;
fixture.Controller.Tick(5.0, 1, 1, playerWorldPosition: null);
Assert.Equal(hiddenPosition, entity.Position);
Assert.True(body.InWorld);
Assert.True(body.IsActive);
Assert.Equal(5.0, body.LastUpdateTime, 8);
Assert.Equal(0.0, record.ObjectClock.PendingSeconds, 8);
Assert.Equal(0, fixture.Engine.ShadowObjects.TotalRegistered);
record.FinalPhysicsState = MissileState;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, record.FinalPhysicsState, 5.0, 1, 1));
record, record.FinalPhysicsState, 5.0, 1, 1));
fixture.Controller.Tick(5.1, 1, 1, playerWorldPosition: null);
Assert.True(entity.Position.X > hiddenPosition.X);
@ -116,7 +116,7 @@ public sealed class ProjectileControllerTests
| PhysicsStateFlags.Hidden
| PhysicsStateFlags.IgnoreCollisions;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, record.FinalPhysicsState, 1.1, 1, 1));
record, record.FinalPhysicsState, 1.1, 1, 1));
remote.Interp.Enqueue(
entity.Position + Vector3.UnitX,
heading: 0f,
@ -148,14 +148,27 @@ public sealed class ProjectileControllerTests
var fixture = new Fixture(loadInitialLandblock: true);
LiveEntityRecord record = fixture.Spawn(instance: 1);
WorldEntity entity = record.WorldEntity!;
fixture.Engine.ShadowObjects.Register(
entity.Id,
entity.SourceGfxObjOrSetupId,
entity.Position,
entity.Rotation,
radius: 0.5f,
worldOffsetX: 0f,
worldOffsetY: 0f,
landblockId: 0x01010000u,
state: (uint)MissileState,
seedCellId: CellA,
isStatic: false);
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 2.0));
ILiveEntityProjectileRuntime runtime = record.ProjectileRuntime!;
Assert.True(fixture.Controller.ApplyAuthoritativePosition(
Guid,
worldPosition: new Vector3(202f, 10f, 5f),
record,
worldPosition: new Vector3(10f, 202f, 5f),
cellLocalPosition: new Vector3(10f, 10f, 5f),
orientation: Quaternion.Identity,
velocity: new Vector3(1f, 2f, 3f),
fullCellId: CellB,
currentTime: 2.1,
liveCenterX: 1,
@ -177,24 +190,36 @@ public sealed class ProjectileControllerTests
Assert.False(runtime.Body.IsActive);
fixture.Spatial.AddLandblock(EmptyLandblock(0x0102FFFFu));
Assert.True(record.IsSpatiallyVisible);
Assert.True(runtime.Body.InWorld);
Assert.True(runtime.Body.IsActive);
Assert.Equal(pendingPosition, entity.Position);
Assert.Equal(1, fixture.Engine.ShadowObjects.TotalRegistered);
ShadowEntry restoredShadow = Assert.Single(
fixture.Engine.ShadowObjects.AllEntriesForDebug());
Assert.Equal(entity.Id, restoredShadow.EntityId);
Assert.Equal(pendingPosition, restoredShadow.Position);
fixture.Controller.Tick(
2.5,
liveCenterX: 1,
liveCenterY: 1,
playerWorldPosition: null);
playerWorldPosition: pendingPosition);
Assert.True(record.IsSpatiallyVisible);
Assert.Same(entity, record.WorldEntity);
Assert.Same(runtime, record.ProjectileRuntime);
Assert.Equal(1, fixture.Resources.Registers);
Assert.Equal(pendingPosition, entity.Position);
Vector3 firstReentryTick = entity.Position;
Assert.True(firstReentryTick.X > pendingPosition.X);
fixture.Controller.Tick(
2.6,
liveCenterX: 1,
liveCenterY: 1,
playerWorldPosition: null);
Assert.True(entity.Position.X > pendingPosition.X);
playerWorldPosition: pendingPosition);
Assert.True(entity.Position.X > firstReentryTick.X);
}
[Fact]
@ -229,7 +254,15 @@ public sealed class ProjectileControllerTests
fixture.Spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
Assert.Single(fixture.Live.SpatialProjectileRuntimes);
fixture.Controller.Tick(1.1, 1, 1, playerWorldPosition: null);
Assert.True(record.PhysicsBody.InWorld);
Assert.True(record.PhysicsBody.IsActive);
Assert.Equal(1, fixture.Engine.ShadowObjects.TotalRegistered);
fixture.Controller.Tick(
1.1,
1,
1,
playerWorldPosition: record.WorldEntity!.Position);
Assert.True(record.PhysicsBody.InWorld);
Assert.True(record.PhysicsBody.IsActive);
@ -304,9 +337,10 @@ public sealed class ProjectileControllerTests
playerWorldPosition: null);
Assert.Equal(0xAAB40001u, record.FullCellId);
Assert.Equal(0xAAB40001u, record.PhysicsBody!.CellPosition.ObjCellId);
Assert.False(record.IsSpatiallyVisible);
Assert.True(record.IsSpatiallyProjected);
Assert.False(record.PhysicsBody!.InWorld);
Assert.False(record.PhysicsBody.InWorld);
Assert.False(record.PhysicsBody.IsActive);
Assert.Empty(fixture.Engine.ShadowObjects.GetObjectsInCell(startCell));
Assert.Empty(fixture.Engine.ShadowObjects.GetObjectsInCell(0xAAB40001u));
@ -315,13 +349,14 @@ public sealed class ProjectileControllerTests
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, record.FinalPhysicsState, 0.06, 0xA9, 0xB4));
record, record.FinalPhysicsState, 0.06, 0xA9, 0xB4));
fixture.Spatial.AddLandblock(EmptyLandblock(0xAAB4FFFFu));
fixture.Controller.Tick(0.07, 0xA9, 0xB4, playerWorldPosition: null);
Assert.True(record.IsSpatiallyVisible);
Assert.Equal(0xAAB40001u, record.PhysicsBody.CellPosition.ObjCellId);
Assert.True(record.PhysicsBody.InWorld);
Assert.False(record.PhysicsBody.IsActive);
Assert.True(record.PhysicsBody.IsActive);
Assert.Contains(
fixture.Engine.ShadowObjects.GetObjectsInCell(0xAAB40001u),
entry => entry.EntityId == entity.Id);
@ -336,7 +371,7 @@ public sealed class ProjectileControllerTests
PhysicsBody body = record.PhysicsBody!;
Assert.True(fixture.Controller.ApplyAuthoritativeVector(
Guid,
record,
new Vector3(7f, 8f, 9f),
new Vector3(0f, 0f, 2f),
currentTime: 4.5));
@ -344,11 +379,13 @@ public sealed class ProjectileControllerTests
Assert.Equal(new Vector3(0f, 0f, 2f), body.Omega);
var correction = new Vector3(30f, 31f, 32f);
var correctedVelocity = new Vector3(3f, 4f, 5f);
Assert.True(fixture.Controller.ApplyAuthoritativePosition(
Guid,
record,
correction,
new Vector3(30f, 31f, 32f),
Quaternion.Identity,
correctedVelocity,
CellA,
currentTime: 5.0,
liveCenterX: 1,
@ -357,7 +394,95 @@ public sealed class ProjectileControllerTests
Assert.Same(body, record.PhysicsBody);
Assert.Equal(correction, body.Position);
Assert.Equal(correction, record.WorldEntity!.Position);
Assert.Equal(new Vector3(7f, 8f, 9f), body.Velocity);
Assert.Equal(correctedVelocity, body.Velocity);
// InboundPhysicsStateController normalizes an absent PositionPack
// velocity to zero before this contract is called.
Assert.True(fixture.Controller.ApplyAuthoritativePosition(
record,
correction,
new Vector3(30f, 31f, 32f),
Quaternion.Identity,
Vector3.Zero,
CellA,
currentTime: 5.1,
liveCenterX: 1,
liveCenterY: 1));
Assert.Equal(Vector3.Zero, body.Velocity);
}
[Theory]
[InlineData(AuthoritativeMutation.Vector)]
[InlineData(AuthoritativeMutation.Position)]
[InlineData(AuthoritativeMutation.State)]
public void AuthoritativeMutationBetweenQuantumHalvesDiscardsPrediction(
AuthoritativeMutation mutation)
{
var fixture = new Fixture();
LiveEntityRecord record = fixture.Spawn(instance: 1);
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0, 1, 1));
PhysicsBody body = record.PhysicsBody!;
Assert.True(fixture.Controller.TryBeginQuantum(
record,
quantum: 0.05f,
out ProjectileController.QuantumStep step));
Vector3 correctedPosition = new(30f, 31f, 32f);
Vector3 correctedVelocity = new(7f, 8f, 9f);
Vector3 correctedOmega = new(0f, 0f, 2f);
PhysicsStateFlags correctedState = MissileState | PhysicsStateFlags.Gravity;
switch (mutation)
{
case AuthoritativeMutation.Vector:
Assert.True(fixture.Controller.ApplyAuthoritativeVector(
record,
correctedVelocity,
correctedOmega,
currentTime: 1.02));
break;
case AuthoritativeMutation.Position:
Assert.True(fixture.Controller.ApplyAuthoritativePosition(
record,
correctedPosition,
correctedPosition,
Quaternion.Identity,
correctedVelocity,
CellA,
currentTime: 1.02,
liveCenterX: 1,
liveCenterY: 1));
break;
case AuthoritativeMutation.State:
record.FinalPhysicsState = correctedState;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
record,
correctedState,
currentTime: 1.02,
liveCenterX: 1,
liveCenterY: 1));
break;
default:
throw new ArgumentOutOfRangeException(nameof(mutation));
}
Assert.False(fixture.Controller.CompleteQuantum(step, 1, 1));
switch (mutation)
{
case AuthoritativeMutation.Vector:
Assert.Equal(correctedVelocity, body.Velocity);
Assert.Equal(correctedOmega, body.Omega);
break;
case AuthoritativeMutation.Position:
Assert.Equal(correctedPosition, body.Position);
Assert.Equal(correctedVelocity, body.Velocity);
break;
case AuthoritativeMutation.State:
Assert.Equal(correctedState, body.State);
break;
}
}
[Fact]
@ -392,7 +517,7 @@ public sealed class ProjectileControllerTests
PhysicsBody body = runtime.Body;
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid,
record,
PhysicsStateFlags.ReportCollisions,
currentTime: 1.1,
liveCenterX: 1,
@ -411,7 +536,7 @@ public sealed class ProjectileControllerTests
record.FinalPhysicsState = MissileState;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid,
record,
MissileState,
currentTime: 1.3,
liveCenterX: 1,
@ -452,7 +577,7 @@ public sealed class ProjectileControllerTests
StateSequence: 2),
out _));
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, record.FinalPhysicsState, 0.06, 0xA9, 0xB4));
record, record.FinalPhysicsState, 0.06, 0xA9, 0xB4));
Assert.Equal(destinationCell, record.FullCellId);
Assert.True(fixture.Live.TryApplyState(
new SetState.Parsed(
@ -462,7 +587,7 @@ public sealed class ProjectileControllerTests
StateSequence: 3),
out _));
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, MissileState, 0.07, 0xA9, 0xB4));
record, MissileState, 0.07, 0xA9, 0xB4));
Assert.Same(runtime, record.ProjectileRuntime);
Assert.Same(runtime.Body, record.PhysicsBody);
@ -481,7 +606,7 @@ public sealed class ProjectileControllerTests
body.LastUpdateTime = 1.0;
Assert.True(fixture.Controller.ApplyAuthoritativeVector(
Guid,
record,
new Vector3(100f, 0f, 0f),
Vector3.Zero,
currentTime: 20.0));
@ -506,7 +631,7 @@ public sealed class ProjectileControllerTests
body.TransientState &= ~TransientStateFlags.Active;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid,
record,
MissileState | PhysicsStateFlags.Inelastic,
currentTime: 2.0,
liveCenterX: 1,
@ -516,11 +641,11 @@ public sealed class ProjectileControllerTests
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, record.FinalPhysicsState, 2.1, 1, 1));
record, record.FinalPhysicsState, 2.1, 1, 1));
Assert.False(body.IsActive);
record.FinalPhysicsState = MissileState;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, MissileState, 2.2, 1, 1));
record, MissileState, 2.2, 1, 1));
Assert.False(body.IsActive);
}
@ -533,10 +658,10 @@ public sealed class ProjectileControllerTests
PhysicsBody body = record.PhysicsBody!;
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, record.FinalPhysicsState, 1.01, 1, 1));
record, record.FinalPhysicsState, 1.01, 1, 1));
Assert.True(fixture.Controller.ApplyAuthoritativeVector(
Guid,
record,
new Vector3(100f, 0f, 0f),
new Vector3(0f, 0f, 3f),
1.02));
@ -561,11 +686,11 @@ public sealed class ProjectileControllerTests
fixture.Live.SetRemoteMotionRuntime(Guid, new GameWindow.RemoteMotion(body));
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, record.FinalPhysicsState, 2.0, 1, 1));
record, record.FinalPhysicsState, 2.0, 1, 1));
Vector3 before = body.Velocity;
Assert.False(fixture.Controller.ApplyAuthoritativeVector(
Guid,
record,
new Vector3(0f, 0f, 12f),
new Vector3(0f, 0f, 2f),
3.0));
@ -586,14 +711,14 @@ public sealed class ProjectileControllerTests
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, record.FinalPhysicsState, 2.0, 1, 1));
record, record.FinalPhysicsState, 2.0, 1, 1));
body.TransientState |= TransientStateFlags.Active;
body.LastUpdateTime = 9.5; // generic remote interval used another clock owner
body.set_velocity(new Vector3(40f, 0f, 0f));
record.FinalPhysicsState = MissileState;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, MissileState, 10.0, 1, 1));
record, MissileState, 10.0, 1, 1));
Assert.Equal(10.0, body.LastUpdateTime, 8);
Assert.True(body.IsActive);
@ -611,13 +736,13 @@ public sealed class ProjectileControllerTests
PhysicsBody body = record.PhysicsBody!;
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, record.FinalPhysicsState, 2.0, 1, 1));
record, record.FinalPhysicsState, 2.0, 1, 1));
PhysicsStateFlags stateBefore = body.State;
double clockBefore = body.LastUpdateTime;
record.FinalPhysicsState = MissileState;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, MissileState, double.NaN, 1, 1));
record, MissileState, double.NaN, 1, 1));
Assert.Equal(record.FinalPhysicsState, body.State);
Assert.NotEqual(stateBefore, body.State);
@ -647,6 +772,41 @@ public sealed class ProjectileControllerTests
Assert.Equal(entity.Position, after.Translation);
}
[Fact]
public void RootPoseCallbackGuidReuse_MakesQuantumReportSuperseded()
{
Fixture? fixture = null;
LiveEntityRecord? first = null;
LiveEntityRecord? replacement = null;
bool replaced = false;
fixture = new Fixture(
publishRootPose: _ =>
{
if (replaced)
return;
replaced = true;
Assert.NotNull(first);
Assert.True(fixture!.Live.UnregisterLiveEntity(
new DeleteObject.Parsed(Guid, first!.Generation),
isLocalPlayer: false));
replacement = fixture.Spawn(instance: 2);
});
first = fixture.Spawn(instance: 1);
Assert.True(fixture.Controller.TryBind(first, ProjectileSetup(), 1.0, 1, 1));
Assert.False(fixture.Controller.AdvanceQuantum(
first,
quantum: 0.05f,
liveCenterX: 1,
liveCenterY: 1));
Assert.True(replaced);
Assert.NotNull(replacement);
Assert.True(fixture.Live.TryGetRecord(Guid, out LiveEntityRecord current));
Assert.Same(replacement, current);
Assert.Null(current.ProjectileRuntime);
}
[Fact]
public void Tick_PartArrayUsesRetailNinetySixUnitActivationBoundary()
{
@ -699,17 +859,18 @@ public sealed class ProjectileControllerTests
Vector3 position = body.Position;
Assert.True(fixture.Controller.ApplyAuthoritativeVector(
Guid,
record,
new Vector3(float.NaN, 0f, 0f),
Vector3.Zero,
currentTime: 2.0));
Assert.Equal(velocity, body.Velocity);
Assert.True(fixture.Controller.ApplyAuthoritativePosition(
Guid,
record,
new Vector3(float.PositiveInfinity, 0f, 0f),
new Vector3(float.PositiveInfinity, 0f, 0f),
Quaternion.Identity,
Vector3.Zero,
CellA,
currentTime: 2.1,
liveCenterX: 1,
@ -772,7 +933,7 @@ public sealed class ProjectileControllerTests
record.FinalPhysicsState = MissileState;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, MissileState, 2.0, 1, 1));
record, MissileState, 2.0, 1, 1));
Assert.NotNull(record.ProjectileRuntime);
Assert.True(float.IsFinite(record.PhysicsBody!.Velocity.X));
Assert.True(float.IsFinite(record.PhysicsBody.Velocity.Y));
@ -793,7 +954,7 @@ public sealed class ProjectileControllerTests
PhysicsStateFlags.Gravity | PhysicsStateFlags.IgnoreCollisions;
record.FinalPhysicsState = ordinaryState;
Assert.False(fixture.Controller.ApplyAuthoritativeState(
Guid, ordinaryState, 1.0, 1, 1));
record, ordinaryState, 1.0, 1, 1));
Assert.Equal(ordinaryState, remote.Body.State);
Assert.Null(record.ProjectileRuntime);
@ -803,7 +964,7 @@ public sealed class ProjectileControllerTests
ordinaryState | PhysicsStateFlags.Missile;
record.FinalPhysicsState = unsupportedMissile;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, unsupportedMissile, 1.1, 1, 1));
record, unsupportedMissile, 1.1, 1, 1));
Assert.Equal(unsupportedMissile, remote.Body.State);
Assert.Null(record.ProjectileRuntime);
@ -819,7 +980,7 @@ public sealed class ProjectileControllerTests
record.FinalPhysicsState = MissileState;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, MissileState, double.NaN, 1, 1));
record, MissileState, double.NaN, 1, 1));
Assert.NotNull(record.ProjectileRuntime);
Assert.Equal(MissileState, record.PhysicsBody!.State);
@ -827,7 +988,7 @@ public sealed class ProjectileControllerTests
ILiveEntityProjectileRuntime runtime = record.ProjectileRuntime!;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, MissileState, 0.1, 1, 1));
record, MissileState, 0.1, 1, 1));
Assert.Same(runtime, record.ProjectileRuntime);
}
@ -843,7 +1004,7 @@ public sealed class ProjectileControllerTests
record.FinalPhysicsState = MissileState;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, MissileState, double.PositiveInfinity, 1, 1));
record, MissileState, double.PositiveInfinity, 1, 1));
Assert.NotNull(record.ProjectileRuntime);
Assert.Same(remote.Body, record.ProjectileRuntime!.Body);
@ -922,7 +1083,7 @@ public sealed class ProjectileControllerTests
record.FinalPhysicsState = MissileState;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid,
record,
MissileState,
currentTime: 1.2,
liveCenterX: 0xA9,
@ -941,7 +1102,7 @@ public sealed class ProjectileControllerTests
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, record.FinalPhysicsState, 1.5, 0xA9, 0xB4));
record, record.FinalPhysicsState, 1.5, 0xA9, 0xB4));
Assert.Equal(destinationCell, remote.CellId);
Assert.Equal(destinationCell, record.FullCellId);
@ -960,7 +1121,7 @@ public sealed class ProjectileControllerTests
record.FinalPhysicsState = MissileState;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, MissileState, 1.7, 0xA9, 0xB4));
record, MissileState, 1.7, 0xA9, 0xB4));
Assert.Equal(startCell, record.FullCellId);
Assert.Equal(startCell, remote.CellId);
Assert.Equal(startCell, body.CellPosition.ObjCellId);
@ -986,7 +1147,7 @@ public sealed class ProjectileControllerTests
record.FinalPhysicsState = MissileState;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, MissileState, 1.0, 1, 1));
record, MissileState, 1.0, 1, 1));
Assert.NotNull(record.ProjectileRuntime);
Assert.Same(remote.Body, record.ProjectileRuntime!.Body);
@ -1010,7 +1171,7 @@ public sealed class ProjectileControllerTests
record.FinalPhysicsState = MissileState;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
Guid, MissileState, 1.0, 1, 1));
record, MissileState, 1.0, 1, 1));
Assert.Null(record.ProjectileRuntime);
Assert.Same(remote.Body, record.PhysicsBody);
@ -1113,15 +1274,22 @@ public sealed class ProjectileControllerTests
_ => throw new InvalidOperationException("re-entry recreated the entity")));
Assert.True(fixture.Controller.ApplyAuthoritativePosition(
Guid,
record,
worldPosition: new Vector3(12f, 10f, 5f),
cellLocalPosition: new Vector3(12f, 10f, 5f),
orientation: Quaternion.Identity,
velocity: Vector3.Zero,
fullCellId: CellA,
currentTime: 10.0,
liveCenterX: 1,
liveCenterY: 1));
Assert.Equal(10.0, record.PhysicsBody!.LastUpdateTime, 8);
// The incarnation-stable RetailObjectQuantumClock is canonical after
// the R6 cutover; PhysicsBody.LastUpdateTime is only a legacy absolute
// clock mirror and need not equal the packet receipt time once the
// visibility edge has already performed prepare_to_enter_world.
Assert.Equal(0d, record.ObjectClock.PendingSeconds, 8);
Assert.True(record.ObjectClock.IsActive);
Assert.True(record.PhysicsBody!.IsActive);
Assert.Contains(
fixture.Engine.ShadowObjects.GetObjectsInCell(CellA),
entry => entry.EntityId == entity.Id);
@ -1130,8 +1298,14 @@ public sealed class ProjectileControllerTests
10.1,
liveCenterX: 1,
liveCenterY: 1,
playerWorldPosition: null);
Assert.Equal(13f, entity.Position.X, 3);
playerWorldPosition: entity.Position);
Assert.Equal(12f, entity.Position.X, 3);
fixture.Controller.Tick(
10.2,
liveCenterX: 1,
liveCenterY: 1,
playerWorldPosition: entity.Position);
Assert.Equal(12f, entity.Position.X, 3);
}
[Fact]
@ -1148,14 +1322,94 @@ public sealed class ProjectileControllerTests
isLocalPlayer: false));
LiveEntityRecord second = fixture.Spawn(instance: 8);
Assert.True(fixture.Controller.TryBind(second, ProjectileSetup(), 2.0));
ILiveEntityProjectileRuntime secondRuntime = second.ProjectileRuntime!;
Assert.False(fixture.Controller.TryBind(first, ProjectileSetup(), 3.0));
Assert.NotSame(firstEntity, second.WorldEntity);
Assert.NotSame(firstRuntime, second.ProjectileRuntime);
Assert.Same(secondRuntime, second.ProjectileRuntime);
Assert.Equal(1, fixture.Controller.Count);
Assert.Equal(2, fixture.Resources.Registers);
Assert.Equal(1, fixture.Resources.Unregisters);
}
[Fact]
public void TryBind_ReentrantVisibilityGuidReuseNeverBindsOldBodyToReplacement()
{
var fixture = new Fixture();
LiveEntityRecord first = fixture.Spawn(
instance: 7,
cellId: CellA,
worldPosition: new Vector3(10f, 202f, 5f),
cellLocalPosition: new Vector3(10f, 202f, 5f));
LiveEntityRecord? replacement = null;
bool replaced = false;
fixture.Live.ProjectionVisibilityChanged += (record, visible) =>
{
if (replaced || visible || !ReferenceEquals(record, first))
return;
replaced = true;
Assert.True(fixture.Live.UnregisterLiveEntity(
new DeleteObject.Parsed(Guid, InstanceSequence: 7),
isLocalPlayer: false));
replacement = fixture.Spawn(instance: 8);
};
Assert.False(fixture.Controller.TryBind(
first,
ProjectileSetup(),
currentTime: 1.0,
liveCenterX: 1,
liveCenterY: 1));
Assert.NotNull(replacement);
Assert.True(fixture.Live.TryGetRecord(Guid, out var current));
Assert.Same(replacement, current);
Assert.Null(current.ProjectileRuntime);
Assert.Null(current.PhysicsBody);
Assert.Equal(0, fixture.Controller.Count);
}
[Fact]
public void AuthoritativePosition_ReentrantGuidReuseStopsOldPostRebucketWork()
{
var fixture = new Fixture();
LiveEntityRecord first = fixture.Spawn(instance: 7);
Assert.True(fixture.Controller.TryBind(first, ProjectileSetup(), 1.0, 1, 1));
LiveEntityRecord? replacement = null;
bool replaced = false;
fixture.Live.ProjectionVisibilityChanged += (record, visible) =>
{
if (replaced || visible || !ReferenceEquals(record, first))
return;
replaced = true;
Assert.True(fixture.Live.UnregisterLiveEntity(
new DeleteObject.Parsed(Guid, InstanceSequence: 7),
isLocalPlayer: false));
replacement = fixture.Spawn(instance: 8);
};
Assert.True(fixture.Controller.ApplyAuthoritativePosition(
first,
worldPosition: new Vector3(10f, 202f, 5f),
cellLocalPosition: new Vector3(10f, 10f, 5f),
orientation: Quaternion.Identity,
velocity: new Vector3(44f, 0f, 0f),
fullCellId: CellB,
currentTime: 1.1,
liveCenterX: 1,
liveCenterY: 1));
Assert.NotNull(replacement);
Assert.True(fixture.Live.TryGetRecord(Guid, out var current));
Assert.Same(replacement, current);
Assert.Null(current.ProjectileRuntime);
Assert.Null(current.PhysicsBody);
Assert.Equal(new Vector3(10f, 10f, 5f), current.WorldEntity!.Position);
Assert.Equal(0, fixture.Controller.Count);
}
[Fact]
public void MissingRetailSphere_FailsWithoutFabricatingCollisionBody()
{
@ -1175,7 +1429,8 @@ public sealed class ProjectileControllerTests
{
internal Fixture(
bool loadInitialLandblock = true,
PhysicsEngine? physicsEngine = null)
PhysicsEngine? physicsEngine = null,
Action<WorldEntity>? publishRootPose = null)
{
if (loadInitialLandblock)
Spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
@ -1185,7 +1440,8 @@ public sealed class ProjectileControllerTests
Live,
Engine,
_ => ResolvedSetup,
entity => Poses.UpdateRoot(entity));
publishRootPose ?? (entity => Poses.UpdateRoot(entity)),
() => LiveCenter);
}
internal GpuWorldState Spatial { get; } = new();
@ -1194,6 +1450,7 @@ public sealed class ProjectileControllerTests
internal PhysicsEngine Engine { get; }
internal EntityEffectPoseRegistry Poses { get; } = new();
internal Setup ResolvedSetup { get; set; } = ProjectileSetup();
internal (int X, int Y) LiveCenter { get; set; } = (1, 1);
internal ProjectileController Controller { get; }
internal LiveEntityRecord Spawn(
@ -1221,6 +1478,7 @@ public sealed class ProjectileControllerTests
elasticity);
LiveEntityRegistrationResult registration = Live.RegisterLiveEntity(spawn);
Assert.NotNull(registration.Record);
registration.Record!.HasPartArray = true;
WorldEntity? entity = Live.MaterializeLiveEntity(
Guid,
cellId,
@ -1242,6 +1500,13 @@ public sealed class ProjectileControllerTests
}
}
public enum AuthoritativeMutation
{
Vector,
Position,
State,
}
private static Setup ProjectileSetup(float radius = 0.102f) => new()
{
Spheres =