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 { /// /// A class providing coroutine helpers. /// public static class Coroutines { /// /// Stalls the coroutine until completes, faults, or is canceled. /// /// the to wait for /// a coroutine waiting for the given task public static IEnumerator WaitForTask(Task task) => WaitForTask(task, false); /// /// Stalls the coroutine until completes, faults, or is canceled. /// /// the to wait for /// whether or not to throw if the task faulted /// a coroutine waiting for the given task 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; } /// /// Binds a to a Unity coroutine, capturing exceptions as well as the coroutine call stack. /// /// /// /// This may be called off of the Unity main thread. If it is, the coroutine start will be scheduled using the default /// and will be run on the main thread as required by Unity. /// /// /// Unity provides a handful of coroutine helpers that are not s. Most of these are not terribly /// helpful on their own, however may be. Instead, prefer to use the typical .NET /// or similar overloads, or use . /// /// /// the coroutine to bind to a task /// a that completes when completes, and fails when it throws public static Task AsTask(IEnumerator coroutine) { if (!UnityGame.OnMainThread) return UnityMainThreadTaskScheduler.Factory.StartNew(() => AsTask(coroutine), default, default, UnityMainThreadTaskScheduler.Default).Unwrap(); var tcs = new TaskCompletionSource(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 locations) : base(string.Join("\n", Utils.StrJP(locations.Select(s => "in " + s)))) { } } private class AsTaskCoroutineExecutor : IEnumerator { private readonly TaskCompletionSource completionSource; public AsTaskCoroutineExecutor(IEnumerator coroutine, TaskCompletionSource completion) { completionSource = completion; enumerators.Push(coroutine); } private readonly Stack 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(); } } }