< Summary

Line coverage
100%
Covered lines: 107
Uncovered lines: 0
Coverable lines: 107
Total lines: 342
Line coverage: 100%
Branch coverage
100%
Covered branches: 20
Total branches: 20
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/Gravitas/Gravitas/src/Gravitas/Settings/PhysicsSettings.cs

#LineLine coverage
 1//=======================================================================
 2// PhysicsSettings.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.Support;
 10using SwiftCollections;
 11using System;
 12using System.Runtime.CompilerServices;
 13
 14namespace Gravitas;
 15
 16/// <summary>
 17/// Stores context-local configuration for deterministic physics simulation.
 18/// </summary>
 19public sealed partial class PhysicsSettings
 20{
 21    /// <summary>
 22    /// Default fixed-step frame rate in simulation frames per second.
 23    /// </summary>
 24    public const int DefaultFrameRate = 32;
 25
 26    /// <summary>
 27    /// Maximum supported fixed-step frame rate.
 28    /// </summary>
 29    /// <remarks>
 30    /// Higher values quantize <see cref="GravitasWorldContext.DeltaTime"/> at or below
 31    /// <see cref="Fixed64.Epsilon"/>, which would make integration and CCD degeneracy
 32    /// checks disagree about whether a step has meaningful duration.
 33    /// </remarks>
 34    public const int MaxResolvableFrameRate = (int)(FixedMath.ONE_L / (FixedMath.DEFAULT_TOLERANCE_L + 1));
 35
 36    /// <summary>
 37    /// Maximum number of physics layers represented by a layer mask.
 38    /// </summary>
 39    public const int MaxLayers = 32;
 40
 41    /// <summary>
 42    /// Default number of frames an empty partition remains retained for reuse.
 43    /// </summary>
 44    public const int DefaultRetainedPartitionTimeToKillFrames = DefaultFrameRate * 10;
 45
 46    /// <summary>
 47    /// Default maximum number of retained partitions checked per retirement sweep.
 48    /// </summary>
 49    public const int DefaultRetainedPartitionRetirementSweepBudget = 64;
 50
 51    /// <summary>
 52    /// Default maximum same-frame time-of-impact iterations for continuous collision.
 53    /// </summary>
 54    public const int DefaultContinuousCollisionMaxToiIterations = 4;
 55
 56    /// <summary>
 57    /// Default projected-impulse iteration count for discrete 3D constraint islands.
 58    /// </summary>
 59    public const int DefaultDiscreteSolverIterations = 6;
 60
 61    /// <summary>
 62    /// Default closing-speed threshold below which restitution is disabled.
 63    /// </summary>
 164    public static readonly Fixed64 DefaultRestitutionVelocityThreshold = (Fixed64)0.25f;
 65
 66    /// <summary>
 67    /// Default Y-axis half-thickness for 2D colliders embedded in mixed queries and contacts.
 68    /// </summary>
 169    public static readonly Fixed64 DefaultMixed2DHalfThickness = Fixed64.Half;
 70
 71    /// <summary>
 72    /// Default include mask used for ground and support checks.
 73    /// </summary>
 174    public static readonly PhysicsLayerMask DefaultGroundCheckLayerMask = PhysicsLayerMask.FromLayer(new PhysicsLayer(0)
 75
 76    /// <summary>
 77    /// Gets the fixed-step frame rate in simulation frames per second.
 78    /// </summary>
 79    public int FrameRate { get; private set; }
 80
 81    private readonly bool[,] _collisionMatrix;
 82
 83    /// <summary>
 84    /// Gets the layer-to-layer physical collision enablement matrix.
 85    /// </summary>
 16810686    public bool[,] CollisionMatrix => _collisionMatrix;
 87
 88    /// <summary>
 89    /// Gets or sets whether reusable runtime collision objects are pooled.
 90    /// </summary>
 91    public bool PoolingEnabled { get; set; } = true;
 92
 93    /// <summary>
 94    /// Gets or sets the include mask used for ground and support checks.
 95    /// </summary>
 96    public PhysicsLayerMask GroundCheckLayerMask { get; set; }
 97
 564898    private int _retainedPartitionTimeToKillFrames = DefaultRetainedPartitionTimeToKillFrames;
 564899    private int _retainedPartitionRetirementSweepBudget = DefaultRetainedPartitionRetirementSweepBudget;
 5648100    private ContinuousCollisionMode _defaultContinuousCollisionMode = ContinuousCollisionMode.Discrete;
 5648101    private int _continuousCollisionMaxToiIterations = DefaultContinuousCollisionMaxToiIterations;
 5648102    private int _discreteSolverIterations = DefaultDiscreteSolverIterations;
 5648103    private Fixed64 _restitutionVelocityThreshold = DefaultRestitutionVelocityThreshold;
 5648104    private PhysicsRuntimeMode _runtimeMode = PhysicsRuntimeMode.ThreeD;
 5648105    private Fixed64 _mixed2DHalfThickness = DefaultMixed2DHalfThickness;
 106
 107    /// <summary>
 108    /// Gets or sets how many simulation frames an empty voxel partition should stay attached for fast reuse.
 109    /// A value of zero retires eligible partitions on the next retirement sweep.
 110    /// </summary>
 111    public int RetainedPartitionTimeToKillFrames
 112    {
 4542113        get => _retainedPartitionTimeToKillFrames;
 114        set
 115        {
 26116            SwiftThrowHelper.ThrowIfNegative(value, nameof(value));
 26117            _retainedPartitionTimeToKillFrames = value;
 26118        }
 119    }
 120
 121    /// <summary>
 122    /// Gets or sets the maximum retained partitions checked for retirement during one collision distribution step.
 123    /// A value of zero disables retirement sweeps.
 124    /// </summary>
 125    public int RetainedPartitionRetirementSweepBudget
 126    {
 4542127        get => _retainedPartitionRetirementSweepBudget;
 128        set
 129        {
 32130            SwiftThrowHelper.ThrowIfNegative(value, nameof(value));
 32131            _retainedPartitionRetirementSweepBudget = value;
 32132        }
 133    }
 134
 135    /// <summary>
 136    /// Gets or sets the default tunneling policy used by bodies configured to inherit from the context.
 137    /// A context default of <see cref="ContinuousCollisionMode.Inherit"/> resolves to
 138    /// <see cref="ContinuousCollisionMode.Discrete"/>.
 139    /// </summary>
 140    /// <exception cref="ArgumentOutOfRangeException">The value is not a declared continuous-collision mode.</exception>
 141    public ContinuousCollisionMode DefaultContinuousCollisionMode
 142    {
 21291143        get => _defaultContinuousCollisionMode;
 144        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 145        set
 146        {
 40147            value.ThrowIfInvalid(nameof(value));
 36148            _defaultContinuousCollisionMode = value;
 36149        }
 150    }
 151
 152    /// <summary>
 153    /// Gets or sets the maximum same-frame continuous-collision TOI iterations one body or handoff queue may consume.
 154    /// </summary>
 155    public int ContinuousCollisionMaxToiIterations
 156    {
 5831157        get => _continuousCollisionMaxToiIterations;
 158        set
 159        {
 85160            SwiftThrowHelper.ThrowIfNegativeOrZero(value, nameof(value));
 83161            _continuousCollisionMaxToiIterations = value;
 83162        }
 163    }
 164
 165    /// <summary>
 166    /// Gets or sets the bounded projected-impulse iteration count used for 3D discrete
 167    /// contact and joint constraint islands. Contact-only single-pair scenes stay on
 168    /// the direct one-pass response path.
 169    /// </summary>
 170    public int DiscreteSolverIterations
 171    {
 2104172        get => _discreteSolverIterations;
 173        set
 174        {
 190175            SwiftThrowHelper.ThrowIfNegativeOrZero(value, nameof(value));
 188176            _discreteSolverIterations = value;
 188177        }
 178    }
 179
 180    /// <summary>
 181    /// Gets or sets the closing speed at or below which contact response uses zero restitution.
 182    /// </summary>
 183    public Fixed64 RestitutionVelocityThreshold
 184    {
 11858185        get => _restitutionVelocityThreshold;
 186        set
 187        {
 53188            SwiftThrowHelper.ThrowIfArgument(
 53189                value < Fixed64.Zero,
 53190                nameof(value),
 53191                "Restitution velocity threshold cannot be negative.");
 52192            _restitutionVelocityThreshold = value;
 52193        }
 194    }
 195
 196    /// <summary>
 197    /// Gets or sets the default half-thickness used when pure 2D colliders are embedded into mixed 2D/3D contacts.
 198    /// </summary>
 199    public Fixed64 Mixed2DHalfThickness
 200    {
 21748201        get => _mixed2DHalfThickness;
 202        set
 203        {
 24204            SwiftThrowHelper.ThrowIfArgument(
 24205                value <= Fixed64.Zero,
 24206                nameof(value),
 24207                "Mixed 2D half-thickness must be greater than zero.");
 22208            _mixed2DHalfThickness = value;
 22209        }
 210    }
 211
 212    /// <summary>
 213    /// Gets or sets which dimensional physics service this context should advance.
 214    /// </summary>
 215    public PhysicsRuntimeMode RuntimeMode
 216    {
 33830217        get => _runtimeMode;
 218        set
 219        {
 3455220            SwiftThrowHelper.ThrowIfArgument(
 3455221                !value.IsValid(),
 3455222                nameof(value),
 3455223                "Physics runtime mode must be TwoD, ThreeD, Both, or Mixed.");
 3450224            _runtimeMode = value;
 3450225        }
 226    }
 227
 228    /// <summary>
 229    /// Creates physics settings, using registered-layer defaults for omitted values.
 230    /// </summary>
 5648231    public PhysicsSettings(
 5648232        int? frameRate,
 5648233        bool[,]? collisionMatrix,
 5648234        PhysicsLayerMask? groundCheckLayerMask = null)
 235    {
 5648236        SetFrameRate(frameRate ?? DefaultFrameRate);
 5647237        _collisionMatrix = collisionMatrix ?? GetRegisteredCollisionMatrix();
 5647238        GroundCheckLayerMask = groundCheckLayerMask ?? DefaultGroundCheckLayerMask;
 5647239    }
 240
 241    /// <summary>
 242    /// Sets the fixed-step frame rate after validating its representable range.
 243    /// </summary>
 244    public void SetFrameRate(int frameRate)
 245    {
 6978246        ThrowIfInvalidFrameRate(frameRate);
 6975247        FrameRate = frameRate;
 6975248    }
 249
 250    internal static void ThrowIfInvalidFrameRate(int frameRate)
 251    {
 13926252        SwiftThrowHelper.ThrowIfNegativeOrZero(frameRate, nameof(frameRate));
 13926253        if (frameRate > MaxResolvableFrameRate)
 254        {
 4255            throw new ArgumentOutOfRangeException(
 4256                nameof(frameRate),
 4257                frameRate,
 4258                $"Frame rate cannot exceed {MaxResolvableFrameRate}.");
 259        }
 13922260    }
 261
 262    /// <summary>
 263    /// Creates settings with default values and the currently registered layer matrix.
 264    /// </summary>
 265    public static PhysicsSettings DefaultSettings()
 266    {
 5092267        bool[,] collisionMatrix = GetRegisteredCollisionMatrix();
 5092268        return new PhysicsSettings(DefaultFrameRate, collisionMatrix);
 269    }
 270
 271    /// <summary>
 272    /// Creates a fully enabled square collision matrix sized to the registered layer names.
 273    /// </summary>
 274    public static bool[,] GetRegisteredCollisionMatrix()
 275    {
 5596276        SwiftList<string> layersList = new();
 277
 369336278        for (int i = 0; i < MaxLayers; ++i)
 279        {
 179072280            string? layerName = PhysicsLayer.LayerToName(i);
 281            // Check if the layer has a name
 179072282            if (!string.IsNullOrEmpty(layerName))
 4349283                layersList.Add(layerName);
 284        }
 285
 5596286        string[] layerNames = layersList.ToArray();
 5596287        int numberOfLayers = layerNames.Length;
 288
 5596289        if (numberOfLayers == 0)
 1248290            return new bool[0, 0];
 291
 4348292        bool[,] collisionMatrix = new bool[numberOfLayers, numberOfLayers];
 17394293        for (int i = 0; i < numberOfLayers; ++i)
 17400294            for (int j = 0; j < numberOfLayers; ++j)
 4351295                collisionMatrix[i, j] = true;
 296
 4348297        return collisionMatrix;
 298    }
 299}

/home/runner/work/Gravitas/Gravitas/src/Gravitas/Settings/PhysicsSettings.ReplayHash.cs

#LineLine coverage
 1//=======================================================================
 2// PhysicsSettings.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;
 10
 11namespace Gravitas;
 12
 13/// <content>
 14/// Contributes physics settings to deterministic replay hashes.
 15/// </content>
 16public sealed partial class PhysicsSettings
 17{
 18    internal void ContributeReplayHash(ref ChronicleHashWriter writer)
 19    {
 100820        writer.WriteSection("settings", 1);
 100821        writer.WriteInt32(FrameRate);
 100822        writer.WriteBool(PoolingEnabled);
 100823        writer.WritePhysicsLayerMask(GroundCheckLayerMask);
 100824        writer.WriteInt32(RetainedPartitionTimeToKillFrames);
 100825        writer.WriteInt32(RetainedPartitionRetirementSweepBudget);
 100826        writer.WriteEnum(DefaultContinuousCollisionMode);
 100827        writer.WriteInt32(ContinuousCollisionMaxToiIterations);
 100828        writer.WriteInt32(DiscreteSolverIterations);
 100829        writer.WriteFixed64(RestitutionVelocityThreshold);
 100830        writer.WriteFixed64(Mixed2DHalfThickness);
 100831        writer.WriteEnum(RuntimeMode);
 32
 100833        int rows = _collisionMatrix.GetLength(0);
 100834        int columns = _collisionMatrix.GetLength(1);
 100835        writer.WriteInt32(rows);
 100836        writer.WriteInt32(columns);
 378037        for (int row = 0; row < rows; row++)
 38        {
 358639            for (int column = 0; column < columns; column++)
 91140                writer.WriteBool(_collisionMatrix[row, column]);
 41        }
 100842    }
 43}