From 5affac9cbd2186a3ce745833cf7e5063544a2863 Mon Sep 17 00:00:00 2001 From: NguyenVanDat Date: Thu, 21 May 2026 16:55:42 +0700 Subject: [PATCH] fix download addressable remote in mobile --- .../Scripts/Addressable/AddressableManager.cs | 54 +++++++- .../Addressable/AddressablesCatalogUpdater.cs | 103 ++++++++++++-- .../Addressable/GameContentBootstrap.cs | 131 +++++++++++++++--- 3 files changed, 257 insertions(+), 31 deletions(-) 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; } ///