using System.Threading; using Best.HTTP.Shared.PlatformSupport.Threading; using UnityEngine; namespace Best.HTTP.Shared { /// /// Threading mode the plugin will use to call HTTPManager.OnUpdate(). /// public enum ThreadingMode : int { /// /// HTTPManager.OnUpdate() is called from the HTTPUpdateDelegator's Update functions (Unity's main thread). /// UnityUpdate, /// /// The plugin starts a dedicated thread to call HTTPManager.OnUpdate() periodically. /// Threaded, /// /// HTTPManager.OnUpdate() will not be called automatically. /// None } /// /// Will route some U3D calls to the HTTPManager. /// [ExecuteInEditMode] [Best.HTTP.Shared.PlatformSupport.IL2CPP.Il2CppEagerStaticClassConstructionAttribute] public sealed class HTTPUpdateDelegator : MonoBehaviour { #region Public Properties /// /// The singleton instance of the HTTPUpdateDelegator /// public static HTTPUpdateDelegator Instance { get { return CheckInstance(); } } private volatile static HTTPUpdateDelegator instance; /// /// True, if the Instance property should hold a valid value. /// public static bool IsCreated { get; private set; } /// /// It's true if the dispatch thread running. /// public static bool IsThreadRunning { get; private set; } /// /// The current threading mode the plugin is in. /// public ThreadingMode CurrentThreadingMode { get { return _currentThreadingMode; } set { SetThreadingMode(value); } } private ThreadingMode _currentThreadingMode = ThreadingMode.UnityUpdate; /// /// How much time the plugin should wait between two update call. Its default value 100 ms. /// public static int ThreadFrequencyInMS { get; set; } /// /// Called in the OnApplicationQuit function. If this function returns False, the plugin will not start to /// shut down itself. /// public static System.Func OnBeforeApplicationQuit; /// /// Called when the Unity application's foreground state changed. /// public static System.Action OnApplicationForegroundStateChanged; public int MainThreadId { get => this.mainThreadId; } #endregion private static bool isSetupCalled; private int isHTTPManagerOnUpdateRunning; private AutoResetEvent pingEvent = new AutoResetEvent(false); private int updateThreadCount = 0; private int mainThreadId; #if UNITY_EDITOR /// /// Called after scene loaded to support Configurable Enter Play Mode (https://docs.unity3d.com/2019.3/Documentation/Manual/ConfigurableEnterPlayMode.html) /// [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] static void ResetSetup() { isSetupCalled = false; instance?.SetThreadingMode(ThreadingMode.UnityUpdate); HTTPManager.Logger.Information(nameof(HTTPUpdateDelegator), "Reset called!"); } #endif static HTTPUpdateDelegator() { ThreadFrequencyInMS = 100; } /// /// Will create the HTTPUpdateDelegator instance and set it up. /// public static HTTPUpdateDelegator CheckInstance() { try { if (!IsCreated) { GameObject go = GameObject.Find("HTTP Update Delegator"); if (go != null) instance = go.GetComponent(); if (instance == null) { go = new GameObject("HTTP Update Delegator"); go.hideFlags = HideFlags.HideAndDontSave; instance = go.AddComponent(); } IsCreated = true; #if UNITY_EDITOR if (!UnityEditor.EditorApplication.isPlaying) { UnityEditor.EditorApplication.update -= instance.Update; UnityEditor.EditorApplication.update += instance.Update; } UnityEditor.EditorApplication.playModeStateChanged -= instance.OnPlayModeStateChanged; UnityEditor.EditorApplication.playModeStateChanged += instance.OnPlayModeStateChanged; #endif // https://docs.unity3d.com/ScriptReference/Application-wantsToQuit.html Application.wantsToQuit -= UnityApplication_WantsToQuit; Application.wantsToQuit += UnityApplication_WantsToQuit; HTTPManager.Logger.Information(nameof(HTTPUpdateDelegator), "Instance Created!"); } } catch { HTTPManager.Logger.Error(nameof(HTTPUpdateDelegator), "Please call the Best.HTTPManager.Setup() from one of Unity's event(eg. awake, start) before you send any request!"); } return instance; } private void Setup() { if (isSetupCalled) return; isSetupCalled = true; // Setup is expected to be called on the Unity main thread only. mainThreadId = System.Threading.Thread.CurrentThread.ManagedThreadId; HTTPManager.Logger.Information(nameof(HTTPUpdateDelegator), $"Setup called Threading Mode: {this._currentThreadingMode}, MainThreadId: {this.mainThreadId}"); HTTPManager.Setup(); SetThreadingMode(this._currentThreadingMode); // Unity doesn't tolerate well if the DontDestroyOnLoad called when purely in editor mode. So, we will set the flag // only when we are playing, or not in the editor. if (!Application.isEditor || Application.isPlaying) GameObject.DontDestroyOnLoad(this.gameObject); HTTPManager.Logger.Information(nameof(HTTPUpdateDelegator), "Setup done!"); } /// /// Return true if the call happens on the Unity main thread. Setup must be called before to save the thread id! /// public bool IsMainThread() => System.Threading.Thread.CurrentThread.ManagedThreadId == mainThreadId; /// /// Set directly the threading mode to use. /// public void SetThreadingMode(ThreadingMode mode) { if (_currentThreadingMode == mode) return; HTTPManager.Logger.Information(nameof(HTTPUpdateDelegator), $"SetThreadingMode({mode}, {isSetupCalled})"); _currentThreadingMode = mode; if (!isSetupCalled) Setup(); switch (_currentThreadingMode) { case ThreadingMode.UnityUpdate: case ThreadingMode.None: IsThreadRunning = false; PingUpdateThread(); break; case ThreadingMode.Threaded: #if !UNITY_WEBGL || UNITY_EDITOR ThreadedRunner.RunLongLiving(ThreadFunc); #else HTTPManager.Logger.Warning(nameof(HTTPUpdateDelegator), "Threading mode set to ThreadingMode.Threaded, but threads aren't supported under WebGL!"); #endif break; } } /// /// Swaps threading mode between Unity's Update function or a distinct thread. /// public void SwapThreadingMode() => SetThreadingMode(_currentThreadingMode == ThreadingMode.Threaded ? ThreadingMode.UnityUpdate : ThreadingMode.Threaded); /// /// Pings the update thread to call HTTPManager.OnUpdate immediately. /// /// Works only when the current threading mode is Threaded! public void PingUpdateThread() => pingEvent.Set(); void ThreadFunc() { HTTPManager.Logger.Information(nameof(HTTPUpdateDelegator), "Update Thread Started"); ThreadedRunner.SetThreadName("Best.HTTP.Update Thread"); try { if (Interlocked.Increment(ref updateThreadCount) > 1) { HTTPManager.Logger.Information(nameof(HTTPUpdateDelegator), "An update thread already started."); return; } // Threading mode might be already changed, so set IsThreadRunning to IsThreaded's value. IsThreadRunning = CurrentThreadingMode == ThreadingMode.Threaded; while (IsThreadRunning) { CallOnUpdate(); pingEvent.WaitOne(ThreadFrequencyInMS); } } finally { Interlocked.Decrement(ref updateThreadCount); HTTPManager.Logger.Information(nameof(HTTPUpdateDelegator), "Update Thread Ended"); } } void Update() { if (!isSetupCalled) Setup(); if (CurrentThreadingMode == ThreadingMode.UnityUpdate) CallOnUpdate(); } private void CallOnUpdate() { // Prevent overlapping call of OnUpdate from unity's main thread and a separate thread if (Interlocked.CompareExchange(ref isHTTPManagerOnUpdateRunning, 1, 0) == 0) { try { HTTPManager.OnUpdate(); } finally { Interlocked.Exchange(ref isHTTPManagerOnUpdateRunning, 0); } } } #if UNITY_EDITOR void OnPlayModeStateChanged(UnityEditor.PlayModeStateChange playMode) { if (playMode == UnityEditor.PlayModeStateChange.EnteredPlayMode) { UnityEditor.EditorApplication.update -= Update; } else if (playMode == UnityEditor.PlayModeStateChange.EnteredEditMode) { UnityEditor.EditorApplication.update -= Update; UnityEditor.EditorApplication.update += Update; HTTPUpdateDelegator.ResetSetup(); HTTPManager.ResetSetup(); } } #endif void OnDisable() { HTTPManager.Logger.Information(nameof(HTTPUpdateDelegator), "OnDisable Called!"); #if UNITY_EDITOR if (UnityEditor.EditorApplication.isPlaying) #endif UnityApplication_WantsToQuit(); } void OnApplicationPause(bool isPaused) { HTTPManager.Logger.Information(nameof(HTTPUpdateDelegator), "OnApplicationPause isPaused: " + isPaused); if (HTTPUpdateDelegator.OnApplicationForegroundStateChanged != null) HTTPUpdateDelegator.OnApplicationForegroundStateChanged(isPaused); } private static bool UnityApplication_WantsToQuit() { HTTPManager.Logger.Information(nameof(HTTPUpdateDelegator), "UnityApplication_WantsToQuit Called!"); if (OnBeforeApplicationQuit != null) { try { if (!OnBeforeApplicationQuit()) { HTTPManager.Logger.Information(nameof(HTTPUpdateDelegator), "OnBeforeApplicationQuit call returned false, postponing plugin and application shutdown."); return false; } } catch (System.Exception ex) { HTTPManager.Logger.Exception(nameof(HTTPUpdateDelegator), string.Empty, ex); } } IsThreadRunning = false; Instance.PingUpdateThread(); if (!IsCreated) return true; IsCreated = false; HTTPManager.OnQuit(); return true; } } }