using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.AddressableAssets.ResourceLocators;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.U2D;
namespace BrewMonster.Scripts
{
///
/// Scene Bootstrap (index 1): Addressables luôn được init thẳng vì
/// đã chạy xong ở scene 0 ( = true).
///
[DefaultExecutionOrder(-1990)]
public class AddressableManager : MonoSingleton
{
private bool _isInitialized = false;
private UniTaskCompletionSource _initializationTcs;
private Dictionary> _loadedPrefabAssets = new();
private Dictionary> _loadedTextAssets = new();
private Dictionary> _loadedAudioAssets = new();
private Dictionary> _loadedSpriteAssets = new();
private Dictionary> _loadedSpriteAtlasAssets = new();
private Dictionary _loadedAssetReferenceCount = new();
///
/// Whenever we release an asset, we store the timestamp of the release.
/// After a certain amount of time, we will release the asset from the cache.
///
private Dictionary _releaseAssetTimestamps = new();
[SerializeField]private float _releaseAssetTimeout = 10f;
public event Action OnDispose;
/// Get the count of currently loaded assets.
public int LoadedPrefabCount => _loadedPrefabAssets.Count;
public bool IsInitialized() => _isInitialized;
///
/// Returns immediately if already initialized; otherwise waits for the
/// Addressables initialization callback without per-frame polling.
///
public UniTask WaitUntilInitializedAsync()
{
if (_isInitialized) return UniTask.CompletedTask;
return _initializationTcs.Task;
}
protected override void Awake()
{
Debug.Log($"[Cuong] AddressableManager: Awake | id={GetInstanceID()} frame={Time.frameCount}");
base.Awake();
}
protected override void Initialize()
{
base.Initialize();
_isInitialized = false;
_initializationTcs = new UniTaskCompletionSource();
StartAddressablesInitAsync().Forget();
}
async UniTaskVoid StartAddressablesInitAsync()
{
Debug.Log("[Cuong] AddressableManager: Đang InitializeAsync Addressables...");
try
{
await AddressablesInitService.EnsureInitializedAsync();
_isInitialized = true;
_initializationTcs.TrySetResult();
BMLogger.Log("AddressableManager: Initialized");
Debug.Log("[Cuong] AddressableManager: InitializeAsync xong — sẵn sàng load asset.");
}
catch (Exception e)
{
BMLogger.LogError($"AddressableManager: Failed to initialize: {e.Message}");
Debug.LogError($"[Cuong] AddressableManager: InitializeAsync thất bại — {e.Message}");
}
}
#region Unity lifecycle
private List _assetToForceRelease = new();
private void Update()
{
foreach (var kvp in _releaseAssetTimestamps)
{
if (Time.realtimeSinceStartup - kvp.Value > _releaseAssetTimeout)
{
ForceReleaseAsset(kvp.Key);
_assetToForceRelease.Add(kvp.Key);
}
}
if (_assetToForceRelease.Count > 0)
{
for (int i = 0; i < _assetToForceRelease.Count; i++)
{
ForceReleaseAsset(_assetToForceRelease[i]);
}
_assetToForceRelease.Clear();
}
}
#endregion
#region private functions
private void RemoveFromReleaseAssetDictionary(string assetPath)
{
if (_releaseAssetTimestamps.ContainsKey(assetPath))
{
_releaseAssetTimestamps.Remove(assetPath);
}
}
private void IncreaseReferenceCount(string assetPath)
{
if (!_loadedAssetReferenceCount.ContainsKey(assetPath))
{
_loadedAssetReferenceCount[assetPath] = 0;
}
_loadedAssetReferenceCount[assetPath]++;
}
private void DecreaseReferenceCount(string assetPath)
{
if (_loadedAssetReferenceCount.TryGetValue(assetPath, out int count))
{
count--;
if (count <= 0)
{
_loadedAssetReferenceCount.Remove(assetPath);
// put into the release asset timestamp dictionary.
_releaseAssetTimestamps[assetPath] = Time.realtimeSinceStartup;
}
}
}
#endregion
#region public functions
AsyncOperationHandle _loadedTextAssetHandle;
///
/// Load a text asset asynchronously.
/// NOTE: The key must match the Addressables "Address" (or another valid key like a label/GUID).
///
///
///
///
public async Task LoadTextAssetAsync(string assetPath)
{
// increase the reference count of the asset.
if (!_loadedAssetReferenceCount.ContainsKey(assetPath))
{
_loadedAssetReferenceCount[assetPath] = 0;
}
_loadedAssetReferenceCount[assetPath]++;
// remove the asset from the release timestamp dictionary. So it won't be released.
RemoveFromReleaseAssetDictionary(assetPath);
if (_loadedTextAssets.TryGetValue(assetPath, out _loadedTextAssetHandle))
{
if (_loadedTextAssetHandle.IsValid() && _loadedTextAssetHandle.Result != null)
{
BMLogger.Log($"AddressableManager: Loaded text asset from cache: {assetPath}");
return _loadedTextAssetHandle.Result;
}
else
{
BMLogger.Log($"AddressableManager: Text asset handle is invalid or result is null, need to load new one: {assetPath}");
}
}
try
{
var handle = Addressables.LoadAssetAsync(assetPath);
await handle.Task;
_loadedTextAssets[assetPath] = handle;
return handle.Result;
}
catch (Exception e)
{
BMLogger.LogError($"AddressableManager: Failed to load TextAsset '{assetPath}': {e}");
return null;
}
}
AsyncOperationHandle _loadedSpriteAtlasHandle;
/// Load a sprite atlas asynchronously.
public async Task LoadSpriteAtlasAsync(string assetPath)
{
// increase the reference count of the asset.
if (!_loadedAssetReferenceCount.ContainsKey(assetPath))
{
_loadedAssetReferenceCount[assetPath] = 0;
}
_loadedAssetReferenceCount[assetPath]++;
// remove the asset from the release timestamp dictionary. So it won't be released.
RemoveFromReleaseAssetDictionary(assetPath);
if (_loadedSpriteAtlasAssets.TryGetValue(assetPath, out _loadedSpriteAtlasHandle))
{
if (_loadedTextAssetHandle.IsValid() && _loadedTextAssetHandle.Result != null)
{
BMLogger.Log($"AddressableManager: Loaded text asset from cache: {assetPath}");
return _loadedSpriteAtlasHandle.Result;
}
else
{
BMLogger.Log($"AddressableManager: Text asset handle is invalid or result is null, need to load new one: {assetPath}");
}
}
try
{
var handle = Addressables.LoadAssetAsync(assetPath);
await handle.Task;
if (handle.OperationException != null)
{
BMLogger.Log($"AddressableManager: Failed to load Sprite Atlas '{assetPath}': {handle.OperationException.Message} {handle.OperationException.StackTrace}");
#if UNITY_EDITOR
_invalidAssetPaths.Add(assetPath);
#endif
return null;
}
_loadedSpriteAtlasAssets[assetPath] = handle;
return handle.Result;
}
catch (Exception e)
{
BMLogger.LogError($"AddressableManager: Failed to load SpriteAtlas '{assetPath}': {e}");
return null;
}
}
AsyncOperationHandle _loadedPrefabHandle;
///
/// Load an asset asynchronously. The address should look like this: "models/npcs/npc/魅灵首领/魅灵首领/魅灵首领.prefab"
///
///
public async Task LoadPrefabAsync(string assetPath)
{
// remove the asset from the release timestamp dictionary. So it won't be released.
RemoveFromReleaseAssetDictionary(assetPath);
if (!KeyExists(assetPath, typeof(GameObject)))
{
BMLogger.LogWarning($"AddressableManager: Prefab '{assetPath}' does not exist");
return null;
}
if (_loadedPrefabAssets.TryGetValue(assetPath, out _loadedPrefabHandle))
{
if (_loadedPrefabHandle.IsValid() && _loadedPrefabHandle.Result != null)
{
// BMLogger.Log($"AddressableManager: Loaded prefab from cache: {assetPath}");
return _loadedPrefabHandle.Result;
}
else
{
// BMLogger.Log($"AddressableManager: Prefab handle is invalid or result is null, need to load new one: {assetPath}");
}
}
try
{
var handle = Addressables.LoadAssetAsync(assetPath);
await handle.Task;
if (handle.OperationException != null)
{
BMLogger.Log($"AddressableManager: Failed to load Prefab '{assetPath}': {handle.OperationException.Message} {handle.OperationException.StackTrace}");
#if UNITY_EDITOR
_invalidAssetPaths.Add(assetPath);
#endif
return null;
}
_loadedPrefabAssets[assetPath] = handle;
return handle.Result;
}
catch (System.Exception e)
{
BMLogger.Log($"AddressableManager: Failed to load Prefab '{assetPath}': {e.Message} {e.StackTrace}");
#if UNITY_EDITOR
_invalidAssetPaths.Add(assetPath);
#endif
return null;
}
}
AsyncOperationHandle _loadedAudioClipHandle;
///
/// Load an AudioClip asynchronously (e.g. skill SFX). Key must match the Addressables address.
///
public async Task LoadAudioClipAsync(string assetPath)
{
RemoveFromReleaseAssetDictionary(assetPath);
if (_loadedAudioAssets.TryGetValue(assetPath, out _loadedAudioClipHandle))
{
if (_loadedAudioClipHandle.IsValid() && _loadedAudioClipHandle.Result != null)
{
BMLogger.Log($"AddressableManager: Loaded audio from cache: {assetPath}");
return _loadedAudioClipHandle.Result;
}
}
try
{
var handle = Addressables.LoadAssetAsync(assetPath.Trim());
await handle.Task;
if (handle.OperationException != null)
{
BMLogger.Log($"AddressableManager: Failed to load AudioClip '{assetPath}': {handle.OperationException.Message}");
return null;
}
_loadedAudioAssets[assetPath] = handle;
return handle.Result;
}
catch (Exception e)
{
BMLogger.LogError($"AddressableManager: Failed to load AudioClip '{assetPath}': {e.Message}");
return null;
}
}
AsyncOperationHandle _loadedSpriteHandle;
///
/// Load a Sprite asynchronously (e.g. UI art). Key must match the Addressables address.
///
public async Task LoadSpriteAsync(string assetPath)
{
RemoveFromReleaseAssetDictionary(assetPath);
if (_loadedSpriteAssets.TryGetValue(assetPath, out _loadedSpriteHandle))
{
if (_loadedSpriteHandle.IsValid() && _loadedSpriteHandle.Result != null)
{
BMLogger.Log($"AddressableManager: Loaded sprite from cache: {assetPath}");
return _loadedSpriteHandle.Result;
}
}
try
{
var handle = Addressables.LoadAssetAsync(assetPath.Trim());
await handle.Task;
if (handle.OperationException != null)
{
BMLogger.Log($"AddressableManager: Failed to load Sprite '{assetPath}': {handle.OperationException.Message}");
return null;
}
_loadedSpriteAssets[assetPath] = handle;
return handle.Result;
}
catch (Exception e)
{
BMLogger.LogError($"AddressableManager: Failed to load Sprite '{assetPath}': {e.Message}");
return null;
}
}
///
/// True if this address is already in the audio cache (play immediately vs async load).
///
public bool TryGetCachedAudioClip(string assetPath, out AudioClip clip)
{
clip = null;
if (string.IsNullOrEmpty(assetPath))
return false;
if (_loadedAudioAssets.TryGetValue(assetPath, out var h) && h.IsValid() && h.Result != null)
{
clip = h.Result;
return true;
}
return false;
}
///
/// When the asset is no longer needed, call this method to unload it.
/// The asset will be released after a certain amount of time.
///
/// The asset path used when loading the asset
public void ReleaseAsset(string assetPath)
{
// If multiple release requests are made for the same asset, we only need to store the earliest release time.
float currentReleaseTime = Time.realtimeSinceStartup;
if (_releaseAssetTimestamps.TryGetValue(assetPath, out var timeStamp))
{
currentReleaseTime = Mathf.Min(currentReleaseTime, timeStamp);
}
_releaseAssetTimestamps[assetPath] = currentReleaseTime;
}
public void ForceReleaseAsset(string assetPath)
{
if (_loadedPrefabAssets.TryGetValue(assetPath, out var loadedPrefabHandle))
{
if (loadedPrefabHandle.IsValid())
{
Addressables.Release(loadedPrefabHandle);
}
_loadedPrefabAssets.Remove(assetPath);
}
else if (_loadedTextAssets.TryGetValue(assetPath, out var loadedTextHandle))
{
if (loadedTextHandle.IsValid())
{
Addressables.Release(loadedTextHandle);
}
_loadedTextAssets.Remove(assetPath);
}
else if (_loadedAudioAssets.TryGetValue(assetPath, out var loadedAudioHandle))
{
if (loadedAudioHandle.IsValid())
{
Addressables.Release(loadedAudioHandle);
}
_loadedAudioAssets.Remove(assetPath);
BMLogger.Log($"AddressableManager: Force released audio asset: {assetPath}");
}
else if (_loadedSpriteAssets.TryGetValue(assetPath, out var loadedSpriteHandle))
{
if (loadedSpriteHandle.IsValid())
{
Addressables.Release(loadedSpriteHandle);
}
_loadedSpriteAssets.Remove(assetPath);
BMLogger.Log($"AddressableManager: Force released sprite asset: {assetPath}");
}
}
///
/// Release a specific asset by its handle directly.
///
/// The async operation handle to release
public void ReleaseAsset(AsyncOperationHandle handle)
{
if (handle.IsValid())
{
Addressables.Release(handle);
}
}
///
/// Release all loaded assets from the cache.
///
public void ReleaseAllAssets()
{
foreach (var kvp in _loadedPrefabAssets)
{
if (kvp.Value.IsValid())
{
Addressables.Release(kvp.Value);
}
}
_loadedPrefabAssets.Clear();
foreach (var kvp in _loadedAudioAssets)
{
if (kvp.Value.IsValid())
{
Addressables.Release(kvp.Value);
}
}
_loadedAudioAssets.Clear();
foreach (var kvp in _loadedSpriteAssets)
{
if (kvp.Value.IsValid())
{
Addressables.Release(kvp.Value);
}
}
_loadedSpriteAssets.Clear();
BMLogger.Log("AddressableManager: Released all assets");
}
///
/// Check if an asset is currently loaded in the cache.
///
/// The asset path to check
/// True if the asset is loaded
public bool IsAssetLoaded(string assetPath)
{
return _loadedPrefabAssets.ContainsKey(assetPath) && _loadedPrefabAssets[assetPath].IsValid();
}
#if UNITY_EDITOR
private HashSet _invalidAssetPaths = new();
#endif
///
/// Checks if a given Addressable key or path exists in the current catalogs.
///
/// The Addressable key, path, or label to check.
/// Optional: The specific type of asset you are looking for.
/// True if the key exists, false otherwise.
public static bool KeyExists(object key, System.Type type = null)
{
string keyStr = key?.ToString();
if (string.IsNullOrEmpty(keyStr))
return false;
#if UNITY_EDITOR
// Failed loads are cached; still verify against catalog (do not assume unknown keys exist).
if (Instance != null && Instance._invalidAssetPaths.Contains(keyStr))
return false;
#endif
foreach (IResourceLocator locator in Addressables.ResourceLocators)
{
if (locator.Locate(key, type, out var locations)
&& locations != null
&& locations.Count > 0)
{
return true;
}
}
return false;
}
#endregion
private void OnDestroy()
{
OnDispose?.Invoke();
ReleaseAllAssets();
}
}
}