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();
|
|
}
|
|
|
|
}
|
|
}
|