309 lines
12 KiB
C#
309 lines
12 KiB
C#
using System;
|
||
using System.Collections;
|
||
using AOT.Boot;
|
||
using AOT.UI;
|
||
using UnityEngine;
|
||
using YooAsset;
|
||
|
||
namespace AOT.Patch
|
||
{
|
||
/// <summary>
|
||
/// 启动补丁:version 门控 → YooAsset 初始化/更新 → 回调。
|
||
/// </summary>
|
||
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<BootContext, string> 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 },
|
||
};
|
||
Debug.LogWarning($"[Boot] Local version fallback: {versionError}");
|
||
}
|
||
else
|
||
{
|
||
Debug.Log($"[Boot] Remote art={version.resVersion} pack={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.4f);
|
||
_view?.SetStatus("正在下载资源…");
|
||
var downloader = package.CreateResourceDownloader(new ResourceDownloaderOptions(10, 3));
|
||
if (downloader.TotalDownloadCount > 0)
|
||
{
|
||
downloader.DownloadProgressChanged += info =>
|
||
{
|
||
_view?.SetProgress(0.4f + info.Progress * 0.15f);
|
||
_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<string>(StringComparer.Ordinal) { _settings.PackageName };
|
||
foreach (var packageName in version.packages ?? Array.Empty<string>())
|
||
{
|
||
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("正在准备营地…");
|
||
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("正在下载营地资源…");
|
||
companionDownload.DownloadProgressChanged += info =>
|
||
{
|
||
_view?.SetProgress(.55f + info.Progress * .35f);
|
||
_view?.SetStatus($"正在下载资源… {info.CurrentDownloadCount}/{info.TotalDownloadCount}");
|
||
};
|
||
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;
|
||
|
||
/// <summary>
|
||
/// pinnedVersion 来自 version.json:热更用 packVersion,Art 用 resVersion。
|
||
/// 为空时再回退到包目录里的 .version 文件。
|
||
/// </summary>
|
||
IEnumerator EnsureManifest(ResourcePackage package, string pinnedVersion, string label)
|
||
{
|
||
_stepError = null;
|
||
var packageVersion = pinnedVersion;
|
||
if (string.IsNullOrEmpty(packageVersion))
|
||
{
|
||
_view?.SetStatus("正在检查资源…");
|
||
var versionOp = package.RequestPackageVersionAsync();
|
||
yield return versionOp;
|
||
if (versionOp.Status != EOperationStatus.Succeeded)
|
||
{
|
||
_stepError = $"请求{label}版本失败: {versionOp.Error}";
|
||
yield break;
|
||
}
|
||
packageVersion = versionOp.PackageVersion;
|
||
}
|
||
|
||
_view?.SetStatus("正在检查资源…");
|
||
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;
|
||
}
|
||
}
|
||
}
|