// ============================================================================
// YooAssetService.cs — YooAsset 封装服务
// 实现 IYooAssetService 和 IYooAssetBridge,替代 AssetBundleUtility
// ============================================================================
using System;
using System.Collections.Generic;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.SceneManagement;
using YooAsset;
namespace ProjSG.Resource
{
///
/// YooAsset 资源加载服务单例。
/// 封装 YooAsset ResourcePackage 的核心加载能力,提供 UniTask 异步 API。
/// 同时实现 IYooAssetBridge 供 Launch 程序集跨程序集调用。
///
public class YooAssetService : Singleton, IYooAssetService, IYooAssetBridge
{
private readonly Dictionary _packages = new Dictionary();
private ResourcePackage _defaultPackage;
private IRemoteServices _remoteServices;
private bool _isInitialized;
private EPlayMode _playMode;
// ====================================================================
// IYooAssetService Properties
// ====================================================================
///
public bool IsInitialized => _isInitialized;
///
public EPlayMode PlayMode => _playMode;
// ====================================================================
// IYooAssetBridge Properties
// ====================================================================
bool IYooAssetBridge.IsRegistered => _isInitialized;
// ====================================================================
// Initialization
// ====================================================================
///
public async UniTask InitializeAsync(EPlayMode playMode, IRemoteServices remoteServices = null)
{
if (_isInitialized)
{
Debug.LogWarning("[YooAssetService] Already initialized.");
return;
}
_playMode = playMode;
_remoteServices = remoteServices;
// YooAsset 全局初始化(幂等操作)
YooAssets.Initialize();
// 初始化所有配置中的包裹
foreach (var pkgName in YooAssetPackageConfig.AllPackages)
{
try
{
// 优先复用 Launch 阶段已创建的包裹
var package = YooAssets.TryGetPackage(pkgName);
if (package != null)
{
Debug.Log($"[YooAssetService] Reusing existing package '{pkgName}' from YooAssetInitializer");
}
else
{
// 自行创建并初始化(首次启动或该包未在 Launch 阶段创建)
package = YooAssets.CreatePackage(pkgName);
var initParams = CreateInitParameters(playMode, remoteServices, pkgName);
var initOp = package.InitializeAsync(initParams);
await initOp.ToUniTask();
if (initOp.Status != EOperationStatus.Succeed)
{
Debug.LogWarning($"[YooAssetService] Package '{pkgName}' init failed: {initOp.Error}");
continue;
}
Debug.Log($"[YooAssetService] Package '{pkgName}' newly initialized.");
}
_packages[pkgName] = package;
// 设置默认包
if (_defaultPackage == null || pkgName == YooAssetPackageConfig.DefaultPackage)
{
_defaultPackage = package;
YooAssets.SetDefaultPackage(package);
}
}
catch (Exception ex)
{
// EditorSimulateMode 下包不在 Collector 中会抛异常,跳过
Debug.LogWarning($"[YooAssetService] Package '{pkgName}' init exception (skipped): {ex.Message}");
}
}
if (_defaultPackage == null)
{
Debug.LogError("[YooAssetService] No packages initialized successfully!");
throw new InvalidOperationException("YooAsset initialization failed: no packages available.");
}
_isInitialized = true;
Debug.Log($"[YooAssetService] Initialized {_packages.Count}/{YooAssetPackageConfig.AllPackages.Length} packages with PlayMode={playMode}");
}
///
/// 初始化指定名称的额外资源包裹。
///
public async UniTask InitializePackageAsync(string packageName, EPlayMode playMode,
IRemoteServices remoteServices = null)
{
if (_packages.ContainsKey(packageName))
{
Debug.LogWarning($"[YooAssetService] Package '{packageName}' already initialized.");
return;
}
// 优先复用已存在的包裹(可能由 Launch 阶段创建)
var package = YooAssets.TryGetPackage(packageName);
if (package == null)
{
package = YooAssets.CreatePackage(packageName);
var initParams = CreateInitParameters(playMode, remoteServices ?? _remoteServices, packageName);
var initOp = package.InitializeAsync(initParams);
await initOp.ToUniTask();
if (initOp.Status != EOperationStatus.Succeed)
{
Debug.LogError($"[YooAssetService] Initialize package '{packageName}' failed: {initOp.Error}");
throw new InvalidOperationException($"YooAsset package '{packageName}' initialization failed: {initOp.Error}");
}
}
_packages[packageName] = package;
Debug.Log($"[YooAssetService] Package '{packageName}' initialized.");
}
private InitializeParameters CreateInitParameters(EPlayMode playMode, IRemoteServices remoteServices, string packageName)
{
switch (playMode)
{
case EPlayMode.EditorSimulateMode:
{
#if UNITY_EDITOR
var simulateResult = EditorSimulateModeHelper.SimulateBuild(packageName);
return new EditorSimulateModeParameters
{
EditorFileSystemParameters = FileSystemParameters
.CreateDefaultEditorFileSystemParameters(simulateResult.PackageRootDirectory)
};
#else
throw new InvalidOperationException("EditorSimulateMode is only available in Unity Editor.");
#endif
}
case EPlayMode.HostPlayMode:
{
return new HostPlayModeParameters
{
BuildinFileSystemParameters = FileSystemParameters
.CreateDefaultBuildinFileSystemParameters(),
CacheFileSystemParameters = FileSystemParameters
.CreateDefaultCacheFileSystemParameters(remoteServices)
};
}
case EPlayMode.OfflinePlayMode:
{
return new OfflinePlayModeParameters
{
BuildinFileSystemParameters = FileSystemParameters
.CreateDefaultBuildinFileSystemParameters()
};
}
case EPlayMode.WebPlayMode:
{
var webParams = new WebPlayModeParameters();
#if UNITY_WEBGL && WEIXINMINIGAME && !UNITY_EDITOR
string packageRoot = $"{WeChatWASM.WX.env.USER_DATA_PATH}/__GAME_FILE_CACHE";
webParams.WebServerFileSystemParameters = WechatFileSystemCreater
.CreateFileSystemParameters(packageRoot, remoteServices);
#elif UNITY_WEBGL && DOUYINMINIGAME && !UNITY_EDITOR
string packageRoot = TTSDK.TTFileSystem.USER_DATA_PATH + "/__GAME_FILE_CACHE";
webParams.WebServerFileSystemParameters = TiktokFileSystemCreater
.CreateFileSystemParameters(packageRoot, remoteServices);
#else
webParams.WebServerFileSystemParameters = FileSystemParameters
.CreateDefaultWebServerFileSystemParameters();
if (remoteServices != null)
{
webParams.WebRemoteFileSystemParameters = FileSystemParameters
.CreateDefaultWebRemoteFileSystemParameters(remoteServices);
}
#endif
return webParams;
}
default:
throw new ArgumentOutOfRangeException(nameof(playMode), playMode, "Unsupported PlayMode.");
}
}
// ====================================================================
// Asset Loading
// ====================================================================
///
/// 资源加载重试配置
///
private const int MAX_RETRY_COUNT = 3;
private const int BASE_RETRY_DELAY_MS = 500; // 500ms, 1000ms, 2000ms (exponential)
///
/// 带重试的异步操作执行器。
/// 使用指数退避策略(500ms → 1000ms → 2000ms)。
///
/// 要执行的异步操作
/// 操作名称(用于日志)
/// 取消令牌
/// 操作结果
private async UniTask ExecuteWithRetryAsync(
Func> operation,
string operationName,
CancellationToken ct = default)
{
Exception lastException = null;
for (int attempt = 0; attempt <= MAX_RETRY_COUNT; attempt++)
{
try
{
ct.ThrowIfCancellationRequested();
return await operation();
}
catch (OperationCanceledException)
{
throw; // Don't retry cancellations
}
catch (Exception ex)
{
lastException = ex;
if (attempt < MAX_RETRY_COUNT)
{
int delayMs = BASE_RETRY_DELAY_MS * (1 << attempt); // Exponential backoff
Debug.LogWarning($"[YooAssetService] {operationName} failed (attempt {attempt + 1}/{MAX_RETRY_COUNT + 1}), retrying in {delayMs}ms: {ex.Message}");
await UniTask.Delay(delayMs, cancellationToken: ct);
}
}
}
Debug.LogError($"[YooAssetService] {operationName} failed after {MAX_RETRY_COUNT + 1} attempts: {lastException?.Message}");
return default;
}
private void ThrowIfNotInitialized()
{
if (!_isInitialized)
throw new InvalidOperationException("[YooAssetService] Service not initialized. Call InitializeAsync first.");
}
///
/// 根据资源路径查找应使用的 ResourcePackage。
/// 使用 YooAssetPackageConfig 路由表确定目标包,找不到则回退到默认包。
///
private ResourcePackage FindPackageForAsset(string location)
{
var packageName = YooAssetPackageConfig.GetPackageForLocation(location);
if (_packages.TryGetValue(packageName, out var package))
return package;
// 路由到的包尚未初始化,回退到默认包
return _defaultPackage;
}
///
public async UniTask LoadAssetAsync(string location, uint priority = 0,
CancellationToken ct = default) where T : UnityEngine.Object
{
ThrowIfNotInitialized();
if (string.IsNullOrEmpty(location))
{
Debug.LogError("[YooAssetService] LoadAssetAsync: location is null or empty.");
return null;
}
var package = FindPackageForAsset(location);
return await ExecuteWithRetryAsync(async () =>
{
var handle = package.LoadAssetAsync(location, priority);
await handle.ToUniTask(cancellationToken: ct);
if (handle.Status != EOperationStatus.Succeed)
{
throw new InvalidOperationException($"LoadAssetAsync failed for '{location}': {handle.LastError}");
}
return handle.GetAssetObject();
}, $"LoadAssetAsync<{typeof(T).Name}>('{location}')", ct);
}
///
public async UniTask LoadAssetAsync(string location, Type type, uint priority = 0,
CancellationToken ct = default)
{
ThrowIfNotInitialized();
if (string.IsNullOrEmpty(location))
{
Debug.LogError("[YooAssetService] LoadAssetAsync: location is null or empty.");
return null;
}
var package = FindPackageForAsset(location);
return await ExecuteWithRetryAsync(async () =>
{
var handle = package.LoadAssetAsync(location, type, priority);
await handle.ToUniTask(cancellationToken: ct);
if (handle.Status != EOperationStatus.Succeed)
{
throw new InvalidOperationException($"LoadAssetAsync failed for '{location}': {handle.LastError}");
}
return handle.AssetObject;
}, $"LoadAssetAsync('{location}', {type.Name})", ct);
}
///
/// 同步加载资产(仅在非 WebGL 平台过渡期使用)。
///
[System.Obsolete("Use LoadAssetAsync instead. Sync loading will be removed in US2.")]
public T LoadAssetSync(string location) where T : UnityEngine.Object
{
ThrowIfNotInitialized();
if (string.IsNullOrEmpty(location))
{
Debug.LogError("[YooAssetService] LoadAssetSync: location is null or empty.");
return null;
}
var package = FindPackageForAsset(location);
var handle = package.LoadAssetSync(location);
if (handle.Status != EOperationStatus.Succeed)
{
Debug.LogError($"[YooAssetService] LoadAssetSync failed for '{location}': {handle.LastError}");
return null;
}
return handle.GetAssetObject();
}
///
public async UniTask LoadSubAssetsAsync(string location, uint priority = 0,
CancellationToken ct = default) where T : UnityEngine.Object
{
ThrowIfNotInitialized();
var package = FindPackageForAsset(location);
var handle = package.LoadSubAssetsAsync(location, priority);
await handle.ToUniTask();
ct.ThrowIfCancellationRequested();
if (handle.Status != EOperationStatus.Succeed)
{
Debug.LogError($"[YooAssetService] LoadSubAssetsAsync failed for '{location}': {handle.LastError}");
}
return handle;
}
///
public async UniTask LoadAllAssetsAsync(string location, uint priority = 0,
CancellationToken ct = default) where T : UnityEngine.Object
{
ThrowIfNotInitialized();
var package = FindPackageForAsset(location);
var handle = package.LoadAllAssetsAsync(location, priority);
await handle.ToUniTask();
ct.ThrowIfCancellationRequested();
if (handle.Status != EOperationStatus.Succeed)
{
Debug.LogError($"[YooAssetService] LoadAllAssetsAsync failed for '{location}': {handle.LastError}");
}
return handle;
}
// ====================================================================
// RawFile Loading
// ====================================================================
///
public async UniTask LoadRawFileTextAsync(string location, CancellationToken ct = default)
{
ThrowIfNotInitialized();
var rawPackage = FindPackageForAsset(location);
return await ExecuteWithRetryAsync(async () =>
{
var handle = rawPackage.LoadRawFileAsync(location);
await handle.ToUniTask(cancellationToken: ct);
if (handle.Status != EOperationStatus.Succeed)
{
throw new InvalidOperationException($"LoadRawFileTextAsync failed for '{location}': {handle.LastError}");
}
return handle.GetRawFileText();
}, $"LoadRawFileTextAsync('{location}')", ct);
}
///
public async UniTask LoadRawFileBytesAsync(string location, CancellationToken ct = default)
{
ThrowIfNotInitialized();
var rawPackage = FindPackageForAsset(location);
return await ExecuteWithRetryAsync(async () =>
{
var handle = rawPackage.LoadRawFileAsync(location);
await handle.ToUniTask(cancellationToken: ct);
if (handle.Status != EOperationStatus.Succeed)
{
throw new InvalidOperationException($"LoadRawFileBytesAsync failed for '{location}': {handle.LastError}");
}
return handle.GetRawFileData();
}, $"LoadRawFileBytesAsync('{location}')", ct);
}
// ====================================================================
// Scene Loading
// ====================================================================
///
public async UniTask LoadSceneAsync(string location, LoadSceneMode sceneMode = LoadSceneMode.Single,
LocalPhysicsMode physicsMode = LocalPhysicsMode.None, bool suspendLoad = false, uint priority = 0, CancellationToken ct = default)
{
ThrowIfNotInitialized();
var package = FindPackageForAsset(location);
var handle = package.LoadSceneAsync(location, sceneMode, physicsMode, suspendLoad, priority);
await handle.ToUniTask();
ct.ThrowIfCancellationRequested();
if (handle.Status != EOperationStatus.Succeed)
{
Debug.LogError($"[YooAssetService] LoadSceneAsync failed for '{location}': {handle.LastError}");
}
return handle;
}
// ====================================================================
// Query
// ====================================================================
///
public bool CheckLocationValid(string location)
{
ThrowIfNotInitialized();
// 先用路由包检查,找不到则遍历所有包
var package = FindPackageForAsset(location);
if (package.CheckLocationValid(location))
return true;
foreach (var kvp in _packages)
{
if (kvp.Value != package && kvp.Value.CheckLocationValid(location))
return true;
}
return false;
}
///
public YooAsset.AssetInfo[] GetAssetInfosByTag(string tag)
{
ThrowIfNotInitialized();
// 从所有包收集指定标签的资源信息
var allInfos = new List();
foreach (var kvp in _packages)
{
var infos = kvp.Value.GetAssetInfos(tag);
if (infos != null && infos.Length > 0)
allInfos.AddRange(infos);
}
return allInfos.ToArray();
}
///
public bool IsNeedDownloadFromRemote(string location)
{
ThrowIfNotInitialized();
var package = FindPackageForAsset(location);
return package.IsNeedDownloadFromRemote(location);
}
// ====================================================================
// Download
// ====================================================================
///
public async UniTask DownloadByTagsAsync(string[] tags, int downloadingMaxNumber = 10,
int failedTryAgain = 3, IProgress progress = null, CancellationToken ct = default)
{
ThrowIfNotInitialized();
foreach (var tag in tags)
{
// 对所有包按标签创建下载器
foreach (var kvp in _packages)
{
var downloader = kvp.Value.CreateResourceDownloader(tag, downloadingMaxNumber, failedTryAgain);
if (downloader.TotalDownloadCount == 0)
continue;
downloader.BeginDownload();
while (!downloader.IsDone)
{
ct.ThrowIfCancellationRequested();
progress?.Report(downloader.Progress);
await UniTask.Yield();
}
if (downloader.Status != EOperationStatus.Succeed)
{
Debug.LogError($"[YooAssetService] Download tag '{tag}' from package '{kvp.Key}' failed: {downloader.Error}");
throw new InvalidOperationException($"Resource download failed for tag '{tag}': {downloader.Error}");
}
}
}
progress?.Report(1f);
}
// ====================================================================
// Version Management
// ====================================================================
///
public async UniTask RequestPackageVersionAsync(CancellationToken ct = default)
{
ThrowIfNotInitialized();
var op = _defaultPackage.RequestPackageVersionAsync();
await op.ToUniTask();
ct.ThrowIfCancellationRequested();
if (op.Status != EOperationStatus.Succeed)
{
Debug.LogError($"[YooAssetService] RequestPackageVersion failed: {op.Error}");
throw new InvalidOperationException($"Request package version failed: {op.Error}");
}
return op.PackageVersion;
}
///
public async UniTask UpdatePackageManifestAsync(string packageVersion, CancellationToken ct = default)
{
ThrowIfNotInitialized();
var op = _defaultPackage.UpdatePackageManifestAsync(packageVersion);
await op.ToUniTask();
ct.ThrowIfCancellationRequested();
if (op.Status != EOperationStatus.Succeed)
{
Debug.LogError($"[YooAssetService] UpdatePackageManifest failed: {op.Error}");
throw new InvalidOperationException($"Update package manifest failed: {op.Error}");
}
}
// ====================================================================
// Release
// ====================================================================
///
public void ReleaseHandle(HandleBase handle)
{
if (handle == null) return;
handle.Release();
}
///
public async UniTask UnloadUnusedAssetsAsync()
{
ThrowIfNotInitialized();
// 对所有包执行卸载
foreach (var kvp in _packages)
{
var op = kvp.Value.UnloadUnusedAssetsAsync();
await op.ToUniTask();
}
}
///
public async UniTask UnloadAllAssetsAsync()
{
ThrowIfNotInitialized();
// 对所有包执行卸载
foreach (var kvp in _packages)
{
var op = kvp.Value.UnloadAllAssetsAsync();
await op.ToUniTask();
}
}
// ====================================================================
// IYooAssetBridge Implementation
// ====================================================================
async UniTask IYooAssetBridge.LoadAssetAsync(string location)
{
return await LoadAssetAsync(location);
}
async UniTask IYooAssetBridge.LoadRawFileTextAsync(string location)
{
return await LoadRawFileTextAsync(location);
}
async UniTask IYooAssetBridge.LoadRawFileBytesAsync(string location)
{
return await LoadRawFileBytesAsync(location);
}
async UniTask IYooAssetBridge.PreloadAsync(string[] locations)
{
// 批量预加载,使用 UniTask.WhenAll 并行
var tasks = new List(locations.Length);
foreach (var loc in locations)
{
tasks.Add(LoadAssetAsync(loc).AsUniTask());
}
await UniTask.WhenAll(tasks);
}
T IYooAssetBridge.GetCached(string location)
{
// 委托给 ResourceCacheManager(US4 已集成)
if (ProjSG.Resource.ResourceCacheManager.IsValid())
{
return ProjSG.Resource.ResourceCacheManager.Instance.GetCached(location);
}
return null;
}
// ====================================================================
// Sync Wrappers (Transitional — removed in US2)
// ====================================================================
///
/// 同步加载所有同类型资源(过渡期使用)。
///
[System.Obsolete("Use LoadAllAssetsAsync instead. Sync loading will be removed in US2.")]
public AllAssetsHandle LoadAllAssetsSync(string location) where T : UnityEngine.Object
{
ThrowIfNotInitialized();
var package = FindPackageForAsset(location);
return package.LoadAllAssetsSync(location);
}
}
}