fix download addressable remote in mobile

This commit is contained in:
NguyenVanDat
2026-05-21 16:55:42 +07:00
parent 84b84567c6
commit 5affac9cbd
3 changed files with 257 additions and 31 deletions
@@ -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
{
/// <summary>Runs after <see cref="GameContentBootstrap"/> (-2000) so bootstrap Awake creates the gate first.</summary>
[DefaultExecutionOrder(-1990)]
public class AddressableManager : MonoSingleton<AddressableManager>
{
private bool _isInitialized = false;
@@ -31,6 +35,14 @@ namespace BrewMonster.Scripts
private Dictionary<string, float> _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;
/// <summary>Get the count of currently loaded assets.</summary>
@@ -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
/// </summary>
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<bool> 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<string> _assetToForceRelease = new();
private void Update()
@@ -64,11 +64,20 @@ namespace BrewMonster.Scripts
/// <param name="autoReleaseHandle">Pass through to Addressables (default true).</param>
public static async UniTask<CatalogCheckResult> CheckForCatalogUpdatesAsync(bool autoReleaseHandle = true)
{
AsyncOperationHandle<List<string>> 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);
}
}
/// <summary>
@@ -97,16 +110,24 @@ namespace BrewMonster.Scripts
bool autoCleanBundleCache = false,
bool autoReleaseHandle = true)
{
AsyncOperationHandle<List<IResourceLocator>> handle = default;
try
{
await EnsureInitializedAsync();
AsyncOperationHandle<List<IResourceLocator>> 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);
}
}
/// <summary>
@@ -144,11 +169,19 @@ namespace BrewMonster.Scripts
/// </summary>
public static async UniTask<long> GetDownloadSizeBytesAsync(object key)
{
AsyncOperationHandle<long> 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);
}
}
/// <summary>
@@ -168,11 +205,15 @@ namespace BrewMonster.Scripts
/// Logs download percent (and MB when known) every <paramref name="progressLogStepPercent"/> percent via <c>[Cuong]</c>.
/// </summary>
/// <param name="progressLogStepPercent">Log when percent crosses each step (1100). Default 5.</param>
/// <remarks>
/// Handle is always retained until this method finishes (autoReleaseHandle forced false) so progress polling
/// does not touch a handle Addressables already released on completion.
/// </remarks>
public static async UniTask<bool> 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
/// </summary>
public static async UniTask<bool> CleanBundleCacheAsync(IEnumerable<string> catalogIds = null)
{
AsyncOperationHandle<bool> 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);
}
}
}
}
@@ -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, <see cref="AddressableManager"/> waits on this before <see cref="UnityEngine.AddressableAssets.Addressables.InitializeAsync"/>.
/// </summary>
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<BootstrapResult> Finished;
public static UniTask WaitForPreAddressablesSetupIfAnyAsync()
/// <summary>Trạng thái gate tĩnh — dùng log chẩn đoán từ <see cref="AddressableManager"/>.</summary>
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;
}
/// <summary>