#if UNITY_2022_1_OR_NEWER #define UNLOAD_BUNDLE_ASYNC #endif using System; using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Threading; using UnityEngine.Networking; using UnityEngine.ResourceManagement.AsyncOperations; using UnityEngine.ResourceManagement.Exceptions; using UnityEngine.ResourceManagement.ResourceLocations; using UnityEngine.ResourceManagement.Util; using TTSDK; namespace UnityEngine.ResourceManagement { internal class TTWebRequestQueueOperation { private bool m_Completed = false; public UnityWebRequestAsyncOperation Result; public Action OnComplete; public bool IsDone { get { return m_Completed || Result != null; } } internal UnityWebRequest m_WebRequest; public UnityWebRequest WebRequest { get { return m_WebRequest; } internal set { m_WebRequest = value; } } public TTWebRequestQueueOperation(UnityWebRequest request) { m_WebRequest = request; } internal void Complete(UnityWebRequestAsyncOperation asyncOp) { m_Completed = true; Result = asyncOp; OnComplete?.Invoke(Result); } } internal static class WebRequestQueue { internal static int s_MaxRequest = 512; internal static Queue s_QueuedOperations = new Queue(); internal static List s_ActiveRequests = new List(); public static void SetMaxConcurrentRequests(int maxRequests) { if (maxRequests < 1) throw new ArgumentException("MaxRequests must be 1 or greater.", "maxRequests"); s_MaxRequest = maxRequests; } public static TTWebRequestQueueOperation QueueRequest(UnityWebRequest request) { TTWebRequestQueueOperation queueOperation = new TTWebRequestQueueOperation(request); if (s_ActiveRequests.Count < s_MaxRequest) BeginWebRequest(queueOperation); else s_QueuedOperations.Enqueue(queueOperation); return queueOperation; } internal static void WaitForRequestToBeActive(TTWebRequestQueueOperation request, int millisecondsTimeout) { var completedRequests = new List(); while (s_QueuedOperations.Contains(request)) { completedRequests.Clear(); foreach (UnityWebRequestAsyncOperation webRequestAsyncOp in s_ActiveRequests) { if (TTUnityWebRequestUtilities.IsAssetBundleDownloaded(webRequestAsyncOp)) completedRequests.Add(webRequestAsyncOp); } foreach (UnityWebRequestAsyncOperation webRequestAsyncOp in completedRequests) { bool requestIsActive = s_QueuedOperations.Peek() == request; webRequestAsyncOp.completed -= OnWebAsyncOpComplete; OnWebAsyncOpComplete(webRequestAsyncOp); if (requestIsActive) return; } Thread.Sleep(millisecondsTimeout); } } internal static void DequeueRequest(UnityWebRequestAsyncOperation operation) { operation.completed -= OnWebAsyncOpComplete; OnWebAsyncOpComplete(operation); } private static void OnWebAsyncOpComplete(AsyncOperation operation) { OnWebAsyncOpComplete(operation as UnityWebRequestAsyncOperation); } private static void OnWebAsyncOpComplete(UnityWebRequestAsyncOperation operation) { if (s_ActiveRequests.Remove(operation) && s_QueuedOperations.Count > 0) { var nextQueuedOperation = s_QueuedOperations.Dequeue(); BeginWebRequest(nextQueuedOperation); } } static void BeginWebRequest(TTWebRequestQueueOperation queueOperation) { var request = queueOperation.m_WebRequest; UnityWebRequestAsyncOperation webRequestAsyncOp = null; try { webRequestAsyncOp = request.SendWebRequest(); if (webRequestAsyncOp != null) { s_ActiveRequests.Add(webRequestAsyncOp); if (webRequestAsyncOp.isDone) OnWebAsyncOpComplete(webRequestAsyncOp); else webRequestAsyncOp.completed += OnWebAsyncOpComplete; } else { OnWebAsyncOpComplete(null); } } catch (Exception e) { Debug.LogError(e.Message); } queueOperation.Complete(webRequestAsyncOp); } } } namespace UnityEngine.ResourceManagement.Util { [Flags] internal enum BundleSource { None = 0, Local = 1, Cache = 2, Download = 4, } } namespace UnityEngine.ResourceManagement.ResourceProviders { [DisplayName("Assets from TTBundles Provider")] public class TTBundledAssetProvider : ResourceProviderBase { internal class InternalOp { AssetBundle m_AssetBundle; AssetBundleRequest m_PreloadRequest; AssetBundleRequest m_RequestOperation; object m_Result; ProvideHandle m_ProvideHandle; string subObjectName = null; internal static T LoadBundleFromDependecies(IList results) where T : class, IAssetBundleResource { if (results == null || results.Count == 0) return default(T); IAssetBundleResource bundle = null; bool firstBundleWrapper = true; for (int i = 0; i < results.Count; i++) { var abWrapper = results[i] as IAssetBundleResource; if (abWrapper != null) { //only use the first asset bundle, even if it is invalid abWrapper.GetAssetBundle(); if (firstBundleWrapper) bundle = abWrapper; firstBundleWrapper = false; } } return bundle as T; } public void Start(ProvideHandle provideHandle) { provideHandle.SetProgressCallback(ProgressCallback); provideHandle.SetWaitForCompletionCallback(WaitForCompletionHandler); subObjectName = null; m_ProvideHandle = provideHandle; m_RequestOperation = null; List deps = new List(); m_ProvideHandle.GetDependencies(deps); var bundleResource = LoadBundleFromDependecies(deps); if (bundleResource == null) { m_ProvideHandle.Complete(null, false, new Exception("Unable to load dependent bundle from location " + m_ProvideHandle.Location)); } else { m_AssetBundle = bundleResource.GetAssetBundle(); if (m_AssetBundle == null) { m_ProvideHandle.Complete(null, false, new Exception("Unable to load dependent bundle from location " + m_ProvideHandle.Location)); return; } var assetBundleResource = bundleResource as TTAssetBundleResource; if (assetBundleResource != null) m_PreloadRequest = assetBundleResource.GetAssetPreloadRequest(); if (m_PreloadRequest == null || m_PreloadRequest.isDone) BeginAssetLoad(); else m_PreloadRequest.completed += operation => BeginAssetLoad(); } } private void BeginAssetLoad() { if (m_AssetBundle == null) { m_ProvideHandle.Complete(null, false, new Exception("Unable to load dependent bundle from location " + m_ProvideHandle.Location)); } else { var assetPath = m_ProvideHandle.ResourceManager.TransformInternalId(m_ProvideHandle.Location); if (m_ProvideHandle.Type.IsArray) { #if !UNITY_2021_1_OR_NEWER if (AsyncOperationHandle.IsWaitingForCompletion) { GetArrayResult(m_AssetBundle.LoadAssetWithSubAssets(assetPath, m_ProvideHandle.Type.GetElementType())); CompleteOperation(); } else #endif m_RequestOperation = m_AssetBundle.LoadAssetWithSubAssetsAsync(assetPath, m_ProvideHandle.Type.GetElementType()); } else if (m_ProvideHandle.Type.IsGenericType && typeof(IList<>) == m_ProvideHandle.Type.GetGenericTypeDefinition()) { #if !UNITY_2021_1_OR_NEWER if (AsyncOperationHandle.IsWaitingForCompletion) { GetListResult(m_AssetBundle.LoadAssetWithSubAssets(assetPath, m_ProvideHandle.Type.GetGenericArguments()[0])); CompleteOperation(); } else #endif m_RequestOperation = m_AssetBundle.LoadAssetWithSubAssetsAsync(assetPath, m_ProvideHandle.Type.GetGenericArguments()[0]); } else { if (ExtractKeyAndSubKey(assetPath, out string mainPath, out string subKey)) { subObjectName = subKey; #if !UNITY_2021_1_OR_NEWER if (AsyncOperationHandle.IsWaitingForCompletion) { GetAssetSubObjectResult(m_AssetBundle.LoadAssetWithSubAssets(mainPath, m_ProvideHandle.Type)); CompleteOperation(); } else #endif m_RequestOperation = m_AssetBundle.LoadAssetWithSubAssetsAsync(mainPath, m_ProvideHandle.Type); } else { #if !UNITY_2021_1_OR_NEWER if (AsyncOperationHandle.IsWaitingForCompletion) { GetAssetResult(m_AssetBundle.LoadAsset(assetPath, m_ProvideHandle.Type)); CompleteOperation(); } else #endif m_RequestOperation = m_AssetBundle.LoadAssetAsync(assetPath, m_ProvideHandle.Type); } } if (m_RequestOperation != null) { if (m_RequestOperation.isDone) ActionComplete(m_RequestOperation); else { #if ENABLE_ADDRESSABLE_PROFILER if (UnityEngine.Profiling.Profiler.enabled && m_ProvideHandle.IsValid) Profiling.ProfilerRuntime.AddAssetOperation(m_ProvideHandle, Profiling.ContentStatus.Loading); #endif m_RequestOperation.completed += ActionComplete; } } } } private bool WaitForCompletionHandler() { if (m_PreloadRequest != null && !m_PreloadRequest.isDone) return m_PreloadRequest.asset == null; if (m_Result != null) return true; if (m_RequestOperation == null) return false; if (m_RequestOperation.isDone) return true; return m_RequestOperation.asset != null; } private void ActionComplete(AsyncOperation obj) { if (m_RequestOperation != null) { if (m_ProvideHandle.Type.IsArray) GetArrayResult(m_RequestOperation.allAssets); else if (m_ProvideHandle.Type.IsGenericType && typeof(IList<>) == m_ProvideHandle.Type.GetGenericTypeDefinition()) GetListResult(m_RequestOperation.allAssets); else if (string.IsNullOrEmpty(subObjectName)) GetAssetResult(m_RequestOperation.asset); else GetAssetSubObjectResult(m_RequestOperation.allAssets); } CompleteOperation(); } private void GetArrayResult(Object[] allAssets) { m_Result = ResourceManagerConfig.CreateArrayResult(m_ProvideHandle.Type, allAssets); } private void GetListResult(Object[] allAssets) { m_Result = ResourceManagerConfig.CreateListResult(m_ProvideHandle.Type, allAssets); } private void GetAssetResult(Object asset) { m_Result = (asset != null && m_ProvideHandle.Type.IsAssignableFrom(asset.GetType())) ? asset : null; } private void GetAssetSubObjectResult(Object[] allAssets) { foreach (var o in allAssets) { if (o.name == subObjectName) { if (m_ProvideHandle.Type.IsAssignableFrom(o.GetType())) { m_Result = o; break; } } } } void CompleteOperation() { #if ENABLE_ADDRESSABLE_PROFILER if (UnityEngine.Profiling.Profiler.enabled && m_Result != null && m_ProvideHandle.IsValid) Profiling.ProfilerRuntime.AddAssetOperation(m_ProvideHandle, Profiling.ContentStatus.Active); #endif Exception e = m_Result == null ? new Exception($"Unable to load asset of type {m_ProvideHandle.Type} from location {m_ProvideHandle.Location}.") : null; m_ProvideHandle.Complete(m_Result, m_Result != null, e); } public float ProgressCallback() { return m_RequestOperation != null ? m_RequestOperation.progress : 0.0f; } /// /// Extracts main and subobject keys if properly formatted /// /// The key as an object. /// The key of the main asset. This will be set to null if a sub key is not found. /// The key of the sub object. This will be set to null if not found. /// internal static bool ExtractKeyAndSubKey(object keyObj, out string mainKey, out string subKey) { var key = keyObj as string; if (key != null) { var i = key.IndexOf('['); if (i > 0) { var j = key.LastIndexOf(']'); if (j > i) { mainKey = key.Substring(0, i); subKey = key.Substring(i + 1, j - (i + 1)); return true; } } } mainKey = null; subKey = null; return false; } } public override void Provide(ProvideHandle provideHandle) { new InternalOp().Start(provideHandle); } } public class TTAssetBundleResource : IAssetBundleResource, IUpdateReceiver { public enum LoadType { None, Local, Web } AssetBundle m_AssetBundle; DownloadHandlerTTAssetBundle m_downloadHandler; AsyncOperation m_RequestOperation; internal TTWebRequestQueueOperation m_WebRequestQueueOperation; internal ProvideHandle m_ProvideHandle; internal AssetBundleRequestOptions m_Options; [NonSerialized] bool m_RequestCompletedCallbackCalled = false; int m_Retries; BundleSource m_Source = BundleSource.None; long m_BytesToDownload; long m_DownloadedBytes; bool m_Completed = false; #if UNLOAD_BUNDLE_ASYNC AssetBundleUnloadOperation m_UnloadOperation; #endif const int k_WaitForWebRequestMainThreadSleep = 1; string m_TransformedInternalId; AssetBundleRequest m_PreloadRequest; bool m_PreloadCompleted = false; ulong m_LastDownloadedByteCount = 0; float m_TimeoutTimer = 0; int m_TimeoutOverFrames = 0; private bool HasTimedOut => m_TimeoutTimer >= m_Options.Timeout && m_TimeoutOverFrames > 5; internal long BytesToDownload { get { if (m_BytesToDownload == -1) { if (m_Options != null) m_BytesToDownload = m_Options.ComputeSize(m_ProvideHandle.Location, m_ProvideHandle.ResourceManager); else m_BytesToDownload = 0; } return m_BytesToDownload; } } internal UnityWebRequest CreateWebRequest(IResourceLocation loc) { var url = m_ProvideHandle.ResourceManager.TransformInternalId(loc); return CreateWebRequest(url); } internal UnityWebRequest CreateWebRequest(string url) { string sanitizedUrl = Uri.UnescapeDataString(url); #if UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN Uri uri = new Uri(sanitizedUrl.Replace(" ", "%20")); #else Uri uri = new Uri(Uri.EscapeUriString(sanitizedUrl)); #endif if (m_Options == null) { m_Source = BundleSource.Download; #if ENABLE_ADDRESSABLE_PROFILER AddBundleToProfiler(Profiling.ContentStatus.Downloading, m_Source); #endif return TTAssetBundle.GetAssetBundle(url); } UnityWebRequest webRequest; if (!string.IsNullOrEmpty(m_Options.Hash)) { CachedAssetBundle cachedBundle = new CachedAssetBundle(m_Options.BundleName, Hash128.Parse(m_Options.Hash)); #if ENABLE_CACHING bool cached = Caching.IsVersionCached(cachedBundle); m_Source = cached ? BundleSource.Cache : BundleSource.Download; if (m_Options.UseCrcForCachedBundle || m_Source == BundleSource.Download) webRequest = UnityWebRequestAssetBundle.GetAssetBundle(uri, cachedBundle, m_Options.Crc); else webRequest = UnityWebRequestAssetBundle.GetAssetBundle(uri, cachedBundle); #else webRequest = UnityWebRequestAssetBundle.GetAssetBundle(uri, cachedBundle, m_Options.Crc); #endif } else { m_Source = BundleSource.Download; webRequest = TTAssetBundle.GetAssetBundle(url, m_Options.Crc); } if (m_Options.RedirectLimit > 0) webRequest.redirectLimit = m_Options.RedirectLimit; if (m_ProvideHandle.ResourceManager.CertificateHandlerInstance != null) { webRequest.certificateHandler = m_ProvideHandle.ResourceManager.CertificateHandlerInstance; webRequest.disposeCertificateHandlerOnDispose = false; } m_ProvideHandle.ResourceManager.WebRequestOverride?.Invoke(webRequest); return webRequest; } public AssetBundleRequest GetAssetPreloadRequest() { if (m_PreloadCompleted || GetAssetBundle() == null) return null; if (m_Options.AssetLoadMode == AssetLoadMode.AllPackedAssetsAndDependencies) { #if !UNITY_2021_1_OR_NEWER if (AsyncOperationHandle.IsWaitingForCompletion) { m_AssetBundle.LoadAllAssets(); m_PreloadCompleted = true; return null; } #endif if (m_PreloadRequest == null) { m_PreloadRequest = m_AssetBundle.LoadAllAssetsAsync(); m_PreloadRequest.completed += operation => m_PreloadCompleted = true; } return m_PreloadRequest; } return null; } float PercentComplete() { return m_RequestOperation != null ? m_RequestOperation.progress : 0.0f; } DownloadStatus GetDownloadStatus() { if (m_Options == null) return default; var status = new DownloadStatus() {TotalBytes = BytesToDownload, IsDone = PercentComplete() >= 1f}; if (BytesToDownload > 0) { if (m_WebRequestQueueOperation != null && string.IsNullOrEmpty(m_WebRequestQueueOperation.m_WebRequest.error)) m_DownloadedBytes = (long)(m_WebRequestQueueOperation.m_WebRequest.downloadedBytes); else if (m_RequestOperation != null && m_RequestOperation is UnityWebRequestAsyncOperation operation && string.IsNullOrEmpty(operation.webRequest.error)) m_DownloadedBytes = (long)operation.webRequest.downloadedBytes; } status.DownloadedBytes = m_DownloadedBytes; return status; } public AssetBundle GetAssetBundle() { if (m_AssetBundle == null) { if (m_downloadHandler != null) { m_AssetBundle = m_downloadHandler.assetBundle; m_downloadHandler.Dispose(); m_downloadHandler = null; } else if (m_RequestOperation is TTAssetBundleRequest r) { m_AssetBundle = r.assetBundle; } } return m_AssetBundle; } #if ENABLE_ADDRESSABLE_PROFILER private void AddBundleToProfiler(Profiling.ContentStatus status, BundleSource source) { if (!Profiler.enabled) return; // if (!m_ProvideHandle.IsValid) // return; if (status == Profiling.ContentStatus.Active && m_AssetBundle == null) Profiling.ProfilerRuntime.BundleReleased(m_Options.BundleName); else Profiling.ProfilerRuntime.AddBundleOperation(m_ProvideHandle, m_Options, status, source); } private void RemoveBundleFromProfiler() { if (m_Options == null) return; Profiling.ProfilerRuntime.BundleReleased(m_Options.BundleName); } #endif #if UNLOAD_BUNDLE_ASYNC void OnUnloadOperationComplete(AsyncOperation op) { m_UnloadOperation = null; BeginOperation(); } #endif #if UNLOAD_BUNDLE_ASYNC public void Start(ProvideHandle provideHandle, AssetBundleUnloadOperation unloadOp) #else public void Start(ProvideHandle provideHandle) #endif { m_Retries = 0; m_AssetBundle = null; m_RequestOperation = null; m_ProvideHandle = provideHandle; m_Options = m_ProvideHandle.Location.Data as AssetBundleRequestOptions; m_BytesToDownload = -1; m_ProvideHandle.SetProgressCallback(PercentComplete); m_ProvideHandle.SetDownloadProgressCallbacks(GetDownloadStatus); m_ProvideHandle.SetWaitForCompletionCallback(WaitForCompletionHandler); #if UNLOAD_BUNDLE_ASYNC m_UnloadOperation = unloadOp; if (m_UnloadOperation != null && !m_UnloadOperation.isDone) m_UnloadOperation.completed += OnUnloadOperationComplete; else #endif BeginOperation(); } private bool WaitForCompletionHandler() { #if UNLOAD_BUNDLE_ASYNC if (m_UnloadOperation != null && !m_UnloadOperation.isDone) { m_UnloadOperation.completed -= OnUnloadOperationComplete; m_UnloadOperation.WaitForCompletion(); m_UnloadOperation = null; BeginOperation(); } #endif if (m_RequestOperation == null) { if (m_WebRequestQueueOperation == null) return false; else WebRequestQueue.WaitForRequestToBeActive(m_WebRequestQueueOperation, k_WaitForWebRequestMainThreadSleep); } //We don't want to wait for request op to complete if it's a LoadFromFileAsync. Only UWR will complete in a tight loop like this. if (m_RequestOperation is UnityWebRequestAsyncOperation op) { while (!TTUnityWebRequestUtilities.IsAssetBundleDownloaded(op)) Thread.Sleep(k_WaitForWebRequestMainThreadSleep); #if ENABLE_ASYNC_ASSETBUNDLE_UWR if (m_Source == BundleSource.Cache) { var downloadHandler = (DownloadHandlerTTAssetBundle)op?.webRequest?.downloadHandler; if (downloadHandler.autoLoadAssetBundle) m_AssetBundle = downloadHandler.assetBundle; } #endif WebRequestQueue.DequeueRequest(op); if (!m_RequestCompletedCallbackCalled) { m_RequestOperation.completed -= WebRequestOperationCompleted; WebRequestOperationCompleted(m_RequestOperation); } } if (!m_Completed && m_Source == BundleSource.Local) { // we don't have to check for done with local files as calling // m_requestOperation.assetBundle is blocking and will wait for the file to load if (!m_RequestCompletedCallbackCalled) { m_RequestOperation.completed -= LocalRequestOperationCompleted; LocalRequestOperationCompleted(m_RequestOperation); } } if (!m_Completed && m_RequestOperation.isDone) { m_ProvideHandle.Complete(this, m_AssetBundle != null, null); m_Completed = true; } return m_Completed; } void AddCallbackInvokeIfDone(AsyncOperation operation, Action callback) { if (operation.isDone) callback(operation); else operation.completed += callback; } public static void GetLoadInfo(ProvideHandle handle, out LoadType loadType, out string path) { GetLoadInfo(handle.Location, handle.ResourceManager, out loadType, out path); } internal static void GetLoadInfo(IResourceLocation location, ResourceManager resourceManager, out LoadType loadType, out string path) { var options = location?.Data as AssetBundleRequestOptions; if (options == null) { loadType = LoadType.None; path = null; return; } path = resourceManager.TransformInternalId(location); if (Application.platform == RuntimePlatform.Android && path.StartsWith("jar:", StringComparison.Ordinal)) loadType = options.UseUnityWebRequestForLocalBundles ? LoadType.Web : LoadType.Local; else if (ResourceManagerConfig.ShouldPathUseWebRequest(path)) loadType = LoadType.Web; else if (options.UseUnityWebRequestForLocalBundles) { path = "file:///" + Path.GetFullPath(path); loadType = LoadType.Web; } else loadType = LoadType.Local; if (loadType == LoadType.Web) path = path.Replace('\\', '/'); } private void BeginOperation() { // retrying a failed request will call BeginOperation multiple times. Any member variables // should be reset at the beginning of the operation m_DownloadedBytes = 0; m_RequestCompletedCallbackCalled = false; GetLoadInfo(m_ProvideHandle, out LoadType loadType, out m_TransformedInternalId); if (loadType == LoadType.Local) { LoadLocalBundle(); return; } if (loadType == LoadType.Web) { m_WebRequestQueueOperation = EnqueueWebRequest(m_TransformedInternalId); AddBeginWebRequestHandler(m_WebRequestQueueOperation); return; } m_Source = BundleSource.None; m_RequestOperation = null; m_ProvideHandle.Complete(null, false, new RemoteProviderException(string.Format("Invalid path in AssetBundleProvider: '{0}'.", m_TransformedInternalId), m_ProvideHandle.Location)); m_Completed = true; } private void LoadLocalBundle() { m_Source = BundleSource.Local; #if !UNITY_2021_1_OR_NEWER if (AsyncOperationHandle.IsWaitingForCompletion) CompleteBundleLoad(AssetBundle.LoadFromFile(m_TransformedInternalId, m_Options == null ? 0 : m_Options.Crc)); else #endif { m_RequestOperation = AssetBundle.LoadFromFileAsync(m_TransformedInternalId, m_Options == null ? 0 : m_Options.Crc); #if ENABLE_ADDRESSABLE_PROFILER AddBundleToProfiler(Profiling.ContentStatus.Loading, m_Source); #endif AddCallbackInvokeIfDone(m_RequestOperation, LocalRequestOperationCompleted); } } internal TTWebRequestQueueOperation EnqueueWebRequest(string internalId) { var req = CreateWebRequest(internalId); #if ENABLE_ASYNC_ASSETBUNDLE_UWR ((DownloadHandlerAssetBundle)req.downloadHandler).autoLoadAssetBundle = true; #endif req.disposeDownloadHandlerOnDispose = false; return WebRequestQueue.QueueRequest(req); } internal void AddBeginWebRequestHandler(TTWebRequestQueueOperation webRequestQueueOperation) { if (webRequestQueueOperation.IsDone) { BeginWebRequestOperation(webRequestQueueOperation.Result); } else { #if ENABLE_ADDRESSABLE_PROFILER AddBundleToProfiler(Profiling.ContentStatus.Queue, m_Source); #endif webRequestQueueOperation.OnComplete += asyncOp => BeginWebRequestOperation(asyncOp); } } private void BeginWebRequestOperation(AsyncOperation asyncOp) { m_TimeoutTimer = 0; m_TimeoutOverFrames = 0; m_LastDownloadedByteCount = 0; m_RequestOperation = asyncOp; if (m_RequestOperation == null || m_RequestOperation.isDone) WebRequestOperationCompleted(m_RequestOperation); else { if (m_Options.Timeout > 0) m_ProvideHandle.ResourceManager.AddUpdateReceiver(this); #if ENABLE_ADDRESSABLE_PROFILER AddBundleToProfiler(m_Source == BundleSource.Cache ? Profiling.ContentStatus.Loading : Profiling.ContentStatus.Downloading, m_Source); #endif m_RequestOperation.completed += WebRequestOperationCompleted; } } public void Update(float unscaledDeltaTime) { if (m_RequestOperation != null && m_RequestOperation is UnityWebRequestAsyncOperation operation && !operation.isDone) { if (m_LastDownloadedByteCount != operation.webRequest.downloadedBytes) { m_TimeoutTimer = 0; m_TimeoutOverFrames = 0; m_LastDownloadedByteCount = operation.webRequest.downloadedBytes; } else { m_TimeoutTimer += unscaledDeltaTime; if (HasTimedOut) operation.webRequest.Abort(); m_TimeoutOverFrames++; } } } private void LocalRequestOperationCompleted(AsyncOperation op) { if (m_RequestCompletedCallbackCalled) { return; } m_RequestCompletedCallbackCalled = true; CompleteBundleLoad((op as TTAssetBundleRequest).assetBundle); } private void CompleteBundleLoad(AssetBundle bundle) { m_AssetBundle = bundle; #if ENABLE_ADDRESSABLE_PROFILER AddBundleToProfiler(Profiling.ContentStatus.Active, m_Source); #endif if (m_AssetBundle != null) m_ProvideHandle.Complete(this, true, null); else m_ProvideHandle.Complete(null, false, new RemoteProviderException(string.Format("Invalid path in AssetBundleProvider: '{0}'.", m_TransformedInternalId), m_ProvideHandle.Location)); m_Completed = true; } private void WebRequestOperationCompleted(AsyncOperation op) { if (m_RequestCompletedCallbackCalled) return; m_RequestCompletedCallbackCalled = true; if (m_Options.Timeout > 0) m_ProvideHandle.ResourceManager.RemoveUpdateReciever(this); UnityWebRequestAsyncOperation remoteReq = op as UnityWebRequestAsyncOperation; var webReq = remoteReq?.webRequest; var downloadHandler = webReq?.downloadHandler as DownloadHandlerTTAssetBundle; TTUnityWebRequestResult uwrResult = null; if (webReq != null && !TTUnityWebRequestUtilities.RequestHasErrors(webReq, out uwrResult)) { if (!m_Completed) { #if ENABLE_ADDRESSABLE_PROFILER AddBundleToProfiler(Profiling.ContentStatus.Active, m_Source); #endif m_AssetBundle = downloadHandler.assetBundle; downloadHandler.Dispose(); downloadHandler = null; m_ProvideHandle.Complete(this, true, null); m_Completed = true; } #if ENABLE_CACHING if (!string.IsNullOrEmpty(m_Options.Hash) && m_Options.ClearOtherCachedVersionsWhenLoaded) Caching.ClearOtherCachedVersions(m_Options.BundleName, Hash128.Parse(m_Options.Hash)); #endif } else { if (HasTimedOut) uwrResult.Error = "Request timeout"; webReq = m_WebRequestQueueOperation.m_WebRequest; if (uwrResult == null) uwrResult = new TTUnityWebRequestResult(m_WebRequestQueueOperation.m_WebRequest); downloadHandler = webReq.downloadHandler as DownloadHandlerTTAssetBundle; downloadHandler.Dispose(); downloadHandler = null; bool forcedRetry = false; string message = $"Web request failed, retrying ({m_Retries}/{m_Options.RetryCount})...\n{uwrResult}"; #if ENABLE_CACHING if (!string.IsNullOrEmpty(m_Options.Hash)) { #if ENABLE_ADDRESSABLE_PROFILER if (m_Source == BundleSource.Cache) #endif { message = $"Web request failed to load from cache. The cached AssetBundle will be cleared from the cache and re-downloaded. Retrying...\n{uwrResult}"; Caching.ClearCachedVersion(m_Options.BundleName, Hash128.Parse(m_Options.Hash)); // When attempted to load from cache we always retry on first attempt and failed if (m_Retries == 0 && uwrResult.ShouldRetryDownloadError()) { Debug.LogFormat(message); BeginOperation(); m_Retries++; //Will prevent us from entering an infinite loop of retrying if retry count is 0 forcedRetry = true; } } } #endif if (!forcedRetry) { if (m_Retries < m_Options.RetryCount && uwrResult.ShouldRetryDownloadError()) { m_Retries++; Debug.LogFormat(message); BeginOperation(); } else { var exception = new TTRemoteProviderException($"Unable to load asset bundle from : {webReq.url}", m_ProvideHandle.Location, uwrResult); m_ProvideHandle.Complete(null, false, exception); m_Completed = true; #if ENABLE_ADDRESSABLE_PROFILER RemoveBundleFromProfiler(); #endif } } } webReq.Dispose(); } #if UNLOAD_BUNDLE_ASYNC public bool Unload(out AssetBundleUnloadOperation unloadOp) #else public void Unload() #endif { #if UNLOAD_BUNDLE_ASYNC unloadOp = null; if (m_AssetBundle != null) { unloadOp = m_AssetBundle.UnloadAsync(true); m_AssetBundle = null; } #else if (m_AssetBundle != null) { m_AssetBundle.TTUnload(true); m_AssetBundle = null; } #endif m_RequestOperation = null; #if ENABLE_ADDRESSABLE_PROFILER RemoveBundleFromProfiler(); #endif #if UNLOAD_BUNDLE_ASYNC return unloadOp != null; #endif } } [DisplayName("TTAssetBundle Provider")] public class TTAssetBundleProvider : ResourceProviderBase { #if UNLOAD_BUNDLE_ASYNC internal static Dictionary m_UnloadingBundles = new Dictionary(); [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void Init() { m_UnloadingBundles = new Dictionary(); } protected internal static Dictionary UnloadingBundles { get { return m_UnloadingBundles; } internal set { m_UnloadingBundles = value; } } internal static int UnloadingAssetBundleCount => m_UnloadingBundles.Count; internal static int AssetBundleCount => AssetBundle.GetAllLoadedAssetBundles().Count() - UnloadingAssetBundleCount; internal static void WaitForAllUnloadingBundlesToComplete() { if (UnloadingAssetBundleCount > 0) { var bundles = m_UnloadingBundles.Values.ToArray(); foreach (var b in bundles) b.WaitForCompletion(); } } #else internal static void WaitForAllUnloadingBundlesToComplete() { } #endif public override void Provide(ProvideHandle providerInterface) { #if UNLOAD_BUNDLE_ASYNC if (m_UnloadingBundles.TryGetValue(providerInterface.Location.InternalId, out var unloadOp)) { if (unloadOp.isDone) unloadOp = null; } new TTAssetBundleResource().Start(providerInterface, unloadOp); #else new TTAssetBundleResource().Start(providerInterface); #endif } public override Type GetDefaultType(IResourceLocation location) { return typeof(IAssetBundleResource); } public override void Release(IResourceLocation location, object asset) { if (location == null) throw new ArgumentNullException("location"); if (asset == null) { Debug.LogWarningFormat("Releasing null asset bundle from location {0}. This is an indication that the bundle failed to load.", location); return; } var bundle = asset as TTAssetBundleResource; if (bundle != null) { #if UNLOAD_BUNDLE_ASYNC if (bundle.Unload(out var unloadOp)) { m_UnloadingBundles.Add(location.InternalId, unloadOp); unloadOp.completed += op => m_UnloadingBundles.Remove(location.InternalId); } #else bundle.Unload(); #endif return; } } } } namespace UnityEngine.ResourceManagement.Exceptions { /// /// Class representing an error occured during an operation that remotely fetch data. /// public class TTRemoteProviderException : ProviderException { /// /// Creates a new instance of . /// /// A message describing the error. /// The resource location that the operation was trying to provide. /// The result of the unity web request, if any. /// The exception that caused the error, if any. public TTRemoteProviderException(string message, IResourceLocation location = null, TTUnityWebRequestResult uwrResult = null, Exception innerException = null) : base(message, location, innerException) { WebRequestResult = uwrResult; } /// public override string Message => this.ToString(); /// /// The result of the unity web request, if any. /// public TTUnityWebRequestResult WebRequestResult { get; } /// Provides a new string object describing the exception. /// A newly allocated managed string. public override string ToString() { if (WebRequestResult != null) return $"{GetType().Name} : {base.Message}\nUnityWebRequest result : {WebRequestResult}\n{InnerException}"; else return base.ToString(); } } } namespace UnityEngine.ResourceManagement.Util { internal class TTUnityWebRequestUtilities { public static bool RequestHasErrors(UnityWebRequest webReq, out TTUnityWebRequestResult result) { result = null; if (webReq == null || !webReq.isDone) return false; #if UNITY_2020_1_OR_NEWER switch (webReq.result) { case UnityWebRequest.Result.InProgress: case UnityWebRequest.Result.Success: return false; case UnityWebRequest.Result.ConnectionError: case UnityWebRequest.Result.ProtocolError: case UnityWebRequest.Result.DataProcessingError: result = new TTUnityWebRequestResult(webReq); return true; default: throw new NotImplementedException($"Cannot determine whether UnityWebRequest succeeded or not from result : {webReq.result}"); } #else var isError = webReq.isHttpError || webReq.isNetworkError; if (isError) result = new UnityWebRequestResult(webReq); return isError; #endif } internal static bool IsAssetBundleDownloaded(UnityWebRequestAsyncOperation op) { #if ENABLE_ASYNC_ASSETBUNDLE_UWR var handler = (DownloadHandlerAssetBundle)op.webRequest.downloadHandler; if (handler != null && handler.autoLoadAssetBundle) return handler.isDownloadComplete; #endif return op.isDone; } } /// /// Container class for the result of a unity web request. /// public class TTUnityWebRequestResult { /// /// Creates a new instance of . /// /// The unity web request. public TTUnityWebRequestResult(UnityWebRequest request) { string error = request.error; #if UNITY_2020_1_OR_NEWER if (request.result == UnityWebRequest.Result.DataProcessingError && request.downloadHandler != null) { // https://docs.unity3d.com/ScriptReference/Networking.DownloadHandler-error.html // When a UnityWebRequest ends with the result, UnityWebRequest.Result.DataProcessingError, the message describing the error is in the download handler error = $"{error} : {request.downloadHandler.error}"; } Result = request.result; #endif Error = error; ResponseCode = request.responseCode; Method = request.method; Url = request.url; } /// Provides a new string object describing the result. /// A newly allocated managed string. public override string ToString() { var sb = new System.Text.StringBuilder(); #if UNITY_2020_1_OR_NEWER sb.AppendLine($"{Result} : {Error}"); #else if (!string.IsNullOrEmpty(Error)) sb.AppendLine(Error); #endif if (ResponseCode > 0) sb.AppendLine($"ResponseCode : {ResponseCode}, Method : {Method}"); sb.AppendLine($"url : {Url}"); return sb.ToString(); } /// /// A string explaining the error that occured. /// public string Error { get; internal set; } /// /// The numeric HTTP response code returned by the server, if any. /// See documentation for more details. /// public long ResponseCode { get; } #if UNITY_2020_1_OR_NEWER /// /// The outcome of the request. /// public UnityWebRequest.Result Result { get; } #endif /// /// The HTTP verb used by this UnityWebRequest, such as GET or POST. /// public string Method { get; } /// /// The target url of the request. /// public string Url { get; } internal bool ShouldRetryDownloadError() { if (string.IsNullOrEmpty(Error)) return true; if (Error == "Request aborted" || Error == "Unable to write data" || Error == "Malformed URL" || Error == "Out of memory" || Error == "Encountered invalid redirect (missing Location header?)" || Error == "Cannot modify request at this time" || Error == "Unsupported Protocol" || Error == "Destination host has an erroneous SSL certificate" || Error == "Unable to load SSL Cipher for verification" || Error == "SSL CA certificate error" || Error == "Unrecognized content-encoding" || Error == "Request already transmitted" || Error == "Invalid HTTP Method" || Error == "Header name contains invalid characters" || Error == "Header value contains invalid characters" || Error == "Cannot override system-specified headers" ) return false; /* Errors that can be retried: "Unknown Error": "No Internet Connection" "Backend Initialization Error": "Cannot resolve proxy": "Cannot resolve destination host": "Cannot connect to destination host": "Access denied": "Generic/unknown HTTP error": "Unable to read data": "Request timeout": "Error during HTTP POST transmission": "Unable to complete SSL connection": "Redirect limit exceeded": "Received no data in response": "Destination host does not support SSL": "Failed to transmit data": "Failed to receive data": "Login failed": "SSL shutdown failed": "Redirect limit is invalid": "Not implemented": "Data Processing Error, see Download Handler error": */ return true; } } }