diff --git a/.cursor/rules/aigame-basics.mdc b/.cursor/rules/aigame-basics.mdc
index da50c2f..846abf2 100644
--- a/.cursor/rules/aigame-basics.mdc
+++ b/.cursor/rules/aigame-basics.mdc
@@ -1,5 +1,5 @@
---
-description: AIGame 项目基础规则(资源登记等必须遵守)
+description: AIGame 项目基础规则(资源登记、UI 自动生成等必须遵守)
alwaysApply: true
---
@@ -14,3 +14,14 @@ alwaysApply: true
- picture → `Doc/ResDesc_Picture.md`
- sound → `Doc/ResDesc_Sound.md`
- effect → `Doc/ResDesc_Effect.md`
+
+2. **UI 自动生成(必须)**:业务面板使用 JSON→Prefab 管线,不要用运行时临时代码拼业务 UI 替代 Prefab。
+ - 总览:`Doc/UI_AutoGen_Overview.md`
+ - 提示词契约:`Doc/Rule/UIGenerationPrompt.md`
+ - 规则详解:`Doc/Rule/UIGenerationRules.md`
+ - 工程约定:`Doc/Rule/UnityUIProject.md`
+ - JSON:`Doc/UIPrefabCreater/{UIName}.json`
+ - Prefab:`Assets/Bundles/UI/Prefab/{UIName}.prefab`
+ - 通用贴图:`Assets/Bundles/UI/Texture/Common/`
+ - 设计分辨率:竖屏 **`[1080, 1920]`**(不要用参考项目的 2048×1024)
+ - Unity 菜单:`Tools/UI/...`;转换器:`AIClient/Assets/Editor/UI/`
diff --git a/.gitignore b/.gitignore
index cbc6039..3a5b43c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -42,9 +42,12 @@ AIClient/*.VC.db
AIClient/.vs/
AIClient/.gradle/
-# HybridCLR intermediate clone repos (LocalIl2Cpp 可按需提交)
-AIClient/HybridCLRData/hybridclr_repo/
-AIClient/HybridCLRData/il2cpp_plus_repo/
+# HybridCLR 本地安装数据(永远不提交)
+AIClient/HybridCLRData/
+
+# YooAsset 构建产出与本地 CDN 镜像
+AIClient/Bundles/
+Tools/cdn_root/
# Crash / OS
*.stackdump
diff --git a/AIClient/Assets/AOT.meta b/AIClient/Assets/AOT.meta
new file mode 100644
index 0000000..aeb60b2
--- /dev/null
+++ b/AIClient/Assets/AOT.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: f2a89839800d8e64abd7f1ab395451eb
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/AIClient/Assets/AOT/Scripts.meta b/AIClient/Assets/AOT/Scripts.meta
new file mode 100644
index 0000000..59b6c95
--- /dev/null
+++ b/AIClient/Assets/AOT/Scripts.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 0f1bd36c12635db48b1db68c45fc67e8
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/AIClient/Assets/AOT/Scripts/AOT.Runtime.asmdef b/AIClient/Assets/AOT/Scripts/AOT.Runtime.asmdef
new file mode 100644
index 0000000..54b282c
--- /dev/null
+++ b/AIClient/Assets/AOT/Scripts/AOT.Runtime.asmdef
@@ -0,0 +1,18 @@
+{
+ "name": "AOT.Runtime",
+ "rootNamespace": "AOT",
+ "references": [
+ "YooAsset",
+ "HybridCLR.Runtime",
+ "UnityEngine.UI"
+ ],
+ "includePlatforms": [],
+ "excludePlatforms": [],
+ "allowUnsafeCode": false,
+ "overrideReferences": false,
+ "precompiledReferences": [],
+ "autoReferenced": true,
+ "defineConstraints": [],
+ "versionDefines": [],
+ "noEngineReferences": false
+}
diff --git a/AIClient/Assets/AOT/Scripts/AOT.Runtime.asmdef.meta b/AIClient/Assets/AOT/Scripts/AOT.Runtime.asmdef.meta
new file mode 100644
index 0000000..39db7aa
--- /dev/null
+++ b/AIClient/Assets/AOT/Scripts/AOT.Runtime.asmdef.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 359f9ae67ab8957448c75b56a2a6a0aa
+AssemblyDefinitionImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/AIClient/Assets/AOT/Scripts/Boot.meta b/AIClient/Assets/AOT/Scripts/Boot.meta
new file mode 100644
index 0000000..632c81f
--- /dev/null
+++ b/AIClient/Assets/AOT/Scripts/Boot.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: bf0509e7c014a1e478f821ac118ec9ad
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/AIClient/Assets/AOT/Scripts/Boot/BootContext.cs b/AIClient/Assets/AOT/Scripts/Boot/BootContext.cs
new file mode 100644
index 0000000..6d54d73
--- /dev/null
+++ b/AIClient/Assets/AOT/Scripts/Boot/BootContext.cs
@@ -0,0 +1,34 @@
+using YooAsset;
+
+namespace AOT.Boot
+{
+ ///
+ /// 壳层传给热更入口的运行时上下文。
+ ///
+ public sealed class BootContext
+ {
+ public string AppVersion;
+ public string ResVersion;
+ public string CdnRoot;
+ public string PackageName;
+ public ResourcePackage Package;
+ public string WsUrlTemplate;
+
+ public string BuildWsUrl(string pid, string token = null)
+ {
+ if (string.IsNullOrEmpty(WsUrlTemplate))
+ {
+ if (string.IsNullOrEmpty(token))
+ return $"wss://game.xworld.ren/ws?pid={pid}";
+ return $"wss://game.xworld.ren/ws?pid={pid}&token={UnityEngine.Networking.UnityWebRequest.EscapeURL(token)}";
+ }
+
+ var url = WsUrlTemplate.Replace("{pid}", pid ?? string.Empty);
+ if (url.Contains("{token}"))
+ url = url.Replace("{token}", UnityEngine.Networking.UnityWebRequest.EscapeURL(token ?? string.Empty));
+ else if (!string.IsNullOrEmpty(token))
+ url += (url.Contains("?") ? "&" : "?") + "token=" + UnityEngine.Networking.UnityWebRequest.EscapeURL(token);
+ return url;
+ }
+ }
+}
diff --git a/AIClient/Assets/AOT/Scripts/Boot/BootContext.cs.meta b/AIClient/Assets/AOT/Scripts/Boot/BootContext.cs.meta
new file mode 100644
index 0000000..7785066
--- /dev/null
+++ b/AIClient/Assets/AOT/Scripts/Boot/BootContext.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 3a57e0242ddf4ff48af5d8e8a763e105
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/AIClient/Assets/AOT/Scripts/Boot/BootDriver.cs b/AIClient/Assets/AOT/Scripts/Boot/BootDriver.cs
new file mode 100644
index 0000000..6b05d00
--- /dev/null
+++ b/AIClient/Assets/AOT/Scripts/Boot/BootDriver.cs
@@ -0,0 +1,71 @@
+using AOT.Boot;
+using AOT.HotUpdate;
+using AOT.Patch;
+using AOT.UI;
+using UnityEngine;
+
+namespace AOT.Boot
+{
+ ///
+ /// 壳层启动入口:版本检查 → 资源补丁 → HybridCLR/热更入口。
+ ///
+ public sealed class BootDriver : MonoBehaviour
+ {
+ [SerializeField] BootSettings settings = new BootSettings();
+
+ BootPatchView _view;
+
+ void Awake()
+ {
+ Application.targetFrameRate = 60;
+ Screen.sleepTimeout = SleepTimeout.NeverSleep;
+ DontDestroyOnLoad(gameObject);
+
+ if (Screen.orientation != ScreenOrientation.Portrait)
+ Screen.orientation = ScreenOrientation.Portrait;
+ }
+
+ void Start()
+ {
+ _view = BootPatchView.Create();
+ StartCoroutine(Bootstrap());
+ }
+
+ System.Collections.IEnumerator Bootstrap()
+ {
+ BootContext ctx = null;
+ string error = null;
+
+ var runner = new PatchRunner(this, settings, _view);
+ yield return runner.Run((c, e) =>
+ {
+ ctx = c;
+ error = e;
+ });
+
+ if (!string.IsNullOrEmpty(error) || ctx == null)
+ {
+ _view?.SetError(error ?? "启动失败");
+ Debug.LogError($"[Boot] {error}");
+ yield break;
+ }
+
+ _view?.SetStatus("加载热更代码…");
+ _view?.SetProgress(0.97f);
+
+ string loadError = null;
+ yield return HotUpdateLoader.LoadAndStart(ctx, settings, e => loadError = e);
+
+ if (!string.IsNullOrEmpty(loadError))
+ {
+ _view?.SetError(loadError);
+ Debug.LogError($"[Boot] {loadError}");
+ yield break;
+ }
+
+ _view?.SetProgress(1f);
+ _view?.SetStatus("热更已启动");
+ Debug.Log($"[Boot] OK app={ctx.AppVersion} res={ctx.ResVersion} cdn={ctx.CdnRoot}");
+ }
+ }
+}
diff --git a/AIClient/Assets/AOT/Scripts/Boot/BootDriver.cs.meta b/AIClient/Assets/AOT/Scripts/Boot/BootDriver.cs.meta
new file mode 100644
index 0000000..9e24e3b
--- /dev/null
+++ b/AIClient/Assets/AOT/Scripts/Boot/BootDriver.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 4d11b543accc52448b82d773c022254c
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/AIClient/Assets/AOT/Scripts/Boot/BootSettings.cs b/AIClient/Assets/AOT/Scripts/Boot/BootSettings.cs
new file mode 100644
index 0000000..0b7bf87
--- /dev/null
+++ b/AIClient/Assets/AOT/Scripts/Boot/BootSettings.cs
@@ -0,0 +1,39 @@
+using UnityEngine;
+using YooAsset;
+
+namespace AOT.Boot
+{
+ ///
+ /// Boot 可序列化配置(挂在 BootDriver 上)。
+ ///
+ [System.Serializable]
+ public class BootSettings
+ {
+ [Tooltip("壳版本,对应提审包 appVersion")]
+ public string AppVersion = "1.0.0";
+
+ [Tooltip("CDN 总控 version.json")]
+ public string VersionUrl = "https://www.xworld.ren/wechat/version.json";
+
+ [Tooltip("CDN 根(version.json 未下发 cdnRoot 时回退;含 Package 名)")]
+ public string DefaultCdnRoot = "https://www.xworld.ren/wechat/1.0.0/DefaultPackage";
+
+ [Tooltip("YooAsset 默认包名")]
+ public string PackageName = "DefaultPackage";
+
+ [Tooltip("热更 DLL 在包内的定位地址")]
+ public string HotUpdateDllLocation = "HotUpdate.Runtime";
+
+ [Tooltip("WebSocket 模板,{pid}/{token} 会被替换")]
+ public string WsUrlTemplate = "wss://game.xworld.ren/ws?pid={pid}&token={token}";
+
+ [Tooltip("运行模式;编辑器默认 EditorSimulateMode;M1 Offline/Host 验证时切换")]
+ public EPlayMode PlayMode = EPlayMode.EditorSimulateMode;
+
+ [Tooltip("编辑器下跳过 YooAsset 补丁,直接反射加载已编译热更程序集(M0 默认开;M1 验证补丁时关掉)")]
+ public bool EditorDirectHotUpdate = true;
+
+ [Tooltip("version.json 拉取失败时是否允许使用 StreamingAssets 本地回退")]
+ public bool AllowStreamingVersionFallback = true;
+ }
+}
diff --git a/AIClient/Assets/AOT/Scripts/Boot/BootSettings.cs.meta b/AIClient/Assets/AOT/Scripts/Boot/BootSettings.cs.meta
new file mode 100644
index 0000000..c02fc8c
--- /dev/null
+++ b/AIClient/Assets/AOT/Scripts/Boot/BootSettings.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: f2c6317a2a3e50f4ca2bed34aafc0291
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/AIClient/Assets/AOT/Scripts/HotUpdate.meta b/AIClient/Assets/AOT/Scripts/HotUpdate.meta
new file mode 100644
index 0000000..025d232
--- /dev/null
+++ b/AIClient/Assets/AOT/Scripts/HotUpdate.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 5891db66601b1c140aaa02117bba81c5
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/AIClient/Assets/AOT/Scripts/HotUpdate/HotUpdateLoader.cs b/AIClient/Assets/AOT/Scripts/HotUpdate/HotUpdateLoader.cs
new file mode 100644
index 0000000..3c8f0d8
--- /dev/null
+++ b/AIClient/Assets/AOT/Scripts/HotUpdate/HotUpdateLoader.cs
@@ -0,0 +1,126 @@
+using System;
+using System.Collections;
+using System.Linq;
+using System.Reflection;
+using AOT.Boot;
+using UnityEngine;
+using YooAsset;
+#if !UNITY_EDITOR
+using HybridCLR;
+#endif
+
+namespace AOT.HotUpdate
+{
+ ///
+ /// 加载热更程序集并调用稳定入口 HotUpdate.Entry.Start(BootContext)。
+ ///
+ public static class HotUpdateLoader
+ {
+ const string EntryTypeName = "HotUpdate.Entry";
+ const string EntryMethodName = "Start";
+
+ public static IEnumerator LoadAndStart(BootContext ctx, BootSettings settings, Action onError)
+ {
+#if UNITY_EDITOR
+ if (settings.EditorDirectHotUpdate || ctx.Package == null)
+ {
+ var err = InvokeEntry(FindLoadedAssembly("HotUpdate.Runtime"), ctx);
+ if (err != null) onError?.Invoke(err);
+ yield break;
+ }
+#endif
+
+ if (ctx.Package == null)
+ {
+ onError?.Invoke("ResourcePackage 为空,无法加载热更 DLL");
+ yield break;
+ }
+
+ var handle = ctx.Package.LoadAssetAsync(settings.HotUpdateDllLocation);
+ yield return handle;
+ if (handle.Status != EOperationStatus.Succeeded)
+ {
+ onError?.Invoke($"加载热更 DLL 失败: {handle.Error}");
+ yield break;
+ }
+
+ var raw = handle.GetAssetObject();
+ if (raw == null)
+ {
+ onError?.Invoke("热更 DLL RawFileObject 为空");
+ yield break;
+ }
+
+ byte[] dllBytes = raw.GetBytes();
+ handle.Release();
+
+ if (dllBytes == null || dllBytes.Length < 64)
+ {
+ onError?.Invoke($"热更 DLL 字节异常 length={dllBytes?.Length ?? 0}");
+ yield break;
+ }
+
+ Debug.Log($"[HotUpdateLoader] RawFile loaded location={settings.HotUpdateDllLocation} bytes={dllBytes.Length}");
+
+#if UNITY_EDITOR
+ // 编辑器域内已加载同名程序集,不能重复 Assembly.Load;验证 RawFile 后走反射入口。
+ {
+ var err = InvokeEntry(FindLoadedAssembly("HotUpdate.Runtime"), ctx);
+ if (err != null) onError?.Invoke(err);
+ yield break;
+ }
+#else
+ // AOT 补充元数据:后续由 HybridCLR Generate 产物批量加载。
+ // HybridCLR.RuntimeApi.LoadMetadataForAOTAssembly(metaBytes, HomologousImageMode.SuperSet);
+
+ Assembly asm;
+ try
+ {
+ asm = Assembly.Load(dllBytes);
+ }
+ catch (Exception e)
+ {
+ onError?.Invoke($"Assembly.Load 失败: {e.Message}");
+ yield break;
+ }
+
+ var invokeErr = InvokeEntry(asm, ctx);
+ if (invokeErr != null)
+ onError?.Invoke(invokeErr);
+#endif
+ }
+
+ static Assembly FindLoadedAssembly(string name)
+ {
+ return AppDomain.CurrentDomain.GetAssemblies()
+ .FirstOrDefault(a => string.Equals(a.GetName().Name, name, StringComparison.Ordinal));
+ }
+
+ static string InvokeEntry(Assembly asm, BootContext ctx)
+ {
+ if (asm == null)
+ return "未找到热更程序集 HotUpdate.Runtime";
+
+ var type = asm.GetType(EntryTypeName, throwOnError: false);
+ if (type == null)
+ return $"未找到类型 {EntryTypeName}";
+
+ var method = type.GetMethod(EntryMethodName, BindingFlags.Public | BindingFlags.Static);
+ if (method == null)
+ return $"未找到方法 {EntryTypeName}.{EntryMethodName}";
+
+ try
+ {
+ method.Invoke(null, new object[] { ctx });
+ return null;
+ }
+ catch (Exception e)
+ {
+ var inner = e is TargetInvocationException tie && tie.InnerException != null
+ ? tie.InnerException
+ : e;
+ return $"调用热更入口失败: {inner.Message}";
+ }
+ }
+ }
+}
diff --git a/AIClient/Assets/AOT/Scripts/HotUpdate/HotUpdateLoader.cs.meta b/AIClient/Assets/AOT/Scripts/HotUpdate/HotUpdateLoader.cs.meta
new file mode 100644
index 0000000..becd4cc
--- /dev/null
+++ b/AIClient/Assets/AOT/Scripts/HotUpdate/HotUpdateLoader.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: f6796557e85f4374e95c149ba3b871e5
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/AIClient/Assets/AOT/Scripts/Patch.meta b/AIClient/Assets/AOT/Scripts/Patch.meta
new file mode 100644
index 0000000..7c135b3
--- /dev/null
+++ b/AIClient/Assets/AOT/Scripts/Patch.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: f909e1ffa39b29349b083bc3a520263d
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/AIClient/Assets/AOT/Scripts/Patch/CdnRemoteService.cs b/AIClient/Assets/AOT/Scripts/Patch/CdnRemoteService.cs
new file mode 100644
index 0000000..dc33ecd
--- /dev/null
+++ b/AIClient/Assets/AOT/Scripts/Patch/CdnRemoteService.cs
@@ -0,0 +1,35 @@
+using System.Collections.Generic;
+using YooAsset;
+
+namespace AOT.Patch
+{
+ ///
+ /// YooAsset 远端地址查询(CDN)。
+ ///
+ 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 GetRemoteUrls(string fileName)
+ {
+ return new[]
+ {
+ $"{_primary}/{fileName}",
+ $"{_fallback}/{fileName}",
+ };
+ }
+
+ static string TrimSlash(string root)
+ {
+ if (string.IsNullOrEmpty(root)) return string.Empty;
+ return root.TrimEnd('/');
+ }
+ }
+}
diff --git a/AIClient/Assets/AOT/Scripts/Patch/CdnRemoteService.cs.meta b/AIClient/Assets/AOT/Scripts/Patch/CdnRemoteService.cs.meta
new file mode 100644
index 0000000..9b81ced
--- /dev/null
+++ b/AIClient/Assets/AOT/Scripts/Patch/CdnRemoteService.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 89730891313aa084e9adf58e6f37542f
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/AIClient/Assets/AOT/Scripts/Patch/PatchRunner.cs b/AIClient/Assets/AOT/Scripts/Patch/PatchRunner.cs
new file mode 100644
index 0000000..d50dfea
--- /dev/null
+++ b/AIClient/Assets/AOT/Scripts/Patch/PatchRunner.cs
@@ -0,0 +1,231 @@
+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",
+ 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;
+ }
+ }
+}
diff --git a/AIClient/Assets/AOT/Scripts/Patch/PatchRunner.cs.meta b/AIClient/Assets/AOT/Scripts/Patch/PatchRunner.cs.meta
new file mode 100644
index 0000000..326a79b
--- /dev/null
+++ b/AIClient/Assets/AOT/Scripts/Patch/PatchRunner.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 57535d94c1b98984da5c887d5f55657a
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/AIClient/Assets/AOT/Scripts/Patch/VersionClient.cs b/AIClient/Assets/AOT/Scripts/Patch/VersionClient.cs
new file mode 100644
index 0000000..fe253df
--- /dev/null
+++ b/AIClient/Assets/AOT/Scripts/Patch/VersionClient.cs
@@ -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 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 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
+ }
+ }
+}
diff --git a/AIClient/Assets/AOT/Scripts/Patch/VersionClient.cs.meta b/AIClient/Assets/AOT/Scripts/Patch/VersionClient.cs.meta
new file mode 100644
index 0000000..5f9bb1c
--- /dev/null
+++ b/AIClient/Assets/AOT/Scripts/Patch/VersionClient.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 3c255f1d996335441ae6dccb3c3dc352
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/AIClient/Assets/AOT/Scripts/Patch/VersionInfo.cs b/AIClient/Assets/AOT/Scripts/Patch/VersionInfo.cs
new file mode 100644
index 0000000..f99d8e5
--- /dev/null
+++ b/AIClient/Assets/AOT/Scripts/Patch/VersionInfo.cs
@@ -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(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;
+ }
+ }
+}
diff --git a/AIClient/Assets/AOT/Scripts/Patch/VersionInfo.cs.meta b/AIClient/Assets/AOT/Scripts/Patch/VersionInfo.cs.meta
new file mode 100644
index 0000000..479ed38
--- /dev/null
+++ b/AIClient/Assets/AOT/Scripts/Patch/VersionInfo.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 925a013a115d7694f8429c8444591d46
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/AIClient/Assets/AOT/Scripts/UI.meta b/AIClient/Assets/AOT/Scripts/UI.meta
new file mode 100644
index 0000000..432c998
--- /dev/null
+++ b/AIClient/Assets/AOT/Scripts/UI.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: eb0e5690dd591654997ad561372bd3a1
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/AIClient/Assets/AOT/Scripts/UI/BootPatchView.cs b/AIClient/Assets/AOT/Scripts/UI/BootPatchView.cs
new file mode 100644
index 0000000..6a4f3e2
--- /dev/null
+++ b/AIClient/Assets/AOT/Scripts/UI/BootPatchView.cs
@@ -0,0 +1,122 @@
+using UnityEngine;
+using UnityEngine.UI;
+
+namespace AOT.UI
+{
+ ///
+ /// 启动更新条(运行时生成,竖屏安全区友好)。
+ ///
+ public sealed class BootPatchView : MonoBehaviour
+ {
+ Text _status;
+ Image _fill;
+
+ public static BootPatchView Create()
+ {
+ var root = new GameObject("BootPatchUI");
+ DontDestroyOnLoad(root);
+
+ var canvasGo = new GameObject("Canvas");
+ canvasGo.transform.SetParent(root.transform, false);
+ var canvas = canvasGo.AddComponent