Add hot-update boot pipeline, UI bundles/atlas assets, and design docs.

Ignore AIClient/Library and AIClient/HybridCLRData permanently; include HybridCLR/YooAsset project setup and UI tooling.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
udx
2026-09-21 16:02:58 +08:00
co-authored by Cursor
parent f3f66ab063
commit b7a0bbcb89
279 changed files with 27332 additions and 14 deletions
@@ -0,0 +1,35 @@
using System.Collections.Generic;
using YooAsset;
namespace AOT.Patch
{
/// <summary>
/// YooAsset 远端地址查询(CDN)。
/// </summary>
public sealed class CdnRemoteService : IRemoteService
{
readonly string _primary;
readonly string _fallback;
public CdnRemoteService(string primaryRoot, string fallbackRoot = null)
{
_primary = TrimSlash(primaryRoot);
_fallback = string.IsNullOrEmpty(fallbackRoot) ? _primary : TrimSlash(fallbackRoot);
}
public IReadOnlyList<string> GetRemoteUrls(string fileName)
{
return new[]
{
$"{_primary}/{fileName}",
$"{_fallback}/{fileName}",
};
}
static string TrimSlash(string root)
{
if (string.IsNullOrEmpty(root)) return string.Empty;
return root.TrimEnd('/');
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 89730891313aa084e9adf58e6f37542f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,231 @@
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",
cdnRoot = _settings.DefaultCdnRoot,
packages = new[] { _settings.PackageName },
};
_view?.SetStatus($"版本回退本地 ({versionError})");
}
else
{
_view?.SetStatus($"远程版本 {version.resVersion}");
}
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,
CdnRoot = cdnRoot,
PackageName = _settings.PackageName,
WsUrlTemplate = _settings.WsUrlTemplate,
};
#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?.SetStatus("请求资源版本…");
_view?.SetProgress(0.35f);
var versionOp = package.RequestPackageVersionAsync();
yield return versionOp;
if (versionOp.Status != EOperationStatus.Succeeded)
{
onDone?.Invoke(null, $"请求 PackageVersion 失败: {versionOp.Error}");
yield break;
}
_view?.SetStatus($"更新清单 {versionOp.PackageVersion}");
_view?.SetProgress(0.5f);
var manifestOp = package.LoadPackageManifestAsync(new LoadPackageManifestOptions(versionOp.PackageVersion, 60));
yield return manifestOp;
if (manifestOp.Status != EOperationStatus.Succeeded)
{
onDone?.Invoke(null, $"更新清单失败: {manifestOp.Error}");
yield break;
}
_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;
}
}
_view?.SetProgress(0.95f);
_view?.SetStatus("资源就绪");
onDone?.Invoke(ctx, null);
}
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(_settings.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;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 57535d94c1b98984da5c887d5f55657a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,88 @@
using System;
using System.Collections;
using System.IO;
using UnityEngine;
using UnityEngine.Networking;
namespace AOT.Patch
{
public sealed class VersionClient
{
readonly string _url;
readonly bool _allowStreamingFallback;
public VersionClient(string url, bool allowStreamingFallback)
{
_url = url;
_allowStreamingFallback = allowStreamingFallback;
}
public IEnumerator Fetch(Action<VersionInfo, string> onDone)
{
string error = null;
VersionInfo info = null;
if (!string.IsNullOrEmpty(_url))
{
using (var req = UnityWebRequest.Get(_url))
{
req.timeout = 15;
yield return req.SendWebRequest();
#if UNITY_2020_2_OR_NEWER
bool failed = req.result != UnityWebRequest.Result.Success;
#else
bool failed = req.isNetworkError || req.isHttpError;
#endif
if (!failed)
{
info = VersionInfo.FromJson(req.downloadHandler.text);
if (info == null)
error = "version.json parse failed";
}
else
{
error = req.error;
}
}
}
if (info == null && _allowStreamingFallback)
{
var localPath = Path.Combine(Application.streamingAssetsPath, "cdn", "version.json");
yield return ReadLocalText(localPath, text =>
{
if (!string.IsNullOrEmpty(text))
{
info = VersionInfo.FromJson(text);
if (info != null)
error = null;
}
});
}
onDone?.Invoke(info, error);
}
static IEnumerator ReadLocalText(string path, Action<string> onDone)
{
#if UNITY_ANDROID && !UNITY_EDITOR
using (var req = UnityWebRequest.Get(path))
{
yield return req.SendWebRequest();
#if UNITY_2020_2_OR_NEWER
bool failed = req.result != UnityWebRequest.Result.Success;
#else
bool failed = req.isNetworkError || req.isHttpError;
#endif
onDone?.Invoke(failed ? null : req.downloadHandler.text);
}
#else
if (File.Exists(path))
onDone?.Invoke(File.ReadAllText(path));
else
onDone?.Invoke(null);
yield break;
#endif
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3c255f1d996335441ae6dccb3c3dc352
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,50 @@
using System;
using UnityEngine;
namespace AOT.Patch
{
[Serializable]
public class VersionInfo
{
public string appVersionMin = "1.0.0";
public string appVersionMax = "1.0.0";
public string resVersion = "0";
public string cdnRoot = "https://www.xworld.ren/wechat/1.0.0";
public string[] packages = { "DefaultPackage" };
public bool forceUpdate;
public string notice = "";
public static VersionInfo FromJson(string json)
{
if (string.IsNullOrEmpty(json))
return null;
try
{
return JsonUtility.FromJson<VersionInfo>(json);
}
catch (Exception e)
{
Debug.LogError($"[VersionInfo] parse failed: {e.Message}");
return null;
}
}
public static int CompareVersion(string a, string b)
{
if (string.IsNullOrEmpty(a) && string.IsNullOrEmpty(b)) return 0;
if (string.IsNullOrEmpty(a)) return -1;
if (string.IsNullOrEmpty(b)) return 1;
var pa = a.Split('.');
var pb = b.Split('.');
int n = Mathf.Max(pa.Length, pb.Length);
for (int i = 0; i < n; i++)
{
int va = i < pa.Length && int.TryParse(pa[i], out var xa) ? xa : 0;
int vb = i < pb.Length && int.TryParse(pb[i], out var xb) ? xb : 0;
if (va != vb) return va.CompareTo(vb);
}
return 0;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 925a013a115d7694f8429c8444591d46
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: