diff --git a/Assets/i5 Toolkit for Unity/Runtime/Experimental/UnityAdapters/ContentLoaders/UnityTextureLoader.cs b/Assets/i5 Toolkit for Unity/Runtime/Experimental/UnityAdapters/ContentLoaders/UnityTextureLoader.cs index 903a81dd..8fc4698f 100644 --- a/Assets/i5 Toolkit for Unity/Runtime/Experimental/UnityAdapters/ContentLoaders/UnityTextureLoader.cs +++ b/Assets/i5 Toolkit for Unity/Runtime/Experimental/UnityAdapters/ContentLoaders/UnityTextureLoader.cs @@ -4,34 +4,34 @@ using i5.Toolkit.Core.Utilities.Async; namespace i5.Toolkit.Core.Utilities.ContentLoaders -{ - /// - /// Adapter class which loads textures using Unity's WebRequestsTexture - /// - public class UnityTextureLoader : IContentLoader - { - /// - /// Loads the texture at the given URI using Unity's built-in methods - /// - /// The uri where the texture is stored - /// Returns a WebResponse with the results of the web request - public async Task> LoadAsync(string uri) - { - using (UnityWebRequest req = UnityWebRequestTexture.GetTexture(uri)) - { - await req.SendWebRequest(); - - if (req.isNetworkError || req.isHttpError) - { - i5Debug.LogError("Error fetching texture: " + req.error, this); - return new WebResponse(req.error, req.responseCode); - } - else - { - Texture2D texture = DownloadHandlerTexture.GetContent(req); - return new WebResponse(texture, req.downloadHandler.data, req.responseCode); - } - } - } - } +{ + /// + /// Adapter class which loads textures using Unity's WebRequestsTexture + /// + public class UnityTextureLoader : IContentLoader + { + /// + /// Loads the texture at the given URI using Unity's built-in methods + /// + /// The uri where the texture is stored + /// Returns a WebResponse with the results of the web request + public async Task> LoadAsync(string uri) + { + using (UnityWebRequest req = UnityWebRequestTexture.GetTexture(uri)) + { + await req.SendWebRequest(); + + if (req.result == UnityWebRequest.Result.Success) + { + Texture2D texture = DownloadHandlerTexture.GetContent(req); + return new WebResponse(texture, req.downloadHandler.data, req.responseCode); + } + else + { + i5Debug.LogError("Error fetching texture: " + req.error, this); + return new WebResponse(req.error, req.responseCode); + } + } + } + } } \ No newline at end of file diff --git a/Assets/i5 Toolkit for Unity/Runtime/Experimental/UnityAdapters/ContentLoaders/UnityWebRequestLoader.cs b/Assets/i5 Toolkit for Unity/Runtime/Experimental/UnityAdapters/ContentLoaders/UnityWebRequestLoader.cs index 43c2ecf3..fa02d434 100644 --- a/Assets/i5 Toolkit for Unity/Runtime/Experimental/UnityAdapters/ContentLoaders/UnityWebRequestLoader.cs +++ b/Assets/i5 Toolkit for Unity/Runtime/Experimental/UnityAdapters/ContentLoaders/UnityWebRequestLoader.cs @@ -21,16 +21,15 @@ public async Task> LoadAsync(string uri) { await req.SendWebRequest(); - if (req.result == UnityWebRequest.Result.ConnectionError || - req.result == UnityWebRequest.Result.ProtocolError) + if (req.result == UnityWebRequest.Result.Success) { - i5Debug.LogError("Get request to: " + uri + " returned with error " + req.error, this); - return new WebResponse(false, req.downloadHandler.text, req.downloadHandler.data, req.responseCode, req.error); + return new WebResponse(req.downloadHandler.text, req.downloadHandler.data, req.responseCode); } else { - return new WebResponse(req.downloadHandler.text, req.downloadHandler.data, req.responseCode); - } + i5Debug.LogError("Get request to: " + uri + " returned with error " + req.error, this); + return new WebResponse(false, req.downloadHandler.text, req.downloadHandler.data, req.responseCode, req.error); + } } } } diff --git a/Assets/i5 Toolkit for Unity/Runtime/Utilities/Async/AwaiterExtensions.cs b/Assets/i5 Toolkit for Unity/Runtime/Utilities/Async/AwaiterExtensions.cs index 1e76678b..b7a2104a 100644 --- a/Assets/i5 Toolkit for Unity/Runtime/Utilities/Async/AwaiterExtensions.cs +++ b/Assets/i5 Toolkit for Unity/Runtime/Utilities/Async/AwaiterExtensions.cs @@ -34,48 +34,49 @@ namespace i5.Toolkit.Core.Utilities.Async { - /// - /// We could just add a generic GetAwaiter to YieldInstruction and CustomYieldInstruction - /// but instead we add specific methods to each derived class to allow for return values - /// that make the most sense for the specific instruction type. - /// - public static class AwaiterExtensions - { - public static SimpleCoroutineAwaiter GetAwaiter(this WaitForSeconds instruction) - { - return GetAwaiterReturnVoid(instruction); - } - - public static SimpleCoroutineAwaiter GetAwaiter(this WaitForUpdate instruction) - { - return GetAwaiterReturnVoid(instruction); - } - - public static SimpleCoroutineAwaiter GetAwaiter(this WaitForEndOfFrame instruction) - { - return GetAwaiterReturnVoid(instruction); - } - - public static SimpleCoroutineAwaiter GetAwaiter(this WaitForFixedUpdate instruction) - { - return GetAwaiterReturnVoid(instruction); - } - - public static SimpleCoroutineAwaiter GetAwaiter(this WaitForSecondsRealtime instruction) - { - return GetAwaiterReturnVoid(instruction); - } - - public static SimpleCoroutineAwaiter GetAwaiter(this WaitUntil instruction) - { - return GetAwaiterReturnVoid(instruction); - } - - public static SimpleCoroutineAwaiter GetAwaiter(this WaitWhile instruction) - { - return GetAwaiterReturnVoid(instruction); - } - + /// + /// We could just add a generic GetAwaiter to YieldInstruction and CustomYieldInstruction + /// but instead we add specific methods to each derived class to allow for return values + /// that make the most sense for the specific instruction type. + /// + public static class AwaiterExtensions + { + public static SimpleCoroutineAwaiter GetAwaiter(this WaitForSeconds instruction) + { + return GetAwaiterReturnVoid(instruction); + } + + public static SimpleCoroutineAwaiter GetAwaiter(this WaitForUpdate instruction) + { + return GetAwaiterReturnVoid(instruction); + } + + public static SimpleCoroutineAwaiter GetAwaiter(this WaitForEndOfFrame instruction) + { + return GetAwaiterReturnVoid(instruction); + } + + public static SimpleCoroutineAwaiter GetAwaiter(this WaitForFixedUpdate instruction) + { + return GetAwaiterReturnVoid(instruction); + } + + public static SimpleCoroutineAwaiter GetAwaiter(this WaitForSecondsRealtime instruction) + { + return GetAwaiterReturnVoid(instruction); + } + + public static SimpleCoroutineAwaiter GetAwaiter(this WaitUntil instruction) + { + return GetAwaiterReturnVoid(instruction); + } + + public static SimpleCoroutineAwaiter GetAwaiter(this WaitWhile instruction) + { + return GetAwaiterReturnVoid(instruction); + } + +#if !UNITY_2023_1_OR_NEWER public static SimpleCoroutineAwaiter GetAwaiter(this AsyncOperation instruction) { return GetAwaiterReturnSelf(instruction); @@ -104,293 +105,301 @@ public static SimpleCoroutineAwaiter GetAwaiter(this AssetBundleRequest InstructionWrappers.AssetBundleRequest(awaiter, instruction))); return awaiter; } - - public static SimpleCoroutineAwaiter GetAwaiter(this IEnumerator coroutine) - { - var awaiter = new SimpleCoroutineAwaiter(); - RunOnUnityScheduler(() => AsyncCoroutineRunner.Instance.StartCoroutine( - new CoroutineWrapper(coroutine, awaiter).Run())); - return awaiter; - } - - public static SimpleCoroutineAwaiter GetAwaiter(this IEnumerator coroutine) - { - var awaiter = new SimpleCoroutineAwaiter(); - RunOnUnityScheduler(() => AsyncCoroutineRunner.Instance.StartCoroutine( - new CoroutineWrapper(coroutine, awaiter).Run())); - return awaiter; - } - - private static SimpleCoroutineAwaiter GetAwaiterReturnVoid(object instruction) - { - var awaiter = new SimpleCoroutineAwaiter(); - RunOnUnityScheduler(() => AsyncCoroutineRunner.Instance.StartCoroutine( - InstructionWrappers.ReturnVoid(awaiter, instruction))); - return awaiter; - } - - private static SimpleCoroutineAwaiter GetAwaiterReturnSelf(T instruction) - { - var awaiter = new SimpleCoroutineAwaiter(); - RunOnUnityScheduler(() => AsyncCoroutineRunner.Instance.StartCoroutine( - InstructionWrappers.ReturnSelf(awaiter, instruction))); - return awaiter; - } - - private static void RunOnUnityScheduler(Action action) - { - if (SynchronizationContext.Current == SyncContextUtility.UnitySynchronizationContext) - { - action(); - } - else - { - AsyncCoroutineRunner.Post(action); - } - } - - /// - /// Processes Coroutine and notifies completion with result. - /// - /// The result type. - public class SimpleCoroutineAwaiter : INotifyCompletion - { - private Exception exception; - private Action continuation; - private T result; - - public bool IsCompleted { get; private set; } - - public T GetResult() - { - Debug.Assert(IsCompleted); - - if (exception != null) - { - ExceptionDispatchInfo.Capture(exception).Throw(); - } - - return result; - } - - public void Complete(T taskResult, Exception e) - { - Debug.Assert(!IsCompleted); - - IsCompleted = true; - exception = e; - result = taskResult; - - // Always trigger the continuation on the unity thread - // when awaiting on unity yield instructions. - if (continuation != null) - { - RunOnUnityScheduler(continuation); - } - } - - void INotifyCompletion.OnCompleted(Action notifyContinuation) - { - Debug.Assert(continuation == null); - Debug.Assert(!IsCompleted); - - continuation = notifyContinuation; - } - } - - /// - /// Processes Coroutine and notifies completion. - /// - public class SimpleCoroutineAwaiter : INotifyCompletion - { - private Exception exception; - private Action continuation; - - public bool IsCompleted { get; private set; } - - public void GetResult() - { - Debug.Assert(IsCompleted); - - if (exception != null) - { - ExceptionDispatchInfo.Capture(exception).Throw(); - } - } - - public void Complete(Exception e) - { - Debug.Assert(!IsCompleted); - - IsCompleted = true; - exception = e; - - // Always trigger the continuation on the unity thread - // when awaiting on unity yield instructions. - if (continuation != null) - { - RunOnUnityScheduler(continuation); - } - } - - void INotifyCompletion.OnCompleted(Action notifyContinuation) - { - Debug.Assert(continuation == null); - Debug.Assert(!IsCompleted); - - continuation = notifyContinuation; - } - } - - private class CoroutineWrapper - { - private readonly SimpleCoroutineAwaiter awaiter; - private readonly Stack processStack; - - public CoroutineWrapper(IEnumerator coroutine, SimpleCoroutineAwaiter awaiter) - { - processStack = new Stack(); - processStack.Push(coroutine); - this.awaiter = awaiter; - } - - public IEnumerator Run() - { - while (true) - { - var topWorker = processStack.Peek(); - - bool isDone; - - try - { - isDone = !topWorker.MoveNext(); - } - catch (Exception e) - { - // The IEnumerators we have in the process stack do not tell us the - // actual names of the coroutine methods but it does tell us the objects - // that the IEnumerators are associated with, so we can at least try - // adding that to the exception output - var objectTrace = GenerateObjectTrace(processStack); - awaiter.Complete(default(T), objectTrace.Any() ? new Exception(GenerateObjectTraceMessage(objectTrace), e) : e); - - yield break; - } - - if (isDone) - { - processStack.Pop(); - - if (processStack.Count == 0) - { - awaiter.Complete((T)topWorker.Current, null); - yield break; - } - } - - // We could just yield return nested IEnumerator's here but we choose to do - // our own handling here so that we can catch exceptions in nested coroutines - // instead of just top level coroutine - var item = topWorker.Current as IEnumerator; - if (item != null) - { - processStack.Push(item); - } - else - { - // Return the current value to the unity engine so it can handle things like - // WaitForSeconds, WaitToEndOfFrame, etc. - yield return topWorker.Current; - } - } - } - - private static string GenerateObjectTraceMessage(List objTrace) - { - var result = new StringBuilder(); - - foreach (var objType in objTrace) - { - if (result.Length != 0) - { - result.Append(" -> "); - } - - result.Append(objType); - } - - result.AppendLine(); - return $"Unity Coroutine Object Trace: {result}"; - } - - private static List GenerateObjectTrace(IEnumerable enumerators) - { - var objTrace = new List(); - - foreach (var enumerator in enumerators) - { - // NOTE: This only works with scripting engine 4.6 - // And could easily stop working with unity updates - var field = enumerator.GetType().GetField("$this", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); - - if (field == null) - { - continue; - } - - var obj = field.GetValue(enumerator); - - if (obj == null) - { - continue; - } - - var objType = obj.GetType(); - - if (!objTrace.Any() || objType != objTrace.Last()) - { - objTrace.Add(objType); - } - } - - objTrace.Reverse(); - return objTrace; - } - } - - private static class InstructionWrappers - { - public static IEnumerator ReturnVoid(SimpleCoroutineAwaiter awaiter, object instruction) - { - // For simple instructions we assume that they don't throw exceptions - yield return instruction; - awaiter.Complete(null); - } - - public static IEnumerator AssetBundleCreateRequest(SimpleCoroutineAwaiter awaiter, AssetBundleCreateRequest instruction) - { - yield return instruction; - awaiter.Complete(instruction.assetBundle, null); - } - - public static IEnumerator ReturnSelf(SimpleCoroutineAwaiter awaiter, T instruction) - { - yield return instruction; - awaiter.Complete(instruction, null); - } - - public static IEnumerator AssetBundleRequest(SimpleCoroutineAwaiter awaiter, AssetBundleRequest instruction) - { - yield return instruction; - awaiter.Complete(instruction.asset, null); - } - - public static IEnumerator ResourceRequest(SimpleCoroutineAwaiter awaiter, ResourceRequest instruction) - { - yield return instruction; - awaiter.Complete(instruction.asset, null); - } - } - } -} +#endif + + public static SimpleCoroutineAwaiter GetAwaiter(this IEnumerator coroutine) + { + var awaiter = new SimpleCoroutineAwaiter(); + RunOnUnityScheduler(() => AsyncCoroutineRunner.Instance.StartCoroutine( + new CoroutineWrapper(coroutine, awaiter).Run())); + return awaiter; + } + + public static SimpleCoroutineAwaiter GetAwaiter(this IEnumerator coroutine) + { + var awaiter = new SimpleCoroutineAwaiter(); + RunOnUnityScheduler(() => AsyncCoroutineRunner.Instance.StartCoroutine( + new CoroutineWrapper(coroutine, awaiter).Run())); + return awaiter; + } + + private static SimpleCoroutineAwaiter GetAwaiterReturnVoid(object instruction) + { + var awaiter = new SimpleCoroutineAwaiter(); + RunOnUnityScheduler(() => AsyncCoroutineRunner.Instance.StartCoroutine( + InstructionWrappers.ReturnVoid(awaiter, instruction))); + return awaiter; + } + + private static SimpleCoroutineAwaiter GetAwaiterReturnSelf(T instruction) + { + var awaiter = new SimpleCoroutineAwaiter(); + RunOnUnityScheduler(() => AsyncCoroutineRunner.Instance.StartCoroutine( + InstructionWrappers.ReturnSelf(awaiter, instruction))); + return awaiter; + } + + private static void RunOnUnityScheduler(Action action) + { + if (SynchronizationContext.Current == SyncContextUtility.UnitySynchronizationContext) + { + action(); + } + else + { + // Make sure there is a running instance of AsyncCoroutineRunner before calling AsyncCoroutineRunner.Post + // If not warn the user. Note we cannot call AsyncCoroutineRunner.Instance here as that getter contains + // calls to Unity functions that can only be run on the Unity thread + if (!AsyncCoroutineRunner.IsInstanceRunning) + { + Debug.LogWarning("There is no active AsyncCoroutineRunner when an action is posted. Place a GameObject " + + "at the root of the scene and attach the AsyncCoroutineRunner script to make it function properly."); + } + AsyncCoroutineRunner.Post(action); + } + } + + /// + /// Processes Coroutine and notifies completion with result. + /// + /// The result type. + public class SimpleCoroutineAwaiter : INotifyCompletion + { + private Exception exception; + private Action continuation; + private T result; + + public bool IsCompleted { get; private set; } + + public T GetResult() + { + Debug.Assert(IsCompleted); + + if (exception != null) + { + ExceptionDispatchInfo.Capture(exception).Throw(); + } + + return result; + } + + public void Complete(T taskResult, Exception e) + { + Debug.Assert(!IsCompleted); + + IsCompleted = true; + exception = e; + result = taskResult; + + // Always trigger the continuation on the unity thread + // when awaiting on unity yield instructions. + if (continuation != null) + { + RunOnUnityScheduler(continuation); + } + } + + void INotifyCompletion.OnCompleted(Action notifyContinuation) + { + Debug.Assert(continuation == null); + Debug.Assert(!IsCompleted); + + continuation = notifyContinuation; + } + } + + /// + /// Processes Coroutine and notifies completion. + /// + public class SimpleCoroutineAwaiter : INotifyCompletion + { + private Exception exception; + private Action continuation; + + public bool IsCompleted { get; private set; } + + public void GetResult() + { + Debug.Assert(IsCompleted); + + if (exception != null) + { + ExceptionDispatchInfo.Capture(exception).Throw(); + } + } + + public void Complete(Exception e) + { + Debug.Assert(!IsCompleted); + + IsCompleted = true; + exception = e; + + // Always trigger the continuation on the unity thread + // when awaiting on unity yield instructions. + if (continuation != null) + { + RunOnUnityScheduler(continuation); + } + } + + void INotifyCompletion.OnCompleted(Action notifyContinuation) + { + Debug.Assert(continuation == null); + Debug.Assert(!IsCompleted); + + continuation = notifyContinuation; + } + } + + private class CoroutineWrapper + { + private readonly SimpleCoroutineAwaiter awaiter; + private readonly Stack processStack; + + public CoroutineWrapper(IEnumerator coroutine, SimpleCoroutineAwaiter awaiter) + { + processStack = new Stack(); + processStack.Push(coroutine); + this.awaiter = awaiter; + } + + public IEnumerator Run() + { + while (true) + { + var topWorker = processStack.Peek(); + + bool isDone; + + try + { + isDone = !topWorker.MoveNext(); + } + catch (Exception e) + { + // The IEnumerators we have in the process stack do not tell us the + // actual names of the coroutine methods but it does tell us the objects + // that the IEnumerators are associated with, so we can at least try + // adding that to the exception output + var objectTrace = GenerateObjectTrace(processStack); + awaiter.Complete(default(T), objectTrace.Any() ? new Exception(GenerateObjectTraceMessage(objectTrace), e) : e); + + yield break; + } + + if (isDone) + { + processStack.Pop(); + + if (processStack.Count == 0) + { + awaiter.Complete((T)topWorker.Current, null); + yield break; + } + } + + // We could just yield return nested IEnumerator's here but we choose to do + // our own handling here so that we can catch exceptions in nested coroutines + // instead of just top level coroutine + if (topWorker.Current is IEnumerator item) + { + processStack.Push(item); + } + else + { + // Return the current value to the unity engine so it can handle things like + // WaitForSeconds, WaitToEndOfFrame, etc. + yield return topWorker.Current; + } + } + } + + private static string GenerateObjectTraceMessage(List objTrace) + { + var result = new StringBuilder(); + + foreach (var objType in objTrace) + { + if (result.Length != 0) + { + result.Append(" -> "); + } + + result.Append(objType); + } + + result.AppendLine(); + return $"Unity Coroutine Object Trace: {result}"; + } + + private static List GenerateObjectTrace(IEnumerable enumerators) + { + var objTrace = new List(); + + foreach (var enumerator in enumerators) + { + // NOTE: This only works with scripting engine 4.6 + // And could easily stop working with unity updates + var field = enumerator.GetType().GetField("$this", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); + + if (field == null) + { + continue; + } + + var obj = field.GetValue(enumerator); + + if (obj == null) + { + continue; + } + + var objType = obj.GetType(); + + if (!objTrace.Any() || objType != objTrace.Last()) + { + objTrace.Add(objType); + } + } + + objTrace.Reverse(); + return objTrace; + } + } + + private static class InstructionWrappers + { + public static IEnumerator ReturnVoid(SimpleCoroutineAwaiter awaiter, object instruction) + { + // For simple instructions we assume that they don't throw exceptions + yield return instruction; + awaiter.Complete(null); + } + + public static IEnumerator AssetBundleCreateRequest(SimpleCoroutineAwaiter awaiter, AssetBundleCreateRequest instruction) + { + yield return instruction; + awaiter.Complete(instruction.assetBundle, null); + } + + public static IEnumerator ReturnSelf(SimpleCoroutineAwaiter awaiter, T instruction) + { + yield return instruction; + awaiter.Complete(instruction, null); + } + + public static IEnumerator AssetBundleRequest(SimpleCoroutineAwaiter awaiter, AssetBundleRequest instruction) + { + yield return instruction; + awaiter.Complete(instruction.asset, null); + } + + public static IEnumerator ResourceRequest(SimpleCoroutineAwaiter awaiter, ResourceRequest instruction) + { + yield return instruction; + awaiter.Complete(instruction.asset, null); + } + } + } +} \ No newline at end of file diff --git a/Assets/i5 Toolkit for Unity/Runtime/Utilities/Async/Internal/AsyncCoroutineRunner.cs b/Assets/i5 Toolkit for Unity/Runtime/Utilities/Async/Internal/AsyncCoroutineRunner.cs index 56068781..b9a59d73 100644 --- a/Assets/i5 Toolkit for Unity/Runtime/Utilities/Async/Internal/AsyncCoroutineRunner.cs +++ b/Assets/i5 Toolkit for Unity/Runtime/Utilities/Async/Internal/AsyncCoroutineRunner.cs @@ -22,116 +22,156 @@ using System; using System.Collections.Generic; -using System.Runtime.CompilerServices; using UnityEngine; -[assembly: InternalsVisibleTo("Microsoft.MixedReality.Toolkit.Tests.PlayModeTests")] namespace i5.Toolkit.Core.Utilities.Async { - /// - /// This Async Coroutine Runner is just an object to - /// ensure that coroutines run properly with async/await. - /// - /// - /// The object that this MonoBehavior is attached to must be a root object in the - /// scene, as it will be marked as DontDestroyOnLoad (so that when scenes are changed, - /// it will persist instead of being destroyed). The runner will force itself to - /// the root of the scene if it's rooted elsewhere. - /// - [AddComponentMenu("Scripts/MRTK/Core/AsyncCoroutineRunner")] - internal sealed class AsyncCoroutineRunner : MonoBehaviour - { - private static AsyncCoroutineRunner instance; - - private static readonly Queue Actions = new Queue(); - - internal static AsyncCoroutineRunner Instance - { - get - { - if (instance == null) - { - instance = FindObjectOfType(); - } - - if (instance == null) - { - var instanceGameObject = GameObject.Find("AsyncCoroutineRunner"); - - if (instanceGameObject != null) - { - instance = instanceGameObject.GetComponent(); - - if (instance == null) - { - Debug.Log("[AsyncCoroutineRunner] Found GameObject but didn't have component"); - - if (Application.isPlaying) - { - Destroy(instanceGameObject); - } - else - { - DestroyImmediate(instanceGameObject); - } - } - } - } - - if (instance == null) - { - instance = new GameObject("AsyncCoroutineRunner").AddComponent(); - } - - instance.gameObject.hideFlags = HideFlags.None; - - // AsyncCoroutineRunner must be at the root so that we can call DontDestroyOnLoad on it. - // This is ultimately to ensure that it persists across scene loads/unloads. - if (instance.transform.parent != null) - { - Debug.LogWarning($"AsyncCoroutineRunner was found as a child of another GameObject {instance.transform.parent}, " + - "it must be a root object in the scene. Moving the AsyncCoroutineRunner to the root."); - instance.transform.parent = null; - } + /// + /// This Async Coroutine Runner is just an object to + /// ensure that coroutines run properly with async/await. + /// + /// + /// The object that this MonoBehavior is attached to must be a root object in the + /// scene, as it will be marked as DontDestroyOnLoad (so that when scenes are changed, + /// it will persist instead of being destroyed). The runner will force itself to + /// the root of the scene if it's rooted elsewhere. + /// + [AddComponentMenu("Scripts/i5/Core/AsyncCoroutineRunner")] + internal sealed class AsyncCoroutineRunner : MonoBehaviour + { + private static AsyncCoroutineRunner instance; + + private static bool isInstanceRunning = false; + + private static readonly Queue Actions = new Queue(); + + internal static AsyncCoroutineRunner Instance + { + get + { + if (instance == null) + { +#if UNITY_2021_3_OR_NEWER + instance = UnityEngine.Object.FindFirstObjectByType(); +#else + instance = GameObject.FindObjectOfType(); +#endif + } + + // FindObjectOfType() only search for objects attached to active GameObjects. The FindObjectOfType(bool includeInactive) variant is not available to Unity 2019.4 and earlier so cannot be used. + // We instead search for GameObject called AsyncCoroutineRunner and see if it has the component attached. + if (instance == null) + { + var instanceGameObject = GameObject.Find("AsyncCoroutineRunner"); + + if (instanceGameObject != null) + { + instance = instanceGameObject.GetComponent(); + + if (instance == null) + { + Debug.Log("[AsyncCoroutineRunner] Found a \"AsyncCoroutineRunner\" GameObject but didn't have the AsyncCoroutineRunner component attached. Attaching the script."); + instance = instanceGameObject.AddComponent(); + } + } + } + + if (instance == null) + { + Debug.Log("[AsyncCoroutineRunner] There is no AsyncCoroutineRunner in the scene. Adding a GameObject with AsyncCoroutineRunner attached at the root of the scene."); + instance = new GameObject("AsyncCoroutineRunner").AddComponent(); + } + else if (!instance.isActiveAndEnabled) + { + if (!instance.enabled) + { + Debug.LogWarning("[AsyncCoroutineRunner] Found a disabled AsyncCoroutineRunner component. Enabling the component."); + instance.enabled = true; + } + if (!instance.gameObject.activeSelf) + { + Debug.LogWarning("[AsyncCoroutineRunner] Found an AsyncCoroutineRunner attached to an inactive GameObject. Setting the GameObject active."); + instance.gameObject.SetActive(true); + } + } + + instance.gameObject.hideFlags = HideFlags.None; + + // AsyncCoroutineRunner must be at the root so that we can call DontDestroyOnLoad on it. + // This is ultimately to ensure that it persists across scene loads/unloads. + if (instance.transform.parent != null) + { + Debug.LogWarning($"[AsyncCoroutineRunner] AsyncCoroutineRunner was found as a child of another GameObject {instance.transform.parent}, " + + "it must be a root object in the scene. Moving the AsyncCoroutineRunner to the root."); + instance.transform.parent = null; + } #if !UNITY_EDITOR DontDestroyOnLoad(instance); #endif - - return instance; - } - } - - internal static void Post(Action task) - { - lock (Actions) - { - Actions.Enqueue(task); - } - } - - private void Update() - { - Debug.Assert(Instance != null); - - int actionCount; - - lock (Actions) - { - actionCount = Actions.Count; - } - - for (int i = 0; i < actionCount; i++) - { - Action next; - - lock (Actions) - { - next = Actions.Dequeue(); - } - - next(); - } - } - } -} + return instance; + } + } + + internal static void Post(Action task) + { + lock (Actions) + { + Actions.Enqueue(task); + } + } + + internal static bool IsInstanceRunning => isInstanceRunning; + + private void Update() + { + if (Instance != this) + { + Debug.Log("[AsyncCoroutineRunner] Multiple active AsyncCoroutineRunners is present in the scene. Disabling duplicate ones."); + enabled = false; + return; + } + isInstanceRunning = true; + + int actionCount; + + lock (Actions) + { + actionCount = Actions.Count; + } + + for (int i = 0; i < actionCount; i++) + { + Action next; + + lock (Actions) + { + next = Actions.Dequeue(); + } + + next(); + } + } + + private void OnDisable() + { + if (instance == this) + { + isInstanceRunning = false; + } + } + + private void OnEnable() + { + if (Instance != this) + { + Debug.Log("[AsyncCoroutineRunner] Multiple active AsyncCoroutineRunners is present in the scene. Disabling duplicate ones."); + enabled = false; + } + else + { + isInstanceRunning = true; + } + } + } +} \ No newline at end of file diff --git a/Assets/i5 Toolkit for Unity/Runtime/Utilities/Rest Connectors/UnityWebRequestRestConnector.cs b/Assets/i5 Toolkit for Unity/Runtime/Utilities/Rest Connectors/UnityWebRequestRestConnector.cs index ed92dbfd..86c5974a 100644 --- a/Assets/i5 Toolkit for Unity/Runtime/Utilities/Rest Connectors/UnityWebRequestRestConnector.cs +++ b/Assets/i5 Toolkit for Unity/Runtime/Utilities/Rest Connectors/UnityWebRequestRestConnector.cs @@ -1,101 +1,102 @@ -using i5.Toolkit.Core.Utilities.Async; -using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; -using UnityEngine.Networking; - -namespace i5.Toolkit.Core.Utilities -{ - public class UnityWebRequestRestConnector : IRestConnector - { - public virtual async Task> DeleteAsync(string uri, Dictionary headers = null) - { - using (UnityWebRequest req = UnityWebRequest.Delete(uri)) - { - AddHeaders(req, headers); - req.downloadHandler = new DownloadHandlerBuffer(); - await req.SendWebRequest(); - - if (req.isHttpError || req.isNetworkError) - { - return new WebResponse(false, req.downloadHandler.text, req.downloadHandler.data, req.responseCode, req.error); - } - else - { - return new WebResponse(req.downloadHandler.text, req.downloadHandler.data, req.responseCode); - } - } - } - - public virtual async Task> GetAsync(string uri, Dictionary headers = null) - { - using (UnityWebRequest req = UnityWebRequest.Get(uri)) - { - AddHeaders(req, headers); - await req.SendWebRequest(); - - if (req.isHttpError || req.isNetworkError) - { - return new WebResponse(false, req.downloadHandler.text, req.downloadHandler.data, req.responseCode, req.error); - } - else - { - return new WebResponse(req.downloadHandler.text, req.downloadHandler.data, req.responseCode); - } - } - } - - public virtual async Task> PostAsync(string uri, string putJson, Dictionary headers = null) - { - using (UnityWebRequest req = UnityWebRequest.PostWwwForm(uri, putJson)) - { - req.downloadHandler = new DownloadHandlerBuffer(); - req.SetRequestHeader("Content-Type", "application/json"); - req.SetRequestHeader("Accept", "application/json"); - - AddHeaders(req, headers); - await req.SendWebRequest(); - - if (req.isHttpError || req.isNetworkError) - { - return new WebResponse(false, req.downloadHandler.text, req.downloadHandler.data, req.responseCode, req.error); - } - else - { - return new WebResponse(req.downloadHandler.text, req.downloadHandler.data, req.responseCode); - } - } - } - - public virtual async Task> PostAsync(string uri, byte[] postData, Dictionary headers = null) +using i5.Toolkit.Core.Utilities.Async; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; +using UnityEngine.Networking; + +namespace i5.Toolkit.Core.Utilities +{ + public class UnityWebRequestRestConnector : IRestConnector + { + public virtual async Task> DeleteAsync(string uri, Dictionary headers = null) + { + using (UnityWebRequest req = UnityWebRequest.Delete(uri)) + { + AddHeaders(req, headers); + req.downloadHandler = new DownloadHandlerBuffer(); + await req.SendWebRequest(); + + if (req.result == UnityWebRequest.Result.Success) + { + return new WebResponse(req.downloadHandler.text, req.downloadHandler.data, req.responseCode); + } + else + { + return new WebResponse(false, req.downloadHandler.text, req.downloadHandler.data, req.responseCode, req.error); + } + } + } + + public virtual async Task> GetAsync(string uri, Dictionary headers = null) + { + using (UnityWebRequest req = UnityWebRequest.Get(uri)) + { + AddHeaders(req, headers); + await req.SendWebRequest(); + + if (req.result == UnityWebRequest.Result.Success) + { + return new WebResponse(req.downloadHandler.text, req.downloadHandler.data, req.responseCode); + } + else + { + return new WebResponse(false, req.downloadHandler.text, req.downloadHandler.data, req.responseCode, req.error); + } + } + } + + public virtual async Task> PostAsync(string uri, string putJson, Dictionary headers = null) + { + using (UnityWebRequest req = UnityWebRequest.PostWwwForm(uri, putJson)) + { + req.downloadHandler = new DownloadHandlerBuffer(); + req.SetRequestHeader("Content-Type", "application/json"); + req.SetRequestHeader("Accept", "application/json"); + + AddHeaders(req, headers); + await req.SendWebRequest(); + + if (req.result == UnityWebRequest.Result.Success) + { + return new WebResponse(req.downloadHandler.text, req.downloadHandler.data, req.responseCode); + } + else + { + return new WebResponse(false, req.downloadHandler.text, req.downloadHandler.data, req.responseCode, req.error); + } + } + } + + public virtual async Task> PostAsync(string uri, byte[] postData, Dictionary headers = null) { return await PostPutDataAsync(uri, "POST", postData, "application/octet-stream", headers); - } - - public virtual async Task> PutAsync(string uri, string putJson, Dictionary headers = null) - { - using (UnityWebRequest req = UnityWebRequest.Put(uri, putJson)) - { - req.SetRequestHeader("Content-Type", "application/json"); - AddHeaders(req, headers); - await req.SendWebRequest(); - - if (req.isHttpError || req.isNetworkError) - { - return new WebResponse(false, req.downloadHandler.text, req.downloadHandler.data, req.responseCode, req.error); - } - else - { - return new WebResponse(req.downloadHandler.text, req.downloadHandler.data, req.responseCode); - } - } - } - + } + + public virtual async Task> PutAsync(string uri, string putJson, Dictionary headers = null) + { + using (UnityWebRequest req = UnityWebRequest.Put(uri, putJson)) + { + req.SetRequestHeader("Content-Type", "application/json"); + AddHeaders(req, headers); + await req.SendWebRequest(); + + if (req.result == UnityWebRequest.Result.Success) + { + return new WebResponse(req.downloadHandler.text, req.downloadHandler.data, req.responseCode); + } + else + { + return new WebResponse(false, req.downloadHandler.text, req.downloadHandler.data, req.responseCode, req.error); + } + } + } + public virtual async Task> PutAsync(string uri, byte[] putData, Dictionary headers = null) { return await PostPutDataAsync(uri, "PUT", putData, "application/octet-stream", headers); - } - + } + protected async Task> PostPutDataAsync(string uri, string method, byte[] data, string contentType, Dictionary headers = null) { using (UnityWebRequest req = new UnityWebRequest(uri, method)) @@ -108,28 +109,28 @@ protected async Task> PostPutDataAsync(string uri, string me await req.SendWebRequest(); - if (req.isHttpError || req.isNetworkError) + if (req.result == UnityWebRequest.Result.Success) { - return new WebResponse(false, req.downloadHandler.text, req.downloadHandler.data, req.responseCode, req.error); + return new WebResponse(req.downloadHandler.text, req.downloadHandler.data, req.responseCode); } else { - return new WebResponse(req.downloadHandler.text, req.downloadHandler.data, req.responseCode); + return new WebResponse(false, req.downloadHandler.text, req.downloadHandler.data, req.responseCode, req.error); } } - } - - - protected void AddHeaders(UnityWebRequest req, Dictionary headers) - { - if (headers == null) - { - return; - } - foreach (KeyValuePair header in headers) - { - req.SetRequestHeader(header.Key, header.Value); - } - } - } + } + + + protected void AddHeaders(UnityWebRequest req, Dictionary headers) + { + if (headers == null) + { + return; + } + foreach (KeyValuePair header in headers) + { + req.SetRequestHeader(header.Key, header.Value); + } + } + } } \ No newline at end of file