< Summary

Information
Class: Gravitas.GravitasWorldContext
Assembly: Gravitas
File(s): /home/runner/work/Gravitas/Gravitas/src/Gravitas/Runtime/GravitasWorldContext.cs
Line coverage
100%
Covered lines: 232
Uncovered lines: 0
Coverable lines: 232
Total lines: 633
Line coverage: 100%
Branch coverage
100%
Covered branches: 62
Total branches: 62
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/Runtime/GravitasWorldContext.cs

#LineLine coverage
 1//=======================================================================
 2// GravitasWorldContext.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;
 10using Gravitas.CollisionHandling;
 11using Gravitas.Diagnostics;
 12using Gravitas.Queries;
 13using Gravitas.Support;
 14using GridForge.Grids;
 15using GridForge.Grids.Topology;
 16using SwiftCollections;
 17using System;
 18
 19namespace Gravitas;
 20
 21/// <summary>
 22/// Owns Gravitas runtime state for one explicit <see cref="GridWorld"/>.
 23/// </summary>
 24/// <remarks>
 25/// This is the context-first host API for multi-world Gravitas usage. Phase 1 owns
 26/// world lifetime, deterministic clock state, and lifecycle hooks; later phases move
 27/// physics registries, collision partitioning, query buffers, and coroutine state here.
 28/// </remarks>
 29public sealed class GravitasWorldContext : IDisposable
 30{
 131    private static readonly object _worldOwnershipLock = new();
 32
 133    private static readonly SwiftDictionary<GridWorld, GravitasWorldContext> _worldOwners = new();
 34
 508335    private readonly GravitasClock _clock = new();
 36
 508337    private readonly GravitasLifecycleHooks _hooks = new();
 38
 39    private readonly bool _ownsWorld;
 40
 41    private bool _disposed;
 42
 43    private int _lateSimulateToken;
 44
 45    private int _simulationPhaseDepth;
 46
 47    private bool _fixedStepOpen;
 48
 764749    internal void EnterSimulationPhase() => _simulationPhaseDepth++;
 50
 764751    internal void ExitSimulationPhase() => _simulationPhaseDepth--;
 52
 53    internal void ThrowIfFixedStepMutationNotAllowed()
 54    {
 26955        SwiftThrowHelper.ThrowIfTrue(
 26956            _fixedStepOpen || _simulationPhaseDepth > 0,
 26957            nameof(GravitasWorldContext),
 26958            "Authoritative body roles, static poses, and loaded state can change only outside the Simulate-to-LateSimula
 25659    }
 60
 508361    private GravitasWorldContext(GridWorld world, bool ownsWorld)
 62    {
 508363        World = world;
 508364        _ownsWorld = ownsWorld;
 508365        Settings = PhysicsSettings.DefaultSettings();
 508366        Environment = PhysicsEnvironment.Default(Settings.FrameRate);
 508367        CollisionScratch = new CollisionSatScratch();
 508368        Diagnostics = new GravitasDiagnosticSink(this);
 508369        Constraints3D = new GravitasConstraint3DService(this);
 508370        Constraints2D = new GravitasConstraint2DService(this);
 508371        Collisions = new GravitasCollisionService(this);
 508372        Collisions2D = new GravitasCollision2DService(this);
 508373        Physics = new GravitasPhysicsService(this);
 508374        Physics2D = new GravitasPhysics2DService(this);
 508375        MixedCollisions = new GravitasMixedCollisionService(this);
 508376        Query2D = new GravitasQuery2DService(this);
 508377        Query3D = new GravitasQuery3DService(this);
 508378        QueryMixed = new GravitasQueryMixedService(this);
 508379        Coroutines = new GravitasCoroutineService(this);
 508380    }
 81
 82    /// <summary>
 83    /// Gets the explicit GridForge world owned or referenced by this context.
 84    /// </summary>
 85    public GridWorld World { get; }
 86
 87    /// <summary>
 88    /// Gets this context's world-local physics settings.
 89    /// </summary>
 90    public PhysicsSettings Settings { get; private set; }
 91
 92    /// <summary>
 93    /// Gets this context's world-local physical environment values.
 94    /// </summary>
 95    public PhysicsEnvironment Environment { get; }
 96
 97    /// <summary>
 98    /// Gets this context's world-local collision partitioning service.
 99    /// </summary>
 100    public GravitasCollisionService Collisions { get; }
 101
 102    /// <summary>
 103    /// Gets this context's world-local pure 2D collision partitioning service.
 104    /// </summary>
 105    public GravitasCollision2DService Collisions2D { get; }
 106
 107    /// <summary>
 108    /// Gets this context's world-local physics registration and pair service.
 109    /// </summary>
 110    public GravitasPhysicsService Physics { get; }
 111
 112    /// <summary>
 113    /// Gets this context's world-local pure 2D physics service.
 114    /// </summary>
 115    public GravitasPhysics2DService Physics2D { get; }
 116
 117    internal GravitasMixedCollisionService MixedCollisions { get; }
 118
 119    /// <summary>
 120    /// Gets this context's world-local pure 2D query service.
 121    /// </summary>
 122    public GravitasQuery2DService Query2D { get; }
 123
 124    /// <summary>
 125    /// Gets this context's world-local 3D query service.
 126    /// </summary>
 127    public GravitasQuery3DService Query3D { get; }
 128
 129    /// <summary>
 130    /// Gets this context's explicit mixed 3D/2D query service.
 131    /// </summary>
 132    public GravitasQueryMixedService QueryMixed { get; }
 133
 134    /// <summary>
 135    /// Gets this context's world-local lockstep coroutine service.
 136    /// </summary>
 137    public GravitasCoroutineService Coroutines { get; }
 138
 139    /// <summary>
 140    /// Gets this context's deterministic diagnostic sink.
 141    /// </summary>
 142    public GravitasDiagnosticSink Diagnostics { get; }
 143
 144    /// <summary>
 145    /// Gets this context's world-local 3D constraint and ragdoll service.
 146    /// </summary>
 147    public GravitasConstraint3DService Constraints3D { get; }
 148
 149    /// <summary>
 150    /// Gets this context's world-local pure 2D constraint and ragdoll service.
 151    /// </summary>
 152    public GravitasConstraint2DService Constraints2D { get; }
 153
 154    internal CollisionSatScratch CollisionScratch { get; }
 155
 156    /// <summary>
 157    /// Gets whether this context has been disposed.
 158    /// </summary>
 80159    public bool IsDisposed => _disposed;
 160
 161    /// <summary>
 162    /// Gets a representative grid cell edge for this context's world.
 163    /// </summary>
 164    public Fixed64 VoxelSize
 165    {
 166        get
 167        {
 6168            ThrowIfDisposed();
 6169            return GridTopologyMetricUtility.GetRepresentativeCellEdge(World);
 170        }
 171    }
 172
 173    /// <summary>
 174    /// Gets this context's fixed simulation frame rate.
 175    /// </summary>
 176    public int FrameRate
 177    {
 178        get
 179        {
 16090180            ThrowIfDisposed();
 16090181            return _clock.FrameRate;
 182        }
 183    }
 184
 185    /// <summary>
 186    /// Gets this context's fixed simulation time step.
 187    /// </summary>
 188    public Fixed64 DeltaTime
 189    {
 190        get
 191        {
 609870192            ThrowIfDisposed();
 609870193            return _clock.DeltaTime;
 194        }
 195    }
 196
 197    /// <summary>
 198    /// Gets the reciprocal of this context's fixed simulation time step.
 199    /// </summary>
 200    public Fixed64 InvDeltaTime
 201    {
 202        get
 203        {
 996204            ThrowIfDisposed();
 996205            return _clock.InvDeltaTime;
 206        }
 207    }
 208
 209    /// <summary>
 210    /// Gets this context's simulated frame count.
 211    /// </summary>
 212    public int FrameCount
 213    {
 214        get
 215        {
 388240216            ThrowIfDisposed();
 388240217            return _clock.FrameCount;
 218        }
 219    }
 220
 221    /// <summary>
 222    /// Gets this context's total simulated time.
 223    /// </summary>
 224    public Fixed64 TotalTime
 225    {
 226        get
 227        {
 1012228            ThrowIfDisposed();
 1012229            return _clock.TotalTime;
 230        }
 231    }
 232
 233    /// <summary>
 234    /// Gets this context's accumulated visualization time.
 235    /// </summary>
 236    public Fixed64 AccumulatedTime
 237    {
 238        get
 239        {
 1240            ThrowIfDisposed();
 1241            return _clock.AccumulatedTime;
 242        }
 243    }
 244
 245    /// <summary>
 246    /// Gets whether this context's visualization accumulation will reset on the next visualize call.
 247    /// </summary>
 248    public bool ResetAccumulation
 249    {
 250        get
 251        {
 3252            ThrowIfDisposed();
 3253            return _clock.ResetAccumulation;
 254        }
 255    }
 256
 257    internal bool ResetAccumulationThisVisualize
 258    {
 259        get
 260        {
 31261            ThrowIfDisposed();
 31262            return _clock.ResetAccumulationThisVisualize;
 263        }
 264    }
 265
 48364266    internal int LateSimulateToken => _lateSimulateToken;
 267
 268    /// <summary>
 269    /// Gets this context's visualization accumulation expressed in simulation frames.
 270    /// </summary>
 271    public Fixed64 ExpectedAccumulation
 272    {
 273        get
 274        {
 4275            ThrowIfDisposed();
 4276            return _clock.ExpectedAccumulation;
 277        }
 278    }
 279
 280    /// <summary>
 281    /// Computes a deterministic fixed-width hash of this context's replay-relevant physics state.
 282    /// </summary>
 283    /// <param name="mode">Selects whether diagnostic solver/cache state is included in addition to authoritative state.
 284    /// <returns>A deterministic hash suitable for lockstep replay conformance checks.</returns>
 285    /// <remarks>
 286    /// The hash includes context settings, environment values, body state, collider shape/filter state,
 287    /// retained pair/contact state, and continuation-affecting CCD handoff state. It excludes host object
 288    /// identity, delegates, diagnostics buffers, debug draw data, query scratch buffers, and visualization
 289    /// interpolation caches.
 290    /// </remarks>
 291    public ChronicleHash ComputeReplayHash(
 292        GravitasReplayHashMode mode = GravitasReplayHashMode.Authoritative)
 293    {
 993294        ThrowIfDisposed();
 993295        return GravitasReplayHashService.Compute(this, mode);
 296    }
 297
 298    /// <summary>
 299    /// Attaches a context to a host-owned <see cref="GridWorld"/>.
 300    /// </summary>
 301    /// <param name="world">The active world to bind.</param>
 302    /// <param name="takeOwnership">True when disposing this context should dispose the supplied world.</param>
 303    /// <returns>A context bound to <paramref name="world"/>.</returns>
 304    public static GravitasWorldContext Attach(GridWorld world, bool takeOwnership = false)
 305    {
 8306        SwiftThrowHelper.ThrowIfNull(world, nameof(world));
 7307        return CreateRegistered(world, takeOwnership);
 308    }
 309
 310    /// <summary>
 311    /// Creates a context with an owned <see cref="GridWorld"/>.
 312    /// </summary>
 313    /// <param name="spatialGridCellSize">Spatial hash cell size for the created world.</param>
 314    /// <returns>A context that owns its created world.</returns>
 315    public static GravitasWorldContext CreateOwned(
 316        int spatialGridCellSize = GridWorld.DefaultSpatialGridCellSize)
 317    {
 5079318        return CreateRegistered(
 5079319            new GridWorld(spatialGridCellSize),
 5079320            ownsWorld: true);
 321    }
 322
 323    /// <summary>
 324    /// Advances this context's deterministic simulation clock and ordered simulate hooks.
 325    /// </summary>
 326    public void Simulate()
 327    {
 2320328        ThrowIfDisposed();
 2320329        _fixedStepOpen = true;
 2320330        EnterSimulationPhase();
 331        try
 332        {
 2320333            _clock.Simulate();
 2320334            PhysicsRuntimeMode runtimeMode = Settings.RuntimeMode;
 2320335            if (runtimeMode.Runs3D())
 2001336                Physics.Simulate();
 2320337            if (runtimeMode.Runs2D())
 479338                Physics2D.Simulate();
 2320339            if (runtimeMode.RunsMixedContacts())
 153340                MixedCollisions.Simulate();
 341
 2320342            Coroutines.Simulate();
 2320343            _hooks.InvokeSimulate();
 2318344        }
 2345        catch
 346        {
 2347            _fixedStepOpen = false;
 2348            throw;
 349        }
 350        finally
 351        {
 2320352            ExitSimulationPhase();
 2320353        }
 2318354    }
 355
 356    /// <summary>
 357    /// Runs this context's late-simulation step.
 358    /// </summary>
 359    public void LateSimulate()
 360    {
 2832361        ThrowIfDisposed();
 2832362        _fixedStepOpen = true;
 2832363        EnterSimulationPhase();
 364        try
 365        {
 2832366            _clock.LateSimulate();
 2832367            PhysicsRuntimeMode runtimeMode = Settings.RuntimeMode;
 2832368            bool willRun3D = runtimeMode.Runs3D() && Physics.SimulatePhysics;
 2832369            bool willRun2D = runtimeMode.Runs2D() && Physics2D.SimulatePhysics;
 2832370            if (willRun3D || willRun2D)
 2829371                AdvanceLateSimulateToken();
 2832372            if (willRun3D)
 2180373                Physics.PrepareContinuousCollisionFrame();
 2831374            if (willRun2D)
 1003375                Physics2D.PrepareContinuousCollisionFrame();
 2830376            bool ran3D = willRun3D
 2830377                && Physics.BeginLateSimulateBodies(continuousCollisionFramePrepared: true);
 2829378            bool ran2D = willRun2D
 2829379                && Physics2D.BeginLateSimulateBodies(continuousCollisionFramePrepared: true);
 2828380            ProcessQueuedContinuousCollisionHandoffs(ran3D, ran2D);
 2827381            if (ran3D)
 2177382                Physics.CompleteLateSimulatePhysicsStep();
 2825383            if (ran2D)
 1000384                Physics2D.CompleteLateSimulatePhysicsStep();
 2825385            if (runtimeMode.RunsMixedContacts())
 345386                MixedCollisions.LateSimulate();
 387
 2825388            _hooks.InvokeLateSimulate();
 2824389        }
 390        finally
 391        {
 2832392            ExitSimulationPhase();
 2832393            _fixedStepOpen = false;
 2832394        }
 2824395    }
 396
 397    private void ProcessQueuedContinuousCollisionHandoffs(bool runs3D, bool runs2D)
 398    {
 2828399        if (!runs3D && !runs2D)
 3400            return;
 401
 402        try
 403        {
 2825404            int iterationLimit = Settings.ContinuousCollisionMaxToiIterations;
 2825405            int remainingIterations = iterationLimit;
 5796406            for (int iteration = 0; iteration < iterationLimit && remainingIterations > 0; iteration++)
 407            {
 2891408                int processedIterations = 0;
 2891409                if (runs3D)
 410                {
 2237411                    int usedIterations = Physics.ProcessQueuedContinuousCollisionHandoffs(remainingIterations);
 2236412                    processedIterations += usedIterations;
 2236413                    remainingIterations -= usedIterations;
 414                }
 415
 2890416                if (remainingIterations > 0 && runs2D)
 417                {
 1050418                    int usedIterations = Physics2D.ProcessQueuedContinuousCollisionHandoffs(remainingIterations);
 1050419                    processedIterations += usedIterations;
 1050420                    remainingIterations -= usedIterations;
 421                }
 422
 2890423                if (processedIterations == 0)
 2817424                    return;
 425            }
 426
 7427            if (runs3D)
 6428                Physics.ProcessQueuedContinuousCollisionHandoffs(iterationBudget: 0);
 7429            if (runs2D)
 6430                Physics2D.ProcessQueuedContinuousCollisionHandoffs(iterationBudget: 0);
 7431        }
 1432        catch
 433        {
 1434            if (runs3D)
 1435                Physics.AbortContinuousCollisionHandoffFrame();
 1436            if (runs2D)
 1437                Physics2D.AbortContinuousCollisionHandoffFrame();
 1438            throw;
 439        }
 2824440    }
 441
 3005442    internal void AdvanceLateSimulateToken() => _lateSimulateToken++;
 443
 444    /// <summary>
 445    /// Runs this context's visualization accumulation step.
 446    /// </summary>
 447    public void Visualize()
 448    {
 29449        ThrowIfDisposed();
 29450        _clock.Visualize();
 29451        PhysicsRuntimeMode runtimeMode = Settings.RuntimeMode;
 29452        if (runtimeMode.Runs3D())
 23453            Physics.Visualize();
 29454        if (runtimeMode.Runs2D())
 7455            Physics2D.Visualize();
 29456        if (runtimeMode.RunsMixedContacts())
 1457            MixedCollisions.Visualize();
 458
 29459        _hooks.InvokeVisualize();
 29460    }
 461
 462    /// <summary>
 463    /// Runs this context's host-owned late-visualization hook phase.
 464    /// </summary>
 465    public void LateVisualize()
 466    {
 15467        ThrowIfDisposed();
 15468        _hooks.InvokeLateVisualize();
 15469    }
 470
 471    /// <summary>
 472    /// Resets this context's deterministic clock and context-local lifecycle hooks.
 473    /// </summary>
 474    public void Reset()
 475    {
 31476        ThrowIfDisposed();
 31477        _lateSimulateToken = 0;
 31478        _clock.Reset();
 31479        Constraints3D.Reset();
 31480        Constraints2D.Reset();
 31481        Collisions.Reset();
 31482        Collisions2D.Reset();
 31483        Physics.Reset();
 31484        Physics2D.Reset();
 31485        MixedCollisions.Reset();
 31486        Query2D.Reset();
 31487        Query3D.Reset();
 31488        QueryMixed.Reset();
 31489        Coroutines.Reset();
 31490        Diagnostics.Reset();
 31491        _hooks.InvokeReset();
 31492    }
 493
 494    /// <summary>
 495    /// Updates this context's fixed simulation frame rate.
 496    /// </summary>
 497    /// <param name="frameRate">The new frame rate. Must be within the supported physics settings range.</param>
 498    public void SetFrameRate(int frameRate)
 499    {
 1329500        ThrowIfDisposed();
 1329501        Settings.SetFrameRate(frameRate);
 1328502        _clock.SetFrameRate(frameRate);
 1328503        _hooks.InvokeFrameRateChanged();
 1328504    }
 505
 506    /// <summary>
 507    /// Applies context-local settings and synchronizes frame-derived clock state.
 508    /// </summary>
 509    /// <param name="settings">The settings instance to own.</param>
 510    public void ApplySettings(PhysicsSettings settings)
 511    {
 535512        ThrowIfDisposed();
 535513        SwiftThrowHelper.ThrowIfNull(settings, nameof(settings));
 514
 535515        Settings = settings;
 535516        _clock.SetFrameRate(settings.FrameRate);
 535517        _hooks.InvokeFrameRateChanged();
 535518    }
 519
 520    /// <summary>
 521    /// Calculates the frame index containing the specified fixed-point timestamp.
 522    /// </summary>
 523    /// <param name="timestamp">The timestamp to resolve.</param>
 524    /// <returns>The zero-based frame index for the timestamp.</returns>
 525    public int GetFrameFromTime(Fixed64 timestamp)
 526    {
 3527        ThrowIfDisposed();
 3528        return _clock.GetFrameFromTime(timestamp);
 529    }
 530
 531    internal IDisposable RegisterOnSimulate(string owner, int order, Action callback)
 532    {
 9533        ThrowIfDisposed();
 9534        return _hooks.RegisterOnSimulate(owner, order, callback);
 535    }
 536
 537    internal IDisposable RegisterOnLateSimulate(string owner, int order, Action callback)
 538    {
 3539        ThrowIfDisposed();
 3540        return _hooks.RegisterOnLateSimulate(owner, order, callback);
 541    }
 542
 543    internal IDisposable RegisterOnVisualize(string owner, int order, Action callback)
 544    {
 1545        ThrowIfDisposed();
 1546        return _hooks.RegisterOnVisualize(owner, order, callback);
 547    }
 548
 549    internal IDisposable RegisterOnLateVisualize(string owner, int order, Action callback)
 550    {
 1551        ThrowIfDisposed();
 1552        return _hooks.RegisterOnLateVisualize(owner, order, callback);
 553    }
 554
 555    internal IDisposable RegisterOnReset(string owner, int order, Action callback)
 556    {
 2557        ThrowIfDisposed();
 2558        return _hooks.RegisterOnReset(owner, order, callback);
 559    }
 560
 561    internal IDisposable RegisterOnFrameRateChanged(string owner, int order, Action callback)
 562    {
 2563        ThrowIfDisposed();
 2564        return _hooks.RegisterOnFrameRateChanged(owner, order, callback);
 565    }
 566
 567    /// <inheritdoc/>
 568    public void Dispose()
 569    {
 5089570        lock (_worldOwnershipLock)
 571        {
 5089572            if (_disposed)
 6573                return;
 574
 5083575            Constraints3D.Reset();
 5083576            Constraints2D.Reset();
 5083577            _disposed = true;
 578            try
 579            {
 5083580                Coroutines.Deactivate();
 5083581            }
 582            finally
 583            {
 584                try
 585                {
 5083586                    if (_ownsWorld && World.IsActive)
 5080587                        World.Dispose();
 5083588                }
 589                finally
 590                {
 5083591                    ReleaseWorldOwnership(this);
 5083592                }
 5083593            }
 594        }
 5089595    }
 596
 597    private static GravitasWorldContext CreateRegistered(GridWorld world, bool ownsWorld)
 598    {
 5086599        lock (_worldOwnershipLock)
 600        {
 5086601            SwiftThrowHelper.ThrowIfTrue(
 5086602                !world.IsActive,
 5086603                nameof(GravitasWorldContext),
 5086604                "GravitasWorldContext requires an active GridWorld.");
 5085605            ThrowIfWorldOwned(world);
 5083606            GravitasWorldContext context = new(world, ownsWorld);
 5083607            _worldOwners[world] = context;
 5083608            return context;
 609        }
 5083610    }
 611
 612    private static void ThrowIfWorldOwned(GridWorld world)
 613    {
 5085614        SwiftThrowHelper.ThrowIfTrue(
 5085615            _worldOwners.ContainsKey(world),
 5085616            nameof(GravitasWorldContext),
 5085617            "GridWorld is already attached to an active GravitasWorldContext.");
 5083618    }
 619
 620    private static void ReleaseWorldOwnership(GravitasWorldContext context)
 621    {
 5083622        _worldOwners.Remove(context.World);
 5083623    }
 624
 625    internal void ThrowIfDisposed()
 626    {
 1025019627        SwiftThrowHelper.ThrowIfDisposed(_disposed, nameof(GravitasWorldContext));
 1025015628        SwiftThrowHelper.ThrowIfTrue(
 1025015629            !World.IsActive,
 1025015630            nameof(GravitasWorldContext),
 1025015631            "GravitasWorldContext is bound to an inactive GridWorld.");
 1025015632    }
 633}