/*=============================================================================== Copyright (C) 2020 PhantomsXR Ltd. All Rights Reserved. This file is part of the AR-MOD SDK. The AR-MOD SDK cannot be copied, distributed, or made available to third-parties for commercial purposes without written permission of PhantomsXR Ltd. Contact info@phantomsxr.com for licensing requests. ===============================================================================*/ using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading.Tasks; using UnityEngine; using UnityEngine.Networking; using Cysharp.Threading.Tasks; using Phantom.XRMOD.XRMODUtilites.Runtime; using Debug = UnityEngine.Debug; using Object = UnityEngine.Object; using Progress = Cysharp.Threading.Tasks.Progress; #if UNITY_EDITOR using UnityEditor; using UnityEngine.Assertions; #endif namespace Phantom.XRMOD.XRMODPackageTools.Runtime { /// /// Provides utility methods for loading and managing asset bundles and assets within the XRMOD framework. /// Handles both local file loading and remote URL loading with caching mechanisms. /// public static class BasePackageLoaderUtility { /// /// A collection of all loaded AssetBundles, mapped by their file names. /// private static readonly IDictionary AssetBundleCollection = new Dictionary(); private static string[] _PACKAGE_NAMES = new[] { ".arexperience", ".xrexperience", ".spatial", ".package", }; /// /// Determines whether assets should be loaded from remote sources (true) or local editor paths (false). /// This flag is primarily used for development and testing in the Unity Editor. /// public static bool LoadFromRemote = false; /// /// Retrieves a list of file names that should be filtered during asset loading, /// typically for assets that do not need to be loaded from temporary asset folders. /// /// The name of the project to get filters for. /// An array of file name strings to filter. private static string[] GetLoadFileFilters(string _projectName) { return new[] { "ARProperty", $"{_projectName}.runtime.dll".ToLower(), $"{_projectName}.runtime.pdb".ToLower(), "LocalizationTable.csv" }; } /// /// Asynchronously loads a specific asset from a local asset bundle file. /// /// The type of the asset to load (must inherit from UnityEngine.Object). /// The full path to the asset bundle file. /// The name of the asset to load from the bundle. /// Callback invoked upon successful asset loading, providing the loaded asset. /// Callback invoked if asset loading fails, providing an error message. public static void LoadAssetFromFileAsync(string _pathWithFile, string _wannaLoadAssetsName, Action _loadFinished, Action _loadFailed) where T : Object { #if UNITY_EDITOR var tmp_BuildTimer = new Stopwatch(); tmp_BuildTimer.Start(); #endif try { if (string.IsNullOrEmpty(_pathWithFile)) { _loadFailed?.Invoke($"Load asset failed at {_pathWithFile}."); throw new Exception($"Load asset failed at {_pathWithFile}."); } string tmp_BundleName = Path.GetFileName(_pathWithFile); // If the asset bundle is already loaded, reload the asset from the existing bundle. if (AssetBundleCollection.TryGetValue(tmp_BundleName, out AssetBundle tmp_LoadedAssetBundle)) { var tmp_ReloadAsset = tmp_LoadedAssetBundle.LoadAssetAsync(_wannaLoadAssetsName); tmp_ReloadAsset.completed += _asyncOperation => { if (tmp_ReloadAsset.asset == null) throw new Exception($"Load asset failed at {_pathWithFile}."); _loadFinished?.Invoke(tmp_ReloadAsset.asset as T); #if UNITY_EDITOR tmp_BuildTimer.Stop(); Debug.Log($"ReLoad:{tmp_BuildTimer.ElapsedMilliseconds} ms"); #endif }; return; } // Load the asset bundle from file and then load the asset. var tmp_AssetBundleCreateRequest = AssetBundle.LoadFromFileAsync(_pathWithFile); tmp_AssetBundleCreateRequest.completed += _operation => { var tmp_AssetBundle = tmp_AssetBundleCreateRequest.assetBundle; var tmp_LoadedAsset = tmp_AssetBundle.LoadAssetAsync(_wannaLoadAssetsName); tmp_LoadedAsset.completed += _asyncOperation => { if (tmp_LoadedAsset.asset == null) throw new Exception($"Load asset failed at {_pathWithFile}."); AddAssetBundleToGlobalManager(tmp_AssetBundle.name, tmp_AssetBundle); _loadFinished?.Invoke(tmp_LoadedAsset.asset as T); #if UNITY_EDITOR tmp_BuildTimer.Stop(); Debug.Log($"ReLoad:{tmp_BuildTimer.ElapsedMilliseconds} ms"); #endif }; }; } catch (Exception tmp_Exception) { _loadFailed?.Invoke(tmp_Exception.Message); throw new Exception($"Load asset failed at {_pathWithFile}.", tmp_Exception); } } /// /// Asynchronously loads a specific asset from an asset bundle downloaded from a URL. /// /// The type of the asset to load (must inherit from UnityEngine.Object). /// The URI of the asset bundle to download. /// The name of the asset to load from the bundle. /// A version hash for caching. If it doesn't match, the bundle will be re-downloaded. /// A checksum for data integrity. If zero, CRC checking is skipped. /// Callback invoked upon successful asset loading, providing the loaded asset. /// Callback invoked if asset loading fails, providing an error message. /// Callback invoked to report download progress (0.0 to 1.0). [Obsolete("This method will be removed in future updates. Use LoadBundleFromUrl(Async) instead.")] public static IEnumerator LoadGameObjectFromUrl(Uri _uri, string _wannaLoadAssetsName, Hash128 _hash, uint _crc, Action _loadFinished, Action _loadFailed, Action _downloadProgressCallback) where T : Object { #if UNITY_EDITOR var tmp_BuildTimer = new Stopwatch(); tmp_BuildTimer.Start(); #endif var tmp_FileName = Path.GetFileName(_uri.AbsoluteUri).Split("?")[0]; var tmp_CachedConfigure = new CachedAssetBundle {hash = _hash}; // If the asset bundle is already loaded, reload the asset from the existing bundle. if (AssetBundleCollection.TryGetValue(tmp_FileName, out AssetBundle tmp_LoadedAssetBundle)) { var tmp_ReloadAsset = tmp_LoadedAssetBundle.LoadAssetAsync(_wannaLoadAssetsName); tmp_ReloadAsset.completed += _asyncOperation => { if (tmp_ReloadAsset.asset == null) throw new Exception($"Load asset failed at {_uri.AbsoluteUri}."); _loadFinished?.Invoke(tmp_ReloadAsset.asset as T); #if UNITY_EDITOR tmp_BuildTimer.Stop(); Debug.Log($"Re Load:{tmp_BuildTimer.ElapsedMilliseconds} ms"); #endif }; yield return null; } // Download the asset bundle from the URL. var tmp_WebRequestAssetBundle = UnityWebRequestAssetBundle.GetAssetBundle(_uri, tmp_CachedConfigure, _crc); yield return tmp_WebRequestAssetBundle.SendWebRequest(); switch (tmp_WebRequestAssetBundle.result) { case UnityWebRequest.Result.ConnectionError: case UnityWebRequest.Result.ProtocolError: case UnityWebRequest.Result.DataProcessingError: Debug.LogError(tmp_WebRequestAssetBundle.error); _loadFailed?.Invoke(tmp_WebRequestAssetBundle.error); tmp_WebRequestAssetBundle.Dispose(); yield break; } while (!tmp_WebRequestAssetBundle.isDone) { _downloadProgressCallback?.Invoke(tmp_WebRequestAssetBundle.downloadProgress); yield return null; } try { AssetBundle tmp_AssetBundle = DownloadHandlerAssetBundle.GetContent(tmp_WebRequestAssetBundle); var tmp_LoadedAsset = tmp_AssetBundle.LoadAssetAsync(_wannaLoadAssetsName); tmp_LoadedAsset.completed += _asyncOperation => { if (tmp_LoadedAsset.asset == null) throw new Exception($"Load asset failed at {_uri.AbsoluteUri}."); AddAssetBundleToGlobalManager(tmp_AssetBundle.name, tmp_AssetBundle); _loadFinished?.Invoke(tmp_LoadedAsset.asset as T); #if UNITY_EDITOR tmp_BuildTimer.Stop(); Debug.Log($"First Load:{tmp_BuildTimer.ElapsedMilliseconds} ms"); Utility.FixEditorAndroidShaders(); #endif }; } catch (Exception tmp_Exception) { Debug.LogError(tmp_Exception.Message); throw; } } /// /// Loads a specific asset from a package by project name. /// /// The type of the asset to load (must inherit from UnityEngine.Object). /// The name of the project (which corresponds to the asset bundle name). /// The name of the asset to load. /// Callback invoked upon successful asset loading, providing the loaded asset. [Obsolete("This method will be removed in future updates. Use LoadAssetFromPackage(Async) instead.")] public static void LoadAssetFromPackage(string _projectName, string _wannaLoadedAssetName, Action _loadedCallback) where T : Object { try { #if UNITY_EDITOR if (!LoadFromRemote) { string tmp_Suffix = Utility.TypeMapping(); var tmp_FullPath = Utility.SearchFolderByName(_projectName); string tmp_FilePath = string.Empty; if (!string.IsNullOrEmpty(tmp_FullPath)) tmp_FilePath = Utility.SearchFileByName(tmp_FullPath, _wannaLoadedAssetName, tmp_Suffix); if (string.IsNullOrEmpty(tmp_FilePath) || !File.Exists(tmp_FullPath)) { var tmp_SearchResultGuids = AssetDatabase.FindAssets($"t:{tmp_Suffix}", new[] {tmp_FullPath}); foreach (string tmp_Guid in tmp_SearchResultGuids) { string tmp_Path = AssetDatabase.GUIDToAssetPath(tmp_Guid); if (Path.GetFileNameWithoutExtension(tmp_Path) == _wannaLoadedAssetName && File.Exists(Path.Combine(Application.dataPath.Replace("Assets", ""), tmp_Path))) { tmp_FilePath = tmp_Path; break; } } } if (!string.IsNullOrEmpty(tmp_FilePath)) { var tmp_ShortenPath = Utility.ShortenPath(tmp_FilePath); var tmp_Asset = AssetDatabase.LoadAssetAtPath(tmp_ShortenPath); if (tmp_Asset == null) throw new Exception($"Load {_wannaLoadedAssetName} asset failed."); _loadedCallback?.Invoke(tmp_Asset); } else { _projectName += ".arexperience"; if (!AssetBundleCollection.TryGetValue(_projectName.ToLower(), out AssetBundle tmp_MatchedAssetBundle)) { throw new NullReferenceException( $"Can not load {_projectName} project's {_wannaLoadedAssetName} asset."); } var tmp_LoadedOperation = tmp_MatchedAssetBundle.LoadAssetAsync(_wannaLoadedAssetName); tmp_LoadedOperation.completed += _operation => { if (tmp_LoadedOperation.asset == null) throw new Exception($"Load {_wannaLoadedAssetName} asset failed."); Utility.FixEditorAndroidShaders(); _loadedCallback?.Invoke(tmp_LoadedOperation.asset as T); }; } return; } #endif { _projectName += ".arexperience"; if (!AssetBundleCollection.TryGetValue(_projectName.ToLower(), out AssetBundle tmp_MatchedAssetBundle)) { throw new NullReferenceException( $"Can not load {_projectName} project's {_wannaLoadedAssetName} asset."); } var tmp_LoadedOperation = tmp_MatchedAssetBundle.LoadAssetAsync(_wannaLoadedAssetName); tmp_LoadedOperation.completed += _operation => { if (tmp_LoadedOperation.asset == null) throw new Exception($"Load {_wannaLoadedAssetName} asset failed."); _loadedCallback?.Invoke(tmp_LoadedOperation.asset as T); }; } } catch (Exception e) { Debug.LogError(e); throw; } } /// /// Asynchronously loads a specific asset from a package by project name. /// In editor, it attempts to load directly from project assets first, then falls back to asset bundles. /// /// The type of the asset to load (must inherit from UnityEngine.Object). /// The name of the project (which corresponds to the asset bundle name). /// The name of the asset to load. /// A Task representing the asynchronous operation, yielding the loaded asset. /// Thrown if the asset bundle cannot be found. /// Thrown if the asset fails to load. public static async Task LoadAssetFromPackage(string _projectName, string _wannaLoadedAssetName) where T : Object { try { #if UNITY_EDITOR if (!LoadFromRemote) { // Attempt to load from Unity Editor project assets first. try { string tmp_Suffix = Utility.TypeMapping(); var tmp_FullPath = Utility.SearchFolderByName(_projectName); string tmp_FilePath = string.Empty; // Filter assets that do not need to be loaded from the TempAssets folder var tmp_InFilters = GetLoadFileFilters(_projectName).Contains(_wannaLoadedAssetName); if (!tmp_InFilters) { tmp_FullPath = Path.Combine(tmp_FullPath, "AutomaticGenerated/TempAssets"); } if (!string.IsNullOrEmpty(tmp_FullPath)) tmp_FilePath = Utility.SearchFileByName(tmp_FullPath, _wannaLoadedAssetName, tmp_Suffix); if (string.IsNullOrEmpty(tmp_FilePath) || !File.Exists(tmp_FilePath)) { var tmp_SearchResultGuids = AssetDatabase.FindAssets($"t:{tmp_Suffix} a:assets", new[] {tmp_FullPath}); foreach (string tmp_Guid in tmp_SearchResultGuids) { string tmp_Path = AssetDatabase.GUIDToAssetPath(tmp_Guid); if (Path.GetFileNameWithoutExtension(tmp_Path) == _wannaLoadedAssetName && File.Exists(Path.Combine(Application.dataPath.Replace("Assets", ""), tmp_Path))) { tmp_FilePath = tmp_Path; break; } } } if (!string.IsNullOrEmpty(tmp_FilePath)) { var tmp_ShortenPath = Utility.ShortenPath(tmp_FilePath); T tmp_Asset = AssetDatabase.LoadAssetAtPath(tmp_ShortenPath); if (tmp_Asset == null) throw new Exception($"Load {_wannaLoadedAssetName} asset failed from Project."); await Task.Delay(1); // Simulate async operation return tmp_Asset; } throw new Exception("No cache found. Attempting to load from server (asset bundle)."); } catch (Exception tmp_Exception) { // Fallback to loading from asset bundle if direct editor loading fails. _projectName += ".arexperience"; if (!AssetBundleCollection.TryGetValue(_projectName.ToLower(), out AssetBundle tmp_MatchedAssetBundle)) { throw new NullReferenceException( $"It cannot load {_wannaLoadedAssetName} asset from {_projectName}.\n {tmp_Exception.Message}"); } var tmp_LoadedOperation = await tmp_MatchedAssetBundle.LoadAssetAsync(_wannaLoadedAssetName); if (tmp_LoadedOperation == null) throw new Exception($"Load {_wannaLoadedAssetName} asset failed from package."); Utility.FixEditorAndroidShaders(); // Ensure shaders are fixed for Android in editor. return tmp_LoadedOperation as T; } } #endif { // Runtime loading from asset bundle. _projectName += ".arexperience"; if (!AssetBundleCollection.TryGetValue(_projectName.ToLower(), out AssetBundle tmp_MatchedAssetBundle)) { throw new NullReferenceException( $"Can not load {_projectName} project's {_wannaLoadedAssetName} asset."); } var tmp_LoadedOperation = await tmp_MatchedAssetBundle.LoadAssetAsync(_wannaLoadedAssetName); if (tmp_LoadedOperation == null) throw new Exception($"Load {_wannaLoadedAssetName} asset failed."); return tmp_LoadedOperation as T; } } catch (Exception e) { Debug.LogError(e); throw; } } /// /// Asynchronously loads all assets of a specific type from a package by project name. /// In editor, it attempts to load directly from project assets first, then falls back to asset bundles. /// /// The type of the assets to load (must inherit from UnityEngine.Object). /// The name of the project (which corresponds to the asset bundle name). /// A Task representing the asynchronous operation, yielding an array of loaded assets. /// Thrown if the asset bundle cannot be found. /// Thrown if no assets of the specified type are found. public static async Task LoadAssetsFromPackage(string _projectName) where T : Object { try { if (!LoadFromRemote) { #if UNITY_EDITOR // Attempt to load from Unity Editor project assets first. try { string tmp_Suffix = Utility.TypeMapping(); var tmp_FullPath = Utility.SearchFolderByName(_projectName); if (string.IsNullOrEmpty(tmp_FullPath)) throw new Exception("No cache found. Attempting to load from server (asset bundle)."); var tmp_AllAssets = AssetDatabase.FindAssets($"t:{tmp_Suffix}", new[] {tmp_FullPath}); if (tmp_AllAssets.Length <= 0) throw new Exception("No cache found. Attempting to load from server (asset bundle)."); List tmp_Assets = new List(); foreach (string tmp_AssetGuid in tmp_AllAssets) { var tmp_Path = AssetDatabase.GUIDToAssetPath(tmp_AssetGuid); if (tmp_Path.Contains(_projectName)) { var tmp_Asset = AssetDatabase.LoadAssetAtPath(tmp_Path); Assert.IsNotNull(tmp_Asset); tmp_Assets.Add(tmp_Asset); } } await Task.Delay(1); // Simulate async operation var tmp_LoadAssets = tmp_Assets.ToArray(); if (tmp_LoadAssets.Length <= 0) throw new Exception("No cache found. Attempting to load from server (asset bundle)."); return tmp_Assets.ToArray(); } catch (Exception tmp_Exception) { // Fallback to loading from asset bundle if direct editor loading fails. _projectName += ".arexperience"; if (!AssetBundleCollection.TryGetValue(_projectName.ToLower(), out AssetBundle tmp_MatchedAssetBundle)) { throw new NullReferenceException( $"Can not load {_projectName} project asset.\n {tmp_Exception.Message}"); } var tmp_LoadedOperation = tmp_MatchedAssetBundle.LoadAllAssetsAsync(); await tmp_LoadedOperation; Utility.FixEditorAndroidShaders(); // Ensure shaders are fixed for Android in editor. var tmp_AllAssets = tmp_LoadedOperation.allAssets as T[]; if (tmp_AllAssets == null || tmp_AllAssets.Length == 0) throw new Exception("Nothing to load from server."); return tmp_LoadedOperation.allAssets as T[]; } #endif } { // Runtime loading from asset bundle. _projectName += ".arexperience"; if (!AssetBundleCollection.TryGetValue(_projectName.ToLower(), out AssetBundle tmp_MatchedAssetBundle)) { throw new NullReferenceException($"Can not load {_projectName} project asset."); } var tmp_LoadedOperation = tmp_MatchedAssetBundle.LoadAllAssetsAsync(); await tmp_LoadedOperation; List tmp_AllTAssets = new List(); foreach (Object tmp_Asset in tmp_LoadedOperation.allAssets) { if (tmp_Asset is T tmp_TAsset) { tmp_AllTAssets.Add(tmp_TAsset); } } var tmp_Assets = tmp_AllTAssets.ToArray(); if (tmp_Assets.Length <= 0) throw new Exception($"Nothing to load from {_projectName}."); return tmp_Assets; } } catch (Exception e) { Debug.LogError(e); throw; } } /// /// Asynchronously loads a specific set of named assets of a given type from a package by project name. /// In editor, it attempts to load directly from project assets first, then falls back to asset bundles. /// /// The type of the assets to load (must inherit from UnityEngine.Object). /// The name of the project (which corresponds to the asset bundle name). /// An array of asset names to load. /// A Task representing the asynchronous operation, yielding an array of loaded assets. /// Thrown if the asset bundle cannot be found. /// Thrown if any specified asset is not found in the package. public static async Task LoadAssetsFromPackage(string _projectName, string[] _assetNames) where T : Object { if (!LoadFromRemote) { #if UNITY_EDITOR // Attempt to load from Unity Editor project assets first. try { string tmp_Suffix = Utility.TypeMapping(); var tmp_FullPath = Utility.SearchFolderByName(_projectName); if (string.IsNullOrEmpty(tmp_FullPath)) throw new Exception("No cache found. Attempting to load from server (asset bundle)."); var tmp_AllAssetGuide = AssetDatabase.FindAssets($"t:{tmp_Suffix}", new[] { tmp_FullPath }); var tmp_AllAssets = tmp_AllAssetGuide .Select(AssetDatabase.GUIDToAssetPath) .Where(_path => _path.Contains(_projectName)) .Select(AssetDatabase.LoadAssetAtPath) .ToArray(); if (tmp_AllAssets.Length < _assetNames.Length) { var tmp_Check1LoadedNames = new HashSet(tmp_AllAssets.Select(a => a.name)); var tmp_Check1MissingAssets = _assetNames.Where(_name => !tmp_Check1LoadedNames.Contains(_name)).ToArray(); if (tmp_Check1MissingAssets.Length > 0) { throw new IndexOutOfRangeException( $"The loaded assets are not in the package. Missing Assets: {string.Join(", ", tmp_Check1MissingAssets)}"); } } var tmp_SortedAssets = _assetNames.Join(tmp_AllAssets, _assetName => _assetName, _asset => _asset.name, (_assetName, _asset) => _asset) .ToArray(); if (tmp_SortedAssets.Length >= _assetNames.Length) return tmp_SortedAssets; var tmp_Check2LoadedNames = new HashSet(tmp_SortedAssets.Select(a => a.name)); var tmp_Check2MissingAssets = _assetNames.Where(_name => !tmp_Check2LoadedNames.Contains(_name)).ToArray(); if (tmp_Check2MissingAssets.Length > 0) { foreach (string tmp_MissingAsset in tmp_Check2MissingAssets) { Debug.LogError(tmp_MissingAsset); } throw new IndexOutOfRangeException( $"The loaded assets are not in the package. Missing Assets[{tmp_Check2MissingAssets.Length}]: {string.Join(", ", tmp_Check2MissingAssets)}"); } return tmp_SortedAssets; } catch (Exception tmp_Exception) { // Fallback to loading from asset bundle if direct editor loading fails. _projectName += ".arexperience"; if (!AssetBundleCollection.TryGetValue(_projectName.ToLower(), out AssetBundle tmp_MatchedAssetBundle)) { throw new NullReferenceException( $"Can not load {_projectName} project asset.\n {tmp_Exception.Message}"); } var tmp_LoadedOperation = tmp_MatchedAssetBundle.LoadAllAssetsAsync(); await tmp_LoadedOperation; var tmp_AllTAssets = tmp_LoadedOperation.allAssets .OfType() .ToArray(); if (tmp_AllTAssets.Length < _assetNames.Length) { var tmp_Check1LoadedNames = new HashSet(tmp_AllTAssets.Select(a => a.name)); var tmp_Check1MissingAssets = _assetNames.Where(_name => !tmp_Check1LoadedNames.Contains(_name)).ToArray(); if (tmp_Check1MissingAssets.Length > 0) { throw new IndexOutOfRangeException( $"The loaded assets are not in the package. Missing Assets: {string.Join(", ", tmp_Check1MissingAssets)}"); } } var tmp_SortedAssets = _assetNames.Join(tmp_AllTAssets, _assetName => _assetName, _asset => _asset.name, (_assetName, _asset) => _asset) .ToArray(); if (tmp_SortedAssets.Length >= _assetNames.Length) return tmp_SortedAssets; var tmp_Check2LoadedNames = new HashSet(tmp_SortedAssets.Select(a => a.name)); var tmp_Check2MissingAssets = _assetNames.Where(_name => !tmp_Check2LoadedNames.Contains(_name)).ToArray(); if (tmp_Check2MissingAssets.Length > 0) { foreach (string tmp_MissingAsset in tmp_Check2MissingAssets) { Debug.LogError(tmp_MissingAsset); } throw new IndexOutOfRangeException( $"The loaded assets are not in the package. Missing Assets[{tmp_Check2MissingAssets.Length}]: {string.Join(", ", tmp_Check2MissingAssets)}"); } return tmp_SortedAssets; } #endif } { // Runtime loading from asset bundle. _projectName += ".arexperience"; if (!AssetBundleCollection.TryGetValue(_projectName.ToLower(), out AssetBundle tmp_MatchedAssetBundle)) { throw new NullReferenceException($"Can not load {_projectName} project asset."); } var tmp_LoadedOperation = tmp_MatchedAssetBundle.LoadAllAssetsAsync(); await tmp_LoadedOperation; var tmp_AllTAssets = tmp_LoadedOperation.allAssets .OfType() .ToArray(); if (tmp_AllTAssets.Length < _assetNames.Length) { var tmp_Check1LoadedNames = new HashSet(tmp_AllTAssets.Select(a => a.name)); var tmp_Check1MissingAssets = _assetNames.Where(_name => !tmp_Check1LoadedNames.Contains(_name)).ToArray(); if (tmp_Check1MissingAssets.Length > 0) { throw new IndexOutOfRangeException( $"The loaded assets are not in the package. Missing Assets: {string.Join(", ", tmp_Check1MissingAssets)}"); } } var tmp_SortedAssets = _assetNames.Join(tmp_AllTAssets, _assetName => _assetName, _asset => _asset.name, (_assetName, _asset) => _asset) .ToArray(); if (tmp_SortedAssets.Length >= _assetNames.Length) return tmp_SortedAssets; var tmp_Check2LoadedNames = new HashSet(tmp_SortedAssets.Select(a => a.name)); var tmp_Check2MissingAssets = _assetNames.Where(_name => !tmp_Check2LoadedNames.Contains(_name)).ToArray(); if (tmp_Check2MissingAssets.Length > 0) { throw new IndexOutOfRangeException( $"The loaded assets are not in the package. Missing Assets: {string.Join(", ", tmp_Check2MissingAssets)}"); } return tmp_SortedAssets; } } /// /// Asynchronously loads a specific asset from an asset bundle downloaded from a URL. /// Supports caching and progress reporting. /// /// The type of the asset to load (must inherit from UnityEngine.Object). /// The URI of the asset bundle to download. /// The timeout duration for the web request in seconds. /// The name of the asset to load from the bundle. /// A version hash for caching. If it doesn't match, the bundle will be re-downloaded. /// A checksum for data integrity. If zero, CRC checking is skipped. /// Callback invoked to report download progress (0.0 to 1.0). /// Callback invoked if asset loading fails, providing an error message. /// A Task representing the asynchronous operation, yielding the loaded asset. public static async Task LoadBundleFromUrl(Uri _uri, int _timeout, string _wannaLoadAssetsName, Hash128 _hash128, uint _crc, Action _progressAction, Action _failedAction) where T : Object { var tmp_FileName = Path.GetFileName(_uri.AbsoluteUri).Split('?')[0]; var tmp_CachedConfigure = new CachedAssetBundle {hash = _hash128}; // If the asset bundle is already loaded, reload the asset from the existing bundle. if (AssetBundleCollection.TryGetValue(tmp_FileName, out AssetBundle tmp_LoadedAssetBundle)) { tmp_LoadedAssetBundle.LoadAssetAsync(_wannaLoadAssetsName).GetAwaiter(); var tmp_ReloadAsset = await tmp_LoadedAssetBundle.LoadAssetAsync(_wannaLoadAssetsName); return tmp_ReloadAsset as T; } #if !UNITY_EDITOR // Auto clean-up old assets only work in real-devices if (!Caching.IsVersionCached(_uri.ToString(), tmp_CachedConfigure.hash)) { var tmp_OldAssetsbundleName = Path.GetFileNameWithoutExtension(_uri.AbsolutePath); var tmp_CachePath = $"{Application.temporaryCachePath.Replace("Caches", "UnityCache")}/Shared/{tmp_OldAssetsbundleName}"; if (Directory.Exists(tmp_CachePath)) { var tmp_AllCacheDirectories = Directory.GetDirectories(tmp_CachePath); for (int tmp_DirectoryIdx = 0; tmp_DirectoryIdx < tmp_AllCacheDirectories.Length; tmp_DirectoryIdx++) { Hash128 tmp_Hash128 = Hash128.Parse(Path.GetFileName(tmp_AllCacheDirectories[tmp_DirectoryIdx])); bool tmp_IsClearCached = Caching.ClearCachedVersion(tmp_OldAssetsbundleName, tmp_Hash128); if (tmp_IsClearCached) { #if UNITY_EDITOR Debug.Log($"{tmp_OldAssetsbundleName}->{tmp_Hash128} asset was cleaned."); #endif } } } } #endif var tmp_Progress = Progress.Create(_progressAction); var tmp_WebRequestAssetBundle = UnityWebRequestAssetBundle.GetAssetBundle(_uri, tmp_CachedConfigure, _crc); await tmp_WebRequestAssetBundle.SendWebRequest().ToUniTask(progress: tmp_Progress); try { AssetBundle tmp_AssetBundle = DownloadHandlerAssetBundle.GetContent(tmp_WebRequestAssetBundle); var tmp_LoadedAsset = await tmp_AssetBundle.LoadAssetAsync(_wannaLoadAssetsName); AddAssetBundleToGlobalManager(tmp_AssetBundle.name, tmp_AssetBundle); return tmp_LoadedAsset as T; } catch (Exception tmp_Exception) { Debug.LogError($"Load package failed: {tmp_Exception.Message}"); _failedAction?.Invoke(tmp_Exception.Message); throw; } } /// /// Asynchronously loads a specific asset from an asset bundle downloaded from a URL string. /// Supports caching and progress reporting. /// /// The type of the asset to load (must inherit from UnityEngine.Object). /// The URI string of the asset bundle to download. /// The timeout duration for the web request in seconds. /// The name of the asset to load from the bundle. /// A version hash for caching. If it doesn't match, the bundle will be re-downloaded. /// A checksum for data integrity. If zero, CRC checking is skipped. /// Callback invoked to report download progress (0.0 to 1.0). /// Callback invoked if asset loading fails, providing an error message. /// A Task representing the asynchronous operation, yielding the loaded asset. public static async Task LoadBundleFromUrl(string _uri, int _timeout, string _wannaLoadAssetsName, Hash128 _hash128, uint _crc, Action _progressAction, Action _failedAction) where T : Object { var tmp_FileName = Path.GetFileName(_uri).Split('?')[0]; var tmp_CachedConfigure = new CachedAssetBundle {hash = _hash128}; // If the asset bundle is already loaded, reload the asset from the existing bundle. if (AssetBundleCollection.TryGetValue(tmp_FileName, out AssetBundle tmp_LoadedAssetBundle)) { tmp_LoadedAssetBundle.LoadAssetAsync(_wannaLoadAssetsName).GetAwaiter(); var tmp_ReloadAsset = await tmp_LoadedAssetBundle.LoadAssetAsync(_wannaLoadAssetsName); return tmp_ReloadAsset as T; } #if !UNITY_EDITOR // Auto clean-up old assets only work in real-devices if (!Caching.IsVersionCached(_uri.ToString(), tmp_CachedConfigure.hash)) { var tmp_OldAssetsbundleName = Path.GetFileNameWithoutExtension(_uri); var tmp_CachePath = $"{Application.temporaryCachePath.Replace("Caches", "UnityCache")}/Shared/{tmp_OldAssetsbundleName}"; if (Directory.Exists(tmp_CachePath)) { var tmp_AllCacheDirectories = Directory.GetDirectories(tmp_CachePath); for (int tmp_DirectoryIdx = 0; tmp_DirectoryIdx < tmp_AllCacheDirectories.Length; tmp_DirectoryIdx++) { Hash128 tmp_Hash128 = Hash128.Parse(Path.GetFileName(tmp_AllCacheDirectories[tmp_DirectoryIdx])); bool tmp_IsClearCached = Caching.ClearCachedVersion(tmp_OldAssetsbundleName, tmp_Hash128); if (tmp_IsClearCached) { #if UNITY_EDITOR Debug.Log($"{tmp_OldAssetsbundleName}->{tmp_Hash128} asset was cleaned."); #endif } } } } #endif var tmp_Progress = Progress.Create(_progressAction); var tmp_WebRequestAssetBundle = UnityWebRequestAssetBundle.GetAssetBundle(_uri, tmp_CachedConfigure, _crc); await tmp_WebRequestAssetBundle.SendWebRequest().ToUniTask(progress: tmp_Progress); try { AssetBundle tmp_AssetBundle = DownloadHandlerAssetBundle.GetContent(tmp_WebRequestAssetBundle); var tmp_LoadedAsset = await tmp_AssetBundle.LoadAssetAsync(_wannaLoadAssetsName); AddAssetBundleToGlobalManager(tmp_AssetBundle.name, tmp_AssetBundle); return tmp_LoadedAsset as T; } catch (Exception tmp_Exception) { Debug.LogError($"Load asset failed: {tmp_Exception.Message}"); _failedAction?.Invoke(tmp_Exception.Message); throw; } } /// /// Asynchronously loads a specific asset from a local asset bundle file. /// Supports progress reporting. /// /// The type of the asset to load (must inherit from UnityEngine.Object). /// The full path to the asset bundle file. /// The name of the asset to load from the bundle. /// Callback invoked to report loading progress (0.0 to 1.0). /// Callback invoked if asset loading fails, providing an error message. /// A Task representing the asynchronous operation, yielding the loaded asset. public static async Task LoadBundleFromLocal(string _path, string _wannaLoadAssetsName, Action _progressAction, Action _failedAction) where T : Object { var tmp_FileName = Path.GetFileName(_path); // If the asset bundle is already loaded, reload the asset from the existing bundle. if (AssetBundleCollection.TryGetValue(tmp_FileName, out AssetBundle tmp_LoadedAssetBundle)) { var tmp_Progress = Progress.Create(_progressAction); var tmp_ReloadAsset = await tmp_LoadedAssetBundle.LoadAssetAsync(_wannaLoadAssetsName).ToUniTask(tmp_Progress); return tmp_ReloadAsset as T; } try { var tmp_Progress = Progress.Create(_progressAction); var tmp_AssetBundle = await AssetBundle.LoadFromFileAsync(_path).ToUniTask(tmp_Progress); AddAssetBundleToGlobalManager(tmp_AssetBundle.name, tmp_AssetBundle); var tmp_LoadedAsset = await tmp_AssetBundle.LoadAssetAsync(_wannaLoadAssetsName); return tmp_LoadedAsset as T; } catch (Exception tmp_Exception) { Debug.LogError($"Load asset failed: {tmp_Exception.Message}"); _failedAction?.Invoke(tmp_Exception.Message); throw; } } /// /// Asynchronously loads a scene from a local asset bundle. /// /// The full path to the asset bundle file containing the scene. /// The name of the scene to load from the bundle. /// A Task representing the asynchronous scene loading operation. public static async Task LoadSceneFromBundle(string _path, string _wannaLoadAssetsName) { var tmp_XRExperienceName = Path.GetFileName(_path); // If the asset bundle is already loaded, load the scene from the existing bundle. if (AssetBundleCollection.TryGetValue(tmp_XRExperienceName, out AssetBundle tmp_LoadedAssetBundle)) { await LoadSceneUtility.LoadSceneAsync(tmp_LoadedAssetBundle, _wannaLoadAssetsName); return; } try { tmp_LoadedAssetBundle = await LoadBundleFromLocal(_path, Debug.LogError); AddAssetBundleToGlobalManager(tmp_LoadedAssetBundle.name, tmp_LoadedAssetBundle); await LoadSceneUtility.LoadSceneAsync(tmp_LoadedAssetBundle, _wannaLoadAssetsName); } catch (Exception tmp_Exception) { Debug.LogError($"Load {_wannaLoadAssetsName} scene failed: \n{tmp_Exception.Message}"); throw; } } /// /// Releases all currently loaded asset bundles that end with "arexperience" and unloads unused assets from memory. /// public static void ReleaseAllAssetBundle() { var tmp_AllLoadedAssetBundles = AssetBundle.GetAllLoadedAssetBundles().ToList(); foreach (AssetBundle tmp_LoadedAssetBundle in tmp_AllLoadedAssetBundles) { // Only unload asset bundles specific to "arexperience" to avoid affecting other bundles. var tmp_BundleSuffix = Path.GetExtension(tmp_LoadedAssetBundle.name); if (!_PACKAGE_NAMES.Contains(tmp_BundleSuffix)) continue; tmp_LoadedAssetBundle.Unload(true); } AssetBundleCollection.Clear(); Resources.UnloadUnusedAssets(); } /// /// Releases a specific loaded asset bundle and destroys associated GameObjects with a matching process ID. /// /// The name of the asset bundle to release. /// The process ID used to identify and destroy related GameObjects. /// If true, all assets in the bundle are unloaded; otherwise, only the bundle itself is unloaded. public static void ReleaseAssetBundle(string _bundleName, string _processId, bool _unloadAll = true) { var tmp_BundleSuffix = Path.GetExtension(_bundleName); if (!AssetBundleCollection.TryGetValue(_bundleName.ToLower(), out var tmp_Bundle)) return; if (!_PACKAGE_NAMES.Contains(tmp_BundleSuffix)) return; // Destroy GameObjects associated with the given process ID. var tmp_AllGoByCurrentProcess = Resources.FindObjectsOfTypeAll().Where(_Pid => _Pid.ProcessId == _processId ); foreach (ProcessIdComponent tmp_ProcessId in tmp_AllGoByCurrentProcess) { if (!tmp_ProcessId || !tmp_ProcessId.gameObject) continue; Object.DestroyImmediate(tmp_ProcessId.gameObject, true); } if (_unloadAll) { tmp_Bundle.UnloadAsync(true).completed += _operation => { AssetBundleCollection.Remove(_bundleName.ToLower()); }; } } /// /// Releases a specific loaded scene asset bundle and destroys associated GameObjects with a matching process ID. /// /// The base name of the scene asset bundle to release (e.g., "myproject"). /// The process ID used to identify and destroy related GameObjects. /// If true, all assets in the bundle are unloaded; otherwise, only the bundle itself is unloaded. public static void ReleaseAssetBundleScene(string _bundleName, string _processId, bool _unloadAll = true) { var tmp_BundleName = Path.GetFileNameWithoutExtension(_bundleName).ToLower(); var tmp_BundleSuffix = Path.GetExtension(_bundleName).ToLower(); var tmp_SceneBundleName = $"{tmp_BundleName}_Scene{tmp_BundleSuffix}"; if (!AssetBundleCollection.TryGetValue(tmp_SceneBundleName, out var tmp_Bundle)) return; if (!_PACKAGE_NAMES.Contains(tmp_BundleSuffix)) return; // Destroy GameObjects associated with the given process ID. var tmp_AllGoByCurrentProcess = Resources.FindObjectsOfTypeAll().Where(_Pid => _Pid.ProcessId == _processId ); foreach (ProcessIdComponent tmp_ProcessId in tmp_AllGoByCurrentProcess) { if (!tmp_ProcessId || !tmp_ProcessId.gameObject) continue; Object.DestroyImmediate(tmp_ProcessId.gameObject, true); } LoadSceneUtility.UnloadRuntimeScenes(tmp_Bundle); if (_unloadAll) { tmp_Bundle.UnloadAsync(true).completed += _operation => { AssetBundleCollection.Remove(tmp_SceneBundleName); }; } } /// /// Adds a loaded AssetBundle to the global collection for tracking and management. /// /// The key (typically the asset bundle name) for the collection. /// The loaded AssetBundle instance. private static void AddAssetBundleToGlobalManager(string _opHashCode, AssetBundle _assetBundle) { AssetBundleCollection.TryAdd(_opHashCode, _assetBundle); } /// /// Asynchronously loads an AssetBundle from a local file path. /// /// The full path to the asset bundle file. /// Callback invoked if asset bundle loading fails, providing an error message. /// A Task representing the asynchronous operation, yielding the loaded AssetBundle. private static async Task LoadBundleFromLocal(string _path, Action _failedAction) { var tmp_FileName = Path.GetFileName(_path); // If the asset bundle is already loaded, return the existing instance. if (AssetBundleCollection.TryGetValue(tmp_FileName, out AssetBundle tmp_LoadedAssetBundle)) { return tmp_LoadedAssetBundle; } try { var tmp_Bundle = await AssetBundle.LoadFromFileAsync(_path).ToUniTask(); return tmp_Bundle; } catch (Exception tmp_Exception) { Debug.LogError($"Load asset failed at {_path}."); _failedAction?.Invoke(tmp_Exception.Message); throw; } } } }