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

@ -1,5 +1,7 @@
using System.Numerics;
using AcDream.App.Streaming;
using AcDream.App.World;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Wb;
using AcDream.App.Rendering.Vfx;
using AcDream.Core.Net;
@ -189,6 +191,195 @@ public sealed class LiveEntityRuntimeTests
Assert.Equal(new[] { false, true }, visibilityEdges);
}
[Fact]
public void StaticRootCommit_HiddenObjectUpdatesPoseWithoutRestoringShadow()
{
const uint guid = 0x70000071u;
var spatial = new GpuWorldState();
spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
var runtime = new LiveEntityRuntime(spatial, new RecordingResources());
WorldSession.EntitySpawn spawn = Spawn(
guid,
instance: 1,
positionSequence: 1,
cell: 0x01010001u,
state: PhysicsStateFlags.Static | PhysicsStateFlags.ReportCollisions);
runtime.RegisterLiveEntity(spawn);
WorldEntity entity = runtime.MaterializeLiveEntity(
guid,
0x01010001u,
id => Entity(id, guid))!;
entity.Position = new Vector3(12f, 12f, 5f);
PhysicsBody body = runtime.GetOrCreatePhysicsBody(
guid,
_ =>
{
var created = new PhysicsBody();
created.SnapToCell(
0x01010001u,
entity.Position,
new Vector3(12f, 12f, 5f));
return created;
});
var shadows = new ShadowObjectRegistry();
shadows.RegisterMultiPart(
entity.Id,
entity.Position,
entity.Rotation,
new[]
{
new ShadowShape(
0x01000001u,
Vector3.UnitX,
Quaternion.Identity,
Scale: 1f,
CollisionType: ShadowCollisionType.Cylinder,
Radius: 0.5f,
CylHeight: 1f),
},
state: (uint)PhysicsStateFlags.ReportCollisions,
flags: EntityCollisionFlags.None,
worldOffsetX: 0f,
worldOffsetY: 0f,
landblockId: 0x01010000u,
seedCellId: 0x01010001u);
int poseUpdates = 0;
var committer = new StaticLiveRootCommitter(
() => runtime,
shadows,
() => (1, 1),
_ => poseUpdates++);
body.SetFrameInCurrentCell(
body.Position,
Quaternion.CreateFromAxisAngle(Vector3.UnitZ, MathF.PI / 2f));
entity.Rotation = body.Orientation;
Assert.True(committer.Commit(entity, body));
ShadowEntry visible = Assert.Single(shadows.AllEntriesForDebug());
Assert.InRange(visible.Position.X, 11.999f, 12.001f);
Assert.InRange(visible.Position.Y, 12.999f, 13.001f);
Assert.True(runtime.TryApplyState(new SetState.Parsed(
guid,
(uint)(PhysicsStateFlags.Static
| PhysicsStateFlags.Hidden
| PhysicsStateFlags.IgnoreCollisions),
InstanceSequence: 1,
StateSequence: 2), out _));
Assert.True(shadows.Suspend(entity.Id));
body.SetFrameInCurrentCell(
body.Position,
Quaternion.CreateFromAxisAngle(Vector3.UnitZ, MathF.PI));
entity.Rotation = body.Orientation;
Assert.True(committer.Commit(entity, body));
Assert.Equal(2, poseUpdates);
Assert.Empty(shadows.AllEntriesForDebug());
Assert.Equal(1, shadows.SuspendedRegistrationCount);
}
[Fact]
public void StaticRootCommit_BeforeLiveRuntimeCompositionIsSafeNoOp()
{
LiveEntityRuntime? runtime = null;
int poseUpdates = 0;
var committer = new StaticLiveRootCommitter(
() => runtime,
new ShadowObjectRegistry(),
() => (0, 0),
_ => poseUpdates++);
Assert.False(committer.Commit(
Entity(0x7F000001u, 0x70000001u),
new PhysicsBody()));
Assert.Equal(0, poseUpdates);
}
[Fact]
public void StaticRootCommit_EffectCallbackReplacesGuid_DoesNotRestoreOldShadow()
{
const uint guid = 0x70000074u;
var spatial = new GpuWorldState();
spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
var runtime = new LiveEntityRuntime(spatial, new RecordingResources());
runtime.RegisterLiveEntity(Spawn(
guid,
instance: 1,
positionSequence: 1,
cell: 0x01010001u,
state: PhysicsStateFlags.Static | PhysicsStateFlags.ReportCollisions));
WorldEntity oldEntity = runtime.MaterializeLiveEntity(
guid,
0x01010001u,
id => Entity(id, guid))!;
oldEntity.Position = new Vector3(12f, 12f, 5f);
PhysicsBody oldBody = runtime.GetOrCreatePhysicsBody(
guid,
_ =>
{
var created = new PhysicsBody();
created.SnapToCell(
0x01010001u,
oldEntity.Position,
oldEntity.Position);
return created;
});
var shadows = new ShadowObjectRegistry();
shadows.RegisterMultiPart(
oldEntity.Id,
oldEntity.Position,
oldEntity.Rotation,
new[]
{
new ShadowShape(
0x01000001u,
Vector3.UnitX,
Quaternion.Identity,
Scale: 1f,
CollisionType: ShadowCollisionType.Cylinder,
Radius: 0.5f,
CylHeight: 1f),
},
state: (uint)PhysicsStateFlags.ReportCollisions,
flags: EntityCollisionFlags.None,
worldOffsetX: 0f,
worldOffsetY: 0f,
landblockId: 0x01010000u,
seedCellId: 0x01010001u);
var committer = new StaticLiveRootCommitter(
() => runtime,
shadows,
() => (1, 1),
_ =>
{
Assert.True(shadows.Suspend(oldEntity.Id));
Assert.True(runtime.UnregisterLiveEntity(
new DeleteObject.Parsed(guid, InstanceSequence: 1),
isLocalPlayer: false));
runtime.RegisterLiveEntity(Spawn(
guid,
instance: 2,
positionSequence: 1,
cell: 0x01010001u));
runtime.MaterializeLiveEntity(
guid,
0x01010001u,
id => Entity(id, guid));
});
oldBody.SetFrameInCurrentCell(
oldBody.Position,
Quaternion.CreateFromAxisAngle(Vector3.UnitZ, MathF.PI / 2f));
oldEntity.Rotation = oldBody.Orientation;
Assert.False(committer.Commit(oldEntity, oldBody));
Assert.Empty(shadows.AllEntriesForDebug());
Assert.Equal(1, shadows.SuspendedRegistrationCount);
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord replacement));
Assert.Equal((ushort)2, replacement.Generation);
Assert.NotSame(oldEntity, replacement.WorldEntity);
}
[Fact]
public void SpatialComponentWorksets_FollowLoadedProjectionWhileLogicalOwnersSurvive()
{
@ -313,6 +504,41 @@ public sealed class LiveEntityRuntimeTests
Assert.Equal(2, runtime.AnimationRuntimes.Count);
}
[Fact]
public void AnimationView_HotSpatialTraversalDoesNotAllocateAfterWarmup()
{
const uint guid = 0x70000075u;
var spatial = new GpuWorldState();
spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
var runtime = new LiveEntityRuntime(spatial, new RecordingResources());
runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u));
WorldEntity entity = runtime.MaterializeLiveEntity(
guid,
0x01010001u,
id => Entity(id, guid))!;
runtime.SetAnimationRuntime(guid, new AnimationRuntime(entity));
var view = new LiveEntityAnimationRuntimeView<AnimationRuntime>(
() => runtime);
var ids = new HashSet<uint>();
view.CopySpatialIdsTo(ids);
foreach (KeyValuePair<uint, AnimationRuntime> _ in view) { }
int visits = 0;
long before = GC.GetAllocatedBytesForCurrentThread();
for (int i = 0; i < 1_000; i++)
{
view.CopySpatialIdsTo(ids);
foreach (KeyValuePair<uint, AnimationRuntime> _ in view)
visits++;
}
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
Assert.Equal(0, allocated);
Assert.Equal(1_000, visits);
Assert.Equal(entity.Id, Assert.Single(ids));
}
[Fact]
public void SpatialComponentWorksets_IsolateGuidReuseAcrossVisibilityCallbacks()
{
@ -350,6 +576,60 @@ public sealed class LiveEntityRuntimeTests
Assert.NotSame(oldAnimation, indexed.Value);
}
[Fact]
public void CanonicalPhysicsBody_IsCreatedOncePerIncarnationAndLaterReused()
{
const uint guid = 0x70000073u;
var spatial = new GpuWorldState();
spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
var runtime = new LiveEntityRuntime(spatial, new RecordingResources());
runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u));
runtime.MaterializeLiveEntity(
guid,
0x01010001u,
id => Entity(id, guid));
int factoryCalls = 0;
PhysicsBody first = runtime.GetOrCreatePhysicsBody(
guid,
_ =>
{
factoryCalls++;
return new PhysicsBody();
});
PhysicsBody retained = runtime.GetOrCreatePhysicsBody(
guid,
_ =>
{
factoryCalls++;
return new PhysicsBody();
});
Assert.Same(first, retained);
Assert.Equal(1, factoryCalls);
Assert.True(runtime.TryGetRecord(guid, out var firstRecord));
Assert.Same(first, firstRecord.PhysicsBody);
Assert.True(runtime.UnregisterLiveEntity(
new DeleteObject.Parsed(guid, InstanceSequence: 1),
isLocalPlayer: false));
runtime.RegisterLiveEntity(Spawn(guid, 2, 1, 0x01010001u));
runtime.MaterializeLiveEntity(
guid,
0x01010001u,
id => Entity(id, guid));
PhysicsBody replacement = runtime.GetOrCreatePhysicsBody(
guid,
_ =>
{
factoryCalls++;
return new PhysicsBody();
});
Assert.NotSame(first, replacement);
Assert.Equal(2, factoryCalls);
}
[Fact]
public void EffectProfile_SurvivesRebucketAndClearsWithLogicalTeardown()
{
@ -667,15 +947,24 @@ public sealed class LiveEntityRuntimeTests
id => Entity(id, guid))!;
var animation = new AnimationRuntime(entity);
runtime.SetAnimationRuntime(guid, animation);
var remote = new RemoteMotionRuntime();
runtime.SetRemoteMotionRuntime(guid, remote);
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord record));
ulong initialClockEpoch = record.ObjectClockEpoch;
Assert.True(record.ObjectClock.IsActive);
Assert.True(remote.Body.IsActive);
uint localId = entity.Id;
Assert.True(runtime.TryMarkWorldSpawnPublished(guid));
Assert.True(runtime.TryApplyPickup(
new PickupEvent.Parsed(guid, spawn.InstanceSequence, 11),
out _));
Assert.False(record.ObjectClock.IsActive);
Assert.False(remote.Body.IsActive);
Assert.Equal(initialClockEpoch + 1, record.ObjectClockEpoch);
Assert.True(runtime.WithdrawLiveEntityProjection(guid));
Assert.Equal(initialClockEpoch + 1, record.ObjectClockEpoch);
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord record));
Assert.Equal(0u, record.FullCellId);
Assert.Equal(0u, record.CanonicalLandblockId);
Assert.False(runtime.ShouldAdvanceRootRuntime(guid));
@ -711,6 +1000,9 @@ public sealed class LiveEntityRuntimeTests
Assert.Same(entity, same);
Assert.Equal(localId, same.Id);
Assert.True(record.ObjectClock.IsActive);
Assert.True(remote.Body.IsActive);
Assert.Equal(initialClockEpoch + 2, record.ObjectClockEpoch);
Assert.False(runtime.TryMarkWorldSpawnPublished(guid));
Assert.True(runtime.ShouldAdvanceRootRuntime(guid));
Assert.Equal(0x01010002u, record.FullCellId);
@ -760,6 +1052,267 @@ public sealed class LiveEntityRuntimeTests
Assert.True(runtime.ShouldAdvanceRootRuntime(guid));
}
[Fact]
public void ObjectClock_SurvivesLoadedRebucketAndRebasesPendingReentry()
{
const uint guid = 0x70000047u;
var spatial = new GpuWorldState();
spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
spatial.AddLandblock(EmptyLandblock(0x0102FFFFu));
var runtime = new LiveEntityRuntime(spatial, new RecordingResources());
WorldSession.EntitySpawn spawn = Spawn(guid, 1, 1, 0x01010001u);
LiveEntityRecord record = runtime.RegisterLiveEntity(spawn).Record!;
runtime.MaterializeLiveEntity(
guid,
spawn.Position!.Value.LandblockId,
id => Entity(id, guid));
RetailObjectQuantumClock clock = record.ObjectClock;
Assert.Equal(0, clock.Advance(0.02).Count);
Assert.True(runtime.RebucketLiveEntity(guid, 0x01020001u));
Assert.Same(clock, record.ObjectClock);
Assert.True(clock.IsActive);
Assert.Equal(0.02, clock.PendingSeconds, 8);
Assert.True(runtime.RebucketLiveEntity(guid, 0x02020001u));
Assert.False(clock.IsActive);
Assert.Equal(0.02, clock.PendingSeconds, 8);
spatial.AddLandblock(EmptyLandblock(0x0202FFFFu));
Assert.True(clock.IsActive);
Assert.Equal(0.0, clock.PendingSeconds, 8);
Assert.Equal(
RetailObjectActivityResult.Active,
RetailObjectActivityGate.Evaluate(
clock,
body: null,
lifecycleEligible: runtime.ShouldAdvanceRootRuntime(guid),
hasPartArray: true,
isStatic: false,
objectPosition: Vector3.Zero,
playerPosition: Vector3.Zero,
elapsedSeconds: 0.1));
Assert.Same(clock, record.ObjectClock);
Assert.Equal(0.0, clock.PendingSeconds, 8);
}
[Fact]
public void InitiallyVisibleStaticObject_RebasesWithoutBecomingActive()
{
const uint guid = 0x70000049u;
var spatial = new GpuWorldState();
spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
var runtime = new LiveEntityRuntime(spatial, new RecordingResources());
WorldSession.EntitySpawn spawn = Spawn(
guid,
1,
1,
0x01010001u,
PhysicsStateFlags.Static);
LiveEntityRecord record = runtime.RegisterLiveEntity(spawn).Record!;
var remote = new RemoteMotionRuntime();
runtime.SetRemoteMotionRuntime(guid, remote);
runtime.MaterializeLiveEntity(
guid,
spawn.Position!.Value.LandblockId,
id => Entity(id, guid));
Assert.False(record.ObjectClock.IsActive);
Assert.Equal(0.0, record.ObjectClock.PendingSeconds, 8);
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 = new LiveEntityRuntime(spatial, new RecordingResources());
LiveEntityRecord ordinary = runtime.RegisterLiveEntity(
Spawn(ordinaryGuid, 1, 1, 0x01010001u)).Record!;
var ordinaryRemote = new RemoteMotionRuntime();
runtime.SetRemoteMotionRuntime(ordinaryGuid, ordinaryRemote);
runtime.MaterializeLiveEntity(
ordinaryGuid,
0x01010001u,
id => Entity(id, ordinaryGuid));
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 = runtime.RegisterLiveEntity(
Spawn(staticGuid, 1, 1, 0x01010001u, PhysicsStateFlags.Static)).Record!;
var staticRemote = new RemoteMotionRuntime();
runtime.SetRemoteMotionRuntime(staticGuid, staticRemote);
runtime.MaterializeLiveEntity(
staticGuid,
0x01010001u,
id => Entity(id, staticGuid));
Assert.False(runtime.TryActivateOrdinaryObject(staticGuid, staticRemote));
Assert.False(staticRecord.ObjectClock.IsActive);
Assert.False(staticRemote.Body.TransientState.HasFlag(TransientStateFlags.Active));
}
[Fact]
public void AuthoritativeVector_WakesOrdinaryClockButPreservesStaticActivity()
{
const uint ordinaryGuid = 0x70000052u;
const uint defensiveGuid = 0x70000053u;
const uint staticGuid = 0x70000054u;
var spatial = new GpuWorldState();
spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
var runtime = new LiveEntityRuntime(spatial, new RecordingResources());
LiveEntityRecord ordinary = runtime.RegisterLiveEntity(
Spawn(ordinaryGuid, 1, 1, 0x01010001u)).Record!;
var ordinaryRemote = new RemoteMotionRuntime();
runtime.SetRemoteMotionRuntime(ordinaryGuid, ordinaryRemote);
runtime.MaterializeLiveEntity(
ordinaryGuid,
0x01010001u,
id => Entity(id, ordinaryGuid));
ordinary.ObjectClock.Deactivate();
ordinaryRemote.Body.TransientState &= ~TransientStateFlags.Active;
ordinaryRemote.Body.LastUpdateTime = 1.0;
Assert.True(runtime.TryCommitAuthoritativeVector(
ordinary,
ordinaryRemote.Body,
new Vector3(1f, 2f, 3f),
new Vector3(4f, 5f, 6f),
currentTime: 8.0));
Assert.True(ordinary.ObjectClock.IsActive);
Assert.True(ordinaryRemote.Body.IsActive);
Assert.Equal(8.0, ordinaryRemote.Body.LastUpdateTime);
Assert.Equal(new Vector3(1f, 2f, 3f), ordinaryRemote.Body.Velocity);
Assert.Equal(new Vector3(4f, 5f, 6f), ordinaryRemote.Body.Omega);
// Defensive split state: the retained object clock is already active,
// but a body consumer cleared its legacy Active bit. set_velocity must
// still rebase that body's compatibility timestamp.
LiveEntityRecord defensive = runtime.RegisterLiveEntity(
Spawn(defensiveGuid, 1, 1, 0x01010001u)).Record!;
var defensiveRemote = new RemoteMotionRuntime();
runtime.SetRemoteMotionRuntime(defensiveGuid, defensiveRemote);
runtime.MaterializeLiveEntity(
defensiveGuid,
0x01010001u,
id => Entity(id, defensiveGuid));
Assert.True(defensive.ObjectClock.IsActive);
defensiveRemote.Body.TransientState &= ~TransientStateFlags.Active;
defensiveRemote.Body.LastUpdateTime = 2.0;
Assert.True(runtime.TryCommitAuthoritativeVelocity(
defensive,
defensiveRemote.Body,
Vector3.UnitX,
currentTime: 9.0));
Assert.True(defensive.ObjectClock.IsActive);
Assert.True(defensiveRemote.Body.IsActive);
Assert.Equal(9.0, defensiveRemote.Body.LastUpdateTime);
LiveEntityRecord staticRecord = runtime.RegisterLiveEntity(
Spawn(
staticGuid,
1,
1,
0x01010001u,
PhysicsStateFlags.Static)).Record!;
var staticRemote = new RemoteMotionRuntime();
runtime.SetRemoteMotionRuntime(staticGuid, staticRemote);
runtime.MaterializeLiveEntity(
staticGuid,
0x01010001u,
id => Entity(id, staticGuid));
staticRemote.Body.TransientState &= ~TransientStateFlags.Active;
staticRemote.Body.LastUpdateTime = 3.0;
Assert.True(runtime.TryCommitAuthoritativeVector(
staticRecord,
staticRemote.Body,
new Vector3(7f, 8f, 9f),
new Vector3(10f, 11f, 12f),
currentTime: 10.0));
Assert.False(staticRecord.ObjectClock.IsActive);
Assert.False(staticRemote.Body.IsActive);
Assert.Equal(3.0, staticRemote.Body.LastUpdateTime);
Assert.Equal(new Vector3(7f, 8f, 9f), staticRemote.Body.Velocity);
Assert.Equal(new Vector3(10f, 11f, 12f), staticRemote.Body.Omega);
}
[Fact]
public void ParentAndPickupInvalidateOlderPositionAndVelocityAuthority()
{
const uint parentGuid = 0x50000010u;
const uint childGuid = 0x70000055u;
var runtime = new LiveEntityRuntime(new GpuWorldState(), new RecordingResources());
runtime.RegisterLiveEntity(Spawn(parentGuid, 1, 1, 0x01010001u));
LiveEntityRecord child = runtime.RegisterLiveEntity(
Spawn(childGuid, 1, 1, 0x01010001u)).Record!;
ulong initialPosition = child.PositionAuthorityVersion;
ulong initialVelocity = child.VelocityAuthorityVersion;
Assert.True(runtime.TryApplyParent(
new ParentEvent.Parsed(
parentGuid,
childGuid,
ParentLocation: 0x00000001u,
PlacementId: 0x00000002u,
ParentInstanceSequence: 1,
ChildPositionSequence: 2),
out _));
Assert.False(runtime.IsCurrentPositionAuthority(child, initialPosition));
Assert.False(runtime.IsCurrentVelocityAuthority(child, initialVelocity));
ulong parentedPosition = child.PositionAuthorityVersion;
ulong parentedVelocity = child.VelocityAuthorityVersion;
Assert.True(runtime.TryApplyPickup(
new PickupEvent.Parsed(childGuid, 1, 3),
out _));
Assert.False(runtime.IsCurrentPositionAuthority(child, parentedPosition));
Assert.False(runtime.IsCurrentVelocityAuthority(child, parentedVelocity));
}
[Fact]
public void ObjectClock_WithdrawalRetainsIncarnationButGuidReuseGetsFreshOwner()
{
const uint guid = 0x70000048u;
var spatial = new GpuWorldState();
spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
var runtime = new LiveEntityRuntime(spatial, new RecordingResources());
WorldSession.EntitySpawn firstSpawn = Spawn(guid, 1, 1, 0x01010001u);
LiveEntityRecord first = runtime.RegisterLiveEntity(firstSpawn).Record!;
runtime.MaterializeLiveEntity(
guid,
firstSpawn.Position!.Value.LandblockId,
id => Entity(id, guid));
RetailObjectQuantumClock firstClock = first.ObjectClock;
Assert.Equal(0, firstClock.Advance(0.02).Count);
Assert.True(runtime.WithdrawLiveEntityProjection(guid));
Assert.Same(firstClock, first.ObjectClock);
Assert.False(firstClock.IsActive);
Assert.True(runtime.UnregisterLiveEntity(
new DeleteObject.Parsed(guid, InstanceSequence: 1),
isLocalPlayer: false));
WorldSession.EntitySpawn secondSpawn = Spawn(guid, 2, 2, 0x01010001u);
LiveEntityRecord second = runtime.RegisterLiveEntity(secondSpawn).Record!;
Assert.NotSame(first, second);
Assert.NotSame(firstClock, second.ObjectClock);
Assert.True(second.ObjectClock.IsActive);
Assert.Equal(0.0, second.ObjectClock.PendingSeconds, 8);
}
[Fact]
public void PhysicsStateAndRemoteBodyStaySynchronizedAcrossEitherArrivalOrder()
{
@ -1625,6 +2178,94 @@ public sealed class LiveEntityRuntimeTests
Assert.Same(record.WorldEntity, Assert.Single(spatial.Entities));
}
[Fact]
public void DeferredWithdrawFalseEdge_RebucketSameRecordSupersedesOuterWithdrawal()
{
const uint guid = 0x70000075u;
var spatial = new GpuWorldState();
var runtime = new LiveEntityRuntime(spatial, new RecordingResources());
runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u));
WorldEntity pending = runtime.MaterializeLiveEntity(
guid,
0x01010001u,
id => Entity(id, guid))!;
Assert.False(runtime.TryGetRecord(guid, out LiveEntityRecord before)
&& before.IsSpatiallyVisible);
bool? nestedWithdrawResult = null;
bool nestedWithdrawStarted = false;
bool reprojected = false;
runtime.ProjectionVisibilityChanged += (_, visible) =>
{
if (visible && !nestedWithdrawStarted)
{
// GpuWorldState is currently draining the outer visible edge,
// so Withdraw must manually publish its false edge.
nestedWithdrawStarted = true;
nestedWithdrawResult = runtime.WithdrawLiveEntityProjection(guid);
return;
}
if (!visible && !reprojected)
{
reprojected = true;
Assert.True(runtime.RebucketLiveEntity(guid, 0x01010022u));
}
};
spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
Assert.False(nestedWithdrawResult);
Assert.True(reprojected);
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord record));
Assert.Equal(0x01010022u, record.FullCellId);
Assert.True(record.IsSpatiallyProjected);
Assert.True(record.IsSpatiallyVisible);
Assert.Same(pending, record.WorldEntity);
Assert.Same(pending, Assert.Single(runtime.MaterializedWorldEntities).Value);
Assert.Same(pending, Assert.Single(runtime.WorldEntities).Value);
Assert.Same(pending, Assert.Single(spatial.Entities));
}
[Fact]
public void MaterializeVisibilityCallback_GuidReplacementNeverReturnsOldTombstone()
{
const uint guid = 0x70000076u;
var spatial = new GpuWorldState();
spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
var resources = new FailingOnceUnregisterResources(guid);
var runtime = new LiveEntityRuntime(spatial, resources);
runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u));
WorldEntity? replacementEntity = null;
bool replaced = false;
runtime.ProjectionVisibilityChanged += (edgeRecord, visible) =>
{
if (replaced || !visible || edgeRecord.Generation != 1)
return;
replaced = true;
LiveEntityRegistrationResult replacement = runtime.RegisterLiveEntity(
Spawn(guid, 2, 1, 0x01010002u));
Assert.NotNull(replacement.PriorGenerationCleanupFailure);
replacementEntity = runtime.MaterializeLiveEntity(
guid,
0x01010002u,
id => Entity(id, guid));
};
WorldEntity? displaced = runtime.MaterializeLiveEntity(
guid,
0x01010001u,
id => Entity(id, guid));
Assert.Null(displaced);
Assert.NotNull(replacementEntity);
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord current));
Assert.Equal((ushort)2, current.Generation);
Assert.Same(replacementEntity, current.WorldEntity);
Assert.Same(replacementEntity, Assert.Single(runtime.WorldEntities).Value);
Assert.Equal(1, runtime.PendingTeardownCount);
Assert.Equal(1, runtime.RetryPendingTeardowns());
Assert.Equal(0, runtime.PendingTeardownCount);
}
[Fact]
public void RebucketSpatialCallback_NewerSameRecordRebucketSupersedesOuterMove()
{