Code/Coroutine.cs
using Sandbox;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using static Sandbox.GameObjectSystem;
namespace Coroutines;
/// <summary>
/// Contains functionality to start, stop, and check completion of coroutines.
/// <br/>
/// <br/>
/// A coroutine is a method that can be paused and resumed later in time.
/// The pauses are controlled by a <see cref="ICoroutineStaller"/>.
/// All coroutines are executed within the main thread and will never leave it.
/// All calls within this class are deferred to the main thread if called from outside of it.
/// </summary>
public class Coroutine : GameObjectSystem
{
/// <summary>
/// The default <see cref="Stage"/> to use in coroutines.
/// </summary>
/// FIXME: Should be a { get; set; } property but Sbox code gen breaks.
public const Stage DefaultPollingStage = Stage.UpdateBones;
/// <summary>
/// A custom <see cref="Stage"/> enum to represent preserving the previously selected <see cref="Stage"/>.
/// </summary>
public const Stage PreservePollingStage = (Stage)int.MinValue;
/// <summary>
/// A thread-safe queue to add new coroutines.
/// </summary>
private static ConcurrentQueue<IEnumerator<ICoroutineStaller>> CoroutinesToAdd { get; } = new();
/// <summary>
/// A thread-safe queue to remove existing coroutines.
/// </summary>
private static ConcurrentQueue<IEnumerator<ICoroutineStaller>> CoroutinesToRemove { get; } = new();
/// <summary>
/// A list of all currently executing coroutines.
/// </summary>
private static List<CoroutineInstance> Coroutines { get; } = new();
/// <summary>
/// A main thread queue to add coroutines once their polling stage has finished.
/// </summary>
private static Dictionary<Stage, Queue<CoroutineInstance>> QueuedCoroutines { get; } = new();
/// <summary>
/// Initializes a new instance of <see cref="Coroutine"/>. This should not need to be constructed in user code.
/// </summary>
/// <param name="scene">The scene that this system is operating within.</param>
public Coroutine( Scene scene ) : base( scene )
{
foreach ( var val in Enum.GetValues<Stage>() )
{
var stage = val;
Listen( stage, int.MaxValue, () => StepCoroutines( stage ), nameof( Coroutine ) );
}
}
/// <summary>
/// Starts a new coroutine that is fetched from the <paramref name="coroutineMethod"/>.
/// </summary>
/// <param name="coroutineMethod">The method to get the coroutine instance from.</param>
/// <returns>The coroutine instance that was retrieved.</returns>
public static IEnumerator<ICoroutineStaller> Start( Func<IEnumerator<ICoroutineStaller>> coroutineMethod )
{
var coroutine = coroutineMethod.Invoke();
CoroutinesToAdd.Enqueue( coroutine );
return coroutine;
}
/// <summary>
/// Starts a new coroutine that is fetched from the <paramref name="coroutineMethod"/>.
/// This will pass <paramref name="firstValue"/> to <paramref name="coroutineMethod"/>.
/// </summary>
/// <typeparam name="T1">The type of the parameter to pass to the <paramref name="coroutineMethod"/>.</typeparam>
/// <param name="coroutineMethod">The method to get the coroutine instance from.</param>
/// <param name="firstValue">The value to pass to the <paramref name="coroutineMethod"/>.</param>
/// <returns>The coroutine instance that was retrieved.</returns>
public static IEnumerator<ICoroutineStaller> Start<T1>( Func<T1, IEnumerator<ICoroutineStaller>> coroutineMethod,
T1 firstValue )
{
var coroutine = coroutineMethod.Invoke( firstValue );
CoroutinesToAdd.Enqueue( coroutine );
return coroutine;
}
/// <summary>
/// Starts a new coroutine that is fetched from the <paramref name="coroutineMethod"/>.
/// This will pass <paramref name="firstValue"/> and <paramref name="secondValue"/> to <paramref name="coroutineMethod"/>.
/// </summary>
/// <typeparam name="T1">The type of the first parameter to pass to the <paramref name="coroutineMethod"/>.</typeparam>
/// <typeparam name="T2">The type of the seccond parameter to pass to the <paramref name="coroutineMethod"/>.</typeparam>
/// <param name="coroutineMethod">The method to get the coroutine instance from.</param>
/// <param name="firstValue">The first value to pass to the <paramref name="coroutineMethod"/>.</param>
/// <param name="secondValue">The second value to pass to the <paramref name="coroutineMethod"/>.</param>
/// <returns>The coroutine instance that was retrieved.</returns>
public static IEnumerator<ICoroutineStaller> Start<T1, T2>( Func<T1, T2, IEnumerator<ICoroutineStaller>> coroutineMethod,
T1 firstValue, T2 secondValue )
{
var coroutine = coroutineMethod.Invoke( firstValue, secondValue );
CoroutinesToAdd.Enqueue( coroutine );
return coroutine;
}
/// <summary>
/// Starts a new coroutine that is fetched from the <paramref name="coroutineMethod"/>.
/// This will pass <paramref name="firstValue"/>, <paramref name="secondValue"/> and <paramref name="thirdVlaue"/> to <paramref name="coroutineMethod"/>.
/// </summary>
/// <typeparam name="T1">The type of the first parameter to pass to the <paramref name="coroutineMethod"/>.</typeparam>
/// <typeparam name="T2">The type of the seccond parameter to pass to the <paramref name="coroutineMethod"/>.</typeparam>
/// <typeparam name="T3">The type of the third parameter to pass to the <paramref name="coroutineMethod"/>.</typeparam>
/// <param name="coroutineMethod">The method to get the coroutine instance from.</param>
/// <param name="firstValue">The first value to pass to the <paramref name="coroutineMethod"/>.</param>
/// <param name="secondValue">The second value to pass to the <paramref name="coroutineMethod"/>.</param>
/// <param name="thirdVlaue">The third value to pass to the <paramref name="coroutineMethod"/>.</param>
/// <returns>The coroutine instance that was retrieved.</returns>
public static IEnumerator<ICoroutineStaller> Start<T1, T2, T3>( Func<T1, T2, T3, IEnumerator<ICoroutineStaller>> coroutineMethod,
T1 firstValue, T2 secondValue, T3 thirdVlaue )
{
var coroutine = coroutineMethod.Invoke( firstValue, secondValue, thirdVlaue );
CoroutinesToAdd.Enqueue( coroutine );
return coroutine;
}
/// <summary>
/// Starts an existing instance of a coroutine.
/// </summary>
/// <param name="coroutine">The coroutine to start.</param>
public static void Start( IEnumerator<ICoroutineStaller> coroutine )
{
CoroutinesToAdd.Enqueue( coroutine );
}
/// <summary>
/// Stops an existing coroutine.
/// </summary>
/// <param name="coroutine">The coroutine to stop.</param>
public static void Stop( IEnumerator<ICoroutineStaller> coroutine )
{
CoroutinesToRemove.Enqueue( coroutine );
}
/// <summary>
/// Stops all coroutines.
/// </summary>
public static void StopAll()
{
foreach ( var existingCoroutine in Coroutines )
CoroutinesToRemove.Enqueue( existingCoroutine.Coroutine );
foreach ( var queuedCoroutine in CoroutinesToAdd )
CoroutinesToRemove.Enqueue( queuedCoroutine );
}
/// <summary>
/// Returns whether or not a coroutine has completed.
/// </summary>
/// <remarks>
/// This will also return true if the coroutine has never been in the system.
/// </remarks>
/// <param name="coroutine">The coroutine to check.</param>
/// <returns>Whether or not the coroutine has completed.</returns>
public static bool IsComplete( IEnumerator<ICoroutineStaller> coroutine )
{
if ( CoroutinesToAdd.Contains( coroutine ) )
return false;
foreach ( var coroutineInstance in Coroutines )
{
if ( ReferenceEquals( coroutineInstance.Coroutine, coroutine ) )
return false;
}
return true;
}
/// <summary>
/// Creates a coroutine wrapper and queues it for the system if it hasn't pre-maturely finished.
/// </summary>
/// <param name="coroutine">The coroutine to add.</param>
private static void QueueCoroutine( IEnumerator<ICoroutineStaller> coroutine )
{
var coroutineInstance = new CoroutineInstance( coroutine );
if ( coroutineInstance.IsFinished )
return;
if ( !QueuedCoroutines.TryGetValue( coroutineInstance.CurrentPollingStage, out var queue ) )
{
queue = new Queue<CoroutineInstance>();
QueuedCoroutines.Add( coroutineInstance.CurrentPollingStage, queue );
}
queue.Enqueue( coroutineInstance );
}
/// <summary>
/// Adds the coroutine instance to the internal system.
/// </summary>
/// <param name="coroutineInstance">The coroutine instance to add.</param>
private static void AddCoroutine( CoroutineInstance coroutineInstance )
{
Coroutines.Add( coroutineInstance );
}
/// <summary>
/// Removes a coroutine from the system.
/// </summary>
/// <param name="coroutine">The coroutine to remove.</param>
private static void RemoveCoroutine( IEnumerator<ICoroutineStaller> coroutine )
{
CoroutineInstance? foundInstance = null;
foreach ( var coroutineInstance in Coroutines )
{
if ( !ReferenceEquals( coroutineInstance.Coroutine, coroutine ) )
continue;
foundInstance = coroutineInstance;
break;
}
if ( foundInstance is not null )
Coroutines.Remove( foundInstance );
}
/// <summary>
/// Empties all coroutine queues steps all coroutines that match the provided <paramref name="stage"/>.
/// </summary>
/// <param name="stage">The <see cref="Stage"/> to step.</param>
private static void StepCoroutines( Stage stage )
{
if ( !GameManager.IsPlaying )
{
StopAll();
return;
}
foreach ( var coroutineInstance in Coroutines )
{
if ( coroutineInstance.CurrentPollingStage != stage )
continue;
try
{
coroutineInstance.Update();
}
catch ( Exception e )
{
Log.Error( e, "An exception occurred during execution of a Coroutine" );
}
finally
{
if ( coroutineInstance.IsFinished )
CoroutinesToRemove.Enqueue( coroutineInstance.Coroutine );
}
}
while ( CoroutinesToAdd.TryDequeue( out var coroutine ) )
QueueCoroutine( coroutine );
if ( QueuedCoroutines.TryGetValue( stage, out var queue ) )
{
while ( queue.TryDequeue( out var coroutine ) )
AddCoroutine( coroutine );
}
while ( CoroutinesToRemove.TryDequeue( out var coroutine ) )
RemoveCoroutine( coroutine );
}
}