< Summary

Information
Class: Gravitas.Support.GravitasCoroutineService
Assembly: Gravitas
File(s): /home/runner/work/Gravitas/Gravitas/src/Gravitas/Support/Coroutines/GravitasCoroutineService.cs
Line coverage
100%
Covered lines: 89
Uncovered lines: 0
Coverable lines: 89
Total lines: 235
Line coverage: 100%
Branch coverage
100%
Covered branches: 38
Total branches: 38
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_Context()100%11100%
get_ActiveCoroutineCount()100%11100%
Initialize()100%22100%
Simulate()100%1616100%
StartCoroutine(...)100%44100%
StopCoroutine(...)100%44100%
Reset()100%1010100%
Deactivate()100%22100%
WaitForFrames(...)100%11100%
WaitForNextSimulate()100%11100%
WaitForRealSeconds(...)100%11100%

File(s)

/home/runner/work/Gravitas/Gravitas/src/Gravitas/Support/Coroutines/GravitasCoroutineService.cs

#LineLine coverage
 1//=======================================================================
 2// GravitasCoroutineService.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 SwiftCollections;
 10using System;
 11using System.Collections.Generic;
 12using System.Runtime.ExceptionServices;
 13
 14namespace Gravitas.Support;
 15
 16/// <summary>
 17/// Owns lockstep coroutine state for one <see cref="GravitasWorldContext"/>.
 18/// </summary>
 19public sealed class GravitasCoroutineService
 20{
 21    private readonly GravitasWorldContext _context;
 508322    private readonly SwiftBucket<LSCoroutine> _coroutines = new();
 508323    private readonly SwiftList<LSCoroutine> _simulationSnapshot = new();
 24    private bool _simulating;
 25    private bool _resetting;
 26    private bool _deactivated;
 27
 28    /// <summary>
 29    /// Initializes a new coroutine service for the supplied context.
 30    /// </summary>
 31    /// <param name="context">The owning world context.</param>
 508332    public GravitasCoroutineService(GravitasWorldContext context)
 33    {
 508334        SwiftThrowHelper.ThrowIfNull(context, nameof(context));
 508335        _context = context;
 508336    }
 37
 38    /// <summary>
 39    /// Gets the owning world context.
 40    /// </summary>
 2541    public GravitasWorldContext Context => _context;
 42
 43    /// <summary>
 44    /// Gets the number of active coroutines owned by this context.
 45    /// </summary>
 2946    public int ActiveCoroutineCount => _coroutines.Count;
 47
 48    /// <summary>
 49    /// Clears context-local coroutine state and reactivates a manually deactivated service.
 50    /// </summary>
 51    /// <exception cref="InvalidOperationException">
 52    /// The service is already resetting or its context has been disposed.
 53    /// </exception>
 54    public void Initialize()
 55    {
 356        SwiftThrowHelper.ThrowIfTrue(
 357            _resetting || _context.IsDisposed,
 358            nameof(GravitasCoroutineService),
 359            "Coroutine service cannot initialize while resetting or after its context is disposed.");
 60
 161        Reset();
 162        _deactivated = false;
 163    }
 64
 65    /// <summary>
 66    /// Advances active coroutines once for the current simulation frame.
 67    /// </summary>
 68    public void Simulate()
 69    {
 235270        if (_simulating || _resetting || _deactivated)
 271            return;
 72
 235073        int peak = _coroutines.PeakCount;
 235074        if (peak == 0)
 230675            return;
 76
 4477        _simulating = true;
 78        try
 79        {
 4480            _simulationSnapshot.EnsureCapacity(_coroutines.Count);
 18881            for (int i = 0; i < peak; i++)
 82            {
 5083                if (_coroutines.TryGetValue(i, out LSCoroutine coroutine))
 4984                    _simulationSnapshot.Add(coroutine);
 85            }
 86
 17487            for (int i = 0; i < _simulationSnapshot.Count; i++)
 88            {
 4989                LSCoroutine coroutine = _simulationSnapshot[i];
 4990                if (!coroutine.Active)
 91                    continue;
 92
 93                try
 94                {
 4895                    coroutine.Simulate();
 4296                }
 697                catch (Exception simulationException)
 98                {
 99                    try
 100                    {
 6101                        StopCoroutine(coroutine);
 5102                    }
 1103                    catch (Exception cleanupException)
 104                    {
 1105                        throw new AggregateException(simulationException, cleanupException);
 106                    }
 107
 5108                    throw;
 109                }
 110            }
 38111        }
 112        finally
 113        {
 44114            _simulationSnapshot.Clear();
 44115            _simulating = false;
 44116        }
 38117    }
 118
 119    /// <summary>
 120    /// Starts a context-local coroutine.
 121    /// </summary>
 122    /// <param name="enumerator">The lockstep yield instruction enumerator to run.</param>
 123    /// <returns>The started coroutine handle.</returns>
 124    /// <exception cref="ArgumentNullException"><paramref name="enumerator"/> is <see langword="null"/>.</exception>
 125    /// <exception cref="InvalidOperationException">
 126    /// The service is resetting or deactivated, or its context has been disposed.
 127    /// </exception>
 128    public LSCoroutine StartCoroutine(IEnumerator<ILockedYieldInstruction> enumerator)
 129    {
 80130        SwiftThrowHelper.ThrowIfNull(enumerator, nameof(enumerator));
 80131        SwiftThrowHelper.ThrowIfTrue(
 80132            _resetting || _deactivated || _context.IsDisposed,
 80133            nameof(GravitasCoroutineService),
 80134            "Coroutine service cannot start work while resetting, deactivated, or disposed.");
 135
 76136        LSCoroutine coroutine = new(this, enumerator);
 76137        coroutine.Index = _coroutines.Add(coroutine);
 76138        return coroutine;
 139    }
 140
 141    /// <summary>
 142    /// Stops a context-local coroutine.
 143    /// </summary>
 144    /// <param name="coroutine">The coroutine to stop.</param>
 145    public void StopCoroutine(LSCoroutine coroutine)
 146    {
 42147        SwiftThrowHelper.ThrowIfNull(coroutine, nameof(coroutine));
 42148        SwiftThrowHelper.ThrowIfArgument(
 42149            !ReferenceEquals(coroutine.Owner, this),
 42150            nameof(coroutine),
 42151            "Coroutine must be stopped through its owning coroutine service.");
 152
 41153        if (!coroutine.Active)
 3154            return;
 155
 156        // Clear while the final slot is still live so SwiftBucket also resets its peak/free-slot state.
 38157        if (_coroutines.Count == 1)
 20158            _coroutines.Clear();
 159        else
 18160            _coroutines.TryRemoveAt(coroutine.Index);
 161
 38162        coroutine.End();
 37163    }
 164
 165    /// <summary>
 166    /// Stops all active coroutines and clears service state.
 167    /// </summary>
 168    public void Reset()
 169    {
 5125170        if (_resetting)
 1171            return;
 172
 5124173        _resetting = true;
 5124174        Exception? firstException = null;
 5124175        int peak = _coroutines.PeakCount;
 176        try
 177        {
 10328178            for (int i = 0; i < peak; i++)
 179            {
 40180                if (!_coroutines.TryGetValue(i, out LSCoroutine coroutine))
 181                    continue;
 182
 183                // End marks the handle inactive before callbacks. Retaining its slot ensures the
 184                // final Clear resets SwiftBucket high-water state even if callbacks stop later handles.
 185                try
 186                {
 38187                    coroutine.End();
 36188                }
 2189                catch (Exception exception)
 190                {
 2191                    firstException ??= exception;
 2192                }
 193            }
 5124194        }
 195        finally
 196        {
 5124197            _coroutines.Clear();
 5124198            _simulationSnapshot.Clear();
 5124199            _resetting = false;
 5124200        }
 201
 5124202        if (firstException != null)
 2203            ExceptionDispatchInfo.Capture(firstException).Throw();
 5122204    }
 205
 206    /// <summary>
 207    /// Deactivates this coroutine service, disposes all active coroutine state, and rejects new work.
 208    /// </summary>
 209    /// <remarks>
 210    /// A manually deactivated service can be reactivated by <see cref="Initialize"/> while its context remains active.
 211    /// </remarks>
 212    public void Deactivate()
 213    {
 5086214        if (_deactivated)
 3215            return;
 216
 5083217        _deactivated = true;
 5083218        Reset();
 5083219    }
 220
 221    /// <summary>
 222    /// Creates a frame-count wait instruction bound to this service's context.
 223    /// </summary>
 8224    public WaitForFrames WaitForFrames(int frames) => new(_context, frames);
 225
 226    /// <summary>
 227    /// Creates a next-simulation-frame wait instruction bound to this service's context.
 228    /// </summary>
 12229    public WaitForNextSimulate WaitForNextSimulate() => new(_context);
 230
 231    /// <summary>
 232    /// Creates a fixed-duration wait instruction bound to this service's context.
 233    /// </summary>
 4234    public WaitForRealSeconds WaitForRealSeconds(Fixed64 seconds) => new(_context, seconds);
 235}