diff --git a/Assets/PerfectWorld/Scripts/Addressable/AddressableManager.cs b/Assets/PerfectWorld/Scripts/Addressable/AddressableManager.cs index b3e6729f1e..2d0c5e27f5 100644 --- a/Assets/PerfectWorld/Scripts/Addressable/AddressableManager.cs +++ b/Assets/PerfectWorld/Scripts/Addressable/AddressableManager.cs @@ -99,7 +99,19 @@ namespace BrewMonster.Scripts } Debug.Log("[Cuong] AddressableManager: Bootstrap gate xong — đang InitializeAsync Addressables..."); - Addressables.InitializeAsync().Completed += OnInitializeComplete; + 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}"); + } } async UniTask WaitForBootstrapGateWithTimeoutAsync() @@ -142,23 +154,6 @@ namespace BrewMonster.Scripts #endregion #region private functions - private void OnInitializeComplete(AsyncOperationHandle handle) - { - if (handle.Status == AsyncOperationStatus.Succeeded) - { - _isInitialized = true; - _initializationTcs.TrySetResult(); - BMLogger.Log($"AddressableManager: Initialized"); - Debug.Log("[Cuong] AddressableManager: InitializeAsync xong — sẵn sàng load asset."); - } - else - { - // print out the error - BMLogger.LogError($"AddressableManager: Failed to initialize: {handle.OperationException?.Message} {handle.OperationException?.StackTrace}"); - Debug.LogError($"[Cuong] AddressableManager: InitializeAsync thất bại — {handle.OperationException?.Message}"); - } - } - private void RemoveFromReleaseAssetDictionary(string assetPath) { if (_releaseAssetTimestamps.ContainsKey(assetPath)) diff --git a/Assets/PerfectWorld/Scripts/Addressable/AddressablesCatalogUpdater.cs b/Assets/PerfectWorld/Scripts/Addressable/AddressablesCatalogUpdater.cs index faac4dc07d..56f29e5d33 100644 --- a/Assets/PerfectWorld/Scripts/Addressable/AddressablesCatalogUpdater.cs +++ b/Assets/PerfectWorld/Scripts/Addressable/AddressablesCatalogUpdater.cs @@ -55,7 +55,7 @@ namespace BrewMonster.Scripts /// public static async UniTask EnsureInitializedAsync() { - await Addressables.InitializeAsync().ToUniTask(); + await AddressablesInitService.EnsureInitializedAsync(); } /// diff --git a/Assets/PerfectWorld/Scripts/Addressable/AddressablesInitService.cs b/Assets/PerfectWorld/Scripts/Addressable/AddressablesInitService.cs new file mode 100644 index 0000000000..9b449f590d --- /dev/null +++ b/Assets/PerfectWorld/Scripts/Addressable/AddressablesInitService.cs @@ -0,0 +1,92 @@ +using System; +using Cysharp.Threading.Tasks; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.ResourceManagement.AsyncOperations; + +namespace BrewMonster.Scripts +{ + /// + /// Single entry for after gate + /// (version fetch + optional ). Prevents early init from UI code + /// (e.g. AUIManager) hitting remote URLs before CDN rewrite. + /// + public static class AddressablesInitService + { + static readonly object s_lock = new(); + static UniTaskCompletionSource s_initTcs; + static bool s_initialized; + + public static bool IsInitialized => s_initialized; + + /// + /// Waits bootstrap gate, then initializes Addressables once. + /// + public static async UniTask EnsureInitializedAsync() + { + if (s_initialized) + return; + + UniTaskCompletionSource waiter; + lock (s_lock) + { + if (s_initialized) + return; + + if (s_initTcs != null) + { + waiter = s_initTcs; + } + else + { + waiter = new UniTaskCompletionSource(); + s_initTcs = waiter; + } + } + + if (waiter != s_initTcs) + { + await waiter.Task; + return; + } + + try + { + await GameContentBootstrap.WaitForPreAddressablesSetupIfAnyAsync(); + Debug.Log("[Cuong] AddressablesInitService: Bootstrap gate OK — InitializeAsync..."); + + var handle = Addressables.InitializeAsync(); + await handle.ToUniTask(); + + if (handle.Status != AsyncOperationStatus.Succeeded) + { + var msg = handle.OperationException?.Message ?? "InitializeAsync failed"; + waiter.TrySetException(new InvalidOperationException(msg)); + lock (s_lock) + s_initTcs = null; + throw handle.OperationException ?? new InvalidOperationException(msg); + } + + s_initialized = true; + waiter.TrySetResult(true); + Debug.Log("[Cuong] AddressablesInitService: InitializeAsync xong."); + } + catch (Exception ex) + { + if (!waiter.Task.Status.IsCompleted()) + waiter.TrySetException(ex); + lock (s_lock) + s_initTcs = null; + throw; + } + } + + /// + /// Sync wait for legacy Init() paths. Prefer async when possible. + /// + public static void EnsureInitializedBlocking() + { + EnsureInitializedAsync().GetAwaiter().GetResult(); + } + } +} diff --git a/Assets/PerfectWorld/Scripts/Addressable/GameContentBootstrap.cs b/Assets/PerfectWorld/Scripts/Addressable/GameContentBootstrap.cs index 92bb05fbc3..de2cdf60c3 100644 --- a/Assets/PerfectWorld/Scripts/Addressable/GameContentBootstrap.cs +++ b/Assets/PerfectWorld/Scripts/Addressable/GameContentBootstrap.cs @@ -236,7 +236,15 @@ namespace BrewMonster.Scripts AddressablesRuntimeUrlRewriter.InstallPrefixRewrite( _bakedRemoteUrlPrefixForRewrite, fetch.AssetsBaseUrl); - Debug.Log("[Cuong] GameContentBootstrap: Đã gắn URL rewrite cho remote CDN."); + Debug.Log( + $"[Cuong] GameContentBootstrap: URL rewrite | from={_bakedRemoteUrlPrefixForRewrite.Trim()} → to={fetch.AssetsBaseUrl.Trim()}"); + } + else if (!string.IsNullOrWhiteSpace(fetch.AssetsBaseUrl) && + string.IsNullOrWhiteSpace(_bakedRemoteUrlPrefixForRewrite)) + { + Debug.LogWarning( + "[Cuong] GameContentBootstrap: assetsBaseUrl có nhưng _bakedRemoteUrlPrefixForRewrite trống — " + + "catalog/bundle vẫn dùng URL bake trong build (vd CDN cũ). Điền prefix URL lúc build."); } AllowAddressablesInit("version-and-url-ready"); @@ -264,8 +272,18 @@ namespace BrewMonster.Scripts var catalog = await AddressablesCatalogUpdater.CheckAndUpdateCatalogsIfNeededAsync(false); if (!catalog.Success) { - Debug.LogError($"[Cuong] GameContentBootstrap: Catalog update thất bại — {catalog.Error?.Message}"); - return new BootstrapResult(false, true, catalog.Error?.Message ?? "Catalog update failed", fetch.ContentVersion); + var errMsg = catalog.Error?.Message ?? "Catalog update failed"; + Debug.LogError($"[Cuong] GameContentBootstrap: Catalog update thất bại — {errMsg}"); + if (IsLikelyRemoteTlsOrNetworkError(errMsg)) + { + Debug.LogError( + "[Cuong] GameContentBootstrap: Gợi ý — SSL CA / mạng trên mobile. " + + "Kiểm tra chuỗi chứng chỉ CDN; đảm bảo _bakedRemoteUrlPrefixForRewrite khớp host trong catalog build " + + "(vd https://prefect-world-asset....wcsapi.com/) và assetsBaseUrl trỏ CDN HTTPS hợp lệ. " + + "Không gọi Addressables.InitializeAsync trước bootstrap (AUIManager đã dùng AddressablesInitService)."); + } + + return new BootstrapResult(false, true, errMsg, fetch.ContentVersion); } Debug.Log("[Cuong] GameContentBootstrap: Catalog OK."); @@ -402,6 +420,16 @@ namespace BrewMonster.Scripts return s.Trim(); } + static bool IsLikelyRemoteTlsOrNetworkError(string message) + { + if (string.IsNullOrEmpty(message)) + return false; + return message.IndexOf("SSL", StringComparison.OrdinalIgnoreCase) >= 0 || + message.IndexOf("certificate", StringComparison.OrdinalIgnoreCase) >= 0 || + message.IndexOf("ConnectionError", StringComparison.OrdinalIgnoreCase) >= 0 || + message.IndexOf("unable to load from url", StringComparison.OrdinalIgnoreCase) >= 0; + } + public readonly struct BootstrapResult { public BootstrapResult(bool success, bool didContentWork, string errorMessage, string serverContentVersion) diff --git a/Assets/PerfectWorld/Scripts/UI/AUIManager.cs b/Assets/PerfectWorld/Scripts/UI/AUIManager.cs index 17c2395458..5c6378d8e3 100644 --- a/Assets/PerfectWorld/Scripts/UI/AUIManager.cs +++ b/Assets/PerfectWorld/Scripts/UI/AUIManager.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using BrewMonster.Scripts; using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.ResourceManagement.AsyncOperations; @@ -282,8 +283,8 @@ namespace BrewMonster.UI { try { - // Initialize Addressables if not already initialized (Unity-safe) - Addressables.InitializeAsync().WaitForCompletion(); + // Wait for GameContentBootstrap gate + URL rewrite, then single shared init. + AddressablesInitService.EnsureInitializedBlocking(); // Load using Addressables directly with WaitForCompletion (Unity-safe, won't deadlock) // This matches the pattern used in EC_Game.cs