using System; using System.Collections; using AOT.Boot; using AOT.UI; using UnityEngine; using YooAsset; namespace AOT.Patch { /// /// 启动补丁:version 门控 → YooAsset 初始化/更新 → 回调。 /// public sealed class PatchRunner { readonly MonoBehaviour _host; readonly BootSettings _settings; readonly BootPatchView _view; public PatchRunner(MonoBehaviour host, BootSettings settings, BootPatchView view) { _host = host; _settings = settings; _view = view; } public IEnumerator Run(Action onDone) { _view?.SetStatus("检查版本…"); _view?.SetProgress(0.05f); VersionInfo version = null; string versionError = null; yield return new VersionClient(_settings.VersionUrl, _settings.AllowStreamingVersionFallback) .Fetch((info, err) => { version = info; versionError = err; }); if (version == null) { // 无 CDN 时仍允许编辑器直通;真机 Host/Web 则失败。 if (!CanContinueWithoutVersion()) { onDone?.Invoke(null, $"获取 version.json 失败: {versionError}"); yield break; } version = new VersionInfo { appVersionMin = _settings.AppVersion, appVersionMax = _settings.AppVersion, resVersion = "local", packVersion = "local", cdnRoot = _settings.DefaultCdnRoot, packages = new[] { _settings.PackageName }, }; _view?.SetStatus($"版本回退本地 ({versionError})"); } else { _view?.SetStatus($"远程版本 资源 {version.resVersion} 热更 {version.packVersion}"); } if (VersionInfo.CompareVersion(_settings.AppVersion, version.appVersionMin) < 0) { onDone?.Invoke(null, $"壳版本过低,请更新微信小游戏包(当前 {_settings.AppVersion} < 最低 {version.appVersionMin})"); yield break; } string cdnRoot = string.IsNullOrEmpty(version.cdnRoot) ? _settings.DefaultCdnRoot : version.cdnRoot; var ctx = new BootContext { AppVersion = _settings.AppVersion, ResVersion = version.resVersion, PackVersion = version.packVersion, CdnRoot = cdnRoot, PackageName = _settings.PackageName, HttpBase = _settings.HttpBase, WsUrlTemplate = _settings.WsUrlTemplate, Notice = version.notice ?? string.Empty, UiTheme = string.IsNullOrWhiteSpace(version.uiTheme) ? _settings.UiTheme : version.uiTheme, }; #if UNITY_EDITOR if (_settings.EditorDirectHotUpdate) { _view?.SetStatus("编辑器直通热更(跳过资源补丁)"); _view?.SetProgress(0.9f); onDone?.Invoke(ctx, null); yield break; } #endif _view?.SetStatus("初始化资源系统…"); _view?.SetProgress(0.15f); if (!YooAssets.IsInitialized) YooAssets.Initialize(); if (!YooAssets.TryGetPackage(_settings.PackageName, out var package)) package = YooAssets.CreatePackage(_settings.PackageName); ctx.Package = package; var playMode = ResolvePlayMode(); var initOp = CreateInitializeOperation(package, playMode, cdnRoot); if (initOp == null) { onDone?.Invoke(null, $"不支持的 PlayMode: {playMode}"); yield break; } yield return initOp; if (initOp.Status != EOperationStatus.Succeeded) { onDone?.Invoke(null, $"YooAsset 初始化失败: {initOp.Error}"); yield break; } _view?.SetProgress(0.35f); yield return EnsureManifest(package, version.packVersion, "热更"); if (_stepError != null) { onDone?.Invoke(null, _stepError); yield break; } _view?.SetProgress(0.5f); _view?.SetStatus("下载资源…"); var downloader = package.CreateResourceDownloader(new ResourceDownloaderOptions(10, 3)); if (downloader.TotalDownloadCount > 0) { downloader.DownloadProgressChanged += info => { _view?.SetProgress(0.5f + info.Progress * 0.4f); _view?.SetStatus($"下载 {info.CurrentDownloadCount}/{info.TotalDownloadCount}"); }; downloader.StartDownload(); yield return downloader; if (downloader.Status != EOperationStatus.Succeeded) { onDone?.Invoke(null, $"下载失败: {downloader.Error}"); yield break; } } // UI Prefabs are AssetBundles in Art; DefaultPackage is the raw hot-update DLL. // Initialize explicitly listed companion packages before gameplay starts. var prepared = new System.Collections.Generic.HashSet(StringComparer.Ordinal) { _settings.PackageName }; foreach (var packageName in version.packages ?? Array.Empty()) { if (string.IsNullOrWhiteSpace(packageName) || !prepared.Add(packageName)) continue; if (!YooAssets.TryGetPackage(packageName, out var companion)) companion = YooAssets.CreatePackage(packageName); var companionRoot = cdnRoot.TrimEnd('/'); var slash = companionRoot.LastIndexOf('/'); companionRoot = companionRoot.Substring(0, slash + 1) + packageName; _view?.SetStatus($"初始化资源包 {packageName}…"); if (companion.InitializeStatus != EOperationStatus.Succeeded) { var companionInit = CreateInitializeOperation(companion, playMode, companionRoot); if (companionInit == null) { onDone?.Invoke(null, $"不支持资源包 {packageName} 的 PlayMode: {playMode}"); yield break; } yield return companionInit; if (companionInit.Status != EOperationStatus.Succeeded) { onDone?.Invoke(null, $"资源包 {packageName} 初始化失败: {companionInit.Error}"); yield break; } } var pinned = packageName == "Art" ? version.resVersion : null; yield return EnsureManifest(companion, pinned, packageName); if (_stepError != null) { onDone?.Invoke(null, _stepError); yield break; } var companionDownload = companion.CreateResourceDownloader(new ResourceDownloaderOptions(10, 3)); if (companionDownload.TotalDownloadCount > 0) { _view?.SetStatus($"下载资源包 {packageName}…"); companionDownload.StartDownload(); yield return companionDownload; if (companionDownload.Status != EOperationStatus.Succeeded) { onDone?.Invoke(null, $"资源包 {packageName} 下载失败: {companionDownload.Error}"); yield break; } } } _view?.SetProgress(0.95f); _view?.SetStatus("资源就绪"); onDone?.Invoke(ctx, null); } string _stepError; /// /// pinnedVersion 来自 version.json:热更用 packVersion,Art 用 resVersion。 /// 为空时再回退到包目录里的 .version 文件。 /// IEnumerator EnsureManifest(ResourcePackage package, string pinnedVersion, string label) { _stepError = null; var packageVersion = pinnedVersion; if (string.IsNullOrEmpty(packageVersion)) { _view?.SetStatus($"请求{label}版本…"); var versionOp = package.RequestPackageVersionAsync(); yield return versionOp; if (versionOp.Status != EOperationStatus.Succeeded) { _stepError = $"请求{label}版本失败: {versionOp.Error}"; yield break; } packageVersion = versionOp.PackageVersion; } _view?.SetStatus($"更新{label}清单 {packageVersion}"); var manifestOp = package.LoadPackageManifestAsync(new LoadPackageManifestOptions(packageVersion, 60)); yield return manifestOp; if (manifestOp.Status != EOperationStatus.Succeeded) _stepError = $"更新{label}清单失败: {manifestOp.Error}"; } bool CanContinueWithoutVersion() { #if UNITY_EDITOR return _settings.EditorDirectHotUpdate; #else return false; #endif } EPlayMode ResolvePlayMode() { #if UNITY_EDITOR return _settings.PlayMode; #elif UNITY_WEBGL return EPlayMode.WebPlayMode; #else return _settings.PlayMode == EPlayMode.EditorSimulateMode ? EPlayMode.HostPlayMode : _settings.PlayMode; #endif } InitializePackageOperation CreateInitializeOperation(ResourcePackage package, EPlayMode playMode, string cdnRoot) { if (playMode == EPlayMode.EditorSimulateMode) { #if UNITY_EDITOR var buildResult = EditorSimulateBuildInvoker.Build(package.PackageName, (int)EBundleType.VirtualAssetBundle); var options = new EditorSimulateModeOptions { EditorFileSystemParameters = FileSystemParameters.CreateDefaultEditorFileSystemParameters(buildResult.PackageRootDirectory) }; return package.InitializePackageAsync(options); #else return null; #endif } if (playMode == EPlayMode.OfflinePlayMode) { var options = new OfflinePlayModeOptions { BuiltinFileSystemParameters = FileSystemParameters.CreateDefaultBuiltinFileSystemParameters() }; return package.InitializePackageAsync(options); } if (playMode == EPlayMode.HostPlayMode) { var remote = new CdnRemoteService(cdnRoot); var options = new HostPlayModeOptions { BuiltinFileSystemParameters = FileSystemParameters.CreateDefaultBuiltinFileSystemParameters(), CacheFileSystemParameters = FileSystemParameters.CreateDefaultSandboxFileSystemParameters(remote) }; options.BuiltinFileSystemParameters.AddParameter(EFileSystemParameter.CopyBuiltinPackageManifest, true); return package.InitializePackageAsync(options); } if (playMode == EPlayMode.WebPlayMode) { // 微信小游戏正式接入时替换为 WechatFileSystem;M0 先用默认 WebNetwork。 var remote = new CdnRemoteService(cdnRoot); var options = new WebPlayModeOptions { WebNetworkFileSystemParameters = FileSystemParameters.CreateDefaultWebNetworkFileSystemParameters(remote, true) }; return package.InitializePackageAsync(options); } return null; } } }