350 lines
15 KiB
C#
350 lines
15 KiB
C#
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
|
||
{
|
||
/// <summary>
|
||
/// 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).
|
||
/// </summary>
|
||
public static class AddressablesCatalogUpdater
|
||
{
|
||
/// <summary>
|
||
/// Result of checking the remote catalog hash list (may be empty when nothing changed).
|
||
/// </summary>
|
||
public readonly struct CatalogCheckResult
|
||
{
|
||
public CatalogCheckResult(bool success, IReadOnlyList<string> catalogsWithUpdates, Exception error)
|
||
{
|
||
Success = success;
|
||
CatalogsWithUpdates = catalogsWithUpdates ?? Array.Empty<string>();
|
||
Error = error;
|
||
}
|
||
|
||
public bool Success { get; }
|
||
public IReadOnlyList<string> CatalogsWithUpdates { get; }
|
||
public bool HasUpdates => CatalogsWithUpdates.Count > 0;
|
||
public Exception Error { get; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// Result after applying <see cref="Addressables.UpdateCatalogs"/>.
|
||
/// </summary>
|
||
public readonly struct CatalogApplyResult
|
||
{
|
||
public CatalogApplyResult(bool success, IReadOnlyList<IResourceLocator> locators, Exception error)
|
||
{
|
||
Success = success;
|
||
Locators = locators ?? Array.Empty<IResourceLocator>();
|
||
Error = error;
|
||
}
|
||
|
||
public bool Success { get; }
|
||
public IReadOnlyList<IResourceLocator> Locators { get; }
|
||
public Exception Error { get; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// Ensures Addressables finished initial catalog load before checking remote updates.
|
||
/// </summary>
|
||
public static async UniTask EnsureInitializedAsync()
|
||
{
|
||
await AddressablesInitService.EnsureInitializedAsync();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Calls <see cref="Addressables.CheckForCatalogUpdates"/> — lightweight compared to full bundle downloads.
|
||
/// </summary>
|
||
/// <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();
|
||
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<string>)list : Array.Empty<string>();
|
||
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);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Downloads new catalog JSON and refreshes locators. Pass <paramref name="catalogIds"/> from
|
||
/// <see cref="CatalogCheckResult.CatalogsWithUpdates"/> or null to use Addressables’ internal list.
|
||
/// </summary>
|
||
/// <param name="autoCleanBundleCache">When true, removes unreferenced bundles after update (see Addressables docs).</param>
|
||
public static async UniTask<CatalogApplyResult> UpdateCatalogsAsync(
|
||
IEnumerable<string> catalogIds = null,
|
||
bool autoCleanBundleCache = false,
|
||
bool autoReleaseHandle = true)
|
||
{
|
||
AsyncOperationHandle<List<IResourceLocator>> 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<IResourceLocator>)locators : Array.Empty<IResourceLocator>();
|
||
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);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Check remote catalog, then apply updates if any catalog ids were returned.
|
||
/// </summary>
|
||
public static async UniTask<CatalogApplyResult> 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<IResourceLocator>(), null);
|
||
|
||
return await UpdateCatalogsAsync(check.CatalogsWithUpdates, autoCleanBundleCache, true);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Bytes that would be downloaded for <paramref name="key"/> given the current catalog and local cache (0 if fully cached).
|
||
/// </summary>
|
||
public static async UniTask<long> GetDownloadSizeBytesAsync(object key)
|
||
{
|
||
AsyncOperationHandle<long> 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);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Ensures dependencies for <paramref name="key"/> exist on disk; uses cache when CRC/catalog match (no redundant full download).
|
||
/// 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 (1–100). 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,
|
||
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}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Removes bundles not referenced by the given catalogs (optional maintenance / “clear old cache”).
|
||
/// </summary>
|
||
public static async UniTask<bool> CleanBundleCacheAsync(IEnumerable<string> catalogIds = null)
|
||
{
|
||
AsyncOperationHandle<bool> 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);
|
||
}
|
||
}
|
||
}
|
||
}
|