fix(runtime): preserve hidden setposition collision ownership

This commit is contained in:
Erik 2026-08-01 18:22:45 +02:00
parent 9b0f59bd1b
commit 0fbc7a1fb7
8 changed files with 343 additions and 43 deletions

View file

@ -1883,7 +1883,7 @@ public sealed class RuntimeCollisionReportingStateTests
}
[Fact]
public void HitGroundHiddenTransitionCannotResumeCollisionTracking()
public void HitGroundHiddenTransitionCommitsPlacementWithoutResumingCollisionTracking()
{
using var lifetime = Lifetime();
RuntimeEntityRecord owner = Entity(
@ -1937,11 +1937,20 @@ public sealed class RuntimeCollisionReportingStateTests
PlacementCommand(owner, new Vector3(16f, 12f, 7f)));
Assert.True(hidden);
Assert.Equal(RuntimeSetPositionStatus.Cancelled, outcome.Status);
Assert.Equal(
RuntimeSetPositionStatus.CommittedHostAcknowledgementPending,
outcome.Status);
Assert.True(lifetime.Physics.SetPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot projection));
Assert.True(owner.FinalPhysicsState.HasFlag(PhysicsStateFlags.Hidden));
Assert.Equal(Cell, owner.FullCellId);
Assert.Equal(projection.WorldPosition, owner.PhysicsBody!.Position);
Assert.True(owner.PhysicsBody.InWorld);
Assert.Empty(observer.Reports);
Assert.Equal(0, lifetime.Physics.CollisionReports.CaptureOwnership()
.TrackedObjectCount);
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
outcome.Projection));
}
[Fact]
@ -2010,7 +2019,7 @@ public sealed class RuntimeCollisionReportingStateTests
}
[Fact]
public void CollisionReportHiddenTransitionStopsResponseAndShadowReflood()
public void CollisionReportHiddenTransitionPreservesCommittedPlacement()
{
using var lifetime = Lifetime();
RuntimeEntityRecord owner = Entity(
@ -2023,7 +2032,6 @@ public sealed class RuntimeCollisionReportingStateTests
RegisterDynamicShadow(lifetime, owner);
RegisterDynamicShadow(lifetime, target);
uint targetId = target.Key!.Value.LocalEntityId;
Vector3 initial = owner.PhysicsBody!.Position;
lifetime.Physics.Engine.TransitionCellCollisionTestHook =
(transition, phase, _, _) =>
{
@ -2062,14 +2070,217 @@ public sealed class RuntimeCollisionReportingStateTests
PlacementCommand(owner, new Vector3(18f, 12f, 7f)));
Assert.True(hidden);
Assert.Equal(RuntimeSetPositionStatus.Cancelled, outcome.Status);
Assert.Equal(
RuntimeSetPositionStatus.CommittedHostAcknowledgementPending,
outcome.Status);
Assert.True(owner.FinalPhysicsState.HasFlag(PhysicsStateFlags.Hidden));
Assert.Equal(0, lifetime.Physics.CollisionReports.CaptureOwnership()
.TrackedObjectCount);
ShadowEntry shadow = Assert.Single(
lifetime.Physics.Engine.ShadowObjects.AllEntriesForDebug(),
Assert.Equal(Cell, owner.FullCellId);
Assert.Equal(new Vector3(18f, 12f, 7f), owner.PhysicsBody!.Position);
ShadowObjectRegistry shadows = lifetime.Physics.Engine.ShadowObjects;
uint ownerId = owner.Key!.Value.LocalEntityId;
ShadowEntry hiddenShadow = Assert.Single(
shadows.AllEntriesForDebug(),
entry => entry.EntityId == ownerId);
Assert.Equal(owner.PhysicsBody.Position, hiddenShadow.Position);
Assert.Equal(0, shadows.SuspendedRegistrationCount);
Assert.True(shadows.TryGetCollisionOwner(
ownerId,
out uint hiddenShadowState,
out _));
Assert.Equal((uint)owner.FinalPhysicsState, hiddenShadowState);
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
outcome.Projection));
Assert.True(lifetime.TryApplyState(
new SetState.Parsed(
owner.ServerGuid,
(uint)PhysicsStateFlags.ReportCollisions,
owner.Incarnation,
StateSequence: 3),
acknowledgeProjection: null,
out _,
out _));
Assert.Equal(0, shadows.SuspendedRegistrationCount);
ShadowEntry restored = Assert.Single(
shadows.AllEntriesForDebug(),
entry => entry.EntityId == ownerId);
Assert.Equal(owner.PhysicsBody.Position, restored.Position);
Assert.Equal((uint)owner.FinalPhysicsState, restored.State);
}
[Fact]
public void PreparedCollisionBatchStopsAfterCallbackHidesOwner()
{
using var lifetime = Lifetime();
RuntimeEntityRecord owner = Entity(
lifetime,
0x7000207Eu,
1,
PhysicsStateFlags.ReportCollisions);
RuntimeEntityRecord first = Entity(
lifetime, 0x7000207Fu, 1, PhysicsStateFlags.None);
RuntimeEntityRecord second = Entity(
lifetime, 0x70002080u, 1, PhysicsStateFlags.None);
RegisterDynamicShadow(lifetime, owner);
RegisterDynamicShadow(lifetime, first);
RegisterDynamicShadow(lifetime, second);
bool hidden = false;
var observer = new CollisionObserver(report =>
{
if (hidden
|| report.Kind is not RuntimeCollisionReportKind.ObjectCollision
|| report.Recipient != owner.Key)
{
return;
}
hidden = lifetime.TryApplyState(
new SetState.Parsed(
owner.ServerGuid,
(uint)(PhysicsStateFlags.ReportCollisions
| PhysicsStateFlags.Hidden),
owner.Incarnation,
StateSequence: 2),
acknowledgeProjection: null,
out _,
out _);
});
using IDisposable subscription = lifetime.Physics.CollisionReports
.Subscribe(observer);
Assert.True(lifetime.Physics.CollisionReports.TryPrepareSetPositionBatch(
owner,
owner.PhysicsBody!,
physicsTime: 12d,
previousContact: false,
previousOnWalkable: false,
finalOnWalkable: false,
collidedWithEnvironment: false,
[
first.Key!.Value.LocalEntityId,
second.Key!.Value.LocalEntityId,
],
out var prepared));
Assert.True(lifetime.Physics.CollisionReports.TryInstallSetPositionBatch(
prepared!, out var receipt));
SetPositionCollisionBatchDispatchResult result = lifetime.Physics
.CollisionReports.DispatchSetPositionBatchResult(receipt);
Assert.True(hidden);
Assert.Equal(SetPositionCollisionBatchDispatchStatus.Completed,
result.Status);
RuntimeCollisionReport report = Assert.Single(
observer.Reports,
candidate => candidate.Kind
is RuntimeCollisionReportKind.ObjectCollision);
Assert.Equal(first.Key, report.Other);
RuntimeCollisionReportingOwnershipSnapshot ownership = lifetime.Physics
.CollisionReports.CaptureOwnership();
Assert.Equal(0, ownership.OwnerCount);
Assert.Equal(0, ownership.TrackedObjectCount);
Assert.Equal(0, ownership.ReversePeerCount);
Assert.Equal(0, ownership.PendingSetPositionDispatchCount);
ShadowObjectRegistry shadows = lifetime.Physics.Engine.ShadowObjects;
Assert.Equal(0, shadows.SuspendedRegistrationCount);
Assert.Contains(
shadows.AllEntriesForDebug(),
entry => entry.EntityId == owner.Key!.Value.LocalEntityId);
Assert.Equal(initial, shadow.Position);
Assert.True(shadows.TryGetCollisionOwner(
owner.Key!.Value.LocalEntityId,
out uint shadowState,
out _));
Assert.Equal((uint)owner.FinalPhysicsState, shadowState);
}
[Fact]
public void NoDrawOwnerStillPublishesItsCollisionReport()
{
using var lifetime = Lifetime();
RuntimeEntityRecord owner = Entity(
lifetime,
0x7000207Au,
1,
PhysicsStateFlags.ReportCollisions | PhysicsStateFlags.NoDraw);
RuntimeEntityRecord target = Entity(
lifetime, 0x7000207Bu, 1, PhysicsStateFlags.None);
RegisterDynamicShadow(lifetime, owner);
RegisterDynamicShadow(lifetime, target);
uint targetId = target.Key!.Value.LocalEntityId;
lifetime.Physics.Engine.TransitionCellCollisionTestHook =
(transition, phase, _, _) =>
{
if (phase is TransitionCellCollisionPhase.Environment)
{
transition.CollisionInfo.CollideObjectGuids.Add(targetId);
transition.CollisionInfo.SetCollisionNormal(Vector3.UnitX);
}
return TransitionState.OK;
};
var observer = new CollisionObserver();
using IDisposable subscription = lifetime.Physics.CollisionReports
.Subscribe(observer);
RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition.Apply(
owner,
owner.PositionAuthorityVersion,
PlacementCommand(owner, new Vector3(19f, 12f, 7f)));
Assert.Equal(
RuntimeSetPositionStatus.CommittedHostAcknowledgementPending,
outcome.Status);
RuntimeCollisionReport report = Assert.Single(
observer.Reports,
report => report.Kind is RuntimeCollisionReportKind.ObjectCollision);
Assert.Equal(owner.Key, report.Recipient);
Assert.Equal(target.Key, report.Other);
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
outcome.Projection));
}
[Fact]
public void NoDrawOwnerStillAllowsReciprocalPeerCollisionReport()
{
using var lifetime = Lifetime();
RuntimeEntityRecord owner = Entity(
lifetime, 0x7000207Cu, 1, PhysicsStateFlags.NoDraw);
RuntimeEntityRecord target = Entity(
lifetime,
0x7000207Du,
1,
PhysicsStateFlags.ReportCollisions);
RegisterDynamicShadow(lifetime, owner);
RegisterDynamicShadow(lifetime, target);
uint targetId = target.Key!.Value.LocalEntityId;
lifetime.Physics.Engine.TransitionCellCollisionTestHook =
(transition, phase, _, _) =>
{
if (phase is TransitionCellCollisionPhase.Environment)
{
transition.CollisionInfo.CollideObjectGuids.Add(targetId);
transition.CollisionInfo.SetCollisionNormal(Vector3.UnitX);
}
return TransitionState.OK;
};
var observer = new CollisionObserver();
using IDisposable subscription = lifetime.Physics.CollisionReports
.Subscribe(observer);
RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition.Apply(
owner,
owner.PositionAuthorityVersion,
PlacementCommand(owner, new Vector3(20f, 12f, 7f)));
Assert.Equal(
RuntimeSetPositionStatus.CommittedHostAcknowledgementPending,
outcome.Status);
RuntimeCollisionReport report = Assert.Single(
observer.Reports,
report => report.Kind is RuntimeCollisionReportKind.ObjectCollision);
Assert.Equal(target.Key, report.Recipient);
Assert.Equal(owner.Key, report.Other);
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
outcome.Projection));
}
[Fact]

View file

@ -113,6 +113,42 @@ public sealed class RuntimeSetPositionStateTests
Assert.Equal(1, ownership.PreparedMoverCount);
}
[Theory]
[InlineData(PhysicsStateFlags.Hidden)]
[InlineData(PhysicsStateFlags.Hidden | PhysicsStateFlags.NoDraw)]
public void HiddenObjectCommitsResidenceWhileSuppressingCollisionReports(
PhysicsStateFlags suppressedState)
{
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
using var lifetime = new RuntimeEntityObjectLifetime(engine);
RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001022u, 1);
PhysicsStateFlags state = PhysicsStateFlags.Gravity | suppressedState;
PhysicsBody body = AttachBody(lifetime, record, SourceCell, state);
var collisionObserver = new CollisionReportObserver();
using IDisposable collisionSubscription = lifetime.Physics
.CollisionReports.Subscribe(collisionObserver);
RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition.Apply(
record,
record.PositionAuthorityVersion,
Command(Request(SourceCell, new Vector3(12f, 18f, 7f))));
Assert.Equal(
RuntimeSetPositionStatus.CommittedHostAcknowledgementPending,
outcome.Status);
Assert.True(body.InWorld);
Assert.Equal(SourceCell, record.FullCellId);
Assert.Equal(new Vector3(12f, 18f, 7f), body.Position);
Assert.True(lifetime.Physics.IsSpatialRoot(record));
Assert.Empty(collisionObserver.Reports);
RuntimeCollisionReportingOwnershipSnapshot reporting = lifetime
.Physics.CollisionReports.CaptureOwnership();
Assert.Equal(0, reporting.OwnerCount);
Assert.Equal(0, reporting.PendingSetPositionDispatchCount);
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
outcome.Projection));
}
[Fact]
public void PublicPlacementChannelObservesRetriesAndAcknowledgesExactToken()
{
@ -2582,6 +2618,15 @@ public sealed class RuntimeSetPositionStateTests
}
}
private sealed class CollisionReportObserver
: IRuntimeCollisionReportObserver
{
internal List<RuntimeCollisionReport> Reports { get; } = [];
public void OnCollisionReport(in RuntimeCollisionReport report) =>
Reports.Add(report);
}
private sealed class EntityObserver : IRuntimeEntityObjectObserver
{
internal List<RuntimeEntityDelta> Deltas { get; } = [];