using System; using System.Collections; using System.IO; using System.Threading; using Cysharp.Threading.Tasks; using UnityEngine; using YooAsset; namespace Game { public class ResourceUpdatePackageHandle { public EPlayMode playMode; public string packageName; public string buildPipeline; public ResourceUpdatePackageHandle(EPlayMode playMode, string packageName, string buildPipeline) { this.playMode = playMode; this.packageName = packageName; this.buildPipeline = buildPipeline; EventManager.Instance.Subscribe(UpdatePackageCallbackEventArgs.EventId, UpdatePackageCallbackEvent); } private void UpdatePackageCallbackEvent(object sender, GameEventArgs e) { var args = e as UpdatePackageCallbackEventArgs; switch (args.callbackType) { case UpdatePackageCallbackType.再次初始化资源包: this.InitPackageAsync().Forget(); break; case UpdatePackageCallbackType.开始下载网络文件: this.BeginDownloadAsync().Forget(); break; case UpdatePackageCallbackType.再次更新静态版本: this.UpdatePackageVersionAsync().Forget(); break; case UpdatePackageCallbackType.再次更新补丁清单: this.UpdateManifestAsync().Forget(); break; case UpdatePackageCallbackType.再次下载网络文件: this.CreateDownloaderAsync().Forget(); break; default: throw new ArgumentOutOfRangeException(); } } public async UniTask InitPackageAsync(CancellationToken token = default) { // 创建资源包裹类 var package = YooAssets.TryGetPackage(packageName); if (package == null) package = YooAssets.CreatePackage(packageName); // 编辑器下的模拟模式 InitializationOperation initializationOperation = null; if (playMode == EPlayMode.EditorSimulateMode) { var createParameters = new EditorSimulateModeParameters(); createParameters.SimulateManifestFilePath = EditorSimulateModeHelper.SimulateBuild(buildPipeline, packageName); initializationOperation = package.InitializeAsync(createParameters); } // 单机运行模式 if (playMode == EPlayMode.OfflinePlayMode) { var createParameters = new OfflinePlayModeParameters(); createParameters.DecryptionServices = new FileStreamDecryption(); initializationOperation = package.InitializeAsync(createParameters); } // 联机运行模式 if (playMode == EPlayMode.HostPlayMode) { string defaultHostServer = GetHostServerURL(); string fallbackHostServer = GetHostServerURL(); var createParameters = new HostPlayModeParameters(); createParameters.DecryptionServices = new FileStreamDecryption(); createParameters.BuildinQueryServices = new GameQueryServices(); createParameters.RemoteServices = new RemoteServices(defaultHostServer, fallbackHostServer); initializationOperation = package.InitializeAsync(createParameters); } // WebGL运行模式 if (playMode == EPlayMode.WebPlayMode) { string defaultHostServer = GetHostServerURL(); string fallbackHostServer = GetHostServerURL(); var createParameters = new WebPlayModeParameters(); createParameters.DecryptionServices = new FileStreamDecryption(); createParameters.BuildinQueryServices = new GameQueryServices(); createParameters.RemoteServices = new RemoteServices(defaultHostServer, fallbackHostServer); initializationOperation = package.InitializeAsync(createParameters); } await initializationOperation; // 如果初始化失败弹出提示界面 if (initializationOperation.Status != EOperationStatus.Succeed) { Debug.LogWarning($"{initializationOperation.Error}"); // 初始化失败 EventManager.Instance.FireNow(InitializeFailedEventArgs.EventId, new InitializeFailedEventArgs()); } else { var version = initializationOperation.PackageVersion; Debug.Log($"Init resource package version : {version}"); await this.UpdatePackageVersionAsync(token); } } #region InitHelper /// /// 获取资源服务器地址 /// private string GetHostServerURL() { //string hostServerIP = "http://10.0.2.2"; //安卓模拟器地址 #if UNITY_EDITOR string hostServerIP = "http://127.0.0.1:8080"; #else string hostServerIP = "http://192.168.188.124:8080"; #endif string appVersion = "v1.0"; #if UNITY_EDITOR if (UnityEditor.EditorUserBuildSettings.activeBuildTarget == UnityEditor.BuildTarget.Android) return $"{hostServerIP}/CDN/Android/{appVersion}"; else if (UnityEditor.EditorUserBuildSettings.activeBuildTarget == UnityEditor.BuildTarget.iOS) return $"{hostServerIP}/CDN/IPhone/{appVersion}"; else if (UnityEditor.EditorUserBuildSettings.activeBuildTarget == UnityEditor.BuildTarget.WebGL) return $"{hostServerIP}/CDN/WebGL/{appVersion}"; else return $"{hostServerIP}/CDN/PC/{appVersion}"; #else if (Application.platform == RuntimePlatform.Android) return $"{hostServerIP}/CDN/Android/{appVersion}"; else if (Application.platform == RuntimePlatform.IPhonePlayer) return $"{hostServerIP}/CDN/IPhone/{appVersion}"; else if (Application.platform == RuntimePlatform.WebGLPlayer) return $"{hostServerIP}/CDN/WebGL/{appVersion}"; else return $"{hostServerIP}/CDN/PC/{appVersion}"; #endif } /// /// 远端资源地址查询服务类 /// private class RemoteServices : IRemoteServices { private readonly string _defaultHostServer; private readonly string _fallbackHostServer; public RemoteServices(string defaultHostServer, string fallbackHostServer) { _defaultHostServer = defaultHostServer; _fallbackHostServer = fallbackHostServer; } string IRemoteServices.GetRemoteMainURL(string fileName) { return $"{_defaultHostServer}/{fileName}"; } string IRemoteServices.GetRemoteFallbackURL(string fileName) { return $"{_fallbackHostServer}/{fileName}"; } } /// /// 资源文件流加载解密类 /// private class FileStreamDecryption : IDecryptionServices { /// /// 同步方式获取解密的资源包对象 /// 注意:加载流对象在资源包对象释放的时候会自动释放 /// AssetBundle IDecryptionServices.LoadAssetBundle(DecryptFileInfo fileInfo, out Stream managedStream) { BundleStream bundleStream = new BundleStream(fileInfo.FileLoadPath, FileMode.Open, FileAccess.Read, FileShare.Read); managedStream = bundleStream; return AssetBundle.LoadFromStream(bundleStream, fileInfo.ConentCRC, GetManagedReadBufferSize()); } /// /// 异步方式获取解密的资源包对象 /// 注意:加载流对象在资源包对象释放的时候会自动释放 /// AssetBundleCreateRequest IDecryptionServices.LoadAssetBundleAsync(DecryptFileInfo fileInfo, out Stream managedStream) { BundleStream bundleStream = new BundleStream(fileInfo.FileLoadPath, FileMode.Open, FileAccess.Read, FileShare.Read); managedStream = bundleStream; return AssetBundle.LoadFromStreamAsync(bundleStream, fileInfo.ConentCRC, GetManagedReadBufferSize()); } private static uint GetManagedReadBufferSize() { return 1024; } } /// /// 资源文件偏移加载解密类 /// private class FileOffsetDecryption : IDecryptionServices { /// /// 同步方式获取解密的资源包对象 /// 注意:加载流对象在资源包对象释放的时候会自动释放 /// AssetBundle IDecryptionServices.LoadAssetBundle(DecryptFileInfo fileInfo, out Stream managedStream) { managedStream = null; return AssetBundle.LoadFromFile(fileInfo.FileLoadPath, fileInfo.ConentCRC, GetFileOffset()); } /// /// 异步方式获取解密的资源包对象 /// 注意:加载流对象在资源包对象释放的时候会自动释放 /// AssetBundleCreateRequest IDecryptionServices.LoadAssetBundleAsync(DecryptFileInfo fileInfo, out Stream managedStream) { managedStream = null; return AssetBundle.LoadFromFileAsync(fileInfo.FileLoadPath, fileInfo.ConentCRC, GetFileOffset()); } private static ulong GetFileOffset() { return 32; } } #endregion string packageVersion; // 最新资源版本 private async UniTask UpdatePackageVersionAsync(CancellationToken token = default) { Debug.Log("获取最新的资源版本 !"); await new WaitForSecondsRealtime(0.5f); // var packageName = (string)_machine.GetBlackboardValue("PackageName"); var package = YooAssets.GetPackage(packageName); var operation = package.UpdatePackageVersionAsync(); await operation; if (operation.Status != EOperationStatus.Succeed) { Debug.LogWarning(operation.Error); // PatchEventDefine.PackageVersionUpdateFailed.SendEventMessage(); EventManager.Instance.FireNow(PackageVersionUpdateFailedEventArgs.EventId, new PackageVersionUpdateFailedEventArgs()); Debug.Log("获取最新的资源版本失败!"); } else { // _machine.SetBlackboardValue("PackageVersion", operation.PackageVersion); // _machine.ChangeState(); this.packageVersion = operation.PackageVersion; Debug.Log($"最新资源版本为:{operation.PackageVersion}"); await this.UpdateManifestAsync(token); } } private async UniTask UpdateManifestAsync(CancellationToken token = default) { Debug.Log("更新资源清单!"); await new WaitForSecondsRealtime(0.5f); // var packageName = (string)_machine.GetBlackboardValue("PackageName"); // var packageVersion = (string)_machine.GetBlackboardValue("PackageVersion"); var package = YooAssets.GetPackage(packageName); bool savePackageVersion = true; var operation = package.UpdatePackageManifestAsync(packageVersion, savePackageVersion); await operation; if (operation.Status != EOperationStatus.Succeed) { Debug.LogWarning(operation.Error); // PatchEventDefine.PatchManifestUpdateFailed.SendEventMessage(); EventManager.Instance.FireNow(PatchManifestUpdateFailedEventArgs.EventId, new PatchManifestUpdateFailedEventArgs()); return; } else { // _machine.ChangeState(); Debug.Log("更新资源清单完成!"); await this.CreateDownloaderAsync(token); } } ResourceDownloaderOperation downloader; private async UniTask CreateDownloaderAsync(CancellationToken token = default) { Debug.Log("创建补丁下载器!"); await new WaitForSecondsRealtime(0.5f); // var packageName = (string)_machine.GetBlackboardValue("PackageName"); var package = YooAssets.GetPackage(packageName); int downloadingMaxNum = 10; int failedTryAgain = 3; downloader = package.CreateResourceDownloader(downloadingMaxNum, failedTryAgain); // _machine.SetBlackboardValue("Downloader", downloader); if (downloader.TotalDownloadCount == 0) { Debug.Log("Not found any download files !"); // _machine.ChangeState(); await this.UpdaterDoneAsync(token); } else { // 发现新更新文件后,挂起流程系统 // TODO: 注意:开发者需要在下载前检测磁盘空间不足 int totalDownloadCount = downloader.TotalDownloadCount; long totalDownloadBytes = downloader.TotalDownloadBytes; // PatchEventDefine.FoundUpdateFiles.SendEventMessage(totalDownloadCount, totalDownloadBytes); Debug.Log("发现新更新文件后,挂起流程系统"); EventManager.Instance.FireNow(DownloadProgressUpdateEventArgs.EventId, new FoundUpdateFilesEventArgs(totalDownloadCount, totalDownloadBytes)); // this.BeginDownloadAsync(token).Forget(); } } private async UniTask BeginDownloadAsync(CancellationToken token = default) { Debug.Log("开始下载补丁文件!"); downloader.OnDownloadErrorCallback = WebFileDownloadFailed; downloader.OnDownloadProgressCallback = DownloadProgressUpdate; downloader.BeginDownload(); await downloader; // 检测下载结果 if (downloader.Status != EOperationStatus.Succeed) return; // _machine.ChangeState(); await this.DownloadPackageOverAsync(token); void DownloadProgressUpdate(int totaldownloadcount, int currentdownloadcount, long totaldownloadbytes, long currentdownloadbytes) { EventManager.Instance.FireNow(DownloadProgressUpdateEventArgs.EventId, new DownloadProgressUpdateEventArgs( totaldownloadcount, currentdownloadcount, totaldownloadbytes, currentdownloadbytes)); } void WebFileDownloadFailed(string filename, string error) { Debug.Log(this.packageName + " : " + error); EventManager.Instance.FireNow(WebFileDownloadFailedEventArgs.EventId, new WebFileDownloadFailedEventArgs(this.packageName, error)); } } private async UniTask DownloadPackageOverAsync(CancellationToken token = default) { Debug.Log("下载完成"); await UniTask.Yield(); await this.ClearPackageCacheAsync(token); } private async UniTask ClearPackageCacheAsync(CancellationToken token = default) { Debug.Log("清理未使用的缓存文件!"); await UniTask.Yield(); var package = YooAssets.GetPackage(packageName); var operation = package.ClearUnusedCacheFilesAsync(); operation.Completed += Operation_Completed; void Operation_Completed(YooAsset.AsyncOperationBase obj) { // _machine.ChangeState(); this.UpdaterDoneAsync(token).Forget(); } } private async UniTask UpdaterDoneAsync(CancellationToken token = default) { Debug.Log("流程更新完毕!"); await UniTask.Yield(); EventManager.Instance.FireNow(UpdaterDoneEventArgs.EventId, new UpdaterDoneEventArgs()); } } } /// /// 资源文件解密流 /// public class BundleStream : FileStream { public const byte KEY = 64; public BundleStream(string path, FileMode mode, FileAccess access, FileShare share) : base(path, mode, access, share) { } public BundleStream(string path, FileMode mode) : base(path, mode) { } public override int Read(byte[] array, int offset, int count) { var index = base.Read(array, offset, count); for (int i = 0; i < array.Length; i++) { array[i] ^= KEY; } return index; } }