feat(runtime): commit dormant SetPosition activation

This commit is contained in:
Erik 2026-08-01 14:25:02 +02:00
parent 99f867f053
commit 5785a07b3e
13 changed files with 4674 additions and 111 deletions

View file

@ -13,6 +13,604 @@ public sealed class RuntimeCollisionReportingStateTests
private const uint Landblock = 0xA9B40000u;
private const uint Cell = Landblock | 0x0001u;
[Fact]
public void PreparedBatchDefersTrackingUntilOrderedDispatchAndDispatchesOnce()
{
using var lifetime = Lifetime();
RuntimeEntityRecord owner = Entity(
lifetime,
0x70001F01u,
1,
PhysicsStateFlags.ReportCollisions);
RuntimeEntityRecord target = Entity(
lifetime,
0x70001F02u,
1,
PhysicsStateFlags.ReportCollisions);
target.PhysicsBody!.TransientState |= TransientStateFlags.Contact;
RegisterDynamicShadow(lifetime, target);
var observer = new CollisionObserver();
using IDisposable subscription = lifetime.Physics.CollisionReports
.Subscribe(observer);
Assert.True(lifetime.Physics.CollisionReports.TryPrepareSetPositionBatch(
owner,
owner.PhysicsBody!,
physicsTime: 10d,
previousContact: false,
previousOnWalkable: false,
finalOnWalkable: false,
collidedWithEnvironment: false,
[target.Key!.Value.LocalEntityId],
out var prepared));
Assert.NotNull(prepared);
Assert.Empty(observer.Reports);
Assert.Equal(0, lifetime.Physics.CollisionReports.CaptureOwnership()
.TrackedObjectCount);
Assert.True(lifetime.Physics.CollisionReports.TryInstallSetPositionBatch(
prepared!,
out var receipt));
Assert.Empty(observer.Reports);
Assert.Equal(0, lifetime.Physics.CollisionReports.CaptureOwnership()
.TrackedObjectCount);
Assert.Equal(1, lifetime.Physics.CaptureOwnership()
.PendingCollisionSetPositionDispatchCount);
lifetime.Physics.CollisionReports.DispatchSetPositionBatch(receipt);
Assert.Equal(2, observer.Reports.Count);
Assert.Equal(1, lifetime.Physics.CollisionReports.CaptureOwnership()
.TrackedObjectCount);
Assert.False(observer.Reports[0].RecipientWasInContact);
Assert.True(observer.Reports[0].OtherWasInContact);
Assert.True(observer.Reports[1].RecipientWasInContact);
Assert.False(observer.Reports[1].OtherWasInContact);
Assert.Equal(0, lifetime.Physics.CaptureOwnership()
.PendingCollisionSetPositionDispatchCount);
lifetime.Physics.CollisionReports.DispatchSetPositionBatch(receipt);
Assert.Equal(2, observer.Reports.Count);
}
[Fact]
public void CombinedPhysicsLedgerIncludesPendingShadowSetPositionReceipt()
{
using var lifetime = Lifetime();
RuntimeEntityRecord owner = Entity(
lifetime, 0x70001F03u, 1, PhysicsStateFlags.None);
RegisterDynamicShadow(lifetime, owner);
ShadowObjectRegistry shadows = lifetime.Physics.Engine.ShadowObjects;
Assert.True(shadows.TryPrepareSetPosition(
owner.Key!.Value.LocalEntityId,
new Vector3(13f, 12f, 7f),
Quaternion.Identity,
Cell,
0f,
0f,
PhysicsShadowCommitAction.Replace,
[Cell],
provenShapeless: false,
suspendOwner: false,
out var prepared));
Assert.True(shadows.TryApplySetPosition(prepared!, out var receipt));
Assert.Equal(1, lifetime.Physics.CaptureOwnership()
.PendingShadowSetPositionDispatchCount);
Assert.True(shadows.DiscardSetPositionCommit(receipt));
Assert.Equal(0, lifetime.Physics.CaptureOwnership()
.PendingShadowSetPositionDispatchCount);
}
[Fact]
public void TwoOwnerPreparedBatchesDispatchExactlyOnceInReverseOrder()
{
using var lifetime = Lifetime();
RuntimeEntityRecord first = Entity(
lifetime, 0x70001E01u, 1, PhysicsStateFlags.ReportCollisions);
RuntimeEntityRecord second = Entity(
lifetime, 0x70001E02u, 1, PhysicsStateFlags.ReportCollisions);
const uint staticId = 0x00F01E01u;
RegisterShadow(lifetime, staticId, PhysicsStateFlags.Static,
isStatic: true);
var observer = new CollisionObserver();
using IDisposable subscription = lifetime.Physics.CollisionReports
.Subscribe(observer);
Assert.True(lifetime.Physics.CollisionReports.TryPrepareSetPositionBatch(
first, first.PhysicsBody!, 10d, false, false, false, false,
[staticId], out var firstPrepared));
Assert.True(lifetime.Physics.CollisionReports.TryInstallSetPositionBatch(
firstPrepared!, out var firstReceipt));
Assert.True(lifetime.Physics.CollisionReports.TryPrepareSetPositionBatch(
second, second.PhysicsBody!, 10d, false, false, false, false,
[staticId], out var secondPrepared));
Assert.True(lifetime.Physics.CollisionReports.TryInstallSetPositionBatch(
secondPrepared!, out var secondReceipt));
Assert.True(lifetime.Physics.CollisionReports.DispatchSetPositionBatch(
secondReceipt));
Assert.True(lifetime.Physics.CollisionReports.DispatchSetPositionBatch(
firstReceipt));
Assert.False(lifetime.Physics.CollisionReports.DispatchSetPositionBatch(
secondReceipt));
Assert.False(lifetime.Physics.CollisionReports.DispatchSetPositionBatch(
firstReceipt));
Assert.Equal(2, observer.Reports.Count);
Assert.Equal(0, lifetime.Physics.CollisionReports.CaptureOwnership()
.PendingSetPositionDispatchCount);
}
[Fact]
public void OldBatchRetirementCannotRemoveBatchInstalledByItsCallback()
{
using var lifetime = Lifetime();
RuntimeEntityRecord owner = Entity(
lifetime, 0x70001E03u, 1, PhysicsStateFlags.ReportCollisions);
const uint staticId = 0x00F01E03u;
RegisterShadow(lifetime, staticId, PhysicsStateFlags.Static,
isStatic: true);
Assert.True(lifetime.Physics.CollisionReports.TryPrepareSetPositionBatch(
owner, owner.PhysicsBody!, 10d, false, false, false, false,
[staticId], out var oldPrepared));
Assert.True(lifetime.Physics.CollisionReports.TryInstallSetPositionBatch(
oldPrepared!, out var oldReceipt));
RuntimeCollisionReportingState.SetPositionCollisionBatchReceipt
newerReceipt = default;
bool installedNewer = false;
var observer = new CollisionObserver(ignoredReport =>
{
if (installedNewer)
return;
installedNewer = true;
Assert.True(lifetime.Physics.CollisionReports
.TryPrepareSetPositionBatch(
owner, owner.PhysicsBody!, 11d, false, false, false, true,
ImmutableArray<uint>.Empty, out var newerPrepared));
Assert.True(lifetime.Physics.CollisionReports
.TryInstallSetPositionBatch(newerPrepared!, out newerReceipt));
_ = lifetime.Physics.CollisionReports.DispatchSetPositionBatch(
newerReceipt);
_ = ignoredReport;
});
using IDisposable subscription = lifetime.Physics.CollisionReports
.Subscribe(observer);
Assert.True(lifetime.Physics.CollisionReports.DispatchSetPositionBatch(
oldReceipt));
Assert.True(installedNewer);
Assert.True(newerReceipt.IsValid);
Assert.Equal(1, lifetime.Physics.CollisionReports.CaptureOwnership()
.OwnerCount);
lifetime.Physics.CollisionReports.RetireSetPositionBatchOwner(
oldReceipt);
Assert.Equal(1, lifetime.Physics.CollisionReports.CaptureOwnership()
.OwnerCount);
lifetime.Physics.CollisionReports.RetireSetPositionBatchOwner(
newerReceipt);
Assert.Equal(0, lifetime.Physics.CollisionReports.CaptureOwnership()
.OwnerCount);
}
[Fact]
public void PreparedBatchTracksHiddenIgnoredPeerButSuppressesCallbacks()
{
using var lifetime = Lifetime();
RuntimeEntityRecord owner = Entity(
lifetime,
0x70001F11u,
1,
PhysicsStateFlags.ReportCollisions);
RuntimeEntityRecord target = Entity(
lifetime,
0x70001F12u,
1,
PhysicsStateFlags.ReportCollisions);
RegisterDynamicShadow(lifetime, target);
var observer = new CollisionObserver();
using IDisposable subscription = lifetime.Physics.CollisionReports
.Subscribe(observer);
Assert.True(lifetime.Physics.CollisionReports.TryPrepareSetPositionBatch(
owner,
owner.PhysicsBody!,
10d,
previousContact: false,
previousOnWalkable: false,
finalOnWalkable: false,
collidedWithEnvironment: false,
[target.Key!.Value.LocalEntityId],
out var prepared));
Assert.True(lifetime.Physics.CollisionReports.TryInstallSetPositionBatch(
prepared!,
out var receipt));
PhysicsStateFlags hidden = PhysicsStateFlags.Hidden
| PhysicsStateFlags.IgnoreCollisions;
lifetime.Entities.SetFinalPhysicsState(target, hidden);
target.PhysicsBody!.State = hidden;
lifetime.Physics.CollisionReports.DispatchSetPositionBatch(receipt);
Assert.Empty(observer.Reports);
Assert.Equal(1, lifetime.Physics.CollisionReports.CaptureOwnership()
.TrackedObjectCount);
}
[Fact]
public void PreparedBatchConvertsLiveReportAsEnvironmentAfterGroundEdge()
{
using var lifetime = Lifetime();
RuntimeEntityRecord owner = Entity(
lifetime,
0x70001F21u,
1,
PhysicsStateFlags.ReportCollisions);
RuntimeEntityRecord target = Entity(
lifetime,
0x70001F22u,
1,
PhysicsStateFlags.ReportCollisions);
RegisterDynamicShadow(lifetime, target);
var observer = new CollisionObserver();
using IDisposable subscription = lifetime.Physics.CollisionReports
.Subscribe(observer);
Assert.True(lifetime.Physics.CollisionReports.TryPrepareSetPositionBatch(
owner,
owner.PhysicsBody!,
10d,
previousContact: false,
previousOnWalkable: false,
finalOnWalkable: false,
collidedWithEnvironment: false,
[target.Key!.Value.LocalEntityId],
out var prepared));
Assert.True(lifetime.Physics.CollisionReports.TryInstallSetPositionBatch(
prepared!,
out var receipt));
lifetime.Entities.SetFinalPhysicsState(
target,
PhysicsStateFlags.ReportAsEnvironment);
target.PhysicsBody!.State = PhysicsStateFlags.ReportAsEnvironment;
lifetime.Physics.CollisionReports.DispatchSetPositionBatch(receipt);
Assert.Single(observer.Reports);
Assert.Equal(RuntimeCollisionReportKind.EnvironmentCollision,
observer.Reports[0].Kind);
Assert.Equal(1, lifetime.Physics.CollisionReports.CaptureOwnership()
.TrackedObjectCount);
}
[Fact]
public void PreparedEnvironmentBatchClearsCapturedMissileWithoutReport()
{
using var lifetime = Lifetime();
RuntimeEntityRecord owner = Entity(
lifetime,
0x70001F31u,
1,
PhysicsStateFlags.Missile | PhysicsStateFlags.AlignPath);
RegisterDynamicShadow(lifetime, owner);
const uint staticId = 0x00F01F31u;
RegisterShadow(
lifetime,
staticId,
PhysicsStateFlags.Static,
isStatic: true);
var observer = new CollisionObserver();
using IDisposable subscription = lifetime.Physics.CollisionReports
.Subscribe(observer);
Assert.True(lifetime.Physics.CollisionReports.TryPrepareSetPositionBatch(
owner,
owner.PhysicsBody!,
10d,
previousContact: false,
previousOnWalkable: false,
finalOnWalkable: false,
collidedWithEnvironment: false,
[staticId],
out var prepared));
Assert.True(lifetime.Physics.CollisionReports.TryInstallSetPositionBatch(
prepared!,
out var receipt));
lifetime.Physics.CollisionReports.DispatchSetPositionBatch(receipt);
Assert.Empty(observer.Reports);
Assert.False(owner.FinalPhysicsState.HasFlag(PhysicsStateFlags.Missile));
Assert.False(owner.PhysicsBody!.State.HasFlag(PhysicsStateFlags.Missile));
}
[Fact]
public void PreparedExpiredEndDegradesToMissingTargetAfterInstall()
{
using var lifetime = Lifetime();
RuntimeEntityRecord owner = Entity(
lifetime,
0x70001F41u,
1,
PhysicsStateFlags.ReportCollisions);
RuntimeEntityRecord target = Entity(
lifetime,
0x70001F42u,
1,
PhysicsStateFlags.ReportCollisions);
target.PhysicsBody!.TransientState |= TransientStateFlags.Contact;
RegisterDynamicShadow(lifetime, target);
var observer = new CollisionObserver();
using IDisposable subscription = lifetime.Physics.CollisionReports
.Subscribe(observer);
Assert.True(lifetime.Physics.CollisionReports.TryPrepareSetPositionBatch(
owner,
owner.PhysicsBody!,
10d,
false,
false,
false,
false,
[target.Key!.Value.LocalEntityId],
out var start));
Assert.True(lifetime.Physics.CollisionReports.TryInstallSetPositionBatch(
start!,
out var startReceipt));
lifetime.Physics.CollisionReports.DispatchSetPositionBatch(startReceipt);
observer.Reports.Clear();
Assert.True(lifetime.Physics.CollisionReports.TryPrepareSetPositionBatch(
owner,
owner.PhysicsBody!,
11.0001d,
false,
false,
false,
false,
ImmutableArray<uint>.Empty,
out var end));
Assert.True(lifetime.Physics.CollisionReports.TryInstallSetPositionBatch(
end!,
out var endReceipt));
Assert.True(lifetime.TryAcceptDelete(
new DeleteObject.Parsed(target.ServerGuid, target.Incarnation),
isLocalPlayer: false,
removeRetainedObject: false,
out RuntimeEntityDeleteAcceptance acceptance));
lifetime.CompleteAcceptedDelete(acceptance);
Assert.Null(lifetime.RetireCanonicalOnly(target));
lifetime.Physics.CollisionReports.DispatchSetPositionBatch(endReceipt);
RuntimeCollisionReport report = Assert.Single(observer.Reports);
Assert.Equal(RuntimeCollisionReportKind.ObjectCollisionEnd, report.Kind);
Assert.Equal(owner.Key, report.Recipient);
Assert.Equal(target.ServerGuid, report.OtherServerGuid);
Assert.False(report.OtherWasInContact);
}
[Fact]
public void PreparedBatchPreservesPreLoopEnvironmentLatchUntilFinalSuffix()
{
using var lifetime = Lifetime();
RuntimeEntityRecord owner = Entity(
lifetime,
0x70001F51u,
1,
PhysicsStateFlags.ReportCollisions);
RuntimeEntityRecord target = Entity(
lifetime,
0x70001F52u,
1,
PhysicsStateFlags.ReportCollisions);
RegisterDynamicShadow(lifetime, target);
var observer = new CollisionObserver();
using IDisposable subscription = lifetime.Physics.CollisionReports
.Subscribe(observer);
Assert.True(lifetime.Physics.CollisionReports.HandleReports(
owner,
owner.PhysicsBody!,
9d,
previousContact: false,
previousOnWalkable: false,
collidedWithEnvironment: true,
ImmutableArray<uint>.Empty));
observer.Reports.Clear();
Assert.True(lifetime.Physics.CollisionReports.TryPrepareSetPositionBatch(
owner,
owner.PhysicsBody!,
10d,
previousContact: false,
previousOnWalkable: false,
finalOnWalkable: false,
collidedWithEnvironment: false,
[target.Key!.Value.LocalEntityId],
out var prepared));
Assert.True(lifetime.Physics.CollisionReports.TryInstallSetPositionBatch(
prepared!,
out var receipt));
lifetime.Entities.SetFinalPhysicsState(
target,
PhysicsStateFlags.ReportAsEnvironment);
target.PhysicsBody!.State = PhysicsStateFlags.ReportAsEnvironment;
lifetime.Physics.CollisionReports.DispatchSetPositionBatch(receipt);
Assert.Empty(observer.Reports);
Assert.Equal(1, lifetime.Physics.CollisionReports.CaptureOwnership()
.TrackedObjectCount);
Assert.True(lifetime.Physics.CollisionReports.TryPrepareSetPositionBatch(
owner,
owner.PhysicsBody!,
11d,
previousContact: false,
previousOnWalkable: false,
finalOnWalkable: false,
collidedWithEnvironment: true,
ImmutableArray<uint>.Empty,
out var next));
Assert.True(lifetime.Physics.CollisionReports.TryInstallSetPositionBatch(
next!,
out var nextReceipt));
lifetime.Physics.CollisionReports.DispatchSetPositionBatch(nextReceipt);
RuntimeCollisionReport report = Assert.Single(observer.Reports);
Assert.Equal(RuntimeCollisionReportKind.EnvironmentCollision,
report.Kind);
}
[Fact]
public void PreparedEnvironmentBatchStopsMissileAddedByCallback()
{
using var lifetime = Lifetime();
RuntimeEntityRecord owner = Entity(
lifetime,
0x70001F61u,
1,
PhysicsStateFlags.ReportCollisions);
RegisterDynamicShadow(lifetime, owner);
const uint staticId = 0x00F01F61u;
RegisterShadow(
lifetime,
staticId,
PhysicsStateFlags.Static,
isStatic: true);
var observer = new CollisionObserver(_ =>
{
PhysicsStateFlags callbackState = owner.PhysicsBody!.State
| PhysicsStateFlags.Missile
| PhysicsStateFlags.AlignPath;
lifetime.Entities.SetFinalPhysicsState(owner, callbackState);
owner.PhysicsBody.State = callbackState;
});
using IDisposable subscription = lifetime.Physics.CollisionReports
.Subscribe(observer);
Assert.True(lifetime.Physics.CollisionReports.TryPrepareSetPositionBatch(
owner,
owner.PhysicsBody!,
10d,
previousContact: false,
previousOnWalkable: false,
finalOnWalkable: false,
collidedWithEnvironment: false,
[staticId],
out var prepared));
Assert.True(lifetime.Physics.CollisionReports.TryInstallSetPositionBatch(
prepared!,
out var receipt));
lifetime.Physics.CollisionReports.DispatchSetPositionBatch(receipt);
Assert.Single(observer.Reports);
Assert.False(owner.FinalPhysicsState.HasFlag(PhysicsStateFlags.Missile));
Assert.False(owner.PhysicsBody!.State.HasFlag(PhysicsStateFlags.Missile));
}
[Fact]
public void PreparedExistingCollisionRefreshesLiveEtherealBeforeExpiry()
{
using var lifetime = Lifetime();
RuntimeEntityRecord owner = Entity(
lifetime,
0x70001F71u,
1,
PhysicsStateFlags.ReportCollisions);
RuntimeEntityRecord target = Entity(
lifetime,
0x70001F72u,
1,
PhysicsStateFlags.ReportCollisions);
RegisterDynamicShadow(lifetime, target);
var observer = new CollisionObserver();
using IDisposable subscription = lifetime.Physics.CollisionReports
.Subscribe(observer);
DispatchPrepared(
lifetime,
owner,
physicsTime: 10d,
[target.Key!.Value.LocalEntityId]);
observer.Reports.Clear();
PhysicsStateFlags ethereal = PhysicsStateFlags.ReportCollisions
| PhysicsStateFlags.Ethereal;
lifetime.Entities.SetFinalPhysicsState(target, ethereal);
target.PhysicsBody!.State = ethereal;
DispatchPrepared(
lifetime,
owner,
physicsTime: 10.5d,
[target.Key!.Value.LocalEntityId]);
Assert.Empty(observer.Reports);
DispatchPrepared(
lifetime,
owner,
physicsTime: 10.5001d,
ImmutableArray<uint>.Empty);
Assert.Equal(2, observer.Reports.Count);
Assert.All(observer.Reports, report => Assert.Equal(
RuntimeCollisionReportKind.ObjectCollisionEnd,
report.Kind));
}
[Fact]
public void PreparedExistingCollisionBecomingStaticKeepsOldAgeForSamePassEnd()
{
using var lifetime = Lifetime();
RuntimeEntityRecord owner = Entity(
lifetime,
0x70001F81u,
1,
PhysicsStateFlags.ReportCollisions);
RuntimeEntityRecord target = Entity(
lifetime,
0x70001F82u,
1,
PhysicsStateFlags.ReportCollisions);
RegisterDynamicShadow(lifetime, target);
var observer = new CollisionObserver();
using IDisposable subscription = lifetime.Physics.CollisionReports
.Subscribe(observer);
DispatchPrepared(
lifetime,
owner,
physicsTime: 10d,
[target.Key!.Value.LocalEntityId]);
observer.Reports.Clear();
Assert.True(lifetime.Physics.CollisionReports.TryPrepareSetPositionBatch(
owner,
owner.PhysicsBody!,
11.0001d,
previousContact: false,
previousOnWalkable: false,
finalOnWalkable: false,
collidedWithEnvironment: false,
[target.Key!.Value.LocalEntityId],
out var prepared));
Assert.True(lifetime.Physics.CollisionReports.TryInstallSetPositionBatch(
prepared!,
out var receipt));
PhysicsStateFlags becameStatic = PhysicsStateFlags.ReportCollisions
| PhysicsStateFlags.Static;
lifetime.Entities.SetFinalPhysicsState(target, becameStatic);
target.PhysicsBody!.State = becameStatic;
lifetime.Physics.CollisionReports.DispatchSetPositionBatch(receipt);
Assert.Equal(3, observer.Reports.Count);
Assert.Equal(RuntimeCollisionReportKind.EnvironmentCollision,
observer.Reports[0].Kind);
Assert.Equal(RuntimeCollisionReportKind.ObjectCollisionEnd,
observer.Reports[1].Kind);
Assert.Equal(RuntimeCollisionReportKind.ObjectCollisionEnd,
observer.Reports[2].Kind);
Assert.Equal(0, lifetime.Physics.CollisionReports.CaptureOwnership()
.TrackedObjectCount);
}
[Fact]
public void ExactObjectReportsAreOrderedDeduplicatedAndExpireAtRetailThreshold()
{
@ -1656,6 +2254,28 @@ public sealed class RuntimeCollisionReportingStateTests
owner.PhysicsBody.OnWalkable,
collision);
private static void DispatchPrepared(
RuntimeEntityObjectLifetime lifetime,
RuntimeEntityRecord owner,
double physicsTime,
ImmutableArray<uint> collidedObjectIds)
{
Assert.True(lifetime.Physics.CollisionReports
.TryPrepareSetPositionBatch(
owner,
owner.PhysicsBody!,
physicsTime,
previousContact: owner.PhysicsBody!.InContact,
previousOnWalkable: owner.PhysicsBody.OnWalkable,
finalOnWalkable: owner.PhysicsBody.OnWalkable,
collidedWithEnvironment: false,
collidedObjectIds,
out var prepared));
Assert.True(lifetime.Physics.CollisionReports
.TryInstallSetPositionBatch(prepared!, out var receipt));
lifetime.Physics.CollisionReports.DispatchSetPositionBatch(receipt);
}
private static RuntimeSetPositionCommand PlacementCommand(
RuntimeEntityRecord owner,
Vector3 position)