Files
bansonic_beta_main/Assets/scripts/_runtimecache/DlcRemoteContentService.cs
T
2026-06-30 21:21:30 +08:00

370 lines
12 KiB
C#

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<AsyncOperationHandle> retainedAssetHandles = new List<AsyncOperationHandle>();
public readonly List<AsyncOperationHandle> retainedCatalogHandles = new List<AsyncOperationHandle>();
public readonly List<DlcRuntimePackage> packages = new List<DlcRuntimePackage>();
}
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<bool> 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<DlcRemoteContentRunner>();
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<DlcManifestRuntimeEntry> 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<bool> reportResult)
{
bool localSuccess = true;
AsyncOperationHandle<IResourceLocator> 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<dlcData> loadedDlcs = new List<dlcData>();
List<SongData> loadedSongs = new List<SongData>();
List<SongDlcContentSO> loadedSongContents = new List<SongDlcContentSO>();
List<HeroSkinSO> loadedHeroSkins = new List<HeroSkinSO>();
List<AllyHero_SO> loadedHeroes = new List<AllyHero_SO>();
// 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<dlcData> 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<T>(
string[] labels,
List<T> output,
List<AsyncOperationHandle> retainedHandles,
Action<bool> reportResult) where T : UnityEngine.Object
{
if (labels == null || labels.Length == 0)
{
reportResult?.Invoke(true);
yield break;
}
List<object> keys = new List<object>(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<IList<T>> loadHandle = Addressables.LoadAssetsAsync<T>(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<T> 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<loadDlcListPrefab>(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();
}
}