using System;
using System.Collections.Generic;
using BrewMonster;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.AddressableAssets.ResourceLocators;
using UnityEngine.ResourceManagement.AsyncOperations;
namespace BrewMonster.Scripts
{
///
/// Remote catalog / bundle flows: CheckForCatalogUpdates, UpdateCatalogs, optional download size and dependencies.
/// Matches the “catalog new → bundle new / cache” behaviour described in project docs (Addressables overview).
///
public static class AddressablesCatalogUpdater
{
///
/// Result of checking the remote catalog hash list (may be empty when nothing changed).
///
public readonly struct CatalogCheckResult
{
public CatalogCheckResult(bool success, IReadOnlyList catalogsWithUpdates, Exception error)
{
Success = success;
CatalogsWithUpdates = catalogsWithUpdates ?? Array.Empty();
Error = error;
}
public bool Success { get; }
public IReadOnlyList CatalogsWithUpdates { get; }
public bool HasUpdates => CatalogsWithUpdates.Count > 0;
public Exception Error { get; }
}
///
/// Result after applying .
///
public readonly struct CatalogApplyResult
{
public CatalogApplyResult(bool success, IReadOnlyList locators, Exception error)
{
Success = success;
Locators = locators ?? Array.Empty();
Error = error;
}
public bool Success { get; }
public IReadOnlyList Locators { get; }
public Exception Error { get; }
}
///
/// Ensures Addressables finished initial catalog load before checking remote updates.
///
public static async UniTask EnsureInitializedAsync()
{
await AddressablesInitService.EnsureInitializedAsync();
}
///
/// Calls — lightweight compared to full bundle downloads.
///
/// Pass through to Addressables (default true).
public static async UniTask CheckForCatalogUpdatesAsync(bool autoReleaseHandle = true)
{
AsyncOperationHandle> handle = default;
try
{
await EnsureInitializedAsync();
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;
BMLogger.LogError($"AddressablesCatalogUpdater: CheckForCatalogUpdates failed: {err?.Message}");
return new CatalogCheckResult(false, null, err);
}
var list = handle.Result;
var ids = list != null ? (IReadOnlyList)list : Array.Empty();
return new CatalogCheckResult(true, ids, null);
}
catch (Exception e)
{
BMLogger.LogError($"AddressablesCatalogUpdater: CheckForCatalogUpdates exception: {e.Message}");
return new CatalogCheckResult(false, null, e);
}
finally
{
ReleaseHandleIfNeeded(handle, autoReleaseHandle: false);
}
}
///
/// Downloads new catalog JSON and refreshes locators. Pass from
/// or null to use Addressables’ internal list.
///
/// When true, removes unreferenced bundles after update (see Addressables docs).
public static async UniTask UpdateCatalogsAsync(
IEnumerable catalogIds = null,
bool autoCleanBundleCache = false,
bool autoReleaseHandle = true)
{
AsyncOperationHandle> handle = default;
try
{
await EnsureInitializedAsync();
if (autoCleanBundleCache)
handle = Addressables.UpdateCatalogs(true, catalogIds, autoReleaseHandle: false);
else
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;
BMLogger.LogError($"AddressablesCatalogUpdater: UpdateCatalogs failed: {err?.Message}");
return new CatalogApplyResult(false, null, err);
}
var locators = handle.Result;
var read = locators != null ? (IReadOnlyList)locators : Array.Empty();
return new CatalogApplyResult(true, read, null);
}
catch (Exception e)
{
BMLogger.LogError($"AddressablesCatalogUpdater: UpdateCatalogs exception: {e.Message}");
return new CatalogApplyResult(false, null, e);
}
finally
{
ReleaseHandleIfNeeded(handle, autoReleaseHandle: false);
}
}
///
/// Check remote catalog, then apply updates if any catalog ids were returned.
///
public static async UniTask CheckAndUpdateCatalogsIfNeededAsync(bool autoCleanBundleCache = false)
{
var check = await CheckForCatalogUpdatesAsync(true);
if (!check.Success)
return new CatalogApplyResult(false, null, check.Error);
if (!check.HasUpdates)
return new CatalogApplyResult(true, Array.Empty(), null);
return await UpdateCatalogsAsync(check.CatalogsWithUpdates, autoCleanBundleCache, true);
}
///
/// Bytes that would be downloaded for given the current catalog and local cache (0 if fully cached).
///
public static async UniTask GetDownloadSizeBytesAsync(object key)
{
AsyncOperationHandle handle = default;
try
{
await EnsureInitializedAsync();
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}");
return -1;
}
return handle.Result;
}
catch (Exception e)
{
BMLogger.LogError($"AddressablesCatalogUpdater: GetDownloadSizeBytesAsync exception: {e.Message}");
return -1;
}
finally
{
ReleaseHandleIfNeeded(handle, autoReleaseHandle: false);
}
}
///
/// Ensures dependencies for exist on disk; uses cache when CRC/catalog match (no redundant full download).
/// 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,
int progressLogStepPercent = 5)
{
AsyncOperationHandle handle = default;
try
{
await EnsureInitializedAsync();
var keyLabel = key?.ToString() ?? "(null)";
var step = Mathf.Clamp(progressLogStepPercent, 1, 100);
Debug.Log($"[Cuong] AddressablesCatalogUpdater: Bắt đầu tải dependencies (key={keyLabel})...");
// Never auto-release while polling; caller passing true is ignored to avoid invalid-handle errors.
handle = Addressables.DownloadDependenciesAsync(key, autoReleaseHandle: false);
var lastLoggedPercent = -1;
if (handle.IsValid())
LogDownloadProgress(keyLabel, handle, ref lastLoggedPercent, step, force: true);
while (!handle.IsDone)
{
if (handle.IsValid())
LogDownloadProgress(keyLabel, handle, ref lastLoggedPercent, step);
await UniTask.Yield();
}
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}");
Debug.LogError($"[Cuong] AddressablesCatalogUpdater: Tải thất bại '{keyLabel}' — {handle.OperationException?.Message}");
return false;
}
Debug.Log($"[Cuong] AddressablesCatalogUpdater: Tải xong dependencies (key={keyLabel}).");
return true;
}
catch (Exception e)
{
BMLogger.LogError($"AddressablesCatalogUpdater: DownloadDependenciesAsync exception: {e.Message}");
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(
string keyLabel,
AsyncOperationHandle handle,
ref int lastLoggedPercent,
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);
if (!force)
{
if (percentRounded <= lastLoggedPercent)
return;
if (percentRounded < 100 && percentRounded - lastLoggedPercent < stepPercent)
return;
}
lastLoggedPercent = percentRounded;
if (status.TotalBytes > 0)
{
var downloadedMb = status.DownloadedBytes / (1024f * 1024f);
var totalMb = status.TotalBytes / (1024f * 1024f);
Debug.Log(
$"[Cuong] AddressablesCatalogUpdater: {percent:F0}% ({downloadedMb:F1}/{totalMb:F1} MB) — key={keyLabel}");
}
else
{
Debug.Log($"[Cuong] AddressablesCatalogUpdater: {percent:F0}% — key={keyLabel}");
}
}
///
/// Removes bundles not referenced by the given catalogs (optional maintenance / “clear old cache”).
///
public static async UniTask CleanBundleCacheAsync(IEnumerable catalogIds = null)
{
AsyncOperationHandle handle = default;
try
{
await EnsureInitializedAsync();
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}");
return false;
}
return handle.Result;
}
catch (Exception e)
{
BMLogger.LogError($"AddressablesCatalogUpdater: CleanBundleCacheAsync exception: {e.Message}");
return false;
}
finally
{
ReleaseHandleIfNeeded(handle, autoReleaseHandle: false);
}
}
}
}