| | | 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 | | |
| | | 8 | | using FixedMathSharp; |
| | | 9 | | using SwiftCollections; |
| | | 10 | | using System; |
| | | 11 | | using System.Collections.Generic; |
| | | 12 | | using System.Runtime.ExceptionServices; |
| | | 13 | | |
| | | 14 | | namespace Gravitas.Support; |
| | | 15 | | |
| | | 16 | | /// <summary> |
| | | 17 | | /// Owns lockstep coroutine state for one <see cref="GravitasWorldContext"/>. |
| | | 18 | | /// </summary> |
| | | 19 | | public sealed class GravitasCoroutineService |
| | | 20 | | { |
| | | 21 | | private readonly GravitasWorldContext _context; |
| | 5083 | 22 | | private readonly SwiftBucket<LSCoroutine> _coroutines = new(); |
| | 5083 | 23 | | 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> |
| | 5083 | 32 | | public GravitasCoroutineService(GravitasWorldContext context) |
| | | 33 | | { |
| | 5083 | 34 | | SwiftThrowHelper.ThrowIfNull(context, nameof(context)); |
| | 5083 | 35 | | _context = context; |
| | 5083 | 36 | | } |
| | | 37 | | |
| | | 38 | | /// <summary> |
| | | 39 | | /// Gets the owning world context. |
| | | 40 | | /// </summary> |
| | 25 | 41 | | public GravitasWorldContext Context => _context; |
| | | 42 | | |
| | | 43 | | /// <summary> |
| | | 44 | | /// Gets the number of active coroutines owned by this context. |
| | | 45 | | /// </summary> |
| | 29 | 46 | | 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 | | { |
| | 3 | 56 | | SwiftThrowHelper.ThrowIfTrue( |
| | 3 | 57 | | _resetting || _context.IsDisposed, |
| | 3 | 58 | | nameof(GravitasCoroutineService), |
| | 3 | 59 | | "Coroutine service cannot initialize while resetting or after its context is disposed."); |
| | | 60 | | |
| | 1 | 61 | | Reset(); |
| | 1 | 62 | | _deactivated = false; |
| | 1 | 63 | | } |
| | | 64 | | |
| | | 65 | | /// <summary> |
| | | 66 | | /// Advances active coroutines once for the current simulation frame. |
| | | 67 | | /// </summary> |
| | | 68 | | public void Simulate() |
| | | 69 | | { |
| | 2352 | 70 | | if (_simulating || _resetting || _deactivated) |
| | 2 | 71 | | return; |
| | | 72 | | |
| | 2350 | 73 | | int peak = _coroutines.PeakCount; |
| | 2350 | 74 | | if (peak == 0) |
| | 2306 | 75 | | return; |
| | | 76 | | |
| | 44 | 77 | | _simulating = true; |
| | | 78 | | try |
| | | 79 | | { |
| | 44 | 80 | | _simulationSnapshot.EnsureCapacity(_coroutines.Count); |
| | 188 | 81 | | for (int i = 0; i < peak; i++) |
| | | 82 | | { |
| | 50 | 83 | | if (_coroutines.TryGetValue(i, out LSCoroutine coroutine)) |
| | 49 | 84 | | _simulationSnapshot.Add(coroutine); |
| | | 85 | | } |
| | | 86 | | |
| | 174 | 87 | | for (int i = 0; i < _simulationSnapshot.Count; i++) |
| | | 88 | | { |
| | 49 | 89 | | LSCoroutine coroutine = _simulationSnapshot[i]; |
| | 49 | 90 | | if (!coroutine.Active) |
| | | 91 | | continue; |
| | | 92 | | |
| | | 93 | | try |
| | | 94 | | { |
| | 48 | 95 | | coroutine.Simulate(); |
| | 42 | 96 | | } |
| | 6 | 97 | | catch (Exception simulationException) |
| | | 98 | | { |
| | | 99 | | try |
| | | 100 | | { |
| | 6 | 101 | | StopCoroutine(coroutine); |
| | 5 | 102 | | } |
| | 1 | 103 | | catch (Exception cleanupException) |
| | | 104 | | { |
| | 1 | 105 | | throw new AggregateException(simulationException, cleanupException); |
| | | 106 | | } |
| | | 107 | | |
| | 5 | 108 | | throw; |
| | | 109 | | } |
| | | 110 | | } |
| | 38 | 111 | | } |
| | | 112 | | finally |
| | | 113 | | { |
| | 44 | 114 | | _simulationSnapshot.Clear(); |
| | 44 | 115 | | _simulating = false; |
| | 44 | 116 | | } |
| | 38 | 117 | | } |
| | | 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 | | { |
| | 80 | 130 | | SwiftThrowHelper.ThrowIfNull(enumerator, nameof(enumerator)); |
| | 80 | 131 | | SwiftThrowHelper.ThrowIfTrue( |
| | 80 | 132 | | _resetting || _deactivated || _context.IsDisposed, |
| | 80 | 133 | | nameof(GravitasCoroutineService), |
| | 80 | 134 | | "Coroutine service cannot start work while resetting, deactivated, or disposed."); |
| | | 135 | | |
| | 76 | 136 | | LSCoroutine coroutine = new(this, enumerator); |
| | 76 | 137 | | coroutine.Index = _coroutines.Add(coroutine); |
| | 76 | 138 | | 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 | | { |
| | 42 | 147 | | SwiftThrowHelper.ThrowIfNull(coroutine, nameof(coroutine)); |
| | 42 | 148 | | SwiftThrowHelper.ThrowIfArgument( |
| | 42 | 149 | | !ReferenceEquals(coroutine.Owner, this), |
| | 42 | 150 | | nameof(coroutine), |
| | 42 | 151 | | "Coroutine must be stopped through its owning coroutine service."); |
| | | 152 | | |
| | 41 | 153 | | if (!coroutine.Active) |
| | 3 | 154 | | return; |
| | | 155 | | |
| | | 156 | | // Clear while the final slot is still live so SwiftBucket also resets its peak/free-slot state. |
| | 38 | 157 | | if (_coroutines.Count == 1) |
| | 20 | 158 | | _coroutines.Clear(); |
| | | 159 | | else |
| | 18 | 160 | | _coroutines.TryRemoveAt(coroutine.Index); |
| | | 161 | | |
| | 38 | 162 | | coroutine.End(); |
| | 37 | 163 | | } |
| | | 164 | | |
| | | 165 | | /// <summary> |
| | | 166 | | /// Stops all active coroutines and clears service state. |
| | | 167 | | /// </summary> |
| | | 168 | | public void Reset() |
| | | 169 | | { |
| | 5125 | 170 | | if (_resetting) |
| | 1 | 171 | | return; |
| | | 172 | | |
| | 5124 | 173 | | _resetting = true; |
| | 5124 | 174 | | Exception? firstException = null; |
| | 5124 | 175 | | int peak = _coroutines.PeakCount; |
| | | 176 | | try |
| | | 177 | | { |
| | 10328 | 178 | | for (int i = 0; i < peak; i++) |
| | | 179 | | { |
| | 40 | 180 | | 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 | | { |
| | 38 | 187 | | coroutine.End(); |
| | 36 | 188 | | } |
| | 2 | 189 | | catch (Exception exception) |
| | | 190 | | { |
| | 2 | 191 | | firstException ??= exception; |
| | 2 | 192 | | } |
| | | 193 | | } |
| | 5124 | 194 | | } |
| | | 195 | | finally |
| | | 196 | | { |
| | 5124 | 197 | | _coroutines.Clear(); |
| | 5124 | 198 | | _simulationSnapshot.Clear(); |
| | 5124 | 199 | | _resetting = false; |
| | 5124 | 200 | | } |
| | | 201 | | |
| | 5124 | 202 | | if (firstException != null) |
| | 2 | 203 | | ExceptionDispatchInfo.Capture(firstException).Throw(); |
| | 5122 | 204 | | } |
| | | 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 | | { |
| | 5086 | 214 | | if (_deactivated) |
| | 3 | 215 | | return; |
| | | 216 | | |
| | 5083 | 217 | | _deactivated = true; |
| | 5083 | 218 | | Reset(); |
| | 5083 | 219 | | } |
| | | 220 | | |
| | | 221 | | /// <summary> |
| | | 222 | | /// Creates a frame-count wait instruction bound to this service's context. |
| | | 223 | | /// </summary> |
| | 8 | 224 | | 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> |
| | 12 | 229 | | public WaitForNextSimulate WaitForNextSimulate() => new(_context); |
| | | 230 | | |
| | | 231 | | /// <summary> |
| | | 232 | | /// Creates a fixed-duration wait instruction bound to this service's context. |
| | | 233 | | /// </summary> |
| | 4 | 234 | | public WaitForRealSeconds WaitForRealSeconds(Fixed64 seconds) => new(_context, seconds); |
| | | 235 | | } |