using System; using System.Collections.Generic; using System.ComponentModel; using System.IO; using UnityEngine.AddressableAssets.ResourceLocators; using UnityEngine.Networking; using UnityEngine.ResourceManagement; using UnityEngine.ResourceManagement.AsyncOperations; using UnityEngine.ResourceManagement.ResourceLocations; using UnityEngine.ResourceManagement.ResourceProviders; using UnityEngine.ResourceManagement.Util; namespace UnityEngine.AddressableAssets.ResourceProviders { /// /// Provider for content catalogs. This provider makes use of a hash file to determine if a newer version of the catalog needs to be downloaded. /// [DisplayName("Content Catalog Provider")] public class ContentCatalogProvider : ResourceProviderBase { /// /// Options for specifying which entry in the catalog dependencies should hold each hash item. /// The Remote should point to the hash on the server. The Cache should point to the /// local cache copy of the remote data. /// public enum DependencyHashIndex { /// /// Use to represent the index of the remote entry in the dependencies list. /// Remote = 0, /// /// Use to represent the index of the cache entry in the dependencies list. /// Cache, /// /// Use to represent the number of entries in the dependencies list. /// Count } /// /// Use to indicate if the updating the catalog on startup should be disabled. /// public bool DisableCatalogUpdateOnStart = false; /// /// Use to indicate if the local catalog is in a bundle. /// public bool IsLocalCatalogInBundle = false; internal Dictionary m_LocationToCatalogLoadOpMap = new Dictionary(); ResourceManager m_RM; /// /// Constructor for this provider. /// /// The resource manager to use. public ContentCatalogProvider(ResourceManager resourceManagerInstance) { m_RM = resourceManagerInstance; m_BehaviourFlags = ProviderBehaviourFlags.CanProvideWithFailedDependencies; } /// public override void Release(IResourceLocation location, object obj) { if (m_LocationToCatalogLoadOpMap.ContainsKey(location)) { m_LocationToCatalogLoadOpMap[location].Release(); m_LocationToCatalogLoadOpMap.Remove(location); } base.Release(location, obj); } internal class InternalOp { // int m_StartFrame; string m_LocalDataPath; string m_RemoteHashValue; internal string m_LocalHashValue; ProvideHandle m_ProviderInterface; internal ContentCatalogData m_ContentCatalogData; AsyncOperationHandle m_ContentCatalogDataLoadOp; private BundledCatalog m_BundledCatalog; private bool m_Retried; private bool m_DisableCatalogUpdateOnStart; private bool m_IsLocalCatalogInBundle; public void Start(ProvideHandle providerInterface, bool disableCatalogUpdateOnStart, bool isLocalCatalogInBundle) { m_ProviderInterface = providerInterface; m_DisableCatalogUpdateOnStart = disableCatalogUpdateOnStart; m_IsLocalCatalogInBundle = isLocalCatalogInBundle; m_ProviderInterface.SetWaitForCompletionCallback(WaitForCompletionCallback); m_LocalDataPath = null; m_RemoteHashValue = null; List deps = new List(); // TODO: garbage. need to pass actual count and reuse the list m_ProviderInterface.GetDependencies(deps); string idToLoad = DetermineIdToLoad(m_ProviderInterface.Location, deps, disableCatalogUpdateOnStart); Addressables.LogFormat("Addressables - Using content catalog from {0}.", idToLoad); bool loadCatalogFromLocalBundle = isLocalCatalogInBundle && CanLoadCatalogFromBundle(idToLoad, m_ProviderInterface.Location); LoadCatalog(idToLoad, loadCatalogFromLocalBundle); } bool WaitForCompletionCallback() { if (m_ContentCatalogData != null) return true; bool ccComplete; if (m_BundledCatalog != null) { ccComplete = m_BundledCatalog.WaitForCompletion(); } else { ccComplete = m_ContentCatalogDataLoadOp.IsDone; if (!ccComplete) m_ContentCatalogDataLoadOp.WaitForCompletion(); } //content catalog op needs the Update to be pumped so we can invoke completion callbacks if (ccComplete && m_ContentCatalogData == null) m_ProviderInterface.ResourceManager.Update(Time.unscaledDeltaTime); return ccComplete; } /// /// Clear all content catalog data. /// public void Release() { m_ContentCatalogData?.CleanData(); } internal bool CanLoadCatalogFromBundle(string idToLoad, IResourceLocation location) { return Path.GetExtension(idToLoad) == ".bundle" && idToLoad.Equals(GetTransformedInternalId(location)); } internal void LoadCatalog(string idToLoad, bool loadCatalogFromLocalBundle) { try { ProviderLoadRequestOptions providerLoadRequestOptions = null; if (m_ProviderInterface.Location.Data is ProviderLoadRequestOptions providerData) providerLoadRequestOptions = providerData.Copy(); if (loadCatalogFromLocalBundle) { int webRequestTimeout = providerLoadRequestOptions?.WebRequestTimeout ?? 0; m_BundledCatalog = new BundledCatalog(idToLoad, webRequestTimeout); m_BundledCatalog.OnLoaded += ccd => { m_ContentCatalogData = ccd; OnCatalogLoaded(ccd); }; m_BundledCatalog.LoadCatalogFromBundleAsync(); } else { #if ENABLE_BINARY_CATALOG ResourceLocationBase location = new ResourceLocationBase(idToLoad, idToLoad, typeof(BinaryAssetProvider).FullName, typeof(ContentCatalogData)); location.Data = providerLoadRequestOptions; m_ProviderInterface.ResourceManager.ResourceProviders.Add(new BinaryAssetProvider()); #else ResourceLocationBase location = new ResourceLocationBase(idToLoad, idToLoad, typeof(JsonAssetProvider).FullName, typeof(ContentCatalogData)); #endif m_ContentCatalogDataLoadOp = m_ProviderInterface.ResourceManager.ProvideResource(location); m_ContentCatalogDataLoadOp.Completed += CatalogLoadOpCompleteCallback; } } catch (Exception ex) { m_ProviderInterface.Complete(null, false, ex); } } void CatalogLoadOpCompleteCallback(AsyncOperationHandle op) { m_ContentCatalogData = op.Result; m_ProviderInterface.ResourceManager.Release(op); OnCatalogLoaded(m_ContentCatalogData); } internal class BundledCatalog { private readonly string m_BundlePath; private bool m_OpInProgress; private AssetBundleCreateRequest m_LoadBundleRequest; internal AssetBundle m_CatalogAssetBundle; private AssetBundleRequest m_LoadTextAssetRequest; private ContentCatalogData m_CatalogData; private WebRequestQueueOperation m_WebRequestQueueOperation; private AsyncOperation m_RequestOperation; private int m_WebRequestTimeout; public event Action OnLoaded; public bool OpInProgress => m_OpInProgress; public bool OpIsSuccess => !m_OpInProgress && m_CatalogData != null; public BundledCatalog(string bundlePath, int webRequestTimeout = 0) { if (string.IsNullOrEmpty(bundlePath)) { throw new ArgumentNullException(nameof(bundlePath), "Catalog bundle path is null."); } else if (!bundlePath.EndsWith(".bundle", StringComparison.OrdinalIgnoreCase)) { throw new ArgumentException("You must supply a valid bundle file path."); } m_BundlePath = bundlePath; m_WebRequestTimeout = webRequestTimeout; } ~BundledCatalog() { Unload(); } private void Unload() { m_CatalogAssetBundle?.Unload(true); m_CatalogAssetBundle = null; } public void LoadCatalogFromBundleAsync() { //Debug.Log($"LoadCatalogFromBundleAsync frame : {Time.frameCount}"); if (m_OpInProgress) { Addressables.LogError($"Operation in progress : A catalog is already being loaded. Please wait for the operation to complete."); return; } m_OpInProgress = true; if (ResourceManagerConfig.ShouldPathUseWebRequest(m_BundlePath)) { var req = UnityWebRequestAssetBundle.GetAssetBundle(m_BundlePath); if (m_WebRequestTimeout > 0) req.timeout = m_WebRequestTimeout; m_WebRequestQueueOperation = WebRequestQueue.QueueRequest(req); if (m_WebRequestQueueOperation.IsDone) { m_RequestOperation = m_WebRequestQueueOperation.Result; if (m_RequestOperation.isDone) WebRequestOperationCompleted(m_RequestOperation); else m_RequestOperation.completed += WebRequestOperationCompleted; } else { m_WebRequestQueueOperation.OnComplete += asyncOp => { m_RequestOperation = asyncOp; m_RequestOperation.completed += WebRequestOperationCompleted; }; } } else { m_LoadBundleRequest = AssetBundle.LoadFromFileAsync(m_BundlePath); m_LoadBundleRequest.completed += loadOp => { if (loadOp is AssetBundleCreateRequest createRequest && createRequest.assetBundle != null) { m_CatalogAssetBundle = createRequest.assetBundle; m_LoadTextAssetRequest = m_CatalogAssetBundle.LoadAllAssetsAsync(); if (m_LoadTextAssetRequest.isDone) LoadTextAssetRequestComplete(m_LoadTextAssetRequest); m_LoadTextAssetRequest.completed += LoadTextAssetRequestComplete; } else { Addressables.LogError($"Unable to load dependent bundle from location : {m_BundlePath}"); m_OpInProgress = false; } }; } } private void WebRequestOperationCompleted(AsyncOperation op) { UnityWebRequestAsyncOperation remoteReq = op as UnityWebRequestAsyncOperation; var webReq = remoteReq.webRequest; DownloadHandlerAssetBundle downloadHandler = webReq.downloadHandler as DownloadHandlerAssetBundle; if (!UnityWebRequestUtilities.RequestHasErrors(webReq, out UnityWebRequestResult uwrResult)) { m_CatalogAssetBundle = downloadHandler.assetBundle; m_LoadTextAssetRequest = m_CatalogAssetBundle.LoadAllAssetsAsync(); if (m_LoadTextAssetRequest.isDone) LoadTextAssetRequestComplete(m_LoadTextAssetRequest); m_LoadTextAssetRequest.completed += LoadTextAssetRequestComplete; } else { Addressables.LogError($"Unable to load dependent bundle from location : {m_BundlePath}"); m_OpInProgress = false; } webReq.Dispose(); } void LoadTextAssetRequestComplete(AsyncOperation op) { if (op is AssetBundleRequest loadRequest && loadRequest.asset is TextAsset textAsset && textAsset.text != null) { m_CatalogData = JsonUtility.FromJson(textAsset.text); OnLoaded?.Invoke(m_CatalogData); } else { Addressables.LogError($"No catalog text assets where found in bundle {m_BundlePath}"); } Unload(); m_OpInProgress = false; } public bool WaitForCompletion() { if (m_LoadBundleRequest.assetBundle == null) return false; return m_LoadTextAssetRequest.asset != null || m_LoadTextAssetRequest.allAssets != null; } } string GetTransformedInternalId(IResourceLocation loc) { if (m_ProviderInterface.ResourceManager == null) return loc.InternalId; return m_ProviderInterface.ResourceManager.TransformInternalId(loc); } const string kCatalogExt = #if ENABLE_BINARY_CATALOG ".bin"; #else ".json"; #endif internal string DetermineIdToLoad(IResourceLocation location, IList dependencyObjects, bool disableCatalogUpdateOnStart = false) { //default to load actual local source catalog string idToLoad = GetTransformedInternalId(location); if (dependencyObjects != null && location.Dependencies != null && dependencyObjects.Count == (int)DependencyHashIndex.Count && location.Dependencies.Count == (int)DependencyHashIndex.Count) { var remoteHash = dependencyObjects[(int)DependencyHashIndex.Remote] as string; m_LocalHashValue = dependencyObjects[(int)DependencyHashIndex.Cache] as string; Addressables.LogFormat("Addressables - ContentCatalogProvider CachedHash = {0}, RemoteHash = {1}.", m_LocalHashValue, remoteHash); if (string.IsNullOrEmpty(remoteHash) || disableCatalogUpdateOnStart) //offline { #if ENABLE_CACHING if (!string.IsNullOrEmpty(m_LocalHashValue) && !m_Retried && !string.IsNullOrEmpty(Application.persistentDataPath)) //cache exists and not forcing a retry state { idToLoad = GetTransformedInternalId(location.Dependencies[(int)DependencyHashIndex.Cache]).Replace(".hash", kCatalogExt); } else { m_LocalHashValue = Hash128.Compute(idToLoad).ToString(); } #else m_LocalHashValue = Hash128.Compute(idToLoad).ToString(); #endif } else //online { if (remoteHash == m_LocalHashValue && !m_Retried) //cache of remote is good and not forcing a retry state { idToLoad = GetTransformedInternalId(location.Dependencies[(int)DependencyHashIndex.Cache]).Replace(".hash", kCatalogExt); } else //remote is different than cache, or no cache { idToLoad = GetTransformedInternalId(location.Dependencies[(int)DependencyHashIndex.Remote]).Replace(".hash", kCatalogExt); m_RemoteHashValue = remoteHash; #if ENABLE_CACHING if (!string.IsNullOrEmpty(Application.persistentDataPath)) m_LocalDataPath = GetTransformedInternalId(location.Dependencies[(int)DependencyHashIndex.Cache]).Replace(".hash", kCatalogExt); #endif } } } return idToLoad; } private void OnCatalogLoaded(ContentCatalogData ccd) { Addressables.LogFormat("Addressables - Content catalog load result = {0}.", ccd); if (ccd != null) { #if ENABLE_ADDRESSABLE_PROFILER ResourceManagement.Profiling.ProfilerRuntime.AddCatalog(Hash128.Parse(ccd.m_BuildResultHash)); #endif ccd.location = m_ProviderInterface.Location; ccd.localHash = m_LocalHashValue; if (!string.IsNullOrEmpty(m_RemoteHashValue) && !string.IsNullOrEmpty(m_LocalDataPath)) { #if ENABLE_CACHING var dir = Path.GetDirectoryName(m_LocalDataPath); var localCachePath = m_LocalDataPath; Addressables.LogFormat("Addressables - Saving cached content catalog to {0}.", localCachePath); try { if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) Directory.CreateDirectory(dir); File.WriteAllText(localCachePath, JsonUtility.ToJson(ccd)); File.WriteAllText(localCachePath.Replace(kCatalogExt, ".hash"), m_RemoteHashValue); } catch (UnauthorizedAccessException uae) { Addressables.LogWarning($"Did not save cached content catalog. Missing access permissions for location {localCachePath} : {uae.Message}"); m_ProviderInterface.Complete(ccd, true, null); return; } catch (Exception e) { string remoteInternalId = GetTransformedInternalId(m_ProviderInterface.Location.Dependencies[(int)DependencyHashIndex.Remote]); var errorMessage = $"Unable to load ContentCatalogData from location {remoteInternalId}. Failed to cache catalog to location {localCachePath}."; ccd = null; m_ProviderInterface.Complete(ccd, false, new Exception(errorMessage, e)); return; } #endif ccd.localHash = m_RemoteHashValue; } #if ENABLE_CACHING else if (string.IsNullOrEmpty(m_LocalDataPath) && string.IsNullOrEmpty(Application.persistentDataPath)) { Addressables.LogWarning($"Did not save cached content catalog because Application.persistentDataPath is an empty path."); } #endif m_ProviderInterface.Complete(ccd, true, null); } else { var errorMessage = $"Unable to load ContentCatalogData from location {m_ProviderInterface.Location}"; if (!m_Retried) { m_Retried = true; //if the prev load path is cache, try to remove cache and reload from remote var cachePath = GetTransformedInternalId(m_ProviderInterface.Location.Dependencies[(int)DependencyHashIndex.Cache]); if (m_ContentCatalogDataLoadOp.LocationName == cachePath.Replace(".hash", kCatalogExt)) { try { #if ENABLE_CACHING File.Delete(cachePath); #endif } catch (Exception) { errorMessage += $". Unable to delete cache data from location {cachePath}"; m_ProviderInterface.Complete(ccd, false, new Exception(errorMessage)); return; } } Addressables.LogWarning(errorMessage + ". Attempting to retry..."); Start(m_ProviderInterface, m_DisableCatalogUpdateOnStart, m_IsLocalCatalogInBundle); } else { m_ProviderInterface.Complete(ccd, false, new Exception(errorMessage + " on second attempt.")); } } } } /// public override void Provide(ProvideHandle providerInterface) { if (!m_LocationToCatalogLoadOpMap.ContainsKey(providerInterface.Location)) m_LocationToCatalogLoadOpMap.Add(providerInterface.Location, new InternalOp()); m_LocationToCatalogLoadOpMap[providerInterface.Location].Start(providerInterface, DisableCatalogUpdateOnStart, IsLocalCatalogInBundle); } } }