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