/*===============================================================================
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;
}
}
}
}