using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.AddressableAssets.ResourceLocators; using UnityEngine.ResourceManagement.AsyncOperations; public static class DlcRemoteContentService { private sealed class RuntimeLoadedState { public readonly List retainedAssetHandles = new List(); public readonly List retainedCatalogHandles = new List(); public readonly List packages = new List(); } private sealed class DlcRemoteContentRunner : MonoBehaviour { private void Start() { if (DlcRemoteManifestSyncService.ShouldDeferInitialRefresh()) { return; } if (DlcRemoteContentService.autoRefreshOnStart) { DlcRemoteContentService.RefreshInstalledDlc(); } } } private static DlcRemoteContentRunner runner; private static RuntimeLoadedState activeState = new RuntimeLoadedState(); private static bool autoRefreshOnStart = true; private static bool refreshRequestedOnBoot; public static bool IsRefreshing { get; private set; } public static event Action RefreshCompleted; [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] private static void EnsureRunner() { if (runner != null) { return; } GameObject go = new GameObject(nameof(DlcRemoteContentService)); UnityEngine.Object.DontDestroyOnLoad(go); runner = go.AddComponent(); refreshRequestedOnBoot = false; } public static void SetAutoRefreshOnStart(bool enabled) { autoRefreshOnStart = enabled; } public static void RefreshInstalledDlc() { EnsureRunner(); if (runner == null) { return; } if (IsRefreshing) { refreshRequestedOnBoot = true; return; } runner.StartCoroutine(RefreshInstalledDlcRoutine()); } public static void ClearRuntimeDlc() { ReleaseState(activeState); activeState = new RuntimeLoadedState(); DlcRuntimeRegistry.ClearAll(); InvalidateRuntimeCaches(); } private static IEnumerator RefreshInstalledDlcRoutine() { if (IsRefreshing) { yield break; } IsRefreshing = true; refreshRequestedOnBoot = false; List manifests = DlcManifestService.LoadInstalledEntries(); RuntimeLoadedState nextState = new RuntimeLoadedState(); bool success = true; for (int i = 0; i < manifests.Count; i++) { DlcManifestRuntimeEntry manifest = manifests[i]; if (manifest == null || manifest.entry == null) { continue; } yield return LoadManifestEntry(manifest, nextState, result => success &= result); } if (success) { RuntimeLoadedState oldState = activeState; activeState = nextState; DlcRuntimeRegistry.ReplaceAll(nextState.packages); InvalidateRuntimeCaches(); ReleaseState(oldState); } else { ReleaseState(nextState); } IsRefreshing = false; RefreshCompleted?.Invoke(success); if (refreshRequestedOnBoot) { RefreshInstalledDlc(); } } private static IEnumerator LoadManifestEntry( DlcManifestRuntimeEntry manifest, RuntimeLoadedState state, Action reportResult) { bool localSuccess = true; AsyncOperationHandle catalogHandle = Addressables.LoadContentCatalogAsync(manifest.resolvedCatalogPath, false); yield return catalogHandle; if (catalogHandle.Status != AsyncOperationStatus.Succeeded) { Debug.LogWarning("[DLC] Failed to load catalog: " + manifest.resolvedCatalogPath); reportResult?.Invoke(false); yield break; } state.retainedCatalogHandles.Add(catalogHandle); List loadedDlcs = new List(); List loadedSongs = new List(); List loadedSongContents = new List(); List loadedHeroSkins = new List(); List loadedHeroes = new List(); // Load the lightweight DLC metadata first so we can decide ownership // before pulling (or even downloading) any heavy content. yield return LoadAssetsByLabels(manifest.entry.GetDlcDataLabels(), loadedDlcs, state.retainedAssetHandles, result => localSuccess &= result); bool owned = IsManifestEntryOwned(manifest, loadedDlcs); if (owned) { if (manifest.entry.autoDownloadDependencies) { string[] dependencyKeys = manifest.entry.GetDependencyKeys(); for (int i = 0; i < dependencyKeys.Length; i++) { string dependencyKey = dependencyKeys[i]; if (string.IsNullOrWhiteSpace(dependencyKey)) { continue; } AsyncOperationHandle downloadHandle = Addressables.DownloadDependenciesAsync(dependencyKey, false); yield return downloadHandle; if (downloadHandle.Status == AsyncOperationStatus.Succeeded) { state.retainedAssetHandles.Add(downloadHandle); } else { localSuccess = false; Debug.LogWarning("[DLC] Failed to download dependencies for key '" + dependencyKey + "'."); Addressables.Release(downloadHandle); } } } yield return LoadAssetsByLabels(manifest.entry.GetSongLabels(), loadedSongs, state.retainedAssetHandles, result => localSuccess &= result); yield return LoadAssetsByLabels(manifest.entry.GetSongContentLabels(), loadedSongContents, state.retainedAssetHandles, result => localSuccess &= result); yield return LoadAssetsByLabels(manifest.entry.GetHeroSkinLabels(), loadedHeroSkins, state.retainedAssetHandles, result => localSuccess &= result); yield return LoadAssetsByLabels(manifest.entry.GetHeroLabels(), loadedHeroes, state.retainedAssetHandles, result => localSuccess &= result); } else { Debug.Log("[DLC] Skipping content load for unowned DLC '" + manifest.entry.GetSafeDlcKey() + "'. Only metadata is registered."); } DlcRuntimePackage package = new DlcRuntimePackage { packageId = manifest.GetRuntimePackageId(), dlcKey = manifest.entry.GetSafeDlcKey(), version = manifest.entry.version ?? string.Empty, sourceCatalogPath = manifest.resolvedCatalogPath, dlcs = loadedDlcs.ToArray(), songs = loadedSongs.ToArray(), songContents = loadedSongContents.ToArray(), heroSkins = loadedHeroSkins.ToArray(), heroes = loadedHeroes.ToArray() }; state.packages.Add(package); reportResult?.Invoke(localSuccess); } private static bool IsManifestEntryOwned(DlcManifestRuntimeEntry manifest, List loadedDlcs) { // If any loaded dlcData asset reports owned, the package is owned. A DLC that // does not enforce entitlement (builtInContent / requiresOwnership == false) // is always owned via DlcOwnershipService. if (loadedDlcs != null) { bool anyEnforced = false; for (int i = 0; i < loadedDlcs.Count; i++) { dlcData dlc = loadedDlcs[i]; if (dlc == null) { continue; } anyEnforced = true; if (DlcOwnershipService.IsDlcOwned(dlc)) { return true; } } if (anyEnforced) { return false; } } // No dlcData asset was found for this manifest entry. Fall back to the manifest // key so a remote/local ownership flag can still gate the content. string key = manifest != null && manifest.entry != null ? manifest.entry.GetSafeDlcKey() : null; if (string.IsNullOrWhiteSpace(key)) { return true; } return DlcOwnershipService.IsDlcOwned(key); } private static IEnumerator LoadAssetsByLabels( string[] labels, List output, List retainedHandles, Action reportResult) where T : UnityEngine.Object { if (labels == null || labels.Length == 0) { reportResult?.Invoke(true); yield break; } List keys = new List(labels.Length); for (int i = 0; i < labels.Length; i++) { if (!string.IsNullOrWhiteSpace(labels[i])) { keys.Add(labels[i].Trim()); } } if (keys.Count == 0) { reportResult?.Invoke(true); yield break; } AsyncOperationHandle> loadHandle = Addressables.LoadAssetsAsync(keys, null, Addressables.MergeMode.Union, false); yield return loadHandle; if (loadHandle.Status != AsyncOperationStatus.Succeeded) { Debug.LogWarning("[DLC] Failed to load runtime assets of type " + typeof(T).Name + "."); Addressables.Release(loadHandle); reportResult?.Invoke(false); yield break; } retainedHandles.Add(loadHandle); IList result = loadHandle.Result; if (result != null) { for (int i = 0; i < result.Count; i++) { T asset = result[i]; if (asset != null && !output.Contains(asset)) { output.Add(asset); } } } reportResult?.Invoke(true); } private static void InvalidateRuntimeCaches() { RuntimeResourcesCache.InvalidateAll(); SongDlcContentResolver.InvalidateCache(); HeroSkinResolver.InvalidateCache(); DlcOwnershipService.InvalidateCache(); SongDataLibrary library = SongDataLibrary.Instance; if (library != null) { library.RefreshLibrary(); } loadDlcListPrefab[] uiPanels = UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None); for (int i = 0; i < uiPanels.Length; i++) { if (uiPanels[i] != null && uiPanels[i].isActiveAndEnabled) { uiPanels[i].RefreshUI(); } } } private static void ReleaseState(RuntimeLoadedState state) { if (state == null) { return; } for (int i = 0; i < state.retainedAssetHandles.Count; i++) { AsyncOperationHandle handle = state.retainedAssetHandles[i]; if (handle.IsValid()) { Addressables.Release(handle); } } state.retainedAssetHandles.Clear(); for (int i = 0; i < state.retainedCatalogHandles.Count; i++) { AsyncOperationHandle handle = state.retainedCatalogHandles[i]; if (handle.IsValid()) { Addressables.Release(handle); } } state.retainedCatalogHandles.Clear(); state.packages.Clear(); } }