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;
}
}
}