Files
test/Assets/PerfectWorld/Scripts/Addressable/AddressablesCatalogUpdater.cs
T
2026-05-21 10:42:51 +07:00

271 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 Addressables.InitializeAsync().ToUniTask();
}
/// <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)
{
try
{
await EnsureInitializedAsync();
var handle = Addressables.CheckForCatalogUpdates(autoReleaseHandle);
await handle.ToUniTask();
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);
}
}
/// <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)
{
try
{
await EnsureInitializedAsync();
AsyncOperationHandle<List<IResourceLocator>> handle;
if (autoCleanBundleCache)
handle = Addressables.UpdateCatalogs(true, catalogIds, autoReleaseHandle);
else
handle = Addressables.UpdateCatalogs(catalogIds, autoReleaseHandle);
await handle.ToUniTask();
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);
}
}
/// <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)
{
try
{
await EnsureInitializedAsync();
var handle = Addressables.GetDownloadSizeAsync(key);
await handle.ToUniTask();
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;
}
}
/// <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 (1100). Default 5.</param>
public static async UniTask<bool> DownloadDependenciesAsync(
object key,
bool autoReleaseHandle = true,
int progressLogStepPercent = 5)
{
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})...");
var handle = Addressables.DownloadDependenciesAsync(key, autoReleaseHandle);
var lastLoggedPercent = -1;
LogDownloadProgress(keyLabel, handle, ref lastLoggedPercent, step, force: true);
while (!handle.IsDone)
{
LogDownloadProgress(keyLabel, handle, ref lastLoggedPercent, step);
await UniTask.Yield();
}
LogDownloadProgress(keyLabel, handle, ref lastLoggedPercent, step, force: true);
await handle.ToUniTask();
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;
}
}
static void LogDownloadProgress(
string keyLabel,
AsyncOperationHandle handle,
ref int lastLoggedPercent,
int stepPercent,
bool force = false)
{
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)
{
try
{
await EnsureInitializedAsync();
var handle = Addressables.CleanBundleCache(catalogIds);
await handle.ToUniTask();
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;
}
}
}
}