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); } } } }