Files
test/Assets/PerfectWorld/Scripts/Addressable/AddressablesCatalogUpdater.cs
T
2026-05-21 17:18:28 +07:00

350 lines
15 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 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 (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,
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);
}
}
}
}