using System.Collections.Immutable; using System.Numerics; using AcDream.Core.Net; using AcDream.Core.Net.Messages; using AcDream.Core.Physics; using AcDream.Runtime.Entities; using AcDream.Runtime.Physics; namespace AcDream.Runtime.Tests.Physics; public sealed class RuntimeCollisionReportingStateTests { private const uint Landblock = 0xA9B40000u; private const uint Cell = Landblock | 0x0001u; [Fact] public void ExactObjectReportsAreOrderedDeduplicatedAndExpireAtRetailThreshold() { using var lifetime = Lifetime(); RuntimeEntityRecord owner = Entity( lifetime, 0x70002001u, 1, PhysicsStateFlags.ReportCollisions); RuntimeEntityRecord first = Entity( lifetime, 0x70002002u, 1, PhysicsStateFlags.ReportCollisions); RuntimeEntityRecord second = Entity( lifetime, 0x70002003u, 1, PhysicsStateFlags.None); RegisterDynamicShadow(lifetime, first); RegisterDynamicShadow(lifetime, second); var observer = new CollisionObserver(); using IDisposable subscription = lifetime.Physics.CollisionReports .Subscribe(observer); Assert.True(Handle( lifetime, owner, time: 10d, Collisions( first.Key!.Value.LocalEntityId, first.Key.Value.LocalEntityId, second.Key!.Value.LocalEntityId))); Assert.Collection( observer.Reports, report => AssertReport( report, RuntimeCollisionReportKind.ObjectCollision, owner, first), report => AssertReport( report, RuntimeCollisionReportKind.ObjectCollision, first, owner), report => AssertReport( report, RuntimeCollisionReportKind.ObjectCollision, owner, second)); RuntimeCollisionReportingOwnershipSnapshot tracked = lifetime.Physics .CollisionReports.CaptureOwnership(); Assert.Equal(1, tracked.OwnerCount); Assert.Equal(2, tracked.TrackedObjectCount); Assert.Equal(2, tracked.ReversePeerCount); observer.Reports.Clear(); Assert.False(Handle(lifetime, owner, time: 11d, Collisions())); Assert.Empty(observer.Reports); Assert.Equal(2, lifetime.Physics.CollisionReports.CaptureOwnership() .TrackedObjectCount); Assert.False(Handle(lifetime, owner, time: 11.0001d, Collisions())); Assert.Collection( observer.Reports, report => AssertReport( report, RuntimeCollisionReportKind.ObjectCollisionEnd, owner, first), report => AssertReport( report, RuntimeCollisionReportKind.ObjectCollisionEnd, first, owner), report => AssertReport( report, RuntimeCollisionReportKind.ObjectCollisionEnd, owner, second)); RuntimeCollisionReportingOwnershipSnapshot ended = lifetime.Physics .CollisionReports.CaptureOwnership(); Assert.Equal(0, ended.OwnerCount); Assert.Equal(0, ended.TrackedObjectCount); Assert.Equal(0, ended.ReversePeerCount); } [Fact] public void NoReportParticipantsTrackWithoutTurningPresenceIntoSuccess() { using var lifetime = Lifetime(); RuntimeEntityRecord owner = Entity( lifetime, 0x70002010u, 1, PhysicsStateFlags.None); RuntimeEntityRecord target = Entity( lifetime, 0x70002011u, 1, PhysicsStateFlags.None); RegisterDynamicShadow(lifetime, target); Assert.False(Handle( lifetime, owner, time: 3d, Collisions(target.Key!.Value.LocalEntityId))); RuntimeCollisionReportingOwnershipSnapshot ownership = lifetime.Physics .CollisionReports.CaptureOwnership(); Assert.Equal(1, ownership.OwnerCount); Assert.Equal(1, ownership.TrackedObjectCount); } [Fact] public void ExactStaticAndReportAsEnvironmentObjectsUseEnvironmentLatch() { using var lifetime = Lifetime(); RuntimeEntityRecord owner = Entity( lifetime, 0x70002020u, 1, PhysicsStateFlags.ReportCollisions); uint staticId = 0x00F00001u; RegisterShadow( lifetime, staticId, PhysicsStateFlags.None, isStatic: true); RuntimeEntityRecord environmentObject = Entity( lifetime, 0x70002021u, 1, PhysicsStateFlags.ReportAsEnvironment); RegisterDynamicShadow(lifetime, environmentObject); var observer = new CollisionObserver(); using IDisposable subscription = lifetime.Physics.CollisionReports .Subscribe(observer); Assert.True(Handle( lifetime, owner, time: 1d, Collisions( staticId, environmentObject.Key!.Value.LocalEntityId, environmentCollision: true))); Assert.Single(observer.Reports); AssertReport( observer.Reports[0], RuntimeCollisionReportKind.EnvironmentCollision, owner, other: null); Assert.Equal(1, lifetime.Physics.CollisionReports.CaptureOwnership() .TrackedObjectCount); observer.Reports.Clear(); Assert.False(Handle( lifetime, owner, time: 1.1d, Collisions(staticId, environmentCollision: true))); Assert.Empty(observer.Reports); Assert.False(Handle(lifetime, owner, time: 1.2d, Collisions())); Assert.True(Handle( lifetime, owner, time: 1.3d, Collisions(staticId, environmentCollision: true))); Assert.Single(observer.Reports); } [Fact] public void UnknownObjectAndNonFiniteClockFailClosedWithoutTracking() { using var lifetime = Lifetime(); RuntimeEntityRecord owner = Entity( lifetime, 0x70002030u, 1, PhysicsStateFlags.ReportCollisions); RuntimeEntityRecord target = Entity( lifetime, 0x70002031u, 1, PhysicsStateFlags.ReportCollisions); RegisterDynamicShadow(lifetime, target); Assert.False(Handle( lifetime, owner, time: double.NaN, Collisions(target.Key!.Value.LocalEntityId))); Assert.False(Handle( lifetime, owner, time: 2d, Collisions(0x00ABCDEFu))); RuntimeCollisionReportingOwnershipSnapshot ownership = lifetime.Physics .CollisionReports.CaptureOwnership(); Assert.Equal(0, ownership.OwnerCount); Assert.Equal(0, ownership.TrackedObjectCount); } [Fact] public void EtherealContactExpiresAfterItsTouchedQuantum() { using var lifetime = Lifetime(); RuntimeEntityRecord owner = Entity( lifetime, 0x70002040u, 1, PhysicsStateFlags.ReportCollisions); RuntimeEntityRecord target = Entity( lifetime, 0x70002041u, 1, PhysicsStateFlags.ReportCollisions | PhysicsStateFlags.Ethereal); RegisterDynamicShadow(lifetime, target); Assert.True(Handle( lifetime, owner, time: 4d, Collisions(target.Key!.Value.LocalEntityId))); Assert.False(Handle(lifetime, owner, time: 4d, Collisions())); Assert.Equal(1, lifetime.Physics.CollisionReports.CaptureOwnership() .TrackedObjectCount); Assert.False(Handle(lifetime, owner, time: 4.0001d, Collisions())); Assert.Equal(0, lifetime.Physics.CollisionReports.CaptureOwnership() .TrackedObjectCount); } [Fact] public void IgnoreAsymmetryAndMissileStopFollowRetailStateReads() { using var lifetime = Lifetime(); PhysicsStateFlags missile = PhysicsStateFlags.ReportCollisions | PhysicsStateFlags.Missile | PhysicsStateFlags.AlignPath | PhysicsStateFlags.PathClipped; RuntimeEntityRecord owner = Entity( lifetime, 0x70002042u, 1, missile); RuntimeEntityRecord ignored = Entity( lifetime, 0x70002043u, 1, PhysicsStateFlags.ReportCollisions | PhysicsStateFlags.IgnoreCollisions); RegisterDynamicShadow(lifetime, owner); RegisterDynamicShadow(lifetime, ignored); var observer = new CollisionObserver(); using IDisposable subscription = lifetime.Physics.CollisionReports .Subscribe(observer); Assert.True(Handle( lifetime, owner, 4d, Collisions(ignored.Key!.Value.LocalEntityId))); RuntimeCollisionReport reciprocal = Assert.Single(observer.Reports); Assert.Equal(ignored.Key, reciprocal.Recipient); Assert.True(owner.FinalPhysicsState.HasFlag(PhysicsStateFlags.Missile)); RuntimeEntityRecord ordinary = Entity( lifetime, 0x70002044u, 1, PhysicsStateFlags.None); RegisterDynamicShadow(lifetime, ordinary); ulong mutation = owner.PhysicsStateMutationVersion; Assert.True(Handle( lifetime, owner, 4.1d, Collisions(ordinary.Key!.Value.LocalEntityId))); Assert.Equal(mutation + 1UL, owner.PhysicsStateMutationVersion); PhysicsStateFlags cleared = PhysicsStateFlags.Missile | PhysicsStateFlags.AlignPath | PhysicsStateFlags.PathClipped; Assert.Equal(0u, (uint)(owner.FinalPhysicsState & cleared)); Assert.Equal(owner.FinalPhysicsState, owner.PhysicsBody!.State); Assert.True(lifetime.Physics.Engine.ShadowObjects .TryGetCollisionOwner( owner.Key!.Value.LocalEntityId, out uint shadowState, out _)); Assert.Equal((uint)owner.FinalPhysicsState, shadowState); RuntimeEntityRecord sourceIgnoring = Entity( lifetime, 0x70002045u, 1, PhysicsStateFlags.ReportCollisions | PhysicsStateFlags.IgnoreCollisions); RuntimeEntityRecord reportingTarget = Entity( lifetime, 0x70002046u, 1, PhysicsStateFlags.ReportCollisions); RegisterDynamicShadow(lifetime, reportingTarget); observer.Reports.Clear(); Assert.True(Handle( lifetime, sourceIgnoring, 4.2d, Collisions(reportingTarget.Key!.Value.LocalEntityId))); RuntimeCollisionReport sourceOnly = Assert.Single(observer.Reports); Assert.Equal(sourceIgnoring.Key, sourceOnly.Recipient); } [Fact] public void ObjectReportDoesNotClearMissileAddedBySourceCallback() { using var lifetime = Lifetime(); RuntimeEntityRecord owner = Entity( lifetime, 0x70002093u, 1, PhysicsStateFlags.ReportCollisions); RuntimeEntityRecord target = Entity( lifetime, 0x70002094u, 1, PhysicsStateFlags.None); RegisterDynamicShadow(lifetime, target); bool added = false; var observer = new CollisionObserver(report => { if (added || report.Kind is not RuntimeCollisionReportKind.ObjectCollision || report.Recipient != owner.Key) { return; } PhysicsStateFlags state = PhysicsStateFlags.ReportCollisions | PhysicsStateFlags.Missile | PhysicsStateFlags.AlignPath | PhysicsStateFlags.PathClipped; added = lifetime.TryApplyState( new SetState.Parsed( owner.ServerGuid, (uint)state, owner.Incarnation, StateSequence: 2), acknowledgeProjection: null, out _, out _); }); using IDisposable subscription = lifetime.Physics.CollisionReports .Subscribe(observer); Assert.True(Handle( lifetime, owner, 4d, Collisions(target.Key!.Value.LocalEntityId))); Assert.True(added); PhysicsStateFlags retained = owner.FinalPhysicsState; Assert.NotEqual(0u, (uint)(retained & PhysicsStateFlags.Missile)); Assert.NotEqual(0u, (uint)(retained & PhysicsStateFlags.AlignPath)); Assert.NotEqual(0u, (uint)(retained & PhysicsStateFlags.PathClipped)); } [Fact] public void PreCallbackMissileGateClearsCurrentPathBitsAfterCallback() { using var lifetime = Lifetime(); PhysicsStateFlags initial = PhysicsStateFlags.ReportCollisions | PhysicsStateFlags.Missile | PhysicsStateFlags.AlignPath | PhysicsStateFlags.PathClipped; RuntimeEntityRecord owner = Entity( lifetime, 0x70002095u, 1, initial); RuntimeEntityRecord target = Entity( lifetime, 0x70002096u, 1, PhysicsStateFlags.None); RegisterDynamicShadow(lifetime, owner); RegisterDynamicShadow(lifetime, target); bool changed = false; var observer = new CollisionObserver(report => { if (changed || report.Kind is not RuntimeCollisionReportKind.ObjectCollision || report.Recipient != owner.Key) { return; } PhysicsStateFlags callbackState = PhysicsStateFlags.ReportCollisions | PhysicsStateFlags.AlignPath | PhysicsStateFlags.PathClipped; changed = lifetime.TryApplyState( new SetState.Parsed( owner.ServerGuid, (uint)callbackState, owner.Incarnation, StateSequence: 2), acknowledgeProjection: null, out _, out _); }); using IDisposable subscription = lifetime.Physics.CollisionReports .Subscribe(observer); Assert.True(Handle( lifetime, owner, 4d, Collisions(target.Key!.Value.LocalEntityId))); Assert.True(changed); const PhysicsStateFlags stopped = PhysicsStateFlags.Missile | PhysicsStateFlags.AlignPath | PhysicsStateFlags.PathClipped; Assert.Equal(0u, (uint)(owner.FinalPhysicsState & stopped)); Assert.Equal(owner.FinalPhysicsState, owner.PhysicsBody!.State); Assert.True(lifetime.Physics.Engine.ShadowObjects .TryGetCollisionOwner( owner.Key!.Value.LocalEntityId, out uint shadowState, out _)); Assert.Equal((uint)owner.FinalPhysicsState, shadowState); } [Fact] public void CanonicalDynamicStateOverridesStaleShadowClassification() { using var lifetime = Lifetime(); PhysicsStateFlags missile = PhysicsStateFlags.ReportCollisions | PhysicsStateFlags.Missile | PhysicsStateFlags.AlignPath | PhysicsStateFlags.PathClipped; RuntimeEntityRecord owner = Entity( lifetime, 0x7000208Bu, 1, missile); RuntimeEntityRecord ignored = Entity( lifetime, 0x7000208Cu, 1, PhysicsStateFlags.None); RegisterDynamicShadow(lifetime, ignored); ignored.PhysicsBody!.State = PhysicsStateFlags.IgnoreCollisions; lifetime.Entities.SetFinalPhysicsState( ignored, PhysicsStateFlags.IgnoreCollisions); var observer = new CollisionObserver(); using IDisposable subscription = lifetime.Physics.CollisionReports .Subscribe(observer); Assert.False(Handle( lifetime, owner, 4d, Collisions(ignored.Key!.Value.LocalEntityId))); Assert.Empty(observer.Reports); Assert.True(owner.FinalPhysicsState.HasFlag(PhysicsStateFlags.Missile)); RuntimeEntityRecord environmentOwner = Entity( lifetime, 0x7000208Du, 1, PhysicsStateFlags.ReportCollisions); RuntimeEntityRecord newlyEnvironment = Entity( lifetime, 0x7000208Eu, 1, PhysicsStateFlags.None); RegisterDynamicShadow(lifetime, newlyEnvironment); newlyEnvironment.PhysicsBody!.State = PhysicsStateFlags.ReportAsEnvironment; lifetime.Entities.SetFinalPhysicsState( newlyEnvironment, PhysicsStateFlags.ReportAsEnvironment); observer.Reports.Clear(); Assert.True(Handle( lifetime, environmentOwner, 4.1d, Collisions(newlyEnvironment.Key!.Value.LocalEntityId))); Assert.Equal( RuntimeCollisionReportKind.EnvironmentCollision, Assert.Single(observer.Reports).Kind); RuntimeEntityRecord ordinaryOwner = Entity( lifetime, 0x7000208Fu, 1, PhysicsStateFlags.ReportCollisions); RuntimeEntityRecord noLongerEnvironment = Entity( lifetime, 0x70002090u, 1, PhysicsStateFlags.Static | PhysicsStateFlags.ReportAsEnvironment); RegisterDynamicShadow(lifetime, noLongerEnvironment); noLongerEnvironment.PhysicsBody!.State = PhysicsStateFlags.None; lifetime.Entities.SetFinalPhysicsState( noLongerEnvironment, PhysicsStateFlags.None); observer.Reports.Clear(); Assert.True(Handle( lifetime, ordinaryOwner, 4.2d, Collisions(noLongerEnvironment.Key!.Value.LocalEntityId))); Assert.Equal( RuntimeCollisionReportKind.ObjectCollision, Assert.Single(observer.Reports).Kind); RuntimeEntityRecord shadowOnlyOwner = Entity( lifetime, 0x70002091u, 1, PhysicsStateFlags.ReportCollisions); const uint shadowOnlyId = 0x00F02091u; RegisterShadow( lifetime, shadowOnlyId, PhysicsStateFlags.ReportAsEnvironment, isStatic: false); observer.Reports.Clear(); Assert.False(Handle( lifetime, shadowOnlyOwner, 4.3d, Collisions(shadowOnlyId))); Assert.Empty(observer.Reports); } [Fact] public void RegisteredStaticAndUnobservedEnvironmentUseExactLatch() { using var lifetime = Lifetime(); RuntimeEntityRecord owner = Entity( lifetime, 0x70002047u, 1, PhysicsStateFlags.ReportCollisions); const uint rawStaticId = 0x00F00047u; RegisterShadow( lifetime, rawStaticId, PhysicsStateFlags.Static, isStatic: true); var observer = new CollisionObserver(); using IDisposable subscription = lifetime.Physics.CollisionReports .Subscribe(observer); Assert.True(Handle( lifetime, owner, 1d, Collisions(rawStaticId))); Assert.Equal( RuntimeCollisionReportKind.EnvironmentCollision, Assert.Single(observer.Reports).Kind); RuntimeEntityRecord unobserved = Entity( lifetime, 0x70002092u, 1, PhysicsStateFlags.None); Assert.False(Handle( lifetime, unobserved, 1.1d, Collisions([], environmentCollision: true))); Assert.Equal(1, lifetime.Physics.CollisionReports.CaptureOwnership() .OwnerCount); Assert.False(Handle( lifetime, unobserved, 1.2d, Collisions([], environmentCollision: true))); Assert.False(Handle(lifetime, unobserved, 1.3d, Collisions())); Assert.Equal(0, lifetime.Physics.CollisionReports.CaptureOwnership() .OwnerCount); } [Fact] public void ReportAsEnvironmentSuppressesBothObjectEndCallbacks() { using var lifetime = Lifetime(); RuntimeEntityRecord owner = Entity( lifetime, 0x70002048u, 1, PhysicsStateFlags.ReportCollisions); RuntimeEntityRecord target = Entity( lifetime, 0x70002049u, 1, PhysicsStateFlags.ReportCollisions | PhysicsStateFlags.ReportAsEnvironment); RegisterDynamicShadow(lifetime, target); var observer = new CollisionObserver(); using IDisposable subscription = lifetime.Physics.CollisionReports .Subscribe(observer); Assert.True(Handle( lifetime, owner, 1d, Collisions(target.Key!.Value.LocalEntityId))); observer.Reports.Clear(); Assert.False(Handle(lifetime, owner, 2.0001d, Collisions())); Assert.Empty(observer.Reports); Assert.Equal(0, lifetime.Physics.CollisionReports.CaptureOwnership() .TrackedObjectCount); } [Fact] public void NestedReportsDrainFifoAndObserverFailureDoesNotCorruptQueue() { using var lifetime = Lifetime(); RuntimeEntityRecord firstOwner = Entity( lifetime, 0x7000204Au, 1, PhysicsStateFlags.ReportCollisions); RuntimeEntityRecord firstTarget = Entity( lifetime, 0x7000204Bu, 1, PhysicsStateFlags.None); RuntimeEntityRecord secondOwner = Entity( lifetime, 0x7000204Cu, 1, PhysicsStateFlags.ReportCollisions); RuntimeEntityRecord secondTarget = Entity( lifetime, 0x7000204Du, 1, PhysicsStateFlags.None); RegisterDynamicShadow(lifetime, firstTarget); RegisterDynamicShadow(lifetime, secondTarget); bool nested = false; var firstObserver = new CollisionObserver(_ => { if (nested) return; nested = true; Assert.True(Handle( lifetime, secondOwner, 2d, Collisions(secondTarget.Key!.Value.LocalEntityId))); }); var secondObserver = new CollisionObserver(); var failing = new ThrowingCollisionObserver(); using IDisposable firstSubscription = lifetime.Physics.CollisionReports .Subscribe(firstObserver); using IDisposable failingSubscription = lifetime.Physics.CollisionReports .Subscribe(failing); using IDisposable secondSubscription = lifetime.Physics.CollisionReports .Subscribe(secondObserver); Assert.Throws(() => lifetime.Physics.CollisionReports.Subscribe(secondObserver)); Assert.True(Handle( lifetime, firstOwner, 1d, Collisions(firstTarget.Key!.Value.LocalEntityId))); Assert.Collection( secondObserver.Reports, report => Assert.Equal(firstOwner.Key, report.Recipient), report => Assert.Equal(secondOwner.Key, report.Recipient)); RuntimeCollisionReportingOwnershipSnapshot ownership = lifetime.Physics .CollisionReports.CaptureOwnership(); Assert.Equal(2, ownership.DispatchFailureCount); Assert.Equal(0, ownership.PendingReportCount); Assert.False(ownership.IsDispatching); } [Fact] public void ReentrantResetDropsRemainingPriorEpochReports() { using var lifetime = Lifetime(); RuntimeEntityRecord owner = Entity( lifetime, 0x7000204Eu, 1, PhysicsStateFlags.ReportCollisions); RuntimeEntityRecord first = Entity( lifetime, 0x7000204Fu, 1, PhysicsStateFlags.None); RuntimeEntityRecord second = Entity( lifetime, 0x70002054u, 1, PhysicsStateFlags.None); RegisterDynamicShadow(lifetime, first); RegisterDynamicShadow(lifetime, second); Assert.True(Handle( lifetime, owner, 1d, Collisions( first.Key!.Value.LocalEntityId, second.Key!.Value.LocalEntityId))); bool reset = false; var observer = new CollisionObserver(_ => { if (reset) return; reset = true; lifetime.Physics.CollisionReports.ResetSession(); }); using IDisposable subscription = lifetime.Physics.CollisionReports .Subscribe(observer); lifetime.Physics.CollisionReports.LeaveWorld(owner); Assert.True(reset); Assert.Single(observer.Reports); RuntimeCollisionReportingOwnershipSnapshot ownership = lifetime.Physics .CollisionReports.CaptureOwnership(); Assert.Equal(0, ownership.OwnerCount); Assert.Equal(0, ownership.PendingReportCount); Assert.False(ownership.IsDispatching); } [Fact] public void ReentrantTargetDeletionCannotDeliverSecondOrRetainStaleContact() { using var lifetime = Lifetime(); RuntimeEntityRecord owner = Entity( lifetime, 0x70002050u, 1, PhysicsStateFlags.ReportCollisions); RuntimeEntityRecord target = Entity( lifetime, 0x70002051u, 1, PhysicsStateFlags.ReportCollisions); RegisterDynamicShadow(lifetime, target); bool deleted = false; var observer = new CollisionObserver(report => { if (deleted || report.Kind is not RuntimeCollisionReportKind.ObjectCollision || report.Recipient != owner.Key) { return; } deleted = true; 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)); }); using IDisposable subscription = lifetime.Physics.CollisionReports .Subscribe(observer); Assert.True(Handle( lifetime, owner, time: 5d, Collisions(target.Key!.Value.LocalEntityId))); Assert.True(deleted); Assert.Single(observer.Reports); RuntimeCollisionReportingOwnershipSnapshot ownership = lifetime.Physics .CollisionReports.CaptureOwnership(); Assert.Equal(1, ownership.OwnerCount); Assert.Equal(1, ownership.TrackedObjectCount); Assert.Equal(1, ownership.ReversePeerCount); Assert.False(Handle(lifetime, owner, time: 6.0001d, Collisions())); Assert.Equal(2, observer.Reports.Count); RuntimeCollisionReport ended = observer.Reports[1]; Assert.Equal(RuntimeCollisionReportKind.ObjectCollisionEnd, ended.Kind); Assert.Equal(target.ServerGuid, ended.OtherServerGuid); Assert.Equal(0, lifetime.Physics.CollisionReports.CaptureOwnership() .TrackedObjectCount); } [Fact] public void DeleteAndGuidReuseNeverTransferTrackedIncarnation() { using var lifetime = Lifetime(); RuntimeEntityRecord owner = Entity( lifetime, 0x70002060u, 1, PhysicsStateFlags.ReportCollisions); const uint reusedGuid = 0x70002061u; RuntimeEntityRecord retired = Entity( lifetime, reusedGuid, 1, PhysicsStateFlags.ReportCollisions); RuntimeEntityKey retiredKey = retired.Key!.Value; RegisterDynamicShadow(lifetime, retired); Assert.True(Handle( lifetime, owner, time: 6d, Collisions(retired.Key!.Value.LocalEntityId))); Assert.True(lifetime.TryAcceptDelete( new DeleteObject.Parsed(reusedGuid, 1), isLocalPlayer: false, removeRetainedObject: false, out RuntimeEntityDeleteAcceptance acceptance)); lifetime.CompleteAcceptedDelete(acceptance); Assert.Null(lifetime.RetireCanonicalOnly(retired)); Assert.Equal(1, lifetime.Physics.CollisionReports.CaptureOwnership() .TrackedObjectCount); RuntimeEntityRecord replacement = Entity( lifetime, reusedGuid, 2, PhysicsStateFlags.ReportCollisions); RegisterDynamicShadow(lifetime, replacement); Assert.NotEqual(retiredKey, replacement.Key); Assert.True(Handle( lifetime, owner, time: 6.1d, Collisions(replacement.Key!.Value.LocalEntityId))); Assert.Equal(2, lifetime.Physics.CollisionReports.CaptureOwnership() .TrackedObjectCount); Assert.False(Handle( lifetime, owner, time: 7.0001d, Collisions(replacement.Key.Value.LocalEntityId))); Assert.Equal(1, lifetime.Physics.CollisionReports.CaptureOwnership() .TrackedObjectCount); } [Fact] public void HiddenLeaveWorldRejectsReentrantContactReaddition() { using var lifetime = Lifetime(); RuntimeEntityRecord owner = Entity( lifetime, 0x70002068u, 1, PhysicsStateFlags.ReportCollisions); RuntimeEntityRecord target = Entity( lifetime, 0x70002069u, 1, PhysicsStateFlags.None); RegisterDynamicShadow(lifetime, target); Assert.True(Handle( lifetime, owner, 1d, Collisions(target.Key!.Value.LocalEntityId))); bool readdResult = true; var observer = new CollisionObserver(report => { if (report.Kind is RuntimeCollisionReportKind.ObjectCollisionEnd) { readdResult = Handle( lifetime, owner, 1.1d, Collisions(target.Key!.Value.LocalEntityId)); } }); using IDisposable subscription = lifetime.Physics.CollisionReports .Subscribe(observer); Assert.True(lifetime.TryApplyState( new SetState.Parsed( owner.ServerGuid, (uint)(PhysicsStateFlags.ReportCollisions | PhysicsStateFlags.Hidden), owner.Incarnation, StateSequence: 2), acknowledgeProjection: null, out _, out RetailPhysicsStateTransition transition)); Assert.Equal( RetailHiddenTransition.BecameHidden, transition.HiddenTransition); Assert.False(readdResult); RuntimeCollisionReportingOwnershipSnapshot ownership = lifetime.Physics .CollisionReports.CaptureOwnership(); Assert.Equal(0, ownership.TrackedObjectCount); Assert.Equal(0, ownership.LeavingOwnerCount); } [Fact] public void FirstForceEndCallbackDeletingOwnerStopsRemainingSourceSuffix() { using var lifetime = Lifetime(); RuntimeEntityRecord owner = Entity( lifetime, 0x70002082u, 1, PhysicsStateFlags.ReportCollisions); RuntimeEntityRecord first = Entity( lifetime, 0x70002083u, 1, PhysicsStateFlags.None); RuntimeEntityRecord second = Entity( lifetime, 0x70002084u, 1, PhysicsStateFlags.None); RegisterDynamicShadow(lifetime, first); RegisterDynamicShadow(lifetime, second); Assert.True(Handle( lifetime, owner, 1d, Collisions( first.Key!.Value.LocalEntityId, second.Key!.Value.LocalEntityId))); bool accepted = false; var observer = new CollisionObserver(report => { if (accepted || report.Kind is not RuntimeCollisionReportKind .ObjectCollisionEnd || report.Recipient != owner.Key) { return; } accepted = true; Assert.True(lifetime.TryAcceptDelete( new DeleteObject.Parsed(owner.ServerGuid, owner.Incarnation), isLocalPlayer: false, removeRetainedObject: false, out _)); }); using IDisposable subscription = lifetime.Physics.CollisionReports .Subscribe(observer); lifetime.Physics.CollisionReports.LeaveWorld(owner); Assert.True(accepted); RuntimeCollisionReport report = Assert.Single(observer.Reports); Assert.Equal(first.Key, report.Other); Assert.Equal(0, lifetime.Physics.CollisionReports.CaptureOwnership() .TrackedObjectCount); } [Fact] public void SessionClearForceEndsBeforeTerminalCollisionReset() { var lifetime = Lifetime(); RuntimeEntityRecord owner = Entity( lifetime, 0x7000206Au, 1, PhysicsStateFlags.ReportCollisions); RuntimeEntityRecord target = Entity( lifetime, 0x7000206Bu, 1, PhysicsStateFlags.ReportCollisions); RegisterDynamicShadow(lifetime, target); var observer = new CollisionObserver(); using IDisposable subscription = lifetime.Physics.CollisionReports .Subscribe(observer); Assert.True(Handle( lifetime, owner, 1d, Collisions(target.Key!.Value.LocalEntityId))); observer.Reports.Clear(); IReadOnlyList retiring = lifetime.BeginSessionClear(); Assert.Collection( observer.Reports, report => Assert.Equal( RuntimeCollisionReportKind.ObjectCollisionEnd, report.Kind), report => Assert.Equal( RuntimeCollisionReportKind.ObjectCollisionEnd, report.Kind)); Assert.Equal(0, lifetime.Physics.CollisionReports.CaptureOwnership() .TrackedObjectCount); foreach (RuntimeEntityRecord record in retiring) lifetime.CompleteSessionEntityRetirement(record); Assert.True(lifetime.CompleteSessionClearIfConverged()); lifetime.Dispose(); } [Fact] public void SessionClearBlocksCrossOwnerReadditionForWholeBatch() { var lifetime = Lifetime(); RuntimeEntityRecord firstOwner = Entity( lifetime, 0x7000207Au, 1, PhysicsStateFlags.ReportCollisions); RuntimeEntityRecord peer = Entity( lifetime, 0x7000207Bu, 1, PhysicsStateFlags.None); RuntimeEntityRecord laterOwner = Entity( lifetime, 0x7000207Cu, 1, PhysicsStateFlags.ReportCollisions); RegisterDynamicShadow(lifetime, peer); Assert.True(Handle( lifetime, firstOwner, 1d, Collisions(peer.Key!.Value.LocalEntityId))); Assert.True(Handle( lifetime, laterOwner, 1d, Collisions(peer.Key.Value.LocalEntityId))); bool attempted = false; bool readded = true; var observer = new CollisionObserver(report => { if (attempted || report.Recipient != laterOwner.Key) return; attempted = true; readded = Handle( lifetime, firstOwner, 1.1d, Collisions(peer.Key.Value.LocalEntityId)); }); using IDisposable subscription = lifetime.Physics.CollisionReports .Subscribe(observer); IReadOnlyList retiring = lifetime.BeginSessionClear(); Assert.True(attempted); Assert.False(readded); RuntimeCollisionReportingOwnershipSnapshot ownership = lifetime.Physics .CollisionReports.CaptureOwnership(); Assert.Equal(0, ownership.OwnerCount); Assert.Equal(0, ownership.AdmissionBlockedOwnerCount); foreach (RuntimeEntityRecord record in retiring) lifetime.CompleteSessionEntityRetirement(record); Assert.True(lifetime.CompleteSessionClearIfConverged()); lifetime.Dispose(); } [Fact] public void SessionClearCallbackDeletingLaterBlockedOwnerStillForceEndsIt() { var lifetime = Lifetime(); RuntimeEntityRecord firstOwner = Entity( lifetime, 0x70002085u, 1, PhysicsStateFlags.ReportCollisions); RuntimeEntityRecord firstPeer = Entity( lifetime, 0x70002086u, 1, PhysicsStateFlags.None); RuntimeEntityRecord laterOwner = Entity( lifetime, 0x70002087u, 1, PhysicsStateFlags.ReportCollisions); RuntimeEntityRecord laterPeer = Entity( lifetime, 0x70002088u, 1, PhysicsStateFlags.None); RegisterDynamicShadow(lifetime, firstPeer); RegisterDynamicShadow(lifetime, laterPeer); Assert.True(Handle( lifetime, firstOwner, 1d, Collisions(firstPeer.Key!.Value.LocalEntityId))); Assert.True(Handle( lifetime, laterOwner, 1d, Collisions(laterPeer.Key!.Value.LocalEntityId))); bool accepted = false; var observer = new CollisionObserver(report => { if (accepted || report.Kind is not RuntimeCollisionReportKind .ObjectCollisionEnd || report.Recipient != firstOwner.Key) { return; } accepted = true; Assert.True(lifetime.TryAcceptDelete( new DeleteObject.Parsed( laterOwner.ServerGuid, laterOwner.Incarnation), isLocalPlayer: false, removeRetainedObject: false, out _)); }); using IDisposable subscription = lifetime.Physics.CollisionReports .Subscribe(observer); IReadOnlyList retiring = lifetime.BeginSessionClear(); Assert.True(accepted); Assert.Contains( observer.Reports, report => report.Kind is RuntimeCollisionReportKind.ObjectCollisionEnd && report.Recipient == laterOwner.Key && report.Other == laterPeer.Key); RuntimeCollisionReportingOwnershipSnapshot ownership = lifetime.Physics .CollisionReports.CaptureOwnership(); Assert.Equal(0, ownership.OwnerCount); Assert.Equal(0, ownership.AdmissionBlockedOwnerCount); foreach (RuntimeEntityRecord record in retiring) lifetime.CompleteSessionEntityRetirement(record); Assert.True(lifetime.CompleteSessionClearIfConverged()); lifetime.Dispose(); } [Fact] public void InvalidSetPositionMapsNewReportThenRepeatToRetailErrors() { using var lifetime = Lifetime(); RuntimeEntityRecord owner = Entity( lifetime, 0x7000206Cu, 1, PhysicsStateFlags.ReportCollisions); RuntimeEntityRecord target = Entity( lifetime, 0x7000206Du, 1, PhysicsStateFlags.None); 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.LastCollidedObjectGuid = targetId; } return TransitionState.Collided; }; RuntimeSetPositionCommand command = PlacementCommand( owner, new Vector3(13f, 12f, 7f)); RuntimeSetPositionOutcome first = lifetime.Physics.SetPosition.Apply( owner, owner.PositionAuthorityVersion, command); Assert.Equal(RuntimeSetPositionStatus.Rejected, first.Status); Assert.Equal(PhysicsSetPositionError.Collided, first.Error); Assert.True(lifetime.Physics.SetPosition.TryGetAwaitingPreparationToken( owner, out RuntimeEntityPlacementToken retry)); RuntimeSetPositionOutcome repeated = lifetime.Physics.SetPosition .SubmitPreparedPlacement(retry, command); Assert.Equal(RuntimeSetPositionStatus.Rejected, repeated.Status); Assert.Equal(PhysicsSetPositionError.NoValidPosition, repeated.Error); } [Fact] public void SuccessfulSetPositionReportsBeforeResponseAndShadowReflood() { using var lifetime = Lifetime(); RuntimeEntityRecord owner = Entity( lifetime, 0x7000206Eu, 1, PhysicsStateFlags.ReportCollisions); RuntimeEntityRecord target = Entity( lifetime, 0x7000206Fu, 1, PhysicsStateFlags.None); RegisterDynamicShadow(lifetime, owner); RegisterDynamicShadow(lifetime, target); uint targetId = target.Key!.Value.LocalEntityId; Vector3 initial = owner.PhysicsBody!.Position; Vector3 destination = new(14f, 12f, 7f); owner.PhysicsBody.set_velocity(-Vector3.UnitX); owner.PhysicsBody.FramesStationaryFall = 2; owner.PhysicsBody.TransientState |= TransientStateFlags.StationaryStop; lifetime.Physics.Engine.TransitionCellCollisionTestHook = (transition, phase, _, _) => { if (phase is TransitionCellCollisionPhase.Environment) { transition.CollisionInfo.CollideObjectGuids.Add(targetId); transition.CollisionInfo.LastCollidedObjectGuid = targetId; transition.CollisionInfo.SetCollisionNormal(Vector3.UnitX); transition.CollisionInfo.FramesStationaryFall = 1; } return TransitionState.OK; }; bool observed = false; var observer = new CollisionObserver(report => { if (report.Kind is not RuntimeCollisionReportKind.ObjectCollision || report.Recipient != owner.Key) { return; } observed = true; Assert.Equal(destination, owner.PhysicsBody.Position); Assert.Equal(-Vector3.UnitX, owner.PhysicsBody.Velocity); Assert.Equal(2, owner.PhysicsBody.FramesStationaryFall); Assert.NotEqual( 0u, (uint)(owner.PhysicsBody.TransientState & TransientStateFlags.StationaryStop)); Assert.Equal( 0u, (uint)(owner.PhysicsBody.TransientState & TransientStateFlags.StationaryFall)); ShadowEntry shadow = Assert.Single( lifetime.Physics.Engine.ShadowObjects.AllEntriesForDebug(), entry => entry.EntityId == owner.Key!.Value.LocalEntityId); Assert.Equal(initial, shadow.Position); }); using IDisposable subscription = lifetime.Physics.CollisionReports .Subscribe(observer); RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition.Apply( owner, owner.PositionAuthorityVersion, PlacementCommand(owner, destination)); Assert.Equal( RuntimeSetPositionStatus.CommittedHostAcknowledgementPending, outcome.Status); Assert.True(observed); ShadowEntry committedShadow = Assert.Single( lifetime.Physics.Engine.ShadowObjects.AllEntriesForDebug(), entry => entry.EntityId == owner.Key!.Value.LocalEntityId); Assert.Equal(destination, committedShadow.Position); Assert.True(owner.PhysicsBody.Velocity.X >= 0f); Assert.Equal(1, owner.PhysicsBody.FramesStationaryFall); Assert.NotEqual( 0u, (uint)(owner.PhysicsBody.TransientState & TransientStateFlags.StationaryFall)); Assert.Equal( 0u, (uint)(owner.PhysicsBody.TransientState & TransientStateFlags.StationaryStop)); } [Fact] public void ReportSurvivesNewVectorWhileStalePhysicalResponseDoesNot() { using var lifetime = Lifetime(); RuntimeEntityRecord owner = Entity( lifetime, 0x70002072u, 1, PhysicsStateFlags.ReportCollisions); RuntimeEntityRecord target = Entity( lifetime, 0x70002073u, 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; }; bool observed = false; var observer = new CollisionObserver(report => { if (observed || report.Recipient != owner.Key) return; observed = true; lifetime.Entities.AdvanceVectorAuthority(owner); owner.PhysicsBody!.set_velocity(new Vector3(-7f, 3f, 0f)); }); using IDisposable subscription = lifetime.Physics.CollisionReports .Subscribe(observer); RuntimeSetPositionCommand command = PlacementCommand( owner, new Vector3(15f, 12f, 7f)) with { ExpectedVelocityAuthorityVersion = owner.VelocityAuthorityVersion, }; RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition.Apply( owner, owner.PositionAuthorityVersion, command); Assert.True(observed); Assert.Equal( RuntimeSetPositionStatus.CommittedHostAcknowledgementPending, outcome.Status); Assert.Equal(new Vector3(-7f, 3f, 0f), owner.PhysicsBody!.Velocity); } [Fact] public void HitGroundHiddenTransitionCannotResumeCollisionTracking() { using var lifetime = Lifetime(); RuntimeEntityRecord owner = Entity( lifetime, 0x70002074u, 1, PhysicsStateFlags.ReportCollisions); RuntimeEntityRecord target = Entity( lifetime, 0x70002075u, 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.SetContactPlane( new Plane(Vector3.UnitZ, 0f), Cell); transition.CollisionInfo.CollideObjectGuids.Add(targetId); } return TransitionState.OK; }; bool hidden = false; var remote = new ReentrantRemotePlacement(owner.PhysicsBody!) { CellId = Cell, OnHitGround = () => { hidden = lifetime.TryApplyState( new SetState.Parsed( owner.ServerGuid, (uint)(PhysicsStateFlags.ReportCollisions | PhysicsStateFlags.Hidden), owner.Incarnation, StateSequence: 2), acknowledgeProjection: null, out _, out _); }, }; lifetime.Entities.SetRemoteMotion(owner, remote); 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(16f, 12f, 7f))); Assert.True(hidden); Assert.Equal(RuntimeSetPositionStatus.Cancelled, outcome.Status); Assert.True(owner.FinalPhysicsState.HasFlag(PhysicsStateFlags.Hidden)); Assert.Empty(observer.Reports); Assert.Equal(0, lifetime.Physics.CollisionReports.CaptureOwnership() .TrackedObjectCount); } [Fact] public void HitGroundHiddenPeerCannotBecomeAStaleTrackedTarget() { using var lifetime = Lifetime(); RuntimeEntityRecord owner = Entity( lifetime, 0x70002076u, 1, PhysicsStateFlags.ReportCollisions); RuntimeEntityRecord target = Entity( lifetime, 0x70002077u, 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.SetContactPlane( new Plane(Vector3.UnitZ, 0f), Cell); transition.CollisionInfo.CollideObjectGuids.Add(targetId); } return TransitionState.OK; }; var remote = new ReentrantRemotePlacement(owner.PhysicsBody!) { CellId = Cell, OnHitGround = () => Assert.True(lifetime.TryApplyState( new SetState.Parsed( target.ServerGuid, (uint)(PhysicsStateFlags.ReportCollisions | PhysicsStateFlags.Hidden), target.Incarnation, StateSequence: 2), acknowledgeProjection: null, out _, out _)), }; lifetime.Entities.SetRemoteMotion(owner, remote); 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(17f, 12f, 7f))); Assert.Equal( RuntimeSetPositionStatus.CommittedHostAcknowledgementPending, outcome.Status); Assert.True(target.FinalPhysicsState.HasFlag(PhysicsStateFlags.Hidden)); Assert.DoesNotContain( observer.Reports, report => report.Kind is RuntimeCollisionReportKind.ObjectCollision); Assert.Equal(0, lifetime.Physics.CollisionReports.CaptureOwnership() .TrackedObjectCount); } [Fact] public void CollisionReportHiddenTransitionStopsResponseAndShadowReflood() { using var lifetime = Lifetime(); RuntimeEntityRecord owner = Entity( lifetime, 0x70002078u, 1, PhysicsStateFlags.ReportCollisions); RuntimeEntityRecord target = Entity( lifetime, 0x70002079u, 1, PhysicsStateFlags.None); RegisterDynamicShadow(lifetime, owner); RegisterDynamicShadow(lifetime, target); uint targetId = target.Key!.Value.LocalEntityId; Vector3 initial = owner.PhysicsBody!.Position; lifetime.Physics.Engine.TransitionCellCollisionTestHook = (transition, phase, _, _) => { if (phase is TransitionCellCollisionPhase.Environment) { transition.CollisionInfo.CollideObjectGuids.Add(targetId); transition.CollisionInfo.SetCollisionNormal(Vector3.UnitX); } return TransitionState.OK; }; bool hidden = false; var observer = new CollisionObserver(report => { if (hidden || report.Kind is not RuntimeCollisionReportKind.ObjectCollision) { 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); RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition.Apply( owner, owner.PositionAuthorityVersion, PlacementCommand(owner, new Vector3(18f, 12f, 7f))); Assert.True(hidden); Assert.Equal(RuntimeSetPositionStatus.Cancelled, 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(), entry => entry.EntityId == owner.Key!.Value.LocalEntityId); Assert.Equal(initial, shadow.Position); } [Fact] public void SessionResetAndDisposalConvergeEveryCollisionOwner() { var lifetime = Lifetime(); RuntimeEntityRecord owner = Entity( lifetime, 0x70002070u, 1, PhysicsStateFlags.ReportCollisions); RuntimeEntityRecord target = Entity( lifetime, 0x70002071u, 1, PhysicsStateFlags.ReportCollisions); RegisterDynamicShadow(lifetime, target); var observer = new CollisionObserver(); IDisposable subscription = lifetime.Physics.CollisionReports .Subscribe(observer); Assert.True(Handle( lifetime, owner, time: 7d, Collisions(target.Key!.Value.LocalEntityId))); IReadOnlyList retiring = lifetime.BeginSessionClear(); RuntimeCollisionReportingOwnershipSnapshot cleared = lifetime.Physics .CollisionReports.CaptureOwnership(); Assert.Equal(0, cleared.OwnerCount); Assert.Equal(0, cleared.TrackedObjectCount); foreach (RuntimeEntityRecord record in retiring) lifetime.CompleteSessionEntityRetirement(record); Assert.True(lifetime.CompleteSessionClearIfConverged()); subscription.Dispose(); lifetime.Dispose(); Assert.True(lifetime.Physics.CollisionReports.CaptureOwnership() .IsConverged); Assert.True(lifetime.Physics.CaptureOwnership().IsConverged); } [Fact] public void IndependentRuntimeRootsProduceDeterministicReportsAndResult() { static (bool Result, RuntimeCollisionReport[] Reports) Run() { using var lifetime = Lifetime(); RuntimeEntityRecord owner = Entity( lifetime, 0x70002080u, 1, PhysicsStateFlags.ReportCollisions); RuntimeEntityRecord target = Entity( lifetime, 0x70002081u, 1, PhysicsStateFlags.ReportCollisions); RegisterDynamicShadow(lifetime, target); var observer = new CollisionObserver(); using IDisposable subscription = lifetime.Physics.CollisionReports .Subscribe(observer); bool result = Handle( lifetime, owner, time: 8d, Collisions(target.Key!.Value.LocalEntityId)); return (result, observer.Reports.ToArray()); } (bool graphicalResult, RuntimeCollisionReport[] graphical) = Run(); (bool noWindowResult, RuntimeCollisionReport[] noWindow) = Run(); Assert.Equal(graphicalResult, noWindowResult); Assert.Equal(graphical, noWindow); } [Fact] public void WarmedSteadyContactRefreshDoesNotAllocate() { using var lifetime = Lifetime(); RuntimeEntityRecord owner = Entity( lifetime, 0x70002089u, 1, PhysicsStateFlags.None); RuntimeEntityRecord target = Entity( lifetime, 0x7000208Au, 1, PhysicsStateFlags.None); RegisterDynamicShadow(lifetime, target); PhysicsSetPositionCollisionReport collision = Collisions(target.Key!.Value.LocalEntityId); Assert.False(Handle(lifetime, owner, 1d, collision)); _ = GC.GetAllocatedBytesForCurrentThread(); long before = GC.GetAllocatedBytesForCurrentThread(); for (int index = 0; index < 10_000; index++) _ = Handle(lifetime, owner, 1.1d, collision); long allocated = GC.GetAllocatedBytesForCurrentThread() - before; Assert.Equal(0, allocated); Assert.Equal(1, lifetime.Physics.CollisionReports.CaptureOwnership() .TrackedObjectCount); } private static RuntimeEntityObjectLifetime Lifetime() { var engine = new PhysicsEngine { DataCache = new PhysicsDataCache(), }; engine.AddLandblock( Landblock, new TerrainSurface(new byte[81], new float[256]), Array.Empty(), Array.Empty(), worldOffsetX: 0f, worldOffsetY: 0f); return new RuntimeEntityObjectLifetime(engine); } private static RuntimeEntityRecord Entity( RuntimeEntityObjectLifetime lifetime, uint guid, ushort incarnation, PhysicsStateFlags state) { RuntimeEntityRecord record = lifetime.RegisterEntity( Spawn(guid, incarnation, state)).Canonical!; lifetime.Entities.SetFullCell(record, Cell, Landblock | 0xFFFFu); lifetime.Entities.SetFinalPhysicsState(record, state); var body = new PhysicsBody { Position = new Vector3(12f, 12f, 7f), Orientation = Quaternion.Identity, State = state, LastUpdateTime = 1d, TransientState = TransientStateFlags.Active, }; body.SnapToCell(Cell, body.Position, body.Position); lifetime.Entities.SetPhysicsBody(record, body); record.ObjectClock.Activate(); lifetime.Physics.AcknowledgeSpatialProjection(record, spatial: true); return record; } private static void RegisterDynamicShadow( RuntimeEntityObjectLifetime lifetime, RuntimeEntityRecord record) => RegisterShadow( lifetime, record.Key!.Value.LocalEntityId, record.PhysicsBody!.State, isStatic: false); private static void RegisterShadow( RuntimeEntityObjectLifetime lifetime, uint localId, PhysicsStateFlags state, bool isStatic) => lifetime.Physics.Engine.ShadowObjects.Register( localId, gfxObjId: 0u, new Vector3(12f, 12f, 7f), Quaternion.Identity, radius: 0.4f, worldOffsetX: 0f, worldOffsetY: 0f, Landblock, ShadowCollisionType.Sphere, state: (uint)state, seedCellId: Cell, isStatic: isStatic); private static bool Handle( RuntimeEntityObjectLifetime lifetime, RuntimeEntityRecord owner, double time, in PhysicsSetPositionCollisionReport collision) => lifetime.Physics.HandleSetPositionCollisions( owner, owner.PositionAuthorityVersion, owner.SpatialAuthorityVersion, owner.VelocityAuthorityVersion, time, owner.PhysicsBody!.InContact, owner.PhysicsBody.OnWalkable, collision); private static RuntimeSetPositionCommand PlacementCommand( RuntimeEntityRecord owner, Vector3 position) { var request = new PhysicsSetPositionRequest( position, Quaternion.Identity, Cell, position, [new FlatCollisionSphere(Vector3.Zero, 0.4f)], Scale: 1f, StepUpHeight: 0.4f, StepDownHeight: 0.4f, MoverPhysicsState: owner.FinalPhysicsState, MovingEntityId: owner.Key!.Value.LocalEntityId, Flags: PhysicsSetPositionFlags.Placement | PhysicsSetPositionFlags.Slide, CurrentCellId: Cell); return new RuntimeSetPositionCommand( request, RuntimeSetPositionOperationKind.RemoteAuthoritative, GameTime: 10d, ExpectedVelocityAuthorityVersion: owner.VelocityAuthorityVersion, ShadowWorldOffsetX: 0f, ShadowWorldOffsetY: 0f); } private static PhysicsSetPositionCollisionReport Collisions( params uint[] objectIds) => Collisions( objectIds, environmentCollision: false); private static PhysicsSetPositionCollisionReport Collisions( uint objectId, bool environmentCollision) => Collisions( [objectId], environmentCollision); private static PhysicsSetPositionCollisionReport Collisions( uint first, uint second, bool environmentCollision) => Collisions( [first, second], environmentCollision); private static PhysicsSetPositionCollisionReport Collisions( uint[] objectIds, bool environmentCollision) => new( ContactPlaneValid: false, ContactPlane: default, ContactPlaneCellId: 0u, ContactPlaneIsWater: false, LastKnownContactPlaneValid: false, LastKnownContactPlane: default, LastKnownContactPlaneCellId: 0u, LastKnownContactPlaneIsWater: false, SlidingNormalValid: false, SlidingNormal: default, CollisionNormalValid: false, CollisionNormal: default, CollidedWithEnvironment: environmentCollision, FramesStationaryFall: 0, AdjustOffset: default, LastCollidedObjectId: objectIds.Length == 0 ? null : objectIds[^1], CollidedObjectIds: objectIds.ToImmutableArray()); private static void AssertReport( RuntimeCollisionReport report, RuntimeCollisionReportKind kind, RuntimeEntityRecord recipient, RuntimeEntityRecord? other) { Assert.Equal(kind, report.Kind); Assert.Equal(recipient.Key, report.Recipient); Assert.Equal(recipient.ServerGuid, report.RecipientServerGuid); Assert.Equal(other?.Key, report.Other); Assert.Equal(other?.ServerGuid, report.OtherServerGuid); } private static WorldSession.EntitySpawn Spawn( uint guid, ushort instance, PhysicsStateFlags state) { var position = new CreateObject.ServerPosition( Cell, 12f, 12f, 7f, 1f, 0f, 0f, 0f); var timestamps = new PhysicsTimestamps( Position: 1, Movement: 1, State: 1, Vector: 1, Teleport: 0, ServerControlledMove: 1, ForcePosition: 0, ObjDesc: 1, Instance: instance); var physics = new PhysicsSpawnData( RawState: (uint)state, Position: position, Movement: null, AnimationFrame: null, SetupTableId: 0x02000001u, MotionTableId: 0x09000001u, SoundTableId: null, PhysicsScriptTableId: null, Parent: null, Children: null, Scale: null, Friction: null, Elasticity: null, Translucency: null, Velocity: null, Acceleration: null, AngularVelocity: null, DefaultScriptType: null, DefaultScriptIntensity: null, Timestamps: timestamps); return new WorldSession.EntitySpawn( guid, position, 0x02000001u, Array.Empty(), Array.Empty(), Array.Empty(), null, null, "collision-report-fixture", null, null, 0x09000001u, PhysicsState: (uint)state, InstanceSequence: instance, MovementSequence: 1, ServerControlSequence: 1, PositionSequence: 1, Physics: physics); } private sealed class CollisionObserver( Action? onReport = null) : IRuntimeCollisionReportObserver { internal List Reports { get; } = []; public void OnCollisionReport(in RuntimeCollisionReport report) { Reports.Add(report); onReport?.Invoke(report); } } private sealed class ThrowingCollisionObserver : IRuntimeCollisionReportObserver { public void OnCollisionReport(in RuntimeCollisionReport report) => throw new InvalidOperationException( $"fixture failure {report.Sequence}"); } private sealed class ReentrantRemotePlacement(PhysicsBody body) : IRuntimeRemotePlacement { private Func? _readCell; private Action? _writeCell; private uint _cellId; public PhysicsBody Body { get; } = body; public uint CellId { get => _readCell?.Invoke() ?? _cellId; set { _cellId = value; _writeCell?.Invoke(value); } } public bool Airborne { get; set; } public Vector3 LastServerPosition { get; set; } public double LastServerPositionTime { get; set; } public Vector3 LastShadowSyncPosition { get; set; } public Quaternion LastShadowSyncOrientation { get; set; } internal Action? OnHitGround { get; init; } internal Action? OnLeaveGround { get; init; } public void BindCanonicalCell(Func read, Action write) { _readCell = read; _writeCell = write; } public void HitGround() => OnHitGround?.Invoke(); public void LeaveGround() => OnLeaveGround?.Invoke(); } }