< Summary

Line coverage
100%
Covered lines: 377
Uncovered lines: 0
Coverable lines: 377
Total lines: 734
Line coverage: 100%
Branch coverage
100%
Covered branches: 138
Total branches: 138
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
File 1: .ctor(...)100%11100%
File 1: get_World()100%11100%
File 1: get_IsNotificationInProgress()100%11100%
File 1: get_LifetimeVersion()100%11100%
File 1: Initialize(...)100%66100%
File 1: AssignPriority(...)100%22100%
File 1: ShouldFirstColliderLead(...)100%88100%
File 1: UpdateCollision()100%11100%
File 1: UpdateCollisionDeferred()100%11100%
File 1: UpdateCollision(...)100%1010100%
File 1: UpdateLastFrame()100%11100%
File 1: DeactivateAndPoolIfRequired()100%22100%
File 1: ProcessCollision(...)100%1414100%
File 1: WakeSleepingBodiesForCollision()100%88100%
File 1: NotifyCollidersOfContact()100%1818100%
File 1: EndNotification(...)100%22100%
File 1: NotifySeparation(...)100%66100%
File 1: ClearPendingNotificationState()100%11100%
File 1: HandleCullingIfNotColliding()100%22100%
File 1: TryPreserveSleepingRestingContact()100%1212100%
File 1: CheckCollision()100%44100%
File 1: IsCullStateInvalidated()100%44100%
File 1: BroadPhaseVersionChanged()100%22100%
File 1: RefreshBroadPhaseVersions()100%11100%
File 1: ShouldPerformCollisionCheck()100%11100%
File 1: BoundsOverlapInclusive(...)100%1010100%
File 1: CalculateCullScore()100%88100%
File 1: GetCullDistanceStep(...)100%11100%
File 1: Reset()100%11100%
File 1: StoreWarmStartImpulse(...)100%11100%
File 1: TryGetWarmStartImpulse(...)100%11100%
File 1: RemoveWarmStartImpulse(...)100%11100%
File 1: ClearWarmStart()100%11100%
File 1: Deactivate()100%1212100%
File 2: ContributeReplayHash(...)100%22100%
File 2: ContributeManifoldReplayHash(...)100%44100%
File 2: WriteMaterial(...)100%11100%
File 2: ContributeWarmStartReplayHash(...)100%22100%

File(s)

/home/runner/work/Gravitas/Gravitas/src/Gravitas/CollisionHandling/Pairs/3D/CollisionPair.cs

#LineLine coverage
 1//=======================================================================
 2// CollisionPair.cs
 3//=======================================================================
 4// MIT License, Copyright (c) 2026–present David Oravsky (mrdav30)
 5// See LICENSE file in the project root for full license information.
 6//=======================================================================
 7
 8using FixedMathSharp;
 9using Gravitas.Colliders;
 10using GridForge.Grids;
 11using GridForge.Grids.Topology;
 12using SwiftCollections;
 13using System;
 14using System.Runtime.CompilerServices;
 15
 16namespace Gravitas.CollisionHandling;
 17
 18internal enum CollisionResponseDispatchMode
 19{
 20    Immediate,
 21    Deferred
 22}
 23
 24/// <summary>
 25/// Owns deterministic 3D pair lifecycle, culling, contact, and notification state.
 26/// </summary>
 27public partial class CollisionPair
 28{
 29    /// <summary>Stores the host-controlled pair diagnostic flag.</summary>
 63930    public bool Debug = true;
 31
 32    /// <summary>Gets whether this pooled pair currently represents two registered colliders.</summary>
 33    public bool Active { get; private set; }
 34
 35    private bool _isPooledForDeactivation;
 36
 37    /// <summary>Gets the world context that owns this pair.</summary>
 38    public GravitasWorldContext Context { get; private set; } = null!;
 39
 40    /// <summary>Gets the GridForge world owned by <see cref="Context"/>.</summary>
 168241    public GridWorld World => Context.World;
 42
 43    // stores order in which they come in
 44    /// <summary>Gets the first collider ID used to identify the pair.</summary>
 45    public int Id1 { get; private set; }
 46    /// <summary>Gets the second collider ID used to identify the pair.</summary>
 47    public int Id2 { get; private set; }
 48
 49    /// <summary>Gets the collider selected to lead narrow-phase dispatch.</summary>
 50    public LSCollider ColliderA { get; private set; } = null!;
 51    /// <summary>Gets the other collider in the pair.</summary>
 52    public LSCollider ColliderB { get; private set; } = null!;
 53
 54    /// <summary>Tracks the collision-service partition pass that last processed this pair.</summary>
 55    public uint PartitionVersion;
 56    /// <summary>Tracks reuse of this pooled pair instance.</summary>
 63957    public ushort PairVersion = 1;
 58
 59    /// <summary>Gets the last simulation frame in which this pair was updated.</summary>
 60    public int LastFrame { get; private set; }
 61    /// <summary>Gets the last simulation frame in which this pair had contact.</summary>
 62    public int LastCollidedFrame { get; private set; }
 63
 64    private Fixed64 _fastCollideDistance;
 65    private Fixed64 _fastDistance;
 66    /// <summary>Gets the narrow-phase dispatch type for this collider combination.</summary>
 67    public CollisionType CollisionType { get; private set; }
 63968    private bool _doPhysics = true;
 69
 70    /// <summary>Gets the remaining deferred updates before this pair is checked again.</summary>
 71    public short CullCounter { get; private set; }
 72    private bool _preventDistanceCull;
 73    private Fixed64 _fastDistanceOffset;
 74    private uint _lastColliderABroadPhaseVersion;
 75    private uint _lastColliderBBroadPhaseVersion;
 76
 77    private bool _isColliding;
 78    private bool _isCollidingChanged;
 79    private bool _notificationInProgress;
 80    private bool _separationPending;
 81    private bool _colliderANotified;
 82    private bool _colliderBNotified;
 83    private SolidBody? _pendingBodyA;
 84    private SolidBody? _pendingBodyB;
 85    private long _lifetimeVersion;
 86
 3687    internal bool IsNotificationInProgress => _notificationInProgress;
 88
 979889    internal long LifetimeVersion => _lifetimeVersion;
 90
 91    /// <summary>Gets the deterministic contact manifold owned by this pair.</summary>
 92    public ContactManifold Manifold { get; } = new();
 93
 94    private ContactWarmStartCache _warmStart;
 95
 127896    internal CollisionPair(LSCollider c1, LSCollider c2) => Initialize(c1, c2);
 97
 98    /// <summary>
 99    /// Initializes the CollisionPair with the given colliders.
 100    /// </summary>
 101    /// <param name="c1">The first collider.</param>
 102    /// <param name="c2">The second collider.</param>
 103    internal void Initialize(LSCollider c1, LSCollider c2)
 104    {
 640105        SwiftThrowHelper.ThrowIfNull(c1, nameof(c1));
 640106        SwiftThrowHelper.ThrowIfNull(c2, nameof(c2));
 640107        SwiftThrowHelper.ThrowIfArgument(c1 == c2, nameof(c2), "Cannot create a CollisionPair with the same collider.");
 640108        GravitasWorldContext context = c1.Context;
 640109        SwiftThrowHelper.ThrowIfArgument(
 640110            !ReferenceEquals(context, c2.Context),
 640111            nameof(c2),
 640112            "Colliders must be in the same context to create a CollisionPair.");
 113
 639114        Context = context;
 639115        _lifetimeVersion++;
 116
 639117        Reset();
 118
 639119        AssignPriority(c1, c2);
 639120        Id1 = ColliderA.Id;
 639121        Id2 = ColliderB.Id;
 122
 639123        CollisionType = ColliderSettings.GetCollisionType(ColliderA.Shape, ColliderB.Shape);
 124
 125        // Calculate the square of the sum of the radii of the bounding spheres
 639126        _fastCollideDistance = ColliderA!.Bounds.Scope.Magnitude + ColliderB!.Bounds.Scope.Magnitude;
 639127        _fastCollideDistance *= _fastCollideDistance;
 128
 639129        _doPhysics = ColliderA!.Body != null && ColliderB!.Body != null && !ColliderA!.IsTrigger && !ColliderB!.IsTrigge
 130
 131        // Immediately check collision. If collision distance is too large, do
 132        // not cull based on distance.
 639133        CullCounter = 0;
 639134        _preventDistanceCull = _fastCollideDistance > Context.Environment.CullFastDistanceMax;
 639135        _fastDistanceOffset = Fixed64.FromRaw((int)_fastCollideDistance) + (Fixed64.One * 2) * (Fixed64.One * 2);
 136
 639137        LastCollidedFrame = Context.FrameCount;
 639138        RefreshBroadPhaseVersions();
 639139        PairVersion++;
 639140        Active = true;
 639141    }
 142
 143    /// <summary>Orders the colliders for stable narrow-phase dispatch.</summary>
 144    public void AssignPriority(LSCollider c1, LSCollider c2)
 145    {
 639146        if (ShouldFirstColliderLead(c1, c2))
 147        {
 558148            ColliderA = c1;
 558149            ColliderB = c2;
 558150            return;
 151        }
 152
 81153        ColliderA = c2;
 81154        ColliderB = c1;
 81155    }
 156
 157    private static bool ShouldFirstColliderLead(LSCollider c1, LSCollider c2)
 158    {
 639159        if (c1.Priority != c2.Priority)
 268160            return c1.Priority > c2.Priority;
 161
 371162        if (c1.Body == null || c2.Body == null)
 26163            return true;
 164
 345165        if (c1.Body.LinearSpeed != c2.Body.LinearSpeed)
 84166            return c1.Body.LinearSpeed > c2.Body.LinearSpeed;
 167
 261168        return true;
 169    }
 170
 171    /// <summary>
 172    /// Checks and distributes collisions between colliders.
 173    /// Called by Partition Manager every fixed update if 2 colliders are on the same partion.
 174    /// </summary>
 22175    public void UpdateCollision() => UpdateCollision(CollisionResponseDispatchMode.Immediate);
 176
 4297177    internal void UpdateCollisionDeferred() => UpdateCollision(CollisionResponseDispatchMode.Deferred);
 178
 179    private void UpdateCollision(CollisionResponseDispatchMode responseMode)
 180    {
 4319181        if (!Active)
 1182            return;
 183
 4318184        UpdateLastFrame();
 4318185        DeactivateAndPoolIfRequired();
 186
 4318187        if (IsCullStateInvalidated())
 4223188            CullCounter = 0;
 189
 4318190        if (CullCounter <= 0)
 191        {
 4317192            ProcessCollision(responseMode);
 4317193            RefreshBroadPhaseVersions();
 4317194            if (_isCollidingChanged && !_isColliding)
 41195                Manifold.Reset();
 196
 4317197            HandleCullingIfNotColliding();
 4317198            return;
 199        }
 200
 1201        CullCounter--;  // Culled and one step closer to checking again.
 1202    }
 203
 4318204    private void UpdateLastFrame() => LastFrame = Context.FrameCount;
 205
 206    private void DeactivateAndPoolIfRequired()
 207    {
 4318208        if (_isPooledForDeactivation)
 4045209            return;
 210
 273211        Context.Physics.PoolForDeactivation(this);
 273212        _isPooledForDeactivation = true;
 273213    }
 214
 215    private void ProcessCollision(CollisionResponseDispatchMode responseMode)
 216    {
 4317217        if (!ShouldPerformCollisionCheck())
 218        {
 1813219            _isCollidingChanged = _isColliding;
 1813220            _isColliding = false;
 1813221            Manifold.Reset();
 1813222            _warmStart.Clear();
 1813223            return;
 224        }
 225
 2504226        bool result = CheckCollision();
 2504227        if (result)
 2488228            Context.Diagnostics.EmitContact(this, result);
 229
 2504230        if (result ^ _isColliding)
 231        {
 191232            _isColliding = result;
 191233            _isCollidingChanged = true;
 234        }
 235
 2504236        if (!result || !Manifold.HasContact)
 237        {
 16238            _warmStart.Clear();
 16239            return;
 240        }
 241
 2488242        if (!_doPhysics)
 1652243            return;
 244
 836245        if (responseMode == CollisionResponseDispatchMode.Deferred)
 246        {
 824247            Context.Physics.QueueDiscreteResponsePair(this);
 824248            return;
 249        }
 250
 12251        WakeSleepingBodiesForCollision();
 12252        CollisionResponse.CalculateImpulse(this);
 12253    }
 254
 255    internal void WakeSleepingBodiesForCollision()
 256    {
 79257        SolidBody? bodyA = ColliderA.Body;
 79258        SolidBody? bodyB = ColliderB.Body;
 79259        if (bodyA == null || bodyB == null)
 2260            return;
 261
 77262        bool bodyAAwake = bodyA.IsAwakeForCollision;
 77263        bool bodyBAwake = bodyB.IsAwakeForCollision;
 264
 77265        if (bodyA.IsSleeping && bodyBAwake)
 1266            bodyA.Wake();
 77267        if (bodyB.IsSleeping && bodyAAwake)
 2268            bodyB.Wake();
 77269    }
 270
 271    /// <summary>Raises contact or separation notifications for the current pair state.</summary>
 272    public void NotifyCollidersOfContact()
 273    {
 3855274        bool isColliding = _isColliding;
 3855275        bool isChanged = _isCollidingChanged;
 276
 3855277        var registrationA = new ColliderLifetimeToken(ColliderA);
 3855278        var registrationB = new ColliderLifetimeToken(ColliderB);
 3855279        LSCollider colliderA = registrationA.Collider;
 3855280        LSCollider colliderB = registrationB.Collider;
 3855281        SolidBody? bodyA = colliderA.Body;
 3855282        SolidBody? bodyB = colliderB.Body;
 3855283        bool isTriggerPair = colliderA.IsTrigger || colliderB.IsTrigger;
 3855284        bool shouldRaiseTriggerA = isTriggerPair && ColliderTriggerEventPolicy.ShouldRaise(colliderA, colliderB);
 3855285        bool shouldRaiseTriggerB = isTriggerPair && ColliderTriggerEventPolicy.ShouldRaise(colliderB, colliderA);
 3855286        _notificationInProgress = true;
 3855287        SwiftList<Exception>? notificationExceptions = null;
 288        try
 289        {
 3855290            if (isColliding)
 291            {
 2848292                bool notifyEnterA = isChanged && !_colliderANotified;
 2848293                _colliderANotified = true;
 2848294                colliderA.NotifyContact(
 2848295                    colliderB,
 2848296                    bodyB,
 2848297                    isColliding: true,
 2848298                    notifyEnterA,
 2848299                    allowInactive: false,
 2848300                    registrationA,
 2848301                    registrationB,
 2848302                    isTriggerPair,
 2848303                    shouldRaiseTriggerA);
 2845304                if (registrationA.IsActive && registrationB.IsActive && !_separationPending)
 305                {
 2837306                    bool notifyEnterB = isChanged && !_colliderBNotified;
 2837307                    _colliderBNotified = true;
 2837308                    colliderB.NotifyContact(
 2837309                        colliderA,
 2837310                        bodyA,
 2837311                        isColliding: true,
 2837312                        notifyEnterB,
 2837313                        allowInactive: false,
 2837314                        registrationB,
 2837315                        registrationA,
 2837316                        isTriggerPair,
 2837317                        shouldRaiseTriggerB);
 318                }
 319            }
 320            else
 321            {
 1007322                NotifySeparation(
 1007323                    registrationA,
 1007324                    registrationB,
 1007325                    bodyA,
 1007326                    bodyB,
 1007327                    isChanged,
 1007328                    isTriggerPair,
 1007329                    shouldRaiseTriggerA,
 1007330                    shouldRaiseTriggerB);
 331            }
 3849332        }
 6333        catch (Exception exception)
 334        {
 6335            CollisionNotificationExceptions.Capture(ref notificationExceptions, exception);
 6336        }
 337
 338        try
 339        {
 3855340            EndNotification(
 3855341                registrationA,
 3855342                registrationB,
 3855343                isTriggerPair,
 3855344                shouldRaiseTriggerA,
 3855345                shouldRaiseTriggerB);
 3854346        }
 1347        catch (Exception exception)
 348        {
 1349            CollisionNotificationExceptions.Capture(ref notificationExceptions, exception);
 1350        }
 351
 3855352        _isCollidingChanged &= _isColliding
 3855353            & !(_colliderANotified & _colliderBNotified);
 354
 3855355        CollisionNotificationExceptions.ThrowIfAny(notificationExceptions);
 3848356    }
 357
 358    private void EndNotification(
 359        in ColliderLifetimeToken registrationA,
 360        in ColliderLifetimeToken registrationB,
 361        bool isTriggerPair,
 362        bool shouldRaiseTriggerA,
 363        bool shouldRaiseTriggerB)
 364    {
 365        try
 366        {
 3855367            if (!_separationPending)
 3846368                return;
 369
 9370            SolidBody? bodyA = _pendingBodyA;
 9371            SolidBody? bodyB = _pendingBodyB;
 9372            ClearPendingNotificationState();
 9373            NotifySeparation(
 9374                registrationA,
 9375                registrationB,
 9376                bodyA,
 9377                bodyB,
 9378                isChanged: true,
 9379                isTriggerPair,
 9380                shouldRaiseTriggerA,
 9381                shouldRaiseTriggerB);
 8382        }
 383        finally
 384        {
 3855385            ClearPendingNotificationState();
 3855386            _notificationInProgress = false;
 3855387        }
 3854388    }
 389
 390    private void NotifySeparation(
 391        in ColliderLifetimeToken registrationA,
 392        in ColliderLifetimeToken registrationB,
 393        SolidBody? bodyA,
 394        SolidBody? bodyB,
 395        bool isChanged,
 396        bool isTriggerPair,
 397        bool shouldRaiseTriggerA,
 398        bool shouldRaiseTriggerB)
 399    {
 1016400        bool notifyA = _colliderANotified;
 1016401        bool notifyB = _colliderBNotified;
 1016402        _colliderANotified = false;
 1016403        _colliderBNotified = false;
 404
 1016405        SwiftList<Exception>? notificationExceptions = null;
 1016406        if (notifyA)
 407        {
 408            try
 409            {
 67410                registrationA.Collider.NotifyContact(
 67411                    registrationB.Collider,
 67412                    bodyB,
 67413                    isColliding: false,
 67414                    isChanged,
 67415                    allowInactive: true,
 67416                    registrationA,
 67417                    registrationB,
 67418                    isTriggerPair,
 67419                    shouldRaiseTriggerA);
 63420            }
 4421            catch (Exception exception)
 422            {
 4423                CollisionNotificationExceptions.Capture(ref notificationExceptions, exception);
 4424            }
 425        }
 426
 1016427        if (notifyB && registrationB.IsCurrentLifetime)
 428        {
 429            try
 430            {
 56431                registrationB.Collider.NotifyContact(
 56432                    registrationA.Collider,
 56433                    bodyA,
 56434                    isColliding: false,
 56435                    isChanged,
 56436                    allowInactive: true,
 56437                    registrationB,
 56438                    registrationA,
 56439                    isTriggerPair,
 56440                    shouldRaiseTriggerB);
 55441            }
 1442            catch (Exception exception)
 443            {
 1444                CollisionNotificationExceptions.Capture(ref notificationExceptions, exception);
 1445            }
 446        }
 447
 1016448        CollisionNotificationExceptions.ThrowIfAny(notificationExceptions);
 1012449    }
 450
 451    private void ClearPendingNotificationState()
 452    {
 4504453        _separationPending = false;
 4504454        _pendingBodyA = null;
 4504455        _pendingBodyB = null;
 4504456    }
 457
 458    private void HandleCullingIfNotColliding()
 459    {
 4317460        if (_isColliding)
 461        {
 2488462            LastCollidedFrame = Context.FrameCount;
 2488463            return;
 464        }
 465
 1829466        CalculateCullScore();
 1829467    }
 468
 469    internal bool TryPreserveSleepingRestingContact()
 470    {
 4728471        if (!_isColliding || !Manifold.HasContact)
 1884472            return false;
 473
 2844474        if (ColliderA.Body?.IsSleeping != true && ColliderB.Body?.IsSleeping != true)
 2454475            return false;
 476
 390477        LastCollidedFrame = Context.FrameCount;
 390478        return true;
 479    }
 480
 481    private bool CheckCollision()
 482    {
 2504483        if (!BroadPhaseVersionChanged() && _isColliding)
 19484            return _isColliding;
 485
 2485486        return CollisionDetection.DoCollisionCheck(this);
 487    }
 488
 489    private bool IsCullStateInvalidated()
 490    {
 4318491        return ColliderA.PartitionChanged
 4318492            || ColliderB.PartitionChanged
 4318493            || BroadPhaseVersionChanged();
 494    }
 495
 496    private bool BroadPhaseVersionChanged()
 497    {
 2601498        return ColliderA.BroadPhaseVersion != _lastColliderABroadPhaseVersion
 2601499            || ColliderB.BroadPhaseVersion != _lastColliderBBroadPhaseVersion;
 500    }
 501
 502    private void RefreshBroadPhaseVersions()
 503    {
 4956504        _lastColliderABroadPhaseVersion = ColliderA.BroadPhaseVersion;
 4956505        _lastColliderBBroadPhaseVersion = ColliderB.BroadPhaseVersion;
 4956506    }
 507
 508    private bool ShouldPerformCollisionCheck()
 509    {
 510        // Center distance remains a scheduling signal only. Canonical geometry
 511        // may extend beyond a scalar face while its conservative broad-phase
 512        // bounds are clipped to the representable domain, which can shorten
 513        // Bounds.Scope without shortening the physical shape.
 4317514        _fastDistance = Vector3d.DistanceSquared(ColliderA.Center, ColliderB.Center);
 515
 516        // Inclusive bounds overlap preserves zero-depth touching contacts for the manifold pass.
 4317517        return BoundsOverlapInclusive(ColliderA, ColliderB);
 518    }
 519
 520    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 521    private static bool BoundsOverlapInclusive(LSCollider colliderA, LSCollider colliderB)
 522    {
 4317523        return colliderA.BoundsMin.X <= colliderB.BoundsMax.X
 4317524            && colliderA.BoundsMax.X >= colliderB.BoundsMin.X
 4317525            && colliderA.BoundsMin.Y <= colliderB.BoundsMax.Y
 4317526            && colliderA.BoundsMax.Y >= colliderB.BoundsMin.Y
 4317527            && colliderA.BoundsMin.Z <= colliderB.BoundsMax.Z
 4317528            && colliderA.BoundsMax.Z >= colliderB.BoundsMin.Z;
 529    }
 530
 531    private void CalculateCullScore()
 532    {
 1829533        int distanceScore = 0;
 1829534        int velocityScore = 0;
 1829535        if (!_preventDistanceCull)
 536        {
 1683537            int distanceMax = Context.Environment.CullDistanceMax;
 1683538            if (distanceMax > 0)
 539            {
 1682540                int step = GetCullDistanceStep(World!);
 1682541                distanceScore = Math.Clamp((int)(_fastDistance - _fastDistanceOffset) / step + Context.Collisions.CullDi
 542            }
 543
 1683544            int cullVelocityStep = Context.Environment.CullVelocityStep;
 1683545            if (cullVelocityStep > 0)
 1682546                velocityScore = Math.Clamp((int)(ColliderA.Velocity - ColliderB.Velocity).Magnitude / cullVelocityStep, 
 547        }
 548
 1829549        int timeScore = 0;
 1829550        int cullTimeStep = Context.Environment.CullTimeStep;
 1829551        if (cullTimeStep > 0)
 1828552            timeScore = Math.Clamp((Context.FrameCount - LastCollidedFrame) / cullTimeStep, 0, Context.Environment.CullT
 553
 1829554        CullCounter = (short)Math.Clamp(distanceScore + timeScore - velocityScore, 0, short.MaxValue);
 1829555    }
 556
 557    /// <summary>
 558    /// Defines the step value for distance-based culling. The score is increased
 559    /// when the distance between objects increases. Higher values make the culling more aggressive for distant objects.
 560    /// </summary>
 561    private int GetCullDistanceStep(GridWorld world)
 562    {
 1682563        int distanceMax = Context.Environment.CullDistanceMax;
 1682564        Fixed64 cellEdge = GridTopologyMetricUtility.GetRepresentativeCellEdge(world);
 1682565        int step = ((cellEdge + Fixed64.One * 2) * (cellEdge + Fixed64.One * 2) / distanceMax).CeilToInt();
 1682566        return Math.Max(1, step);
 567    }
 568
 569    /// <summary>Clears transient contact and notification state before reuse.</summary>
 570    public void Reset()
 571    {
 640572        Manifold.Reset();
 640573        _warmStart.Clear();
 640574        _isColliding = false;
 640575        _isCollidingChanged = false;
 640576        _isPooledForDeactivation = false;
 640577        _notificationInProgress = false;
 640578        _colliderANotified = false;
 640579        _colliderBNotified = false;
 640580        ClearPendingNotificationState();
 640581    }
 582
 583    internal void StoreWarmStartImpulse(
 584        ulong contactId,
 585        Vector3d normal,
 586        Fixed64 normalImpulse,
 587        Fixed64 tangentImpulse,
 588        Fixed64 secondaryTangentImpulse = default) =>
 9118589        _warmStart.Set(contactId, normal, normalImpulse, tangentImpulse, secondaryTangentImpulse);
 590
 591    internal bool TryGetWarmStartImpulse(ulong contactId, out ContactWarmStartImpulse impulse) =>
 9132592        _warmStart.TryGet(contactId, out impulse);
 593
 594    internal void RemoveWarmStartImpulse(ulong contactId) =>
 11595        _warmStart.Remove(contactId);
 596
 2597    internal void ClearWarmStart() => _warmStart.Clear();
 598
 599    /// <summary>
 600    /// Deactivates the CollisionPair.
 601    /// </summary>
 602    public void Deactivate()
 603    {
 39604        bool notifySeparation = _isColliding || _colliderANotified || _colliderBNotified;
 39605        if (notifySeparation)
 606        {
 30607            _isColliding = false;
 30608            _isCollidingChanged = true;
 30609            if (_notificationInProgress)
 610            {
 9611                _separationPending = true;
 9612                _pendingBodyA = ColliderA.Body;
 9613                _pendingBodyB = ColliderB.Body;
 614            }
 615        }
 616
 39617        Manifold.Reset();
 39618        _warmStart.Clear();
 39619        _isPooledForDeactivation = false;
 39620        Active = false;
 621
 39622        if (notifySeparation && !_notificationInProgress)
 21623            NotifyCollidersOfContact();
 38624    }
 625}

/home/runner/work/Gravitas/Gravitas/src/Gravitas/CollisionHandling/Pairs/3D/CollisionPair.ReplayHash.cs

#LineLine coverage
 1//=======================================================================
 2// CollisionPair.ReplayHash.cs
 3//=======================================================================
 4// MIT License, Copyright (c) 2026-present David Oravsky (mrdav30)
 5// See LICENSE file in the project root for full license information.
 6//=======================================================================
 7
 8using Chronicler;
 9using FixedMathSharp.Chronicler;
 10using Gravitas.Materials;
 11
 12namespace Gravitas.CollisionHandling;
 13
 14public partial class CollisionPair
 15{
 16    internal void ContributeReplayHash(
 17        ref ChronicleHashWriter writer,
 18        GravitasReplayHashMode mode)
 19    {
 4320        writer.WriteSection("pair.3d", 2);
 4321        writer.WriteBool(Active);
 4322        writer.WriteInt32(ColliderA.ReplayOrdinal);
 4323        writer.WriteInt32(ColliderB.ReplayOrdinal);
 4324        writer.WriteEnum(CollisionType);
 4325        writer.WriteUInt32(PartitionVersion);
 4326        writer.WriteInt32(PairVersion);
 4327        writer.WriteInt32(LastFrame);
 4328        writer.WriteInt32(LastCollidedFrame);
 4329        writer.WriteBool(_doPhysics);
 4330        writer.WriteInt32(CullCounter);
 4331        writer.WriteBool(_preventDistanceCull);
 4332        writer.WriteBool(_isColliding);
 4333        writer.WriteBool(_isCollidingChanged);
 4334        writer.WriteFixed64(_fastCollideDistance);
 4335        writer.WriteFixed64(_fastDistance);
 4336        writer.WriteFixed64(_fastDistanceOffset);
 4337        writer.WriteUInt32(_lastColliderABroadPhaseVersion);
 4338        writer.WriteUInt32(_lastColliderBBroadPhaseVersion);
 4339        ContributeManifoldReplayHash(ref writer, Manifold);
 4340        ContributeWarmStartReplayHash(ref writer, _warmStart);
 41
 4342        if (mode != GravitasReplayHashMode.AuthoritativeWithSolverCaches)
 3943            return;
 44
 445        writer.WriteSection("pair.3d.caches", 1);
 446        writer.WriteBool(_isPooledForDeactivation);
 447    }
 48
 49    private static void ContributeManifoldReplayHash(
 50        ref ChronicleHashWriter writer,
 51        ContactManifold manifold)
 52    {
 4353        writer.WriteSection("manifold.3d", 6);
 4354        writer.WriteInt32(manifold.LastUpdatedFrame);
 4355        writer.WriteInt32(manifold.Count);
 10056        for (int i = 0; i < manifold.Count; i++)
 57        {
 758            ManifoldContact contact = manifold[i];
 759            writer.WriteUInt64(contact.ContactId);
 760            writer.WriteInt32(contact.FeatureNamespaceA);
 761            writer.WriteVector3d(contact.AnchorA.Origin);
 762            writer.WriteQuaternion(contact.AnchorA.Rotation);
 763            writer.WriteVector3d(contact.AnchorA.LocalPoint);
 764            writer.WriteVector3d(contact.AnchorA.LocalDisplacement);
 765            writer.WriteUInt64(contact.AnchorA.GetLocalFeatureHash64());
 766            writer.WriteInt32(contact.FeatureNamespaceB);
 767            writer.WriteVector3d(contact.AnchorB.Origin);
 768            writer.WriteQuaternion(contact.AnchorB.Rotation);
 769            writer.WriteVector3d(contact.AnchorB.LocalPoint);
 770            writer.WriteVector3d(contact.AnchorB.LocalDisplacement);
 771            writer.WriteUInt64(contact.AnchorB.GetLocalFeatureHash64());
 772            writer.WriteFixed64(contact.Depth);
 773            writer.WriteBool(contact.DepthIsClamped);
 774            writer.WriteVector3d(contact.Normal);
 775            writer.WriteBool(contact.HasMaterialOverride);
 776            if (contact.HasMaterialOverride)
 77            {
 378                WriteMaterial(ref writer, contact.MaterialA);
 379                WriteMaterial(ref writer, contact.MaterialB);
 80            }
 81        }
 4382    }
 83
 84    private static void WriteMaterial(ref ChronicleHashWriter writer, PhysicsMaterial material)
 85    {
 686        writer.WriteFixed64(material.StaticFriction);
 687        writer.WriteFixed64(material.DynamicFriction);
 688        writer.WriteFixed64(material.Restitution);
 689        writer.WriteEnum(material.FrictionCombine);
 690        writer.WriteEnum(material.RestitutionCombine);
 691    }
 92
 93    private static void ContributeWarmStartReplayHash(
 94        ref ChronicleHashWriter writer,
 95        ContactWarmStartCache warmStart)
 96    {
 4397        writer.WriteSection("warm-start.3d", 1);
 4398        writer.WriteInt32(warmStart.Count);
 8899        for (int i = 0; i < warmStart.Count; i++)
 100        {
 1101            writer.WriteUInt64(warmStart.GetContactIdForReplayHash(i));
 1102            ContactWarmStartImpulse impulse = warmStart.GetImpulseForReplayHash(i);
 1103            writer.WriteVector3d(impulse.Normal);
 1104            writer.WriteFixed64(impulse.NormalImpulse);
 1105            writer.WriteFixed64(impulse.TangentImpulse);
 1106            writer.WriteFixed64(impulse.SecondaryTangentImpulse);
 107        }
 43108    }
 109}

Methods/Properties

.ctor(Gravitas.Colliders.LSCollider,Gravitas.Colliders.LSCollider)
get_World()
get_IsNotificationInProgress()
get_LifetimeVersion()
Initialize(Gravitas.Colliders.LSCollider,Gravitas.Colliders.LSCollider)
AssignPriority(Gravitas.Colliders.LSCollider,Gravitas.Colliders.LSCollider)
ShouldFirstColliderLead(Gravitas.Colliders.LSCollider,Gravitas.Colliders.LSCollider)
UpdateCollision()
UpdateCollisionDeferred()
UpdateCollision(Gravitas.CollisionHandling.CollisionResponseDispatchMode)
UpdateLastFrame()
DeactivateAndPoolIfRequired()
ProcessCollision(Gravitas.CollisionHandling.CollisionResponseDispatchMode)
WakeSleepingBodiesForCollision()
NotifyCollidersOfContact()
EndNotification(Gravitas.Colliders.ColliderLifetimeToken&,Gravitas.Colliders.ColliderLifetimeToken&,System.Boolean,System.Boolean,System.Boolean)
NotifySeparation(Gravitas.Colliders.ColliderLifetimeToken&,Gravitas.Colliders.ColliderLifetimeToken&,Gravitas.SolidBody,Gravitas.SolidBody,System.Boolean,System.Boolean,System.Boolean,System.Boolean)
ClearPendingNotificationState()
HandleCullingIfNotColliding()
TryPreserveSleepingRestingContact()
CheckCollision()
IsCullStateInvalidated()
BroadPhaseVersionChanged()
RefreshBroadPhaseVersions()
ShouldPerformCollisionCheck()
BoundsOverlapInclusive(Gravitas.Colliders.LSCollider,Gravitas.Colliders.LSCollider)
CalculateCullScore()
GetCullDistanceStep(GridForge.Grids.GridWorld)
Reset()
StoreWarmStartImpulse(System.UInt64,FixedMathSharp.Vector3d,FixedMathSharp.Fixed64,FixedMathSharp.Fixed64,FixedMathSharp.Fixed64)
TryGetWarmStartImpulse(System.UInt64,Gravitas.CollisionHandling.ContactWarmStartImpulse&)
RemoveWarmStartImpulse(System.UInt64)
ClearWarmStart()
Deactivate()
ContributeReplayHash(Chronicler.ChronicleHashWriter&,Gravitas.GravitasReplayHashMode)
ContributeManifoldReplayHash(Chronicler.ChronicleHashWriter&,Gravitas.CollisionHandling.ContactManifold)
WriteMaterial(Chronicler.ChronicleHashWriter&,Gravitas.Materials.PhysicsMaterial)
ContributeWarmStartReplayHash(Chronicler.ChronicleHashWriter&,Gravitas.CollisionHandling.ContactWarmStartCache)