From 99bf1751bbb0e6b2cd5b95e08a2f3f42e3a05c5a Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 1 Aug 2026 16:05:46 +0200 Subject: [PATCH] feat(runtime): quiesce collision prefix replacements --- .../Physics/RuntimePhysicsState.cs | 74 +- .../Physics/RuntimeSetPositionState.cs | 826 ++++++++++++- .../RuntimeCollisionPrefixQuiescenceTests.cs | 1037 +++++++++++++++++ 3 files changed, 1897 insertions(+), 40 deletions(-) create mode 100644 tests/AcDream.Runtime.Tests/Physics/RuntimeCollisionPrefixQuiescenceTests.cs diff --git a/src/AcDream.Runtime/Physics/RuntimePhysicsState.cs b/src/AcDream.Runtime/Physics/RuntimePhysicsState.cs index 67fd2f26..7b3c2e06 100644 --- a/src/AcDream.Runtime/Physics/RuntimePhysicsState.cs +++ b/src/AcDream.Runtime/Physics/RuntimePhysicsState.cs @@ -35,6 +35,8 @@ public readonly record struct RuntimePhysicsOwnershipSnapshot( int PendingCollisionSetPositionDispatchCount, int PendingShadowSetPositionDispatchCount, bool IsCollisionReportDispatching, + int CollisionPrefixQuiescenceCount, + int PendingCollisionPrefixProjectionCount, int CollisionAdmissionCount, int CollisionGenerationCount, bool OwnsProductionDataCache, @@ -71,6 +73,8 @@ public readonly record struct RuntimePhysicsOwnershipSnapshot( && PendingCollisionSetPositionDispatchCount == 0 && PendingShadowSetPositionDispatchCount == 0 && !IsCollisionReportDispatching + && CollisionPrefixQuiescenceCount == 0 + && PendingCollisionPrefixProjectionCount == 0 && CollisionAdmissionCount == 0 && CollisionGenerationCount == 0 && OwnsProductionDataCache; @@ -1169,6 +1173,8 @@ public sealed class RuntimePhysicsState : IDisposable collisionReports.PendingSetPositionDispatchCount, Engine.ShadowObjects.PendingSetPositionDispatchCount, collisionReports.IsDispatching, + setPosition.CollisionPrefixQuiescenceCount, + setPosition.PendingQuiescenceProjectionCount, _collisionAdmissions.Count, _collisionGenerations.Count, ReferenceEquals(Engine.DataCache, DataCache), @@ -1901,6 +1907,64 @@ public sealed class RuntimePhysicsState : IDisposable _spatialRoots.Clear(); } + internal RuntimeCollisionPrefixQuiescenceToken + BeginCollisionPrefixQuiescence( + uint landblockId, + ulong collisionGeneration, + bool includeOutdoorCells) + { + EnsureNotDisposed(); + EnsureCollisionMutationThread(); + uint canonical = CanonicalLandblock(landblockId); + if (canonical == 0u) + throw new ArgumentOutOfRangeException(nameof(landblockId)); + return SetPosition.BeginCollisionPrefixQuiescence( + canonical, + collisionGeneration, + includeOutdoorCells); + } + + internal bool TryAcquireCollisionPrefixMutationPermission( + in RuntimeCollisionPrefixQuiescenceToken token, + out RuntimeCollisionPrefixMutationPermission permission) + { + EnsureNotDisposed(); + EnsureCollisionMutationThread(); + return SetPosition.TryAcquireCollisionPrefixMutationPermission( + token, + out permission); + } + + internal bool IsCollisionPrefixMutationPermissionCurrent( + in RuntimeCollisionPrefixMutationPermission permission) + { + EnsureNotDisposed(); + EnsureCollisionMutationThread(); + return SetPosition.IsCollisionPrefixMutationPermissionCurrent( + permission); + } + + internal bool CancelCollisionPrefixQuiescence( + in RuntimeCollisionPrefixQuiescenceToken token, + ulong successorGeneration = 0UL, + bool successorReady = false) + { + EnsureNotDisposed(); + EnsureCollisionMutationThread(); + return SetPosition.CancelCollisionPrefixQuiescence( + token, + successorGeneration, + successorReady); + } + + internal bool CompleteCollisionPrefixQuiescence( + in RuntimeCollisionPrefixMutationPermission permission) + { + EnsureNotDisposed(); + EnsureCollisionMutationThread(); + return SetPosition.CompleteCollisionPrefixQuiescence(permission); + } + public RuntimeCollisionAdmission BeginCollisionAdmission( uint landblockId) { @@ -2598,6 +2662,9 @@ public sealed class RuntimePhysicsState : IDisposable private void AdvanceCollisionWorldAuthority() => _collisionWorldAuthority = checked(_collisionWorldAuthority + 1UL); + internal void AdvanceCollisionQuiescenceAuthority() => + AdvanceCollisionWorldAuthority(); + internal bool TrySealCollisionEvaluationAuthority( in PhysicsSetPositionResult result, ulong expectedCollisionWorldAuthority, @@ -2639,7 +2706,8 @@ public sealed class RuntimePhysicsState : IDisposable } foreach (uint prefix in prefixes) { - if (_collisionAdmissions.ContainsKey(prefix)) + if (_collisionAdmissions.ContainsKey(prefix) + || SetPosition.IsCollisionPrefixQuiescing(prefix)) return false; } @@ -2680,6 +2748,8 @@ public sealed class RuntimePhysicsState : IDisposable { if (generation.LandblockId == 0u || _collisionAdmissions.ContainsKey(generation.LandblockId) + || SetPosition.IsCollisionPrefixQuiescing( + generation.LandblockId) || CollisionGenerationAuthority(generation.LandblockId) != generation.Generation) { @@ -2706,6 +2776,8 @@ public sealed class RuntimePhysicsState : IDisposable { if (generation.LandblockId == 0u || _collisionAdmissions.ContainsKey(generation.LandblockId) + || SetPosition.IsCollisionPrefixQuiescing( + generation.LandblockId) || CollisionGenerationAuthority(generation.LandblockId) != generation.Generation) { diff --git a/src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs b/src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs index dbe197f2..f651eb92 100644 --- a/src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs +++ b/src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs @@ -30,6 +30,7 @@ internal enum RuntimeEntityPlacementStage AwaitingPreparation, AwaitingWithdrawalAcknowledgement, AwaitingCell, + QuiescenceHeld, AwaitingFinalShadowPreparation, AwaitingCommitAcknowledgement, CancelledAwaitingAcknowledgement, @@ -89,6 +90,26 @@ internal readonly record struct RuntimeCollisionGenerationAuthority( uint LandblockId, ulong Generation); +internal readonly record struct RuntimeCollisionPrefixQuiescenceToken( + ulong SessionLifetimeVersion, + uint LandblockPrefix, + ulong CollisionGeneration, + ulong OperationId) +{ + internal bool IsValid => LandblockPrefix != 0u + && (LandblockPrefix & 0xFFFFu) == 0u + && CollisionGeneration != 0UL + && OperationId != 0UL; +} + +internal readonly record struct RuntimeCollisionPrefixMutationPermission( + RuntimeCollisionPrefixQuiescenceToken Quiescence, + ImmutableArray Withdrawals) +{ + internal bool IsValid => Quiescence.IsValid + && !Withdrawals.IsDefault; +} + internal readonly record struct RuntimeCollisionEvaluationAuthority( ulong CollisionWorldAuthority, ulong ShadowWorldAuthority, @@ -229,7 +250,9 @@ internal readonly record struct RuntimeSetPositionOwnershipSnapshot( int UnboundDeferredCellCount, int UnboundDeferredCellOrderCount, int PreparedMoverCount, - int MoverPreparationAuthorityCount) + int MoverPreparationAuthorityCount, + int CollisionPrefixQuiescenceCount, + int PendingQuiescenceProjectionCount) { internal bool IndexesConsistent => LostDeadlineCount == LostDeadlineNodeCount @@ -253,7 +276,9 @@ internal readonly record struct RuntimeSetPositionOwnershipSnapshot( && UnboundDeferredCellCount == 0 && UnboundDeferredCellOrderCount == 0 && PreparedMoverCount == 0 - && MoverPreparationAuthorityCount == 0; + && MoverPreparationAuthorityCount == 0 + && CollisionPrefixQuiescenceCount == 0 + && PendingQuiescenceProjectionCount == 0; } /// @@ -273,8 +298,13 @@ internal sealed class RuntimeSetPositionState : IDisposable private readonly record struct CellGenerationKey( uint CellId, + uint CollisionPrefix, ulong CollisionGeneration); + private readonly record struct UnboundCellKey( + uint CellId, + uint CollisionPrefix); + private readonly record struct MoverPreparationAuthority( ulong OperationId, CreateObject.ServerPosition AcceptedPosition, @@ -307,8 +337,10 @@ internal sealed class RuntimeSetPositionState : IDisposable internal ulong PlacementCommitVersion { get; set; } internal uint ExactCellId { get; set; } internal ulong CollisionGeneration { get; set; } + internal uint CollisionPrefix { get; set; } internal bool WithdrawalAcknowledged { get; set; } internal bool CollisionGenerationReady { get; set; } + internal bool CollisionQuiescenceHeld { get; set; } internal ulong ProjectionSequence { get; set; } internal bool WakeableLostCell { get; set; } internal RuntimeEntityPlacementStage Stage { get; set; } @@ -327,6 +359,24 @@ internal sealed class RuntimeSetPositionState : IDisposable } } + private sealed class CollisionPrefixQuiescence + { + internal required RuntimeCollisionPrefixQuiescenceToken Token + { get; init; } + internal required bool IncludeOutdoorCells { get; init; } + internal required ulong ProjectionBarrierSequence { get; init; } + internal SortedDictionary + PendingWithdrawals { get; } = []; + internal SortedDictionary + PendingRestorePlacements { get; } = []; + internal List RetainedWithdrawals + { get; } = []; + internal bool ResidentsParked { get; set; } + internal bool PermissionIssued { get; set; } + internal bool AbortReleaseInProgress { get; set; } + internal ulong AbortRestoreGeneration { get; set; } + } + private sealed class ContactCommitGuard( RuntimeSetPositionState owner, Operation operation, @@ -354,9 +404,9 @@ internal sealed class RuntimeSetPositionState : IDisposable private SortedDictionary _pendingProjection = []; private readonly List _deferredBucketOrder = []; - private readonly Dictionary> + private readonly Dictionary> _unboundDeferredByCell = []; - private readonly List _unboundDeferredCellOrder = []; + private readonly List _unboundDeferredCellOrder = []; private readonly Dictionary _lostDeadlines = []; private readonly List _lostDeadlineNodes = []; private readonly Dictionary @@ -365,11 +415,14 @@ internal sealed class RuntimeSetPositionState : IDisposable _preparedMovers = []; private readonly Dictionary _moverPreparationAuthorities = []; + private readonly Dictionary + _collisionPrefixQuiescence = []; private readonly LinkedList _expiredLostCells = []; private readonly Dictionary> _expiredLostCellNodes = []; private ulong _nextProjectionSequence; private ulong _nextOperationId; + private ulong _nextCollisionPrefixQuiescenceOperationId; private ulong _nextLostDeadlineSequence; private RuntimeEntityObjectEventStream? _events; private bool _disposed; @@ -401,6 +454,14 @@ internal sealed class RuntimeSetPositionState : IDisposable if (operation.WakeableLostCell) deferred++; } + int pendingQuiescenceProjections = 0; + foreach (CollisionPrefixQuiescence quiescence + in _collisionPrefixQuiescence.Values) + { + pendingQuiescenceProjections += + quiescence.PendingWithdrawals.Count + + quiescence.PendingRestorePlacements.Count; + } return new RuntimeSetPositionOwnershipSnapshot( _operations.Count, awaitingPreparation, @@ -416,11 +477,217 @@ internal sealed class RuntimeSetPositionState : IDisposable _unboundDeferredByCell.Count, _unboundDeferredCellOrder.Count, _preparedMovers.Count, - _moverPreparationAuthorities.Count); + _moverPreparationAuthorities.Count, + _collisionPrefixQuiescence.Count, + pendingQuiescenceProjections); } internal int PendingProjectionCount => _pendingProjection.Count; + internal bool IsCollisionPrefixQuiescing(uint landblockId) => + _collisionPrefixQuiescence.ContainsKey( + landblockId & 0xFFFF0000u); + + internal RuntimeCollisionPrefixQuiescenceToken + BeginCollisionPrefixQuiescence( + uint landblockId, + ulong collisionGeneration, + bool includeOutdoorCells) + { + EnsureNotDisposed(); + if (collisionGeneration == 0UL) + throw new ArgumentOutOfRangeException(nameof(collisionGeneration)); + uint prefix = landblockId & 0xFFFF0000u; + if (prefix == 0u) + throw new ArgumentOutOfRangeException(nameof(landblockId)); + + if (_collisionPrefixQuiescence.TryGetValue( + prefix, + out CollisionPrefixQuiescence? active) + && active.AbortReleaseInProgress) + { + throw new InvalidOperationException( + $"Collision quiescence 0x{prefix:X8}/{active.Token.OperationId} is restoring its retained generation."); + } + _collisionPrefixQuiescence.Remove( + prefix, + out CollisionPrefixQuiescence? superseded); + + var token = new RuntimeCollisionPrefixQuiescenceToken( + _entities.SessionLifetimeVersion, + prefix, + collisionGeneration, + checked(++_nextCollisionPrefixQuiescenceOperationId)); + var replacement = new CollisionPrefixQuiescence + { + Token = token, + IncludeOutdoorCells = includeOutdoorCells, + ProjectionBarrierSequence = superseded is null + ? _nextProjectionSequence + : Math.Max( + _nextProjectionSequence, + superseded.ProjectionBarrierSequence), + ResidentsParked = superseded?.ResidentsParked ?? false, + }; + if (superseded is not null) + { + foreach ((ulong sequence, RuntimePlacementProjectionToken pending) + in superseded.PendingWithdrawals) + replacement.PendingWithdrawals.Add(sequence, pending); + replacement.RetainedWithdrawals.AddRange( + superseded.RetainedWithdrawals); + } + _collisionPrefixQuiescence.Add(prefix, replacement); + if (superseded is not null) + { + RebindQuiescedDeferredOperations( + superseded.Token, + collisionGeneration, + ready: false); + } + _physics.AdvanceCollisionQuiescenceAuthority(); + return token; + } + + internal bool TryAcquireCollisionPrefixMutationPermission( + in RuntimeCollisionPrefixQuiescenceToken token, + out RuntimeCollisionPrefixMutationPermission permission) + { + EnsureNotDisposed(); + permission = default; + if (!TryGetCurrentQuiescence(token, out CollisionPrefixQuiescence? state)) + return false; + CollisionPrefixQuiescence current = state!; + + if (HasPendingProjectionThrough(current.ProjectionBarrierSequence)) + return false; + if (HasOldPrefixPlacementDebt(current)) + return false; + + if (!current.ResidentsParked) + { + ParkCollisionResidentsForQuiescence(current); + current.ResidentsParked = true; + return false; + } + else if (HasAffectedCollisionResident( + token.LandblockPrefix, + current.IncludeOutdoorCells)) + { + ParkCollisionResidentsForQuiescence(current); + return false; + } + + RemoveRetiredQuiescenceWithdrawals(current); + if (current.PendingWithdrawals.Count != 0 + || HasAffectedCollisionResident( + token.LandblockPrefix, + current.IncludeOutdoorCells) + || HasOldPrefixPlacementDebt(current) + || HasCollisionDispatchDebt()) + { + return false; + } + + current.PermissionIssued = true; + permission = new RuntimeCollisionPrefixMutationPermission( + current.Token, + current.RetainedWithdrawals.ToImmutableArray()); + return true; + } + + internal bool IsCollisionPrefixMutationPermissionCurrent( + in RuntimeCollisionPrefixMutationPermission permission) + { + EnsureNotDisposed(); + return permission.IsValid + && TryGetCurrentQuiescence( + permission.Quiescence, + out CollisionPrefixQuiescence? state) + && state!.PermissionIssued + && state.PendingWithdrawals.Count == 0 + && !HasAffectedCollisionResident( + state.Token.LandblockPrefix, + state.IncludeOutdoorCells) + && !HasOldPrefixPlacementDebt(state) + && !HasCollisionDispatchDebt(); + } + + internal bool CancelCollisionPrefixQuiescence( + in RuntimeCollisionPrefixQuiescenceToken token, + ulong successorGeneration = 0UL, + bool successorReady = false) + { + EnsureNotDisposed(); + if (!TryGetCurrentQuiescence(token, out CollisionPrefixQuiescence? state)) + return false; + + CollisionPrefixQuiescence current = state!; + if (!current.ResidentsParked + && current.PendingWithdrawals.Count == 0 + && current.PendingRestorePlacements.Count == 0 + && !HasQuiescedDeferredOperations(token.LandblockPrefix)) + { + bool removedBeforePark = _collisionPrefixQuiescence.Remove( + token.LandblockPrefix); + if (removedBeforePark) + _physics.AdvanceCollisionQuiescenceAuthority(); + return removedBeforePark; + } + + if (successorGeneration == 0UL + || !successorReady + || current.PendingWithdrawals.Count != 0) + { + return false; + } + + if (!current.AbortReleaseInProgress) + { + current.AbortReleaseInProgress = true; + current.AbortRestoreGeneration = successorGeneration; + current.PermissionIssued = false; + RebindQuiescedDeferredOperations( + token, + successorGeneration, + ready: true); + } + else if (current.AbortRestoreGeneration != successorGeneration) + { + return false; + } + + // A re-entrant/network placement may have joined the still-closed + // prefix after abort release started. Transfer every exact newcomer + // on each poll before deciding the barrier can open. + RebindQuiescedDeferredOperations( + token, + successorGeneration, + ready: true); + + if (current.PendingRestorePlacements.Count != 0 + || HasQuiescedDeferredOperations(token.LandblockPrefix)) + return false; + bool removed = _collisionPrefixQuiescence.Remove( + token.LandblockPrefix); + if (removed) + _physics.AdvanceCollisionQuiescenceAuthority(); + return removed; + } + + internal bool CompleteCollisionPrefixQuiescence( + in RuntimeCollisionPrefixMutationPermission permission) + { + EnsureNotDisposed(); + if (!IsCollisionPrefixMutationPermissionCurrent(permission)) + return false; + bool removed = _collisionPrefixQuiescence.Remove( + permission.Quiescence.LandblockPrefix); + if (removed) + _physics.AdvanceCollisionQuiescenceAuthority(); + return removed; + } + internal void BindEventStream(RuntimeEntityObjectEventStream events) { EnsureNotDisposed(); @@ -923,6 +1190,7 @@ internal sealed class RuntimeSetPositionState : IDisposable { var bucketKey = new CellGenerationKey( result.CellId, + result.CellId & 0xFFFF0000u, deferredCollisionGeneration); if (_deferredByCellGeneration.TryGetValue( bucketKey, @@ -1064,6 +1332,7 @@ internal sealed class RuntimeSetPositionState : IDisposable operation.WakeableLostCell = true; operation.CollisionGeneration = prepared .DeferredCollisionGeneration; + operation.CollisionPrefix = result.CellId & 0xFFFF0000u; operation.CollisionGenerationReady = false; operation.Stage = RuntimeEntityPlacementStage.AwaitingCell; _preparedMovers[operation.Key] = prepared.Evaluation.Command.Physics; @@ -1071,6 +1340,7 @@ internal sealed class RuntimeSetPositionState : IDisposable { var bucketKey = new CellGenerationKey( result.CellId, + operation.CollisionPrefix, prepared.DeferredCollisionGeneration); if (prepared.DeferredBucketIsNew) { @@ -1712,6 +1982,41 @@ internal sealed class RuntimeSetPositionState : IDisposable return Outcome(RuntimeSetPositionStatus.Rejected, invalid, default); } + if (TryGetBlockingQuiescence( + canonicalRequest, + out CollisionPrefixQuiescence? quiescence)) + { + var deferred = new PhysicsSetPositionResult( + PhysicsSetPositionError.Ok, + PhysicsResidenceDisposition.DeferredCell, + canonicalRequest.Position, + canonicalRequest.Orientation, + canonicalRequest.CellId, + canonicalRequest.CellLocalPosition, + InContact: operation.PreviousContact, + OnWalkable: operation.PreviousOnWalkable, + ContactPlane: body.ContactPlane, + ContactPlaneCellId: body.ContactPlaneCellId, + ContactPlaneIsWater: body.ContactPlaneIsWater, + SlidingNormalValid: body.SlidingNormal != Vector3.Zero, + SlidingNormal: body.SlidingNormal, + FramesStationaryFall: body.FramesStationaryFall, + CrossCellIds: ImmutableArray.Empty, + CollidedObjectIds: ImmutableArray.Empty, + QueriedCellIds: ImmutableArray.Empty); + operation.Result = deferred; + operation.RequiresPreparation = false; + operation.ExactCellId = deferred.CellId; + _preparedMovers[operation.Key] = canonicalRequest; + return ParkDeferred( + operation, + deferred, + collisionGenerationOverride: + quiescence!.Token.CollisionGeneration, + collisionPrefixOverride: + quiescence.Token.LandblockPrefix); + } + PhysicsSetPositionResult result = _physics.Engine.SetPosition( canonicalRequest, @@ -1726,6 +2031,27 @@ internal sealed class RuntimeSetPositionState : IDisposable report)); if (!IsCurrent(operation)) return Outcome(RuntimeSetPositionStatus.Cancelled, result, default); + if (result.IsSuccessful + && TryGetBlockingQuiescence( + result, + out CollisionPrefixQuiescence? queriedQuiescence)) + { + PhysicsSetPositionResult held = result with + { + Residence = PhysicsResidenceDisposition.DeferredCell, + }; + operation.Result = held; + operation.RequiresPreparation = false; + operation.ExactCellId = held.CellId; + _preparedMovers[operation.Key] = canonicalRequest; + return ParkDeferred( + operation, + held, + collisionGenerationOverride: + queriedQuiescence!.Token.CollisionGeneration, + collisionPrefixOverride: + queriedQuiescence.Token.LandblockPrefix); + } operation.Result = result; if (!result.IsSuccessful) { @@ -1791,6 +2117,7 @@ internal sealed class RuntimeSetPositionState : IDisposable if (pending.Kind is RuntimePlacementProjectionKind.Discard) { _pendingProjection.Remove(token.Sequence); + RetireQuiescenceProjectionSequence(token.Sequence); return true; } if (!_operations.TryGetValue( @@ -1805,6 +2132,7 @@ internal sealed class RuntimeSetPositionState : IDisposable } _pendingProjection.Remove(token.Sequence); + RetireQuiescenceProjectionSequence(token.Sequence); operation.ProjectionSequence = 0UL; if (pending.Kind is RuntimePlacementProjectionKind.Place) { @@ -1823,7 +2151,9 @@ internal sealed class RuntimeSetPositionState : IDisposable operation.Stage = operation.RequiresPreparation || operation.InheritedLostDeadline ? RuntimeEntityPlacementStage.AwaitingPreparation - : RuntimeEntityPlacementStage.AwaitingCell; + : operation.CollisionQuiescenceHeld + ? RuntimeEntityPlacementStage.QuiescenceHeld + : RuntimeEntityPlacementStage.AwaitingCell; if (operation.PreparedCommandAwaitingWithdrawalAck is { } prepared) { operation.PreparedCommandAwaitingWithdrawalAck = null; @@ -2004,20 +2334,26 @@ internal sealed class RuntimeSetPositionState : IDisposable uint prefix = landblockId & 0xFFFF0000u; var roots = new List(); _physics.CopySpatialRootsTo(roots); - for (int index = 0; index < roots.Count; index++) + RuntimeEntityRecord[] affected = roots + .Where(record => IsAffectedCollisionResident( + record, + prefix, + includeOutdoorCells)) + .ToArray(); + for (int index = 0; index < affected.Length; index++) { - RuntimeEntityRecord record = roots[index]; - if (IsAffectedCollisionResident( - record, - prefix, - includeOutdoorCells) - && record.Key is { } key + if (affected[index].Key is { } key && _operations.ContainsKey(key)) { throw new InvalidOperationException( - $"Collision retirement for 0x{prefix:X8} cannot overlap active placement for 0x{record.ServerGuid:X8}/{record.Incarnation}."); + $"Collision retirement for 0x{prefix:X8} cannot overlap active placement for 0x{affected[index].ServerGuid:X8}/{affected[index].Incarnation}."); } } + // Close every exact collision-report owner before the first force-end + // callback can re-enter. The prefix admission barrier is already in + // place for transactional callers, so a callback cannot evaluate a + // new placement against the retiring rows. + _physics.CollisionReports.LeaveWorldBatch(affected); var stagedWithdrawals = new List( roots.Count); for (int index = 0; index < roots.Count; index++) @@ -2029,12 +2365,15 @@ internal sealed class RuntimeSetPositionState : IDisposable prefix, includeOutdoorCells) || record.Key is not { } key - || record.PhysicsBody is not { } body) + || record.PhysicsBody is not { } body + || _operations.ContainsKey(key) + || !affected.Any(candidate => + ReferenceEquals(candidate, record) + && candidate.Key == key)) { continue; } - PublishCancellation(CancelCore(key)); bool hasPrepared = _preparedMovers.TryGetValue( key, out PhysicsSetPositionRequest prepared); @@ -2116,10 +2455,16 @@ internal sealed class RuntimeSetPositionState : IDisposable body.Orientation), prepared: hasPrepared, command); + CollisionPrefixQuiescence? quiescence = + _collisionPrefixQuiescence.GetValueOrDefault(prefix); RuntimeSetPositionOutcome parked = ParkDeferred( operation, result, - publishImmediately: false); + publishImmediately: false, + collisionGenerationOverride: + quiescence?.Token.CollisionGeneration ?? 0UL, + collisionPrefixOverride: + quiescence?.Token.LandblockPrefix ?? 0u); if (_pendingProjection.TryGetValue( parked.Projection.Sequence, out RuntimePlacementProjectionSnapshot staged)) @@ -2157,13 +2502,289 @@ internal sealed class RuntimeSetPositionState : IDisposable bool includeOutdoorCells) { uint cellId = record.FullCellId; - return (cellId & 0xFFFF0000u) == prefix - && (includeOutdoorCells || (cellId & 0xFFFFu) >= 0x0100u) + bool exactResidence = (cellId & 0xFFFF0000u) == prefix + && (includeOutdoorCells || (cellId & 0xFFFFu) >= 0x0100u); + return exactResidence + && record.Key is not null + && _entities.IsCurrent(record) + && record.PhysicsBody is not null + && _physics.IsSpatialRoot(record) && (record.FinalPhysicsState & PhysicsStateFlags.Static) == 0 && !_entities.ParentAttachments.HasCommittedParent( record.ServerGuid); } + private void ParkCollisionResidentsForQuiescence( + CollisionPrefixQuiescence state) + { + if (!TryGetCurrentQuiescence(state.Token, out CollisionPrefixQuiescence? current) + || !ReferenceEquals(current, state)) + { + return; + } + ParkCollisionResidents( + state.Token.LandblockPrefix, + state.IncludeOutdoorCells); + } + + private bool HasAffectedCollisionResident( + uint prefix, + bool includeOutdoorCells) + { + var roots = new List(); + _physics.CopySpatialRootsTo(roots); + for (int index = 0; index < roots.Count; index++) + { + if (IsAffectedCollisionResident( + roots[index], + prefix, + includeOutdoorCells)) + { + return true; + } + } + return false; + } + + private bool TryGetCurrentQuiescence( + in RuntimeCollisionPrefixQuiescenceToken token, + out CollisionPrefixQuiescence? state) + { + if (token.IsValid + && token.SessionLifetimeVersion == _entities.SessionLifetimeVersion + && _collisionPrefixQuiescence.TryGetValue( + token.LandblockPrefix, + out state) + && state.Token == token) + { + return true; + } + state = null; + return false; + } + + private bool HasPendingProjectionThrough(ulong barrierSequence) => + barrierSequence != 0UL + && _pendingProjection.Count != 0 + && _pendingProjection.First().Key <= barrierSequence; + + private bool HasCollisionDispatchDebt() + { + RuntimeCollisionReportingOwnershipSnapshot reports = + _physics.CollisionReports.CaptureOwnership(); + return reports.PendingReportCount != 0 + || reports.LeavingOwnerCount != 0 + || reports.AdmissionBlockedOwnerCount != 0 + || reports.PendingSetPositionDispatchCount != 0 + || reports.IsDispatching + || _physics.Engine.ShadowObjects + .PendingSetPositionDispatchCount != 0; + } + + private bool HasOldPrefixPlacementDebt(CollisionPrefixQuiescence state) + { + uint prefix = state.Token.LandblockPrefix; + foreach (Operation operation in _operations.Values) + { + if (operation.WakeableLostCell) + continue; + if (PlacementTouchesPrefix(operation.Command.Physics, prefix) + || ResultTouchesPrefix(operation.Result, prefix) + || _moverPreparationAuthorities.TryGetValue( + operation.Key, + out MoverPreparationAuthority preparation) + && preparation.OperationId + == operation.Token.OperationId + && (preparation.AcceptedPosition.LandblockId + & 0xFFFF0000u) == prefix + || IsAffectedCollisionResident( + operation.Record, + prefix, + state.IncludeOutdoorCells)) + { + return true; + } + } + return false; + } + + private static bool PlacementTouchesPrefix( + in PhysicsSetPositionRequest request, + uint prefix) => + (request.CellId & 0xFFFF0000u) == prefix + || request.CurrentCellId is uint current + && (current & 0xFFFF0000u) == prefix; + + private static bool ResultTouchesPrefix( + in PhysicsSetPositionResult result, + uint prefix) + { + if ((result.CellId & 0xFFFF0000u) == prefix) + return true; + if (!result.QueriedCellIds.IsDefaultOrEmpty) + { + foreach (uint cellId in result.QueriedCellIds) + { + if ((cellId & 0xFFFF0000u) == prefix) + return true; + } + } + return false; + } + + private bool TryGetBlockingQuiescence( + in PhysicsSetPositionRequest request, + out CollisionPrefixQuiescence? state) + { + state = null; + foreach (CollisionPrefixQuiescence candidate + in _collisionPrefixQuiescence.Values) + { + if (PlacementTouchesPrefix( + request, + candidate.Token.LandblockPrefix) + && (state is null + || candidate.Token.OperationId + < state.Token.OperationId)) + { + state = candidate; + } + } + return state is not null; + } + + private bool TryGetBlockingQuiescence( + in PhysicsSetPositionResult result, + out CollisionPrefixQuiescence? state, + in RuntimeCollisionPrefixQuiescenceToken excluded = default) + { + state = null; + foreach (CollisionPrefixQuiescence candidate + in _collisionPrefixQuiescence.Values) + { + if (excluded.IsValid && candidate.Token == excluded) + continue; + if (ResultTouchesPrefix( + result, + candidate.Token.LandblockPrefix) + && (state is null + || candidate.Token.OperationId + < state.Token.OperationId)) + { + state = candidate; + } + } + return state is not null; + } + + private void TrackQuiescenceWithdrawal( + Operation operation, + in RuntimePlacementProjectionSnapshot snapshot) + { + if (snapshot.Kind is not RuntimePlacementProjectionKind.Withdraw + || !_collisionPrefixQuiescence.TryGetValue( + operation.CollisionPrefix, + out CollisionPrefixQuiescence? state) + || snapshot.Token.CollisionGeneration + != state.Token.CollisionGeneration) + { + return; + } + state.PendingWithdrawals[snapshot.Token.Sequence] = snapshot.Token; + state.RetainedWithdrawals.Add(snapshot.Token); + state.PermissionIssued = false; + } + + private void TrackQuiescenceRestorePlacement( + Operation operation, + in RuntimePlacementProjectionSnapshot snapshot) + { + if (snapshot.Kind is not RuntimePlacementProjectionKind.Place + || !_collisionPrefixQuiescence.TryGetValue( + operation.CollisionPrefix, + out CollisionPrefixQuiescence? state) + || !state.AbortReleaseInProgress) + { + return; + } + state.PendingRestorePlacements[snapshot.Token.Sequence] = + snapshot.Token; + } + + private void RetireQuiescenceProjectionSequence(ulong sequence) + { + foreach (CollisionPrefixQuiescence state + in _collisionPrefixQuiescence.Values) + { + if (state.PendingWithdrawals.TryGetValue( + sequence, + out _)) + { + state.PendingWithdrawals.Remove(sequence); + state.PermissionIssued = false; + return; + } + if (state.PendingRestorePlacements.Remove(sequence)) + return; + } + } + + private void RemoveRetiredQuiescenceWithdrawals( + CollisionPrefixQuiescence state) + { + if (state.PendingWithdrawals.Count == 0) + return; + ulong[] stale = state.PendingWithdrawals + .Where(pair => !_pendingProjection.ContainsKey(pair.Key)) + .Select(pair => pair.Key) + .ToArray(); + for (int index = 0; index < stale.Length; index++) + state.PendingWithdrawals.Remove(stale[index]); + } + + private void RebindQuiescedDeferredOperations( + in RuntimeCollisionPrefixQuiescenceToken token, + ulong successorGeneration, + bool ready) + { + foreach (Operation operation in _operations.Values.ToArray()) + { + if (!operation.WakeableLostCell + || operation.CollisionGeneration != token.CollisionGeneration + || operation.CollisionPrefix != token.LandblockPrefix) + { + continue; + } + UnindexDeferred(operation); + operation.CollisionGeneration = successorGeneration; + operation.CollisionGenerationReady = ready + && successorGeneration != 0UL; + if (successorGeneration != 0UL) + IndexDeferred(operation); + else + IndexUnboundDeferred(operation); + if (operation.CollisionGenerationReady + && operation.WithdrawalAcknowledged) + { + RetryDeferred(operation); + } + } + } + + private bool HasQuiescedDeferredOperations(uint prefix) + { + foreach (Operation operation in _operations.Values) + { + if (operation.WakeableLostCell + && operation.CollisionQuiescenceHeld + && operation.CollisionPrefix == prefix) + { + return true; + } + } + return false; + } + internal void BeginCollisionGeneration(uint landblockId, ulong generation) { EnsureNotDisposed(); @@ -2172,17 +2793,20 @@ internal sealed class RuntimeSetPositionState : IDisposable uint prefix = landblockId & 0xFFFF0000u; for (int index = 0; index < _unboundDeferredCellOrder.Count;) { - uint cellId = _unboundDeferredCellOrder[index]; - if ((cellId & 0xFFFF0000u) != prefix + UnboundCellKey unboundKey = _unboundDeferredCellOrder[index]; + if (unboundKey.CollisionPrefix != prefix || !_unboundDeferredByCell.Remove( - cellId, + unboundKey, out List? retained)) { index++; continue; } _unboundDeferredCellOrder.RemoveAt(index); - var bucket = new CellGenerationKey(cellId, generation); + var bucket = new CellGenerationKey( + unboundKey.CellId, + unboundKey.CollisionPrefix, + generation); var rebound = new List(retained.Count); for (int entityIndex = 0; entityIndex < retained.Count; @@ -2191,7 +2815,8 @@ internal sealed class RuntimeSetPositionState : IDisposable RuntimeEntityKey entity = retained[entityIndex]; if (_operations.TryGetValue(entity, out Operation? operation) && operation.WakeableLostCell - && operation.ExactCellId == cellId + && operation.ExactCellId == unboundKey.CellId + && operation.CollisionPrefix == prefix && operation.CollisionGeneration == 0UL) { operation.CollisionGeneration = generation; @@ -2224,7 +2849,7 @@ internal sealed class RuntimeSetPositionState : IDisposable { if (!operation.WakeableLostCell || operation.CollisionGeneration != 0UL - || (operation.ExactCellId & 0xFFFF0000u) != prefix) + || operation.CollisionPrefix != prefix) { continue; } @@ -2241,7 +2866,7 @@ internal sealed class RuntimeSetPositionState : IDisposable uint prefix = landblockId & 0xFFFF0000u; CellGenerationKey[] matching = _deferredBucketOrder .Where(key => key.CollisionGeneration == generation - && (key.CellId & 0xFFFF0000u) == prefix) + && key.CollisionPrefix == prefix) .ToArray(); for (int index = 0; index < matching.Length; index++) { @@ -2264,7 +2889,7 @@ internal sealed class RuntimeSetPositionState : IDisposable { CellGenerationKey key = _deferredBucketOrder[index]; if (key.CollisionGeneration == generation - && (key.CellId & 0xFFFF0000u) == prefix) + && key.CollisionPrefix == prefix) { cells.Add(key); } @@ -2291,6 +2916,7 @@ internal sealed class RuntimeSetPositionState : IDisposable out Operation? operation) || !operation.WakeableLostCell || operation.ExactCellId != cell.CellId + || operation.CollisionPrefix != prefix || operation.CollisionGeneration != generation) { continue; @@ -2323,6 +2949,7 @@ internal sealed class RuntimeSetPositionState : IDisposable _lostDeadlineNodeIndex.Clear(); _preparedMovers.Clear(); _moverPreparationAuthorities.Clear(); + _collisionPrefixQuiescence.Clear(); _pendingProjection.Clear(); _expiredLostCells.Clear(); _expiredLostCellNodes.Clear(); @@ -2331,7 +2958,9 @@ internal sealed class RuntimeSetPositionState : IDisposable private RuntimeSetPositionOutcome ParkDeferred( Operation operation, in PhysicsSetPositionResult result, - bool publishImmediately = true) + bool publishImmediately = true, + ulong collisionGenerationOverride = 0UL, + uint collisionPrefixOverride = 0u) { PhysicsBody body = operation.Body!; body.Orientation = result.Orientation; @@ -2366,8 +2995,13 @@ internal sealed class RuntimeSetPositionState : IDisposable operation.WakeableLostCell = true; operation.EnteringWorldFromCelllessResidence = true; ArmLostFamilyDeadlines(operation); - operation.CollisionGeneration = _physics - .ExpectedCollisionGeneration(result.CellId); + operation.CollisionGeneration = collisionGenerationOverride != 0UL + ? collisionGenerationOverride + : _physics.ExpectedCollisionGeneration(result.CellId); + operation.CollisionPrefix = collisionPrefixOverride != 0u + ? collisionPrefixOverride + : result.CellId & 0xFFFF0000u; + operation.CollisionQuiescenceHeld = collisionPrefixOverride != 0u; operation.Command = operation.Command with { Physics = operation.Command.Physics with @@ -2398,7 +3032,9 @@ internal sealed class RuntimeSetPositionState : IDisposable IndexDeferred(operation); operation.Stage = operation.RequiresPreparation ? RuntimeEntityPlacementStage.AwaitingPreparation - : RuntimeEntityPlacementStage.AwaitingWithdrawalAcknowledgement; + : operation.CollisionQuiescenceHeld + ? RuntimeEntityPlacementStage.QuiescenceHeld + : RuntimeEntityPlacementStage.AwaitingWithdrawalAcknowledgement; RuntimePlacementProjectionToken projection = PublishProjection( operation, RuntimePlacementProjectionKind.Withdraw, @@ -2432,7 +3068,36 @@ internal sealed class RuntimeSetPositionState : IDisposable if (!IsDeferredWakePreparationCurrent(operation)) return; + RuntimeCollisionPrefixQuiescenceToken restoringQuiescence = default; + if (TryGetBlockingQuiescence( + operation.Command.Physics, + out CollisionPrefixQuiescence? blocking)) + { + if (blocking!.AbortReleaseInProgress + && operation.CollisionQuiescenceHeld + && operation.CollisionPrefix + == blocking.Token.LandblockPrefix) + { + // The old collision generation remains active. Keep the + // admission barrier closed to new commands while this exact + // parked operation restores and its Place receipt drains. + restoringQuiescence = blocking.Token; + } + else + { + UnindexDeferred(operation); + operation.CollisionPrefix = blocking.Token.LandblockPrefix; + operation.CollisionGeneration = blocking.Token.CollisionGeneration; + operation.CollisionGenerationReady = false; + operation.CollisionQuiescenceHeld = true; + operation.Stage = RuntimeEntityPlacementStage.QuiescenceHeld; + IndexDeferred(operation); + return; + } + } + UnindexDeferred(operation); + operation.CollisionQuiescenceHeld = false; operation.Command = operation.Command with { GameTime = _physics.PlacementSimulationTime( @@ -2456,11 +3121,34 @@ internal sealed class RuntimeSetPositionState : IDisposable operation.CollisionGenerationReady = false; if (!IsCurrent(operation)) return; + if (result.IsSuccessful + && TryGetBlockingQuiescence( + result, + out CollisionPrefixQuiescence? queriedQuiescence, + restoringQuiescence)) + { + PhysicsSetPositionResult held = result with + { + Residence = PhysicsResidenceDisposition.DeferredCell, + }; + operation.Result = held; + operation.ExactCellId = held.CellId; + operation.CollisionPrefix = + queriedQuiescence!.Token.LandblockPrefix; + operation.CollisionGeneration = + queriedQuiescence.Token.CollisionGeneration; + operation.CollisionQuiescenceHeld = true; + operation.Stage = RuntimeEntityPlacementStage.QuiescenceHeld; + _preparedMovers[operation.Key] = operation.Command.Physics; + IndexDeferred(operation); + return; + } if (result.IsDeferred) { operation.Result = result; operation.ExactCellId = result.CellId; _preparedMovers[operation.Key] = operation.Command.Physics; + operation.CollisionPrefix = result.CellId & 0xFFFF0000u; operation.CollisionGeneration = _physics .ExpectedCollisionGeneration(result.CellId); IndexDeferred(operation); @@ -2473,6 +3161,8 @@ internal sealed class RuntimeSetPositionState : IDisposable operation.RequiresPreparation = true; operation.Stage = RuntimeEntityPlacementStage .AwaitingPreparation; + operation.CollisionPrefix = operation.ExactCellId + & 0xFFFF0000u; operation.CollisionGeneration = _physics .ExpectedCollisionGeneration(operation.ExactCellId); IndexDeferred(operation); @@ -2480,6 +3170,8 @@ internal sealed class RuntimeSetPositionState : IDisposable else { operation.Stage = RuntimeEntityPlacementStage.AwaitingCell; + operation.CollisionPrefix = operation.ExactCellId + & 0xFFFF0000u; operation.CollisionGeneration = _physics .ExpectedCollisionGeneration(operation.ExactCellId); IndexDeferred(operation); @@ -2778,6 +3470,8 @@ internal sealed class RuntimeSetPositionState : IDisposable result.OnWalkable); operation.ProjectionSequence = sequence; _pendingProjection.Add(sequence, snapshot); + TrackQuiescenceWithdrawal(operation, snapshot); + TrackQuiescenceRestorePlacement(operation, snapshot); if (publishImmediately) PublishPlacement(snapshot); return token; @@ -3091,6 +3785,32 @@ internal sealed class RuntimeSetPositionState : IDisposable Kind = RuntimePlacementProjectionKind.Discard, }; _pendingProjection[operation.ProjectionSequence] = cancelled; + if (pending.Kind is RuntimePlacementProjectionKind.Withdraw) + { + foreach (CollisionPrefixQuiescence state + in _collisionPrefixQuiescence.Values) + { + if (state.PendingWithdrawals.ContainsKey( + operation.ProjectionSequence)) + { + state.PendingWithdrawals[ + operation.ProjectionSequence] = cancelled.Token; + for (int retainedIndex = 0; + retainedIndex < state.RetainedWithdrawals.Count; + retainedIndex++) + { + if (state.RetainedWithdrawals[retainedIndex].Sequence + == operation.ProjectionSequence) + { + state.RetainedWithdrawals[retainedIndex] = + cancelled.Token; + break; + } + } + break; + } + } + } operation.Stage = RuntimeEntityPlacementStage .CancelledAwaitingAcknowledgement; discard = cancelled; @@ -3126,6 +3846,7 @@ internal sealed class RuntimeSetPositionState : IDisposable } var bucket = new CellGenerationKey( operation.ExactCellId, + operation.CollisionPrefix, operation.CollisionGeneration); if (!_deferredByCellGeneration.TryGetValue( bucket, @@ -3139,6 +3860,25 @@ internal sealed class RuntimeSetPositionState : IDisposable entities.Add(operation.Key); } + private void IndexUnboundDeferred(Operation operation) + { + if (operation.ExactCellId == 0u) + return; + var key = new UnboundCellKey( + operation.ExactCellId, + operation.CollisionPrefix); + if (!_unboundDeferredByCell.TryGetValue( + key, + out List? entities)) + { + entities = []; + _unboundDeferredByCell.Add(key, entities); + _unboundDeferredCellOrder.Add(key); + } + if (!entities.Contains(operation.Key)) + entities.Add(operation.Key); + } + private void UnindexDeferred(Operation operation) { if (operation.ExactCellId == 0u) @@ -3147,8 +3887,11 @@ internal sealed class RuntimeSetPositionState : IDisposable } if (operation.CollisionGeneration == 0UL) { + var key = new UnboundCellKey( + operation.ExactCellId, + operation.CollisionPrefix); if (_unboundDeferredByCell.TryGetValue( - operation.ExactCellId, + key, out List? unbound)) { int unboundIndex = unbound.IndexOf(operation.Key); @@ -3160,14 +3903,15 @@ internal sealed class RuntimeSetPositionState : IDisposable } if (unbound.Count == 0) { - _unboundDeferredByCell.Remove(operation.ExactCellId); - _unboundDeferredCellOrder.Remove(operation.ExactCellId); + _unboundDeferredByCell.Remove(key); + _unboundDeferredCellOrder.Remove(key); } } return; } var bucket = new CellGenerationKey( operation.ExactCellId, + operation.CollisionPrefix, operation.CollisionGeneration); if (_deferredByCellGeneration.TryGetValue( bucket, @@ -3194,13 +3938,16 @@ internal sealed class RuntimeSetPositionState : IDisposable return; } RemoveDeferredBucket(bucket); + var unboundKey = new UnboundCellKey( + bucket.CellId, + bucket.CollisionPrefix); if (!_unboundDeferredByCell.TryGetValue( - bucket.CellId, + unboundKey, out List? unbound)) { unbound = []; - _unboundDeferredByCell.Add(bucket.CellId, unbound); - _unboundDeferredCellOrder.Add(bucket.CellId); + _unboundDeferredByCell.Add(unboundKey, unbound); + _unboundDeferredCellOrder.Add(unboundKey); } for (int index = 0; index < entities.Count; index++) { @@ -3208,6 +3955,7 @@ internal sealed class RuntimeSetPositionState : IDisposable if (!_operations.TryGetValue(key, out Operation? operation) || !operation.WakeableLostCell || operation.ExactCellId != bucket.CellId + || operation.CollisionPrefix != bucket.CollisionPrefix || operation.CollisionGeneration != bucket.CollisionGeneration) { continue; @@ -3219,8 +3967,8 @@ internal sealed class RuntimeSetPositionState : IDisposable } if (unbound.Count == 0) { - _unboundDeferredByCell.Remove(bucket.CellId); - _unboundDeferredCellOrder.Remove(bucket.CellId); + _unboundDeferredByCell.Remove(unboundKey); + _unboundDeferredCellOrder.Remove(unboundKey); } } diff --git a/tests/AcDream.Runtime.Tests/Physics/RuntimeCollisionPrefixQuiescenceTests.cs b/tests/AcDream.Runtime.Tests/Physics/RuntimeCollisionPrefixQuiescenceTests.cs new file mode 100644 index 00000000..b49bc996 --- /dev/null +++ b/tests/AcDream.Runtime.Tests/Physics/RuntimeCollisionPrefixQuiescenceTests.cs @@ -0,0 +1,1037 @@ +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; +using DatReaderWriter.Enums; +using DatReaderWriter.Types; + +namespace AcDream.Runtime.Tests.Physics; + +public sealed class RuntimeCollisionPrefixQuiescenceTests +{ + private const uint PrefixP = 0xA9B40000u; + private const uint CellP = PrefixP | 0x0001u; + private const uint PrefixQ = 0xAAB40000u; + private const uint CellQ = PrefixQ | 0x0001u; + + [Fact] + public void EarlierUnrelatedFifoHeadDrainsBeforeEveryResidentWithdraw() + { + using var fixture = new Fixture(); + RuntimeEntityRecord unrelated = fixture.Add( + 0x70003001u, + 1, + CellQ, + new Vector3(10f, 20f, 7f)); + RuntimeEntityRecord first = fixture.Add( + 0x70003002u, + 1, + CellP, + new Vector3(11f, 20f, 7f)); + RuntimeEntityRecord second = fixture.Add( + 0x70003003u, + 1, + CellP, + new Vector3(12f, 20f, 7f)); + RuntimeSetPositionOutcome unrelatedPlace = fixture.Place( + unrelated, + CellQ, + new Vector3(13f, 20f, 7f)); + RuntimeCollisionPrefixQuiescenceToken token = fixture.Begin(2UL); + + Assert.False(fixture.TryAcquire(token, out _)); + Assert.True(fixture.Lifetime.Physics.IsSpatialRoot(first)); + Assert.True(fixture.Lifetime.Physics.IsSpatialRoot(second)); + + Assert.True(fixture.Lifetime.Physics.SetPosition + .AcknowledgeProjection(unrelatedPlace.Projection)); + Assert.False(fixture.TryAcquire(token, out _)); + Assert.False(fixture.Lifetime.Physics.IsSpatialRoot(first)); + Assert.False(fixture.Lifetime.Physics.IsSpatialRoot(second)); + + var withdrawals = new List(); + while (fixture.Lifetime.Physics.SetPosition.TryPeekProjection( + out RuntimePlacementProjectionSnapshot pending)) + { + Assert.Equal(RuntimePlacementProjectionKind.Withdraw, pending.Kind); + withdrawals.Add(pending.Token); + Assert.True(fixture.Lifetime.Physics.SetPosition + .AcknowledgeProjection(pending.Token)); + } + + Assert.Equal(2, withdrawals.Count); + Assert.True(fixture.TryAcquire(token, out var permission)); + Assert.Equal(withdrawals, permission.Withdrawals); + Assert.True(fixture.Lifetime.Physics + .IsCollisionPrefixMutationPermissionCurrent(permission)); + Assert.True(fixture.Lifetime.Physics + .CompleteCollisionPrefixQuiescence(permission)); + Assert.False(fixture.Lifetime.Physics + .IsCollisionPrefixMutationPermissionCurrent(permission)); + } + + [Fact] + public void ThrowAndFalseSinkKeepExactWithdrawRetryableUntilAck() + { + using var fixture = new Fixture(bindGeneration: true); + _ = fixture.Add( + 0x70003004u, + 1, + CellP, + new Vector3(10f, 21f, 7f)); + int attempt = 0; + var sink = new RecordingSink(_ => ++attempt switch + { + 1 => throw new InvalidOperationException("host unavailable"), + 2 => false, + _ => true, + }); + using var subscription = fixture.Subscribe(sink); + RuntimeCollisionPrefixQuiescenceToken token = fixture.Begin(2UL); + + Assert.False(fixture.TryAcquire(token, out _)); + Assert.Equal(1, fixture.Lifetime.Placements.PendingCount); + Assert.False(fixture.TryAcquire(token, out _)); + Assert.Equal(1, fixture.Lifetime.Events.DispatchFailureCount); + + Assert.True(subscription.RetryPending()); + Assert.Equal(1, fixture.Lifetime.Placements.PendingCount); + Assert.True(subscription.RetryPending()); + Assert.Equal(0, fixture.Lifetime.Placements.PendingCount); + + Assert.True(fixture.TryAcquire(token, out var permission)); + Assert.Single(permission.Withdrawals); + Assert.Equal(3, sink.Applied.Count); + Assert.All(sink.Applied, + projection => Assert.Equal( + permission.Withdrawals[0].Sequence, + projection.Token.Sequence)); + } + + [Fact] + public void SourceToOutsidePlacementIsHeldThenRestoredBeforeBarrierOpens() + { + using var fixture = new Fixture(); + RuntimeEntityRecord record = fixture.Add( + 0x70003005u, + 1, + CellP, + new Vector3(10f, 22f, 7f)); + RuntimeCollisionPrefixQuiescenceToken token = fixture.Begin(2UL); + + RuntimeSetPositionOutcome held = fixture.Place( + record, + CellQ, + new Vector3(14f, 22f, 7f), + currentCell: CellP); + + Assert.Equal(RuntimeSetPositionStatus.DeferredCell, held.Status); + Assert.Equal(2UL, held.Projection.CollisionGeneration); + Assert.Equal(CellQ, held.ExactCellId); + Assert.False(fixture.Lifetime.Physics.IsSpatialRoot(record)); + Assert.True(fixture.Lifetime.Physics.SetPosition + .AcknowledgeProjection(held.Projection)); + Assert.False(fixture.TryAcquire(token, out _)); + Assert.True(fixture.TryAcquire(token, out _)); + + Assert.False(fixture.Lifetime.Physics.CancelCollisionPrefixQuiescence( + token, + successorGeneration: 1UL, + successorReady: true)); + Assert.True(fixture.Lifetime.Physics.SetPosition.TryPeekProjection( + out RuntimePlacementProjectionSnapshot restored)); + Assert.Equal(RuntimePlacementProjectionKind.Place, restored.Kind); + Assert.Equal(CellQ, restored.Token.ExactCellId); + Assert.False(fixture.Lifetime.Physics.CancelCollisionPrefixQuiescence( + token, + successorGeneration: 1UL, + successorReady: true)); + Assert.True(fixture.Lifetime.Physics.SetPosition + .AcknowledgeProjection(restored.Token)); + Assert.True(fixture.Lifetime.Physics.CancelCollisionPrefixQuiescence( + token, + successorGeneration: 1UL, + successorReady: true)); + Assert.Equal(CellQ, record.FullCellId); + Assert.True(fixture.Lifetime.Physics.IsSpatialRoot(record)); + } + + [Fact] + public void OutsideToTargetPlacementIsHeldWithoutEvaluatingRetiringRows() + { + using var fixture = new Fixture(); + RuntimeEntityRecord record = fixture.Add( + 0x70003006u, + 1, + CellQ, + new Vector3(10f, 23f, 7f)); + RuntimeCollisionPrefixQuiescenceToken token = fixture.Begin(2UL); + + RuntimeSetPositionOutcome held = fixture.Place( + record, + CellP, + new Vector3(15f, 23f, 7f), + currentCell: CellQ); + + Assert.Equal(RuntimeSetPositionStatus.DeferredCell, held.Status); + Assert.Equal(CellP, held.ExactCellId); + Assert.Equal(2UL, held.Projection.CollisionGeneration); + Assert.True(fixture.Lifetime.Physics.SetPosition + .AcknowledgeProjection(held.Projection)); + Assert.False(fixture.TryAcquire(token, out _)); + Assert.True(fixture.TryAcquire(token, out _)); + + Assert.False(fixture.Lifetime.Physics.CancelCollisionPrefixQuiescence( + token, + successorGeneration: 1UL, + successorReady: true)); + Assert.True(fixture.Lifetime.Physics.SetPosition.TryPeekProjection( + out RuntimePlacementProjectionSnapshot restored)); + Assert.Equal(CellP, restored.Token.ExactCellId); + Assert.True(fixture.Lifetime.Physics.SetPosition + .AcknowledgeProjection(restored.Token)); + Assert.True(fixture.Lifetime.Physics.CancelCollisionPrefixQuiescence( + token, + successorGeneration: 1UL, + successorReady: true)); + Assert.Equal(restored.Token.ExactCellId, record.FullCellId); + } + + [Fact] + public void SupersessionTransfersParkedReceiptAndOnlySuccessorCanAcquire() + { + using var fixture = new Fixture(); + _ = fixture.Add( + 0x70003007u, + 1, + CellP, + new Vector3(10f, 24f, 7f)); + RuntimeCollisionPrefixQuiescenceToken first = fixture.Begin(2UL); + Assert.False(fixture.TryAcquire(first, out _)); + Assert.True(fixture.Lifetime.Physics.SetPosition.TryPeekProjection( + out RuntimePlacementProjectionSnapshot withdrawal)); + + RuntimeCollisionPrefixQuiescenceToken successor = fixture.Begin(3UL); + + Assert.False(fixture.TryAcquire(first, out _)); + Assert.False(fixture.TryAcquire(successor, out _)); + Assert.True(fixture.Lifetime.Physics.SetPosition + .AcknowledgeProjection(withdrawal.Token)); + Assert.True(fixture.TryAcquire(successor, out var permission)); + Assert.Equal(successor, permission.Quiescence); + Assert.Single(permission.Withdrawals); + Assert.Equal(withdrawal.Token.Sequence, + permission.Withdrawals[0].Sequence); + } + + [Fact] + public void ResetAndDisposeConvergePrefixBarrierOwnership() + { + var fixture = new Fixture(); + _ = fixture.Add( + 0x70003008u, + 1, + CellP, + new Vector3(10f, 25f, 7f)); + RuntimeCollisionPrefixQuiescenceToken token = fixture.Begin(2UL); + Assert.False(fixture.TryAcquire(token, out _)); + Assert.Equal(1, fixture.Lifetime.Physics.CaptureOwnership() + .CollisionPrefixQuiescenceCount); + + fixture.Lifetime.Physics.SetPosition.ResetSession(); + + RuntimePhysicsOwnershipSnapshot reset = + fixture.Lifetime.Physics.CaptureOwnership(); + Assert.Equal(0, reset.CollisionPrefixQuiescenceCount); + Assert.Equal(0, reset.PendingCollisionPrefixProjectionCount); + fixture.Dispose(); + Assert.True(fixture.Lifetime.Physics.CaptureOwnership().IsConverged); + } + + [Fact] + public void RevisedDiscardRetainsExactBarrierDebtUntilDiscardAck() + { + using var fixture = new Fixture(); + RuntimeEntityRecord record = fixture.Add( + 0x70003009u, + 1, + CellP, + new Vector3(10f, 26f, 7f)); + RuntimeCollisionPrefixQuiescenceToken token = fixture.Begin(2UL); + + Assert.False(fixture.TryAcquire(token, out _)); + Assert.True(fixture.Lifetime.Physics.SetPosition.TryPeekProjection( + out RuntimePlacementProjectionSnapshot withdrawal)); + Assert.Equal(RuntimePlacementProjectionKind.Withdraw, withdrawal.Kind); + + RuntimePlacementCancellationReceipt cancellation = fixture.Lifetime + .Physics.SetPosition.Forget(record); + Assert.True(cancellation.IsValid); + Assert.True(fixture.Lifetime.Physics.SetPosition.TryPeekProjection( + out RuntimePlacementProjectionSnapshot discard)); + Assert.Equal(RuntimePlacementProjectionKind.Discard, discard.Kind); + Assert.Equal(withdrawal.Token.Sequence, discard.Token.Sequence); + Assert.True(discard.Token.Revision > withdrawal.Token.Revision); + Assert.False(fixture.TryAcquire(token, out _)); + + Assert.True(fixture.Lifetime.Physics.SetPosition + .AcknowledgeProjection(discard.Token)); + Assert.True(fixture.TryAcquire(token, out var permission)); + Assert.Single(permission.Withdrawals); + Assert.Equal(discard.Token, permission.Withdrawals[0]); + } + + [Fact] + public void AcceptedPreparationDebtKeepsParkingFailAtomicAndTokenRetryable() + { + using var fixture = new Fixture(); + RuntimeEntityRecord record = fixture.Add( + 0x7000300Au, + 1, + CellP, + new Vector3(10f, 27f, 7f)); + RuntimeEntityPlacementToken placement = fixture.Lifetime.Physics + .SetPosition.BeginAcceptedPlacement( + record, + record.PositionAuthorityVersion, + RuntimeSetPositionOperationKind.RemoteAuthoritative); + RuntimeCollisionPrefixQuiescenceToken token = fixture.Begin(2UL); + + Assert.False(fixture.TryAcquire(token, out _)); + Assert.True(fixture.Lifetime.Physics.IsSpatialRoot(record)); + Assert.False(fixture.Lifetime.Physics.SetPosition.TryPeekProjection( + out _)); + Assert.True(fixture.Lifetime.Physics + .CancelCollisionPrefixQuiescence(token)); + + RuntimeSetPositionOutcome submitted = fixture.Lifetime.Physics + .SetPosition.SubmitPreparedPlacement( + placement, + Command(CellP, new Vector3(11f, 27f, 7f), CellP)); + Assert.Equal( + RuntimeSetPositionStatus.CommittedHostAcknowledgementPending, + submitted.Status); + } + + [Fact] + public void QueriedNeighborPrefixHoldsResultWithoutRequestDependency() + { + uint start = PrefixQ | 0x0101u; + uint rejected = PrefixP | 0x0101u; + uint winner = PrefixQ | 0x0102u; + PhysicsEngine engine = FlatEngine(); + PhysicsDataCache cache = engine.DataCache!; + cache.RegisterCellStructForTest( + start, + ContainmentCell( + new Plane(new Vector3(0f, -1f, 0f), 3f), + [rejected, winner])); + cache.RegisterCellStructForTest( + rejected, + ContainmentCell( + new Plane(new Vector3(0f, 1f, 0f), -20f), + [])); + cache.RegisterCellStructForTest( + winner, + ContainmentCell( + new Plane(new Vector3(0f, 1f, 0f), -7f), + [])); + using var fixture = new Fixture(engine: engine); + RuntimeEntityRecord record = fixture.Add( + 0x7000300Bu, + 1, + start, + new Vector3(0f, 8f, 1f)); + RuntimeCollisionPrefixQuiescenceToken token = fixture.Begin(2UL); + + RuntimeSetPositionOutcome held = fixture.Place( + record, + start, + new Vector3(0f, 8f, 1f)); + + Assert.Equal(RuntimeSetPositionStatus.DeferredCell, held.Status); + Assert.Equal(winner, held.ExactCellId); + Assert.Equal(2UL, held.Projection.CollisionGeneration); + Assert.False(fixture.Lifetime.Physics.IsSpatialRoot(record)); + Assert.True(fixture.Lifetime.Physics.SetPosition + .AcknowledgeProjection(held.Projection)); + Assert.False(fixture.TryAcquire(token, out _)); + Assert.True(fixture.TryAcquire(token, out _)); + } + + [Fact] + public void SimultaneousPrefixesParkAndDrainIndependentlyInOneFifo() + { + using var fixture = new Fixture(); + RuntimeEntityRecord first = fixture.Add( + 0x7000300Cu, + 1, + CellP, + new Vector3(10f, 28f, 7f)); + RuntimeEntityRecord second = fixture.Add( + 0x7000300Du, + 1, + CellQ, + new Vector3(11f, 28f, 7f)); + RuntimeCollisionPrefixQuiescenceToken p = fixture.Begin( + PrefixP, + 2UL, + includeOutdoorCells: true); + RuntimeCollisionPrefixQuiescenceToken q = fixture.Begin( + PrefixQ, + 3UL, + includeOutdoorCells: true); + + Assert.False(fixture.TryAcquire(p, out _)); + Assert.False(fixture.TryAcquire(q, out _)); + Assert.False(fixture.Lifetime.Physics.IsSpatialRoot(first)); + Assert.False(fixture.Lifetime.Physics.IsSpatialRoot(second)); + + while (fixture.Lifetime.Physics.SetPosition.TryPeekProjection( + out RuntimePlacementProjectionSnapshot pending)) + { + Assert.True(fixture.Lifetime.Physics.SetPosition + .AcknowledgeProjection(pending.Token)); + } + + Assert.True(fixture.TryAcquire(p, out var pPermission)); + Assert.True(fixture.TryAcquire(q, out var qPermission)); + Assert.Single(pPermission.Withdrawals); + Assert.Single(qPermission.Withdrawals); + Assert.NotEqual( + pPermission.Withdrawals[0].Sequence, + qPermission.Withdrawals[0].Sequence); + } + + [Fact] + public void PermissionIsInvalidatedByNewPlacementAfterAcquire() + { + using var fixture = new Fixture(); + RuntimeCollisionPrefixQuiescenceToken token = fixture.Begin(2UL); + Assert.False(fixture.TryAcquire(token, out _)); + Assert.True(fixture.TryAcquire(token, out var permission)); + Assert.True(fixture.Lifetime.Physics + .IsCollisionPrefixMutationPermissionCurrent(permission)); + RuntimeEntityRecord record = fixture.Add( + 0x7000300Eu, + 1, + CellQ, + new Vector3(10f, 29f, 7f)); + + RuntimeSetPositionOutcome held = fixture.Place( + record, + CellP, + new Vector3(12f, 29f, 7f), + currentCell: CellQ); + + Assert.Equal(RuntimeSetPositionStatus.DeferredCell, held.Status); + Assert.False(fixture.Lifetime.Physics + .IsCollisionPrefixMutationPermissionCurrent(permission)); + Assert.True(fixture.Lifetime.Physics.SetPosition + .AcknowledgeProjection(held.Projection)); + Assert.True(fixture.TryAcquire(token, out var replacement)); + Assert.NotEqual(permission.Withdrawals, replacement.Withdrawals); + } + + [Fact] + public void IndoorOnlyQuiescenceLeavesOutdoorResidentActive() + { + uint indoor = PrefixP | 0x0101u; + using var fixture = new Fixture(); + RuntimeEntityRecord outdoor = fixture.Add( + 0x7000300Fu, + 1, + CellP, + new Vector3(10f, 30f, 7f)); + RuntimeEntityRecord interior = fixture.Add( + 0x70003010u, + 1, + indoor, + new Vector3(11f, 30f, 7f)); + RuntimeCollisionPrefixQuiescenceToken token = fixture.Begin( + PrefixP, + 2UL, + includeOutdoorCells: false); + + Assert.False(fixture.TryAcquire(token, out _)); + Assert.True(fixture.Lifetime.Physics.IsSpatialRoot(outdoor)); + Assert.False(fixture.Lifetime.Physics.IsSpatialRoot(interior)); + Assert.True(fixture.Lifetime.Physics.SetPosition.TryPeekProjection( + out RuntimePlacementProjectionSnapshot withdrawal)); + Assert.True(fixture.Lifetime.Physics.SetPosition + .AcknowledgeProjection(withdrawal.Token)); + Assert.True(fixture.TryAcquire(token, out var permission)); + Assert.Single(permission.Withdrawals); + Assert.Equal(interior.Key, permission.Withdrawals[0].Entity); + } + + [Fact] + public void WithdrawCallbackDeleteGuidReuseAndNewPlacementCannotEscapeBarrier() + { + using var fixture = new Fixture(); + RuntimeEntityRecord first = fixture.Add( + 0x70003011u, + 1, + CellP, + new Vector3(10f, 31f, 7f)); + RuntimeEntityRecord retired = fixture.Add( + 0x70003012u, + 1, + CellP, + new Vector3(11f, 31f, 7f)); + RuntimeEntityRecord? replacement = null; + RuntimeSetPositionOutcome reentrant = default; + var observer = new PlacementObserver(delta => + { + if (replacement is not null + || delta.Placement.Kind + is not RuntimePlacementProjectionKind.Withdraw + || delta.Placement.Token.Entity != first.Key) + { + return; + } + + Assert.True(fixture.Lifetime.TryAcceptDelete( + new DeleteObject.Parsed(retired.ServerGuid, retired.Incarnation), + isLocalPlayer: false, + removeRetainedObject: false, + out RuntimeEntityDeleteAcceptance acceptance)); + fixture.Lifetime.CompleteAcceptedDelete(acceptance); + Assert.Null(fixture.Lifetime.RetireCanonicalOnly(retired)); + replacement = fixture.Add( + retired.ServerGuid, + 2, + CellQ, + new Vector3(12f, 31f, 7f)); + reentrant = fixture.Place( + replacement, + CellP, + new Vector3(13f, 31f, 7f), + currentCell: CellQ); + }); + using IDisposable subscription = fixture.Lifetime.Events + .SubscribePlacement(observer); + RuntimeCollisionPrefixQuiescenceToken token = fixture.Begin(2UL); + + Assert.False(fixture.TryAcquire(token, out _)); + Assert.NotNull(replacement); + Assert.Equal(RuntimeSetPositionStatus.DeferredCell, reentrant.Status); + Assert.False(fixture.Lifetime.Entities.IsCurrent(retired)); + Assert.True(fixture.Lifetime.Entities.IsCurrent(replacement)); + Assert.False(fixture.Lifetime.Physics.IsSpatialRoot(replacement)); + + int receipts = 0; + while (fixture.Lifetime.Physics.SetPosition.TryPeekProjection( + out RuntimePlacementProjectionSnapshot pending)) + { + receipts++; + Assert.True(fixture.Lifetime.Physics.SetPosition + .AcknowledgeProjection(pending.Token)); + } + Assert.Equal(3, receipts); + Assert.True(fixture.TryAcquire(token, out var permission)); + Assert.Equal(3, permission.Withdrawals.Length); + } + + [Fact] + public void AbortReleaseKeepsNewcomerBehindBarrierUntilEveryPlaceAck() + { + using var fixture = new Fixture(); + RuntimeEntityRecord resident = fixture.Add( + 0x70003013u, + 1, + CellP, + new Vector3(10f, 32f, 7f)); + RuntimeSetPositionOutcome prepared = fixture.Place( + resident, + CellP, + new Vector3(11f, 32f, 7f), + currentCell: CellP); + Assert.True(fixture.Lifetime.Physics.SetPosition + .AcknowledgeProjection(prepared.Projection)); + RuntimeCollisionPrefixQuiescenceToken token = fixture.Begin(2UL); + Assert.False(fixture.TryAcquire(token, out _)); + Assert.True(fixture.Lifetime.Physics.SetPosition.TryPeekProjection( + out RuntimePlacementProjectionSnapshot residentWithdrawal)); + Assert.True(fixture.Lifetime.Physics.SetPosition + .AcknowledgeProjection(residentWithdrawal.Token)); + Assert.True(fixture.TryAcquire(token, out _)); + + Assert.False(fixture.Lifetime.Physics.CancelCollisionPrefixQuiescence( + token, + successorGeneration: 1UL, + successorReady: true)); + Assert.True(fixture.Lifetime.Physics.SetPosition.TryPeekProjection( + out RuntimePlacementProjectionSnapshot residentPlace)); + Assert.Equal(RuntimePlacementProjectionKind.Place, residentPlace.Kind); + + RuntimeEntityRecord newcomer = fixture.Add( + 0x70003014u, + 1, + CellQ, + new Vector3(12f, 32f, 7f)); + RuntimeSetPositionOutcome newcomerHeld = fixture.Place( + newcomer, + CellP, + new Vector3(13f, 32f, 7f), + currentCell: CellQ); + Assert.Equal(RuntimeSetPositionStatus.DeferredCell, newcomerHeld.Status); + Assert.False(fixture.Lifetime.Physics.CancelCollisionPrefixQuiescence( + token, + successorGeneration: 1UL, + successorReady: true)); + + Assert.True(fixture.Lifetime.Physics.SetPosition + .AcknowledgeProjection(residentPlace.Token)); + Assert.True(fixture.Lifetime.Physics.SetPosition.TryPeekProjection( + out RuntimePlacementProjectionSnapshot newcomerWithdrawal)); + Assert.Equal( + RuntimePlacementProjectionKind.Withdraw, + newcomerWithdrawal.Kind); + Assert.True(fixture.Lifetime.Physics.SetPosition + .AcknowledgeProjection(newcomerWithdrawal.Token)); + Assert.False(fixture.Lifetime.Physics.CancelCollisionPrefixQuiescence( + token, + successorGeneration: 1UL, + successorReady: true)); + Assert.True(fixture.Lifetime.Physics.SetPosition.TryPeekProjection( + out RuntimePlacementProjectionSnapshot newcomerPlace)); + Assert.Equal(RuntimePlacementProjectionKind.Place, newcomerPlace.Kind); + Assert.True(fixture.Lifetime.Physics.SetPosition + .AcknowledgeProjection(newcomerPlace.Token)); + Assert.True(fixture.Lifetime.Physics.CancelCollisionPrefixQuiescence( + token, + successorGeneration: 1UL, + successorReady: true)); + Assert.True(fixture.Lifetime.Physics.IsSpatialRoot(resident)); + Assert.True(fixture.Lifetime.Physics.IsSpatialRoot(newcomer)); + } + + [Fact] + public void ColdAbortRestoreWaitsForExactMoverPreparation() + { + using var fixture = new Fixture(); + RuntimeEntityRecord resident = fixture.Add( + 0x70003015u, + 1, + CellP, + new Vector3(10f, 33f, 7f)); + RuntimeCollisionPrefixQuiescenceToken token = fixture.Begin(2UL); + Assert.False(fixture.TryAcquire(token, out _)); + Assert.True(fixture.Lifetime.Physics.SetPosition.TryPeekProjection( + out RuntimePlacementProjectionSnapshot withdrawal)); + Assert.True(fixture.Lifetime.Physics.SetPosition + .AcknowledgeProjection(withdrawal.Token)); + Assert.True(fixture.TryAcquire(token, out _)); + + Assert.False(fixture.Lifetime.Physics.CancelCollisionPrefixQuiescence( + token, + successorGeneration: 1UL, + successorReady: true)); + Assert.False(fixture.Lifetime.Physics.SetPosition.TryPeekProjection( + out _)); + Assert.True(fixture.Lifetime.Physics.SetPosition + .TryGetAwaitingPreparationToken( + resident, + out RuntimeEntityPlacementToken preparationToken)); + RuntimeSetPositionCommand command = PrepareAuthoredCommand( + fixture.Lifetime, + preparationToken); + + RuntimeSetPositionOutcome submitted = fixture.Lifetime.Physics + .SetPosition.SubmitPreparedPlacement(preparationToken, command); + + Assert.Equal(RuntimeSetPositionStatus.DeferredCell, submitted.Status); + Assert.True(fixture.Lifetime.Physics.SetPosition.TryPeekProjection( + out RuntimePlacementProjectionSnapshot restored)); + Assert.Equal(RuntimePlacementProjectionKind.Place, restored.Kind); + Assert.False(fixture.Lifetime.Physics.CancelCollisionPrefixQuiescence( + token, + successorGeneration: 1UL, + successorReady: true)); + Assert.True(fixture.Lifetime.Physics.SetPosition + .AcknowledgeProjection(restored.Token)); + Assert.True(fixture.Lifetime.Physics.CancelCollisionPrefixQuiescence( + token, + successorGeneration: 1UL, + successorReady: true)); + } + + [Fact] + public void ResetConvergesPendingDiscardAndPendingRestoreReceipts() + { + using (var discardFixture = new Fixture()) + { + RuntimeEntityRecord record = discardFixture.Add( + 0x70003016u, + 1, + CellP, + new Vector3(10f, 34f, 7f)); + RuntimeCollisionPrefixQuiescenceToken token = + discardFixture.Begin(2UL); + Assert.False(discardFixture.TryAcquire(token, out _)); + _ = discardFixture.Lifetime.Physics.SetPosition.Forget(record); + Assert.Equal(1, discardFixture.Lifetime.Physics.CaptureOwnership() + .PendingCollisionPrefixProjectionCount); + + discardFixture.Lifetime.Physics.SetPosition.ResetSession(); + + RuntimePhysicsOwnershipSnapshot reset = discardFixture.Lifetime + .Physics.CaptureOwnership(); + Assert.Equal(0, reset.CollisionPrefixQuiescenceCount); + Assert.Equal(0, reset.PendingCollisionPrefixProjectionCount); + } + + using var restoreFixture = new Fixture(); + RuntimeEntityRecord resident = restoreFixture.Add( + 0x70003017u, + 1, + CellP, + new Vector3(10f, 35f, 7f)); + RuntimeSetPositionOutcome prepared = restoreFixture.Place( + resident, + CellP, + new Vector3(11f, 35f, 7f), + currentCell: CellP); + Assert.True(restoreFixture.Lifetime.Physics.SetPosition + .AcknowledgeProjection(prepared.Projection)); + RuntimeCollisionPrefixQuiescenceToken restoreToken = + restoreFixture.Begin(2UL); + Assert.False(restoreFixture.TryAcquire(restoreToken, out _)); + Assert.True(restoreFixture.Lifetime.Physics.SetPosition + .TryPeekProjection(out RuntimePlacementProjectionSnapshot withdraw)); + Assert.True(restoreFixture.Lifetime.Physics.SetPosition + .AcknowledgeProjection(withdraw.Token)); + Assert.True(restoreFixture.TryAcquire(restoreToken, out _)); + Assert.False(restoreFixture.Lifetime.Physics + .CancelCollisionPrefixQuiescence( + restoreToken, + successorGeneration: 1UL, + successorReady: true)); + Assert.Equal(1, restoreFixture.Lifetime.Physics.CaptureOwnership() + .PendingCollisionPrefixProjectionCount); + + restoreFixture.Lifetime.Physics.SetPosition.ResetSession(); + + RuntimePhysicsOwnershipSnapshot restoreReset = restoreFixture.Lifetime + .Physics.CaptureOwnership(); + Assert.Equal(0, restoreReset.CollisionPrefixQuiescenceCount); + Assert.Equal(0, restoreReset.PendingCollisionPrefixProjectionCount); + } + + [Fact] + public void CancelBeforeFirstAcquireCannotStrandNewHeldPlacement() + { + using var fixture = new Fixture(); + RuntimeEntityRecord record = fixture.Add( + 0x70003018u, + 1, + CellQ, + new Vector3(10f, 36f, 7f)); + RuntimeCollisionPrefixQuiescenceToken token = fixture.Begin(2UL); + RuntimeSetPositionOutcome held = fixture.Place( + record, + CellP, + new Vector3(11f, 36f, 7f), + currentCell: CellQ); + + Assert.Equal(RuntimeSetPositionStatus.DeferredCell, held.Status); + Assert.False(fixture.Lifetime.Physics.CancelCollisionPrefixQuiescence( + token)); + Assert.Equal(1, fixture.Lifetime.Physics.CaptureOwnership() + .CollisionPrefixQuiescenceCount); + Assert.True(fixture.Lifetime.Physics.SetPosition + .AcknowledgeProjection(held.Projection)); + Assert.False(fixture.Lifetime.Physics.CancelCollisionPrefixQuiescence( + token)); + + Assert.False(fixture.Lifetime.Physics.CancelCollisionPrefixQuiescence( + token, + successorGeneration: 1UL, + successorReady: true)); + Assert.True(fixture.Lifetime.Physics.SetPosition.TryPeekProjection( + out RuntimePlacementProjectionSnapshot restored)); + Assert.Equal(RuntimePlacementProjectionKind.Place, restored.Kind); + Assert.True(fixture.Lifetime.Physics.SetPosition + .AcknowledgeProjection(restored.Token)); + Assert.True(fixture.Lifetime.Physics.CancelCollisionPrefixQuiescence( + token, + successorGeneration: 1UL, + successorReady: true)); + Assert.Equal(restored.Token.ExactCellId, record.FullCellId); + Assert.True(fixture.Lifetime.Physics.IsSpatialRoot(record)); + } + + private sealed class Fixture : IDisposable + { + private readonly RuntimeGenerationToken _generation = new(7UL); + + internal Fixture( + bool bindGeneration = false, + PhysicsEngine? engine = null) + { + Lifetime = new RuntimeEntityObjectLifetime(engine ?? FlatEngine()); + if (bindGeneration) + { + Lifetime.BindEventContext( + () => _generation, + static () => 11UL); + } + } + + internal RuntimeEntityObjectLifetime Lifetime { get; } + + internal RuntimeEntityRecord Add( + uint guid, + ushort incarnation, + uint cell, + Vector3 position) + { + RuntimeEntityRecord record = Lifetime.RegisterEntity( + Spawn(guid, incarnation, cell, position)).Canonical!; + Lifetime.Entities.SetFinalPhysicsState( + record, + PhysicsStateFlags.Gravity); + Lifetime.Entities.SetFullCell( + record, + cell, + (cell & 0xFFFF0000u) | 0xFFFFu); + var body = new PhysicsBody + { + Position = position, + Orientation = Quaternion.Identity, + LastUpdateTime = 1d, + State = PhysicsStateFlags.Gravity, + TransientState = TransientStateFlags.Active, + }; + body.SnapToCell(cell, position, position); + Lifetime.Entities.SetPhysicsBody(record, body); + record.ObjectClock.Activate(); + Lifetime.Physics.AcknowledgeSpatialProjection(record, spatial: true); + return record; + } + + internal RuntimeSetPositionOutcome Place( + RuntimeEntityRecord record, + uint cell, + Vector3 position, + uint? currentCell = null) => Lifetime.Physics.SetPosition.Apply( + record, + record.PositionAuthorityVersion, + Command(cell, position, currentCell)); + + internal RuntimeCollisionPrefixQuiescenceToken Begin( + ulong generation) => Lifetime.Physics + .BeginCollisionPrefixQuiescence( + PrefixP, + generation, + includeOutdoorCells: true); + + internal RuntimeCollisionPrefixQuiescenceToken Begin( + uint prefix, + ulong generation, + bool includeOutdoorCells) => Lifetime.Physics + .BeginCollisionPrefixQuiescence( + prefix, + generation, + includeOutdoorCells); + + internal bool TryAcquire( + RuntimeCollisionPrefixQuiescenceToken token, + out RuntimeCollisionPrefixMutationPermission permission) => + Lifetime.Physics.TryAcquireCollisionPrefixMutationPermission( + token, + out permission); + + internal RuntimePlacementProjectionSubscription Subscribe( + IRuntimePlacementProjectionSink sink) => new( + Lifetime.Placements, + () => _generation, + sink, + retryPendingOnSubscribe: true); + + public void Dispose() => Lifetime.Dispose(); + } + + private sealed class RecordingSink( + Func apply) + : IRuntimePlacementProjectionSink + { + internal List Applied { get; } = []; + + public bool TryApply(in RuntimePlacementProjectionSnapshot projection) + { + Applied.Add(projection); + return apply(projection); + } + } + + private sealed class PlacementObserver( + Action? onPlacement = null) + : IRuntimePlacementObserver + { + public void OnPlacement(in RuntimePlacementDelta delta) => + onPlacement?.Invoke(delta); + } + + private static RuntimeSetPositionCommand Command( + uint cell, + Vector3 position, + uint? currentCell) => new( + new PhysicsSetPositionRequest( + position, + Quaternion.Identity, + cell, + position, + ImmutableArray.Empty, + Scale: 1f, + StepUpHeight: 0.4f, + StepDownHeight: 0.4f, + Flags: PhysicsSetPositionFlags.Placement + | PhysicsSetPositionFlags.Slide, + CurrentCellId: currentCell), + RuntimeSetPositionOperationKind.RemoteAuthoritative, + GameTime: 10d, + ExpectedVelocityAuthorityVersion: 0UL); + + private static RuntimeSetPositionCommand PrepareAuthoredCommand( + RuntimeEntityObjectLifetime lifetime, + RuntimeEntityPlacementToken token) + { + var setup = new FlatSetupCollision( + ImmutableArray.Empty, + [new FlatCollisionSphere(Vector3.Zero, 0.4f)], + height: 0f, + radius: 0f, + stepUpHeight: 0.4f, + stepDownHeight: 0.4f); + var preparation = new RuntimeSetPositionMoverPreparation( + RuntimeSetPositionMoverSetup.Resolved(0x02000001u, setup), + RuntimeSetPositionOperationKind.RemoteAuthoritative, + GameTime: 10d, + PhysicsPlacementClass.Ordinary, + PhysicsSetPositionFlags.Placement + | PhysicsSetPositionFlags.Slide); + Assert.Equal( + RuntimeSetPositionMoverPreparationStatus.Prepared, + lifetime.Physics.SetPosition.PrepareMover( + token, + preparation, + out RuntimeSetPositionCommand command)); + return command; + } + + private static PhysicsEngine FlatEngine() + { + var engine = new PhysicsEngine + { + DataCache = new PhysicsDataCache(), + }; + AddFlatLandblock(engine, PrefixP); + AddFlatLandblock(engine, PrefixQ); + return engine; + } + + private static void AddFlatLandblock(PhysicsEngine engine, uint prefix) => + engine.AddLandblock( + prefix, + new TerrainSurface(new byte[81], new float[256]), + Array.Empty(), + Array.Empty(), + worldOffsetX: 0f, + worldOffsetY: 0f); + + private static CellPhysics ContainmentCell( + Plane plane, + uint[] visibleCells) => new() + { + BSP = new PhysicsBSPTree + { + Root = new PhysicsBSPNode { Type = BSPNodeType.Leaf }, + }, + WorldTransform = Matrix4x4.Identity, + InverseWorldTransform = Matrix4x4.Identity, + Resolved = new Dictionary(), + CellBSP = new CellBSPTree + { + Root = new CellBSPNode + { + SplittingPlane = plane, + PosNode = new CellBSPNode { Type = BSPNodeType.Leaf }, + }, + }, + Portals = [new PortalInfo(0xFFFF, 0, 0)], + PortalPolygons = new Dictionary(), + VisibleCellIds = new HashSet(visibleCells), + }; + + private static WorldSession.EntitySpawn Spawn( + uint guid, + ushort instance, + uint cell, + Vector3 position) + { + var serverPosition = new CreateObject.ServerPosition( + cell, + position.X, + position.Y, + position.Z, + 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)PhysicsStateFlags.Gravity, + Position: serverPosition, + 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, + serverPosition, + 0x02000001u, + Array.Empty(), + Array.Empty(), + Array.Empty(), + null, + null, + "collision-prefix-quiescence-fixture", + null, + null, + 0x09000001u, + PhysicsState: (uint)PhysicsStateFlags.Gravity, + InstanceSequence: instance, + MovementSequence: 1, + ServerControlSequence: 1, + PositionSequence: 1, + Physics: physics); + } +}