From a45d423447400c15ed52f40cc4b8066f2fbdd234 Mon Sep 17 00:00:00 2001 From: Anairkoen Schno Date: Sat, 9 May 2020 18:42:10 -0500 Subject: [PATCH] Added some more helpers to Async.Coroutines for easier interop between Tasks and Coroutines --- IPA.Loader/Loader/PluginComponent.cs | 27 +++--- IPA.Loader/Utilities/Async/Coroutines.cs | 108 ++++++++++++++++++++++- 2 files changed, 121 insertions(+), 14 deletions(-) diff --git a/IPA.Loader/Loader/PluginComponent.cs b/IPA.Loader/Loader/PluginComponent.cs index add00600..8b256dc8 100644 --- a/IPA.Loader/Loader/PluginComponent.cs +++ b/IPA.Loader/Loader/PluginComponent.cs @@ -15,14 +15,15 @@ namespace IPA.Loader private CompositeBSPlugin bsPlugins; private CompositeIPAPlugin ipaPlugins; private bool quitting; + public static PluginComponent Instance; private static bool initialized = false; internal static PluginComponent Create() { - return new GameObject("IPA_PluginManager").AddComponent(); + return Instance = new GameObject("IPA_PluginManager").AddComponent(); } - void Awake() + internal void Awake() { DontDestroyOnLoad(gameObject); @@ -38,7 +39,7 @@ namespace IPA.Loader ipaPlugins = new CompositeIPAPlugin(PluginManager.Plugins); #pragma warning restore 618 -#if BeatSaber +#if BeatSaber // TODO: remove this gameObject.AddComponent(); #endif @@ -61,7 +62,7 @@ namespace IPA.Loader } } - void Update() + internal void Update() { bsPlugins.OnUpdate(); ipaPlugins.OnUpdate(); @@ -71,27 +72,27 @@ namespace IPA.Loader StartCoroutine(unitySched.Coroutine()); } - void LateUpdate() + internal void LateUpdate() { bsPlugins.OnLateUpdate(); ipaPlugins.OnLateUpdate(); } - void FixedUpdate() + internal void FixedUpdate() { bsPlugins.OnFixedUpdate(); ipaPlugins.OnFixedUpdate(); } - void OnDestroy() + internal void OnDestroy() { if (!quitting) { Create(); } - } - - void OnApplicationQuit() + } + + internal void OnApplicationQuit() { SceneManager.activeSceneChanged -= OnActiveSceneChanged; SceneManager.sceneLoaded -= OnSceneLoaded; @@ -105,17 +106,17 @@ namespace IPA.Loader quitting = true; } - void OnLevelWasLoaded(int level) + internal void OnLevelWasLoaded(int level) { ipaPlugins.OnLevelWasLoaded(level); } - void OnLevelWasInitialized(int level) + internal void OnLevelWasInitialized(int level) { ipaPlugins.OnLevelWasInitialized(level); } - void OnSceneLoaded(Scene scene, LoadSceneMode sceneMode) + private void OnSceneLoaded(Scene scene, LoadSceneMode sceneMode) { bsPlugins.OnSceneLoaded(scene, sceneMode); } diff --git a/IPA.Loader/Utilities/Async/Coroutines.cs b/IPA.Loader/Utilities/Async/Coroutines.cs index e94e3550..b4134dfd 100644 --- a/IPA.Loader/Utilities/Async/Coroutines.cs +++ b/IPA.Loader/Utilities/Async/Coroutines.cs @@ -1,4 +1,5 @@ -using System; +using IPA.Loader; +using System; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -18,9 +19,114 @@ namespace IPA.Utilities.Async /// 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)).Unwrap(); + + var tcs = new TaskCompletionSource(coroutine, TaskCreationOptions.RunContinuationsAsynchronously); + PluginComponent.Instance.StartCoroutine(new AsTaskCoroutineExecutor(coroutine, tcs)); + return tcs.Task; } + + private struct VoidStruct { } + private class ExceptionLocation : Exception + { + public ExceptionLocation(IEnumerable locations) + : base(string.Join("\n", 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 Stack(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(); + } + } }