diff --git a/Assets/PerfectWorld/Scripts/Addressable/AddressableManager.cs b/Assets/PerfectWorld/Scripts/Addressable/AddressableManager.cs
index 6508892ad4..b3e6729f1e 100644
--- a/Assets/PerfectWorld/Scripts/Addressable/AddressableManager.cs
+++ b/Assets/PerfectWorld/Scripts/Addressable/AddressableManager.cs
@@ -1,16 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Threading;
using System.Threading.Tasks;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.AddressableAssets.ResourceLocators;
using UnityEngine.ResourceManagement.AsyncOperations;
+using UnityEngine.SceneManagement;
using UnityEngine.U2D;
namespace BrewMonster.Scripts
{
+ /// Runs after (-2000) so bootstrap Awake creates the gate first.
+ [DefaultExecutionOrder(-1990)]
public class AddressableManager : MonoSingleton
{
private bool _isInitialized = false;
@@ -31,6 +35,14 @@ namespace BrewMonster.Scripts
private Dictionary _releaseAssetTimestamps = new();
[SerializeField]private float _releaseAssetTimeout = 10f;
+ [Header("Bootstrap gate")]
+ [Tooltip("Max seconds to wait for GameContentBootstrap before initializing Addressables anyway.")]
+ [SerializeField]
+ float _bootstrapGateWaitTimeoutSeconds = 50f;
+
+ [SerializeField]
+ bool _verboseBootstrapWaitDebug = true;
+
public event Action OnDispose;
/// Get the count of currently loaded assets.
@@ -48,6 +60,18 @@ namespace BrewMonster.Scripts
return _initializationTcs.Task;
}
+ protected override void Awake()
+ {
+ if (_verboseBootstrapWaitDebug)
+ {
+ Debug.Log(
+ $"[Cuong] AddressableManager: Awake | id={GetInstanceID()} scene={SceneManager.GetActiveScene().name} " +
+ $"frame={Time.frameCount} bootstrapGate={GameContentBootstrap.GetGateDebugState()}");
+ }
+
+ base.Awake();
+ }
+
protected override void Initialize()
{
base.Initialize();
@@ -61,12 +85,38 @@ namespace BrewMonster.Scripts
///
async UniTaskVoid StartAddressablesInitAfterBootstrapGate()
{
- Debug.Log("[Cuong] AddressableManager: Đang chờ GameContentBootstrap (version / URL rewrite)...");
- await GameContentBootstrap.WaitForPreAddressablesSetupIfAnyAsync();
+ var gateState = GameContentBootstrap.GetGateDebugState();
+ Debug.Log(
+ $"[Cuong] AddressableManager: Đang chờ GameContentBootstrap (version / URL rewrite)... | " +
+ $"id={GetInstanceID()} scene={SceneManager.GetActiveScene().name} gate={gateState}");
+
+ var waited = await WaitForBootstrapGateWithTimeoutAsync();
+ if (!waited)
+ {
+ Debug.LogWarning(
+ $"[Cuong] AddressableManager: Bootstrap gate timeout ({_bootstrapGateWaitTimeoutSeconds:F0}s) — " +
+ "InitializeAsync anyway. Check GameContentBootstrap lifecycle logs.");
+ }
+
Debug.Log("[Cuong] AddressableManager: Bootstrap gate xong — đang InitializeAsync Addressables...");
Addressables.InitializeAsync().Completed += OnInitializeComplete;
}
+ async UniTask WaitForBootstrapGateWithTimeoutAsync()
+ {
+ var timeoutSec = Mathf.Max(5f, _bootstrapGateWaitTimeoutSeconds);
+ using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSec));
+ try
+ {
+ await GameContentBootstrap.WaitForPreAddressablesSetupIfAnyAsync(cts.Token);
+ return true;
+ }
+ catch (OperationCanceledException)
+ {
+ return false;
+ }
+ }
+
#region Unity lifecycle
private List _assetToForceRelease = new();
private void Update()
diff --git a/Assets/PerfectWorld/Scripts/Addressable/AddressablesCatalogUpdater.cs b/Assets/PerfectWorld/Scripts/Addressable/AddressablesCatalogUpdater.cs
index bca4ebaf8f..faac4dc07d 100644
--- a/Assets/PerfectWorld/Scripts/Addressable/AddressablesCatalogUpdater.cs
+++ b/Assets/PerfectWorld/Scripts/Addressable/AddressablesCatalogUpdater.cs
@@ -64,11 +64,20 @@ namespace BrewMonster.Scripts
/// Pass through to Addressables (default true).
public static async UniTask CheckForCatalogUpdatesAsync(bool autoReleaseHandle = true)
{
+ AsyncOperationHandle> handle = default;
try
{
await EnsureInitializedAsync();
- var handle = Addressables.CheckForCatalogUpdates(autoReleaseHandle);
+ handle = Addressables.CheckForCatalogUpdates(autoReleaseHandle: false);
await handle.ToUniTask();
+
+ if (!handle.IsValid())
+ {
+ var err = new InvalidOperationException("CheckForCatalogUpdates handle was released before result could be read.");
+ BMLogger.LogError($"AddressablesCatalogUpdater: {err.Message}");
+ return new CatalogCheckResult(false, null, err);
+ }
+
if (handle.Status != AsyncOperationStatus.Succeeded)
{
var err = handle.OperationException;
@@ -85,6 +94,10 @@ namespace BrewMonster.Scripts
BMLogger.LogError($"AddressablesCatalogUpdater: CheckForCatalogUpdates exception: {e.Message}");
return new CatalogCheckResult(false, null, e);
}
+ finally
+ {
+ ReleaseHandleIfNeeded(handle, autoReleaseHandle: false);
+ }
}
///
@@ -97,16 +110,24 @@ namespace BrewMonster.Scripts
bool autoCleanBundleCache = false,
bool autoReleaseHandle = true)
{
+ AsyncOperationHandle> handle = default;
try
{
await EnsureInitializedAsync();
- AsyncOperationHandle> handle;
if (autoCleanBundleCache)
- handle = Addressables.UpdateCatalogs(true, catalogIds, autoReleaseHandle);
+ handle = Addressables.UpdateCatalogs(true, catalogIds, autoReleaseHandle: false);
else
- handle = Addressables.UpdateCatalogs(catalogIds, autoReleaseHandle);
+ handle = Addressables.UpdateCatalogs(catalogIds, autoReleaseHandle: false);
await handle.ToUniTask();
+
+ if (!handle.IsValid())
+ {
+ var err = new InvalidOperationException("UpdateCatalogs handle was released before result could be read.");
+ BMLogger.LogError($"AddressablesCatalogUpdater: {err.Message}");
+ return new CatalogApplyResult(false, null, err);
+ }
+
if (handle.Status != AsyncOperationStatus.Succeeded)
{
var err = handle.OperationException;
@@ -123,6 +144,10 @@ namespace BrewMonster.Scripts
BMLogger.LogError($"AddressablesCatalogUpdater: UpdateCatalogs exception: {e.Message}");
return new CatalogApplyResult(false, null, e);
}
+ finally
+ {
+ ReleaseHandleIfNeeded(handle, autoReleaseHandle: false);
+ }
}
///
@@ -144,11 +169,19 @@ namespace BrewMonster.Scripts
///
public static async UniTask GetDownloadSizeBytesAsync(object key)
{
+ AsyncOperationHandle handle = default;
try
{
await EnsureInitializedAsync();
- var handle = Addressables.GetDownloadSizeAsync(key);
+ handle = Addressables.GetDownloadSizeAsync(key);
await handle.ToUniTask();
+
+ if (!handle.IsValid())
+ {
+ BMLogger.LogError("AddressablesCatalogUpdater: GetDownloadSizeAsync handle was released before result could be read.");
+ return -1;
+ }
+
if (handle.Status != AsyncOperationStatus.Succeeded)
{
BMLogger.LogError($"AddressablesCatalogUpdater: GetDownloadSizeAsync failed: {handle.OperationException?.Message}");
@@ -161,6 +194,10 @@ namespace BrewMonster.Scripts
BMLogger.LogError($"AddressablesCatalogUpdater: GetDownloadSizeBytesAsync exception: {e.Message}");
return -1;
}
+ finally
+ {
+ ReleaseHandleIfNeeded(handle, autoReleaseHandle: false);
+ }
}
///
@@ -168,11 +205,15 @@ namespace BrewMonster.Scripts
/// Logs download percent (and MB when known) every percent via [Cuong].
///
/// Log when percent crosses each step (1–100). Default 5.
+ ///
+ /// Handle is always retained until this method finishes (autoReleaseHandle forced false) so progress polling
+ /// does not touch a handle Addressables already released on completion.
+ ///
public static async UniTask DownloadDependenciesAsync(
object key,
- bool autoReleaseHandle = true,
int progressLogStepPercent = 5)
{
+ AsyncOperationHandle handle = default;
try
{
await EnsureInitializedAsync();
@@ -180,18 +221,30 @@ namespace BrewMonster.Scripts
var step = Mathf.Clamp(progressLogStepPercent, 1, 100);
Debug.Log($"[Cuong] AddressablesCatalogUpdater: Bắt đầu tải dependencies (key={keyLabel})...");
- var handle = Addressables.DownloadDependenciesAsync(key, autoReleaseHandle);
+ // Never auto-release while polling; caller passing true is ignored to avoid invalid-handle errors.
+ handle = Addressables.DownloadDependenciesAsync(key, autoReleaseHandle: false);
var lastLoggedPercent = -1;
- LogDownloadProgress(keyLabel, handle, ref lastLoggedPercent, step, force: true);
+
+ if (handle.IsValid())
+ LogDownloadProgress(keyLabel, handle, ref lastLoggedPercent, step, force: true);
while (!handle.IsDone)
{
- LogDownloadProgress(keyLabel, handle, ref lastLoggedPercent, step);
+ if (handle.IsValid())
+ LogDownloadProgress(keyLabel, handle, ref lastLoggedPercent, step);
await UniTask.Yield();
}
- LogDownloadProgress(keyLabel, handle, ref lastLoggedPercent, step, force: true);
- await handle.ToUniTask();
+ if (!handle.IsValid())
+ {
+ BMLogger.LogError("AddressablesCatalogUpdater: DownloadDependenciesAsync handle invalid after completion.");
+ Debug.LogError($"[Cuong] AddressablesCatalogUpdater: Handle không hợp lệ sau khi tải '{keyLabel}'.");
+ return false;
+ }
+
+ if (handle.IsValid())
+ LogDownloadProgress(keyLabel, handle, ref lastLoggedPercent, step, force: true);
+
if (handle.Status != AsyncOperationStatus.Succeeded)
{
BMLogger.LogError($"AddressablesCatalogUpdater: DownloadDependenciesAsync failed: {handle.OperationException?.Message}");
@@ -208,6 +261,17 @@ namespace BrewMonster.Scripts
Debug.LogError($"[Cuong] AddressablesCatalogUpdater: Exception khi tải — {e.Message}");
return false;
}
+ finally
+ {
+ ReleaseHandleIfNeeded(handle, autoReleaseHandle: false);
+ }
+ }
+
+ static void ReleaseHandleIfNeeded(AsyncOperationHandle handle, bool autoReleaseHandle)
+ {
+ if (autoReleaseHandle || !handle.IsValid())
+ return;
+ Addressables.Release(handle);
}
static void LogDownloadProgress(
@@ -217,6 +281,9 @@ namespace BrewMonster.Scripts
int stepPercent,
bool force = false)
{
+ if (!handle.IsValid())
+ return;
+
var status = handle.GetDownloadStatus();
var percent = Mathf.Clamp(status.Percent * 100f, 0f, 100f);
var percentRounded = Mathf.RoundToInt(percent);
@@ -248,11 +315,19 @@ namespace BrewMonster.Scripts
///
public static async UniTask CleanBundleCacheAsync(IEnumerable catalogIds = null)
{
+ AsyncOperationHandle handle = default;
try
{
await EnsureInitializedAsync();
- var handle = Addressables.CleanBundleCache(catalogIds);
+ handle = Addressables.CleanBundleCache(catalogIds);
await handle.ToUniTask();
+
+ if (!handle.IsValid())
+ {
+ BMLogger.LogError("AddressablesCatalogUpdater: CleanBundleCache handle was released before result could be read.");
+ return false;
+ }
+
if (handle.Status != AsyncOperationStatus.Succeeded)
{
BMLogger.LogError($"AddressablesCatalogUpdater: CleanBundleCache failed: {handle.OperationException?.Message}");
@@ -265,6 +340,10 @@ namespace BrewMonster.Scripts
BMLogger.LogError($"AddressablesCatalogUpdater: CleanBundleCacheAsync exception: {e.Message}");
return false;
}
+ finally
+ {
+ ReleaseHandleIfNeeded(handle, autoReleaseHandle: false);
+ }
}
}
}
diff --git a/Assets/PerfectWorld/Scripts/Addressable/GameContentBootstrap.cs b/Assets/PerfectWorld/Scripts/Addressable/GameContentBootstrap.cs
index 47549e6318..92bb05fbc3 100644
--- a/Assets/PerfectWorld/Scripts/Addressable/GameContentBootstrap.cs
+++ b/Assets/PerfectWorld/Scripts/Addressable/GameContentBootstrap.cs
@@ -1,7 +1,9 @@
using System;
+using System.Threading;
using BrewMonster;
using Cysharp.Threading.Tasks;
using UnityEngine;
+using UnityEngine.SceneManagement;
namespace BrewMonster.Scripts
{
@@ -21,6 +23,17 @@ namespace BrewMonster.Scripts
/// When non-null, waits on this before .
///
static UniTaskCompletionSource s_addressablesInitGate;
+ static int s_activeBootstrapInstanceId;
+ static bool s_bootstrapRunStarted;
+
+ [Header("Debug")]
+ [Tooltip("Log lifecycle (Awake/Start/OnDestroy), gate state, scene name — dùng khi debug mobile.")]
+ [SerializeField]
+ bool _verboseLifecycleDebug = true;
+
+ [Tooltip("Nếu gate chưa mở sau N giây, tự release để AddressableManager không treo vĩnh viễn.")]
+ [SerializeField]
+ float _gateReleaseTimeoutSeconds = 45f;
[Header("Version source")]
[Tooltip("True = gọi HTTP tới URL bên dưới. False = không request server, chỉ dùng hardcode (test / API chưa chốt).")]
@@ -76,41 +89,121 @@ namespace BrewMonster.Scripts
public event Action Finished;
- public static UniTask WaitForPreAddressablesSetupIfAnyAsync()
+ /// Trạng thái gate tĩnh — dùng log chẩn đoán từ .
+ public static string GetGateDebugState()
+ {
+ if (!_holdAddressablesInitConfigured)
+ return "holdAddressablesInit=false (no gate)";
+ if (s_addressablesInitGate == null)
+ return "gate=null (released or not created)";
+ return $"gate=pending, activeInstanceId={s_activeBootstrapInstanceId}, runStarted={s_bootstrapRunStarted}";
+ }
+
+ static bool _holdAddressablesInitConfigured = true;
+
+ public static UniTask WaitForPreAddressablesSetupIfAnyAsync(CancellationToken cancellationToken = default)
{
if (s_addressablesInitGate == null)
return UniTask.CompletedTask;
- return s_addressablesInitGate.Task;
+ return s_addressablesInitGate.Task.AttachExternalCancellation(cancellationToken);
}
void Awake()
{
+ s_activeBootstrapInstanceId = GetInstanceID();
+ _holdAddressablesInitConfigured = _holdAddressablesInitUntilVersionChecked;
+ LogLifecycle("Awake");
+
if (_holdAddressablesInitUntilVersionChecked)
+ {
s_addressablesInitGate ??= new UniTaskCompletionSource();
+ GateTimeoutWatchdog().Forget();
+ }
+ else
+ LogLifecycle("Awake: holdAddressablesInit=false — AddressableManager will not wait on gate.");
+
+ TryStartBootstrapRun("Awake");
}
void Start()
{
- RunAsync().Forget();
+ LogLifecycle("Start");
+ TryStartBootstrapRun("Start");
}
void OnDestroy()
{
- ReleaseGateIfPending();
+ LogLifecycle("OnDestroy — releasing gate if still pending");
+ ReleaseGateIfPending("OnDestroy");
+ if (s_activeBootstrapInstanceId == GetInstanceID())
+ s_activeBootstrapInstanceId = 0;
}
void OnApplicationQuit()
{
- ReleaseGateIfPending();
+ LogLifecycle("OnApplicationQuit");
+ ReleaseGateIfPending("OnApplicationQuit");
}
- static void ReleaseGateIfPending()
+ void TryStartBootstrapRun(string caller)
{
- if (s_addressablesInitGate != null)
+ if (!isActiveAndEnabled)
{
- s_addressablesInitGate.TrySetResult();
- s_addressablesInitGate = null;
+ LogLifecycle($"{caller}: SKIP bootstrap — GameObject inactive or component disabled.");
+ return;
}
+
+ if (s_bootstrapRunStarted)
+ {
+ LogLifecycle($"{caller}: bootstrap already started (skip duplicate).");
+ return;
+ }
+
+ s_bootstrapRunStarted = true;
+ LogLifecycle($"{caller}: starting RunAsync.");
+ RunAsync().Forget();
+ }
+
+ async UniTaskVoid GateTimeoutWatchdog()
+ {
+ var timeout = Mathf.Max(5f, _gateReleaseTimeoutSeconds);
+ await UniTask.Delay(TimeSpan.FromSeconds(timeout));
+
+ if (s_addressablesInitGate == null)
+ return;
+
+ Debug.LogWarning(
+ $"[Cuong] GameContentBootstrap: Gate timeout ({timeout:F0}s) — releasing gate. " +
+ $"runStarted={s_bootstrapRunStarted}, scene={SceneManager.GetActiveScene().name}, {DescribeSelf()}");
+ AllowAddressablesInit("GateTimeoutWatchdog");
+ }
+
+ static void ReleaseGateIfPending(string reason)
+ {
+ if (s_addressablesInitGate == null)
+ return;
+
+ Debug.LogWarning(
+ $"[Cuong] GameContentBootstrap: ReleaseGate ({reason}) — gate opened without normal bootstrap completion.");
+ s_addressablesInitGate.TrySetResult();
+ s_addressablesInitGate = null;
+ }
+
+ void LogLifecycle(string phase)
+ {
+ if (!_verboseLifecycleDebug)
+ return;
+
+ Debug.Log(
+ $"[Cuong] GameContentBootstrap: {phase} | {DescribeSelf()} | gate={GetGateDebugState()} | " +
+ $"scene={SceneManager.GetActiveScene().name} frame={Time.frameCount}");
+ }
+
+ string DescribeSelf()
+ {
+ return
+ $"id={GetInstanceID()} active={gameObject.activeInHierarchy} enabled={enabled} " +
+ $"holdInit={_holdAddressablesInitUntilVersionChecked} server={_useServerForVersionInfo}";
}
async UniTaskVoid RunAsync()
@@ -130,7 +223,7 @@ namespace BrewMonster.Scripts
if (!fetch.Ok)
{
Debug.LogError($"[Cuong] GameContentBootstrap: Lỗi lấy version — {fetch.Error}");
- AllowAddressablesInit();
+ AllowAddressablesInit("version-fetch-failed");
return new BootstrapResult(false, false, fetch.Error, null);
}
@@ -146,7 +239,7 @@ namespace BrewMonster.Scripts
Debug.Log("[Cuong] GameContentBootstrap: Đã gắn URL rewrite cho remote CDN.");
}
- AllowAddressablesInit();
+ AllowAddressablesInit("version-and-url-ready");
Debug.Log("[Cuong] GameContentBootstrap: Đang khởi tạo Addressables...");
await AddressablesCatalogUpdater.EnsureInitializedAsync();
@@ -201,7 +294,7 @@ namespace BrewMonster.Scripts
$"[Cuong] GameContentBootstrap: Đang tải remote content (label={bulkLabel}) — không lấy được dung lượng trước khi tải.");
}
- bool dl = await AddressablesCatalogUpdater.DownloadDependenciesAsync(bulkLabel, true);
+ bool dl = await AddressablesCatalogUpdater.DownloadDependenciesAsync(bulkLabel);
if (!dl)
{
Debug.LogError("[Cuong] GameContentBootstrap: Tải remote content thất bại.");
@@ -220,18 +313,22 @@ namespace BrewMonster.Scripts
{
BMLogger.LogError($"[Cuong] GameContentBootstrap: {e.Message}");
Debug.LogError($"[Cuong] GameContentBootstrap: Exception — {e.Message}");
- AllowAddressablesInit();
+ AllowAddressablesInit("exception");
return new BootstrapResult(false, true, e.Message, null);
}
}
- static void AllowAddressablesInit()
+ static void AllowAddressablesInit(string reason)
{
- if (s_addressablesInitGate != null)
+ if (s_addressablesInitGate == null)
{
- s_addressablesInitGate.TrySetResult();
- s_addressablesInitGate = null;
+ Debug.Log($"[Cuong] GameContentBootstrap: AllowAddressablesInit({reason}) — gate already null.");
+ return;
}
+
+ Debug.Log($"[Cuong] GameContentBootstrap: AllowAddressablesInit({reason}) — opening gate for AddressableManager.");
+ s_addressablesInitGate.TrySetResult();
+ s_addressablesInitGate = null;
}
///