You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

138 lines
5.7 KiB

using IPA.Loader;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IPA.Utilities.Async
{
/// <summary>
/// A class providing coroutine helpers.
/// </summary>
public static class Coroutines
{
/// <summary>
/// Stalls the coroutine until <paramref name="task"/> completes, faults, or is canceled.
/// </summary>
/// <param name="task">the <see cref="Task"/> to wait for</param>
/// <returns>a coroutine waiting for the given task</returns>
public static IEnumerator WaitForTask(Task task)
=> WaitForTask(task, false);
/// <summary>
/// Stalls the coroutine until <paramref name="task"/> completes, faults, or is canceled.
/// </summary>
/// <param name="task">the <see cref="Task"/> to wait for</param>
/// <param name="throwOnFault">whether or not to throw if the task faulted</param>
/// <returns>a coroutine waiting for the given task</returns>
public static IEnumerator WaitForTask(Task task, bool throwOnFault = false)
{
while (!task.IsCompleted && !task.IsCanceled && !task.IsFaulted)
yield return null;
if (throwOnFault && task.IsFaulted)
throw task.Exception;
}
/// <summary>
/// Binds a <see cref="Task"/> to a Unity coroutine, capturing exceptions as well as the coroutine call stack.
/// </summary>
/// <remarks>
/// <para>
/// This may be called off of the Unity main thread. If it is, the coroutine start will be scheduled using the default
/// <see cref="UnityMainThreadTaskScheduler"/> and will be run on the main thread as required by Unity.
/// </para>
/// <para>
/// Unity provides a handful of coroutine helpers that are not <see cref="IEnumerable"/>s. Most of these are not terribly
/// helpful on their own, however <see cref="UnityEngine.WaitForSeconds"/> may be. Instead, prefer to use the typical .NET
/// <see cref="Task.Wait(TimeSpan)"/> or similar overloads, or use <see cref="UnityEngine.WaitForSecondsRealtime"/>.
/// </para>
/// </remarks>
/// <param name="coroutine">the coroutine to bind to a task</param>
/// <returns>a <see cref="Task"/> that completes when <paramref name="coroutine"/> completes, and fails when it throws</returns>
public static Task AsTask(IEnumerator coroutine)
{
if (!UnityGame.OnMainThread)
return UnityMainThreadTaskScheduler.Factory.StartNew(() => AsTask(coroutine), default, default, UnityMainThreadTaskScheduler.Default).Unwrap();
var tcs = new TaskCompletionSource<VoidStruct>(coroutine, AsTaskSourceOptions);
_ = PluginComponent.Instance.StartCoroutine(new AsTaskCoroutineExecutor(coroutine, tcs));
return tcs.Task;
}
#if NET4
private static readonly TaskCreationOptions AsTaskSourceOptions = TaskCreationOptions.RunContinuationsAsynchronously;
#else
private static readonly TaskCreationOptions AsTaskSourceOptions = TaskCreationOptions.None;
#endif
private struct VoidStruct { }
private class ExceptionLocation : Exception
{
public ExceptionLocation(IEnumerable<string> locations)
: base(string.Join("\n", Utils.StrJP(locations.Select(s => "in " + s))))
{
}
}
private class AsTaskCoroutineExecutor : IEnumerator
{
private readonly TaskCompletionSource<VoidStruct> completionSource;
public AsTaskCoroutineExecutor(IEnumerator coroutine, TaskCompletionSource<VoidStruct> completion)
{
completionSource = completion;
enumerators.Push(coroutine);
}
private readonly Stack<IEnumerator> enumerators = new(2);
public object Current => enumerators.FirstOrDefault()?.Current; // effectively a TryPeek
public bool MoveNext()
{
do
{
if (enumerators.Count == 0)
{
completionSource.SetResult(new VoidStruct());
return false;
}
try
{
var top = enumerators.Peek();
if (top.MoveNext())
{
if (top.Current is IEnumerator enumerator)
{
enumerators.Push(enumerator);
continue;
}
else
{
return true;
}
}
else
{ // this enumerator completed, so pop it and continue
_ = enumerators.Pop();
continue;
}
}
catch (Exception e)
{ // execution errored
completionSource.SetException(new AggregateException(
e, new ExceptionLocation(enumerators.Select(e => e.GetType().ToString()))
));
return false;
}
}
while (true);
}
public void Reset() => throw new InvalidOperationException();
}
}
}