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,139 @@
#if UNITY_EDITOR
using System;
using System.IO;
using HybridCLR.Editor;
using HybridCLR.Editor.Commands;
using UnityEditor;
using UnityEngine;
using YooAsset;
using YooAsset.Editor;
namespace AOT.Editor.YooAssetTools
{
/// <summary>
/// CompileDll → 拷贝到 Bundles/Raw → RawFile 构建 → 发布到 Tools/cdn_root + StreamingAssets。
/// </summary>
public static class BuildAndStageHotUpdate
{
public const string PackageName = SetupBundleCollector.PackageName;
public const string AppVersion = "1.0.0";
public const string Channel = "wechat";
static string RepoRoot => Path.GetFullPath(Path.Combine(Application.dataPath, "..", ".."));
static string CdnRoot => Path.Combine(RepoRoot, "Tools", "cdn_root");
static string ProjectBundlesRoot => BundleBuilderHelper.GetDefaultBuildOutputRoot();
[MenuItem("AIGame/YooAsset/Build And Stage (Raw + CDN)")]
public static string BuildAndStage()
{
SetupBundleCollector.Setup();
var target = EditorUserBuildSettings.activeBuildTarget;
CompileDllCommand.CompileDll(target, false);
var dllSrc = Path.Combine(SettingsUtil.GetHotUpdateDllsOutputDirByTarget(target), "HotUpdate.Runtime.dll");
if (!File.Exists(dllSrc))
return $"fail: missing compiled dll: {dllSrc}";
Directory.CreateDirectory(Path.GetDirectoryName(SetupBundleCollector.DllBytesPath) ?? "Assets/Bundles/Raw");
File.Copy(dllSrc, SetupBundleCollector.DllBytesPath, overwrite: true);
AssetDatabase.ImportAsset(SetupBundleCollector.DllBytesPath, ImportAssetOptions.ForceUpdate);
string packageVersion = DateTime.Now.ToString("yyyyMMdd.HHmmss");
var buildResult = BuildRawPackage(target, packageVersion);
if (!buildResult.Success)
return $"fail build: {buildResult.ErrorInfo}";
string packageDir = buildResult.OutputPackageDirectory;
string stageDir = Path.Combine(CdnRoot, Channel, AppVersion, PackageName);
StageDirectory(packageDir, stageDir);
string publicCdnRoot = $"https://www.xworld.ren/{Channel}/{AppVersion}/{PackageName}";
string localCdnRoot = $"http://127.0.0.1:8787/{Channel}/{AppVersion}/{PackageName}";
// 本地 serve 用 local;上传前用 Tools/cdn_publish.py write-version 切到 public
WriteVersionJson(Path.Combine(CdnRoot, Channel, "version.json"), packageVersion, localCdnRoot);
WriteVersionJson(Path.Combine(CdnRoot, Channel, "version.prod.json"), packageVersion, publicCdnRoot);
WriteVersionJson(Path.Combine(Application.streamingAssetsPath, "cdn", "version.json"), packageVersion, localCdnRoot);
WritePublishMeta(packageVersion, packageDir, stageDir, dllSrc);
AssetDatabase.Refresh();
return $"ok ver={packageVersion} packageDir={packageDir} stage={stageDir} dllBytes={new FileInfo(SetupBundleCollector.DllBytesPath).Length}";
}
static BuildResult BuildRawPackage(BuildTarget target, string packageVersion)
{
var buildParameters = new RawFileBuildParameters
{
BuildOutputRoot = ProjectBundlesRoot,
BundledFileRoot = BundleBuilderHelper.GetStreamingAssetsRoot(),
BuildPipeline = EBuildPipeline.RawFileBuildPipeline.ToString(),
BuildBundleType = (int)EBundleType.RawBundle,
BuildTarget = target,
PackageName = PackageName,
PackageVersion = packageVersion,
VerifyBuildingResult = true,
FileNameStyle = EFileNameStyle.HashName,
// 拷贝到 StreamingAssets/yoo,便于 OfflinePlayMode 冒烟
BundledCopyOption = EBundledCopyOption.ClearAndCopyAll,
BundledCopyParams = string.Empty,
ClearBuildCacheFiles = false,
UseAssetDependencyDB = true,
};
var pipeline = new RawFileBuildPipeline();
return pipeline.Run(buildParameters, true);
}
static void StageDirectory(string src, string dst)
{
if (Directory.Exists(dst))
Directory.Delete(dst, true);
Directory.CreateDirectory(dst);
foreach (var file in Directory.GetFiles(src, "*", SearchOption.AllDirectories))
{
var rel = file.Substring(src.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
var destFile = Path.Combine(dst, rel);
Directory.CreateDirectory(Path.GetDirectoryName(destFile) ?? dst);
File.Copy(file, destFile, true);
}
}
static void WriteVersionJson(string path, string resVersion, string cdnRoot)
{
Directory.CreateDirectory(Path.GetDirectoryName(path) ?? ".");
// JsonUtility 不支持 string[],手写 JSON
var json =
"{\n" +
$" \"appVersionMin\": \"{AppVersion}\",\n" +
$" \"appVersionMax\": \"{AppVersion}\",\n" +
$" \"resVersion\": \"{resVersion}\",\n" +
$" \"cdnRoot\": \"{cdnRoot}\",\n" +
$" \"packages\": [ \"{PackageName}\" ],\n" +
" \"forceUpdate\": false,\n" +
" \"notice\": \"\"\n" +
"}\n";
File.WriteAllText(path, json);
}
static void WritePublishMeta(string resVersion, string packageDir, string stageDir, string dllSrc)
{
var metaPath = Path.Combine(CdnRoot, Channel, "last_publish.json");
Directory.CreateDirectory(Path.GetDirectoryName(metaPath) ?? CdnRoot);
var json =
"{\n" +
$" \"resVersion\": \"{resVersion}\",\n" +
$" \"appVersion\": \"{AppVersion}\",\n" +
$" \"packageDir\": \"{Escape(packageDir)}\",\n" +
$" \"stageDir\": \"{Escape(stageDir)}\",\n" +
$" \"dllSrc\": \"{Escape(dllSrc)}\",\n" +
$" \"builtAt\": \"{DateTime.Now:o}\"\n" +
"}\n";
File.WriteAllText(metaPath, json);
}
static string Escape(string path) => (path ?? string.Empty).Replace("\\", "\\\\").Replace("\"", "\\\"");
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b90533025225c0149a438a078fe71011
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,56 @@
#if UNITY_EDITOR
using AOT.Boot;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
using YooAsset;
namespace AOT.Editor.YooAssetTools
{
/// <summary>
/// 切换 BootDriver 的 M1 验证模式(Offline / Host / Direct)。
/// </summary>
public static class ConfigureBootPlayMode
{
const string BootScene = "Assets/Scenes/Boot.unity";
[MenuItem("AIGame/Boot/Mode/Editor Direct (M0)")]
public static string SetDirect() => Apply(
EPlayMode.EditorSimulateMode,
editorDirect: true,
versionUrl: "https://www.xworld.ren/wechat/version.json",
defaultCdn: "https://www.xworld.ren/wechat/1.0.0/DefaultPackage");
[MenuItem("AIGame/Boot/Mode/Offline Smoke (M1)")]
public static string SetOffline() => Apply(EPlayMode.OfflinePlayMode, editorDirect: false);
[MenuItem("AIGame/Boot/Mode/Host Local CDN (M1)")]
public static string SetHostLocal() => Apply(EPlayMode.HostPlayMode, editorDirect: false,
versionUrl: "http://127.0.0.1:8787/wechat/version.json",
defaultCdn: "http://127.0.0.1:8787/wechat/1.0.0/DefaultPackage");
static string Apply(EPlayMode playMode, bool editorDirect, string versionUrl = null, string defaultCdn = null)
{
var scene = EditorSceneManager.OpenScene(BootScene, OpenSceneMode.Single);
var boot = Object.FindObjectOfType<BootDriver>();
if (boot == null)
return "fail: BootDriver not found in Boot scene";
var so = new SerializedObject(boot);
var settings = so.FindProperty("settings");
settings.FindPropertyRelative("PlayMode").enumValueIndex = (int)playMode;
settings.FindPropertyRelative("EditorDirectHotUpdate").boolValue = editorDirect;
if (!string.IsNullOrEmpty(versionUrl))
settings.FindPropertyRelative("VersionUrl").stringValue = versionUrl;
if (!string.IsNullOrEmpty(defaultCdn))
settings.FindPropertyRelative("DefaultCdnRoot").stringValue = defaultCdn;
so.ApplyModifiedPropertiesWithoutUndo();
EditorSceneManager.MarkSceneDirty(scene);
EditorSceneManager.SaveScene(scene);
return $"ok playMode={playMode} editorDirect={editorDirect} versionUrl={settings.FindPropertyRelative("VersionUrl").stringValue}";
}
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 63c6c0b31c0ce5040a28cb40125ae1ae
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,93 @@
#if UNITY_EDITOR
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
using YooAsset.Editor;
namespace AOT.Editor.YooAssetTools
{
/// <summary>
/// 初始化 DefaultPackage 收集配置(Raw 热更 DLL)。
/// </summary>
public static class SetupBundleCollector
{
public const string PackageName = "DefaultPackage";
public const string CollectPath = "Assets/Bundles/Raw";
public const string DllBytesPath = "Assets/Bundles/Raw/HotUpdate.Runtime.bytes";
[MenuItem("AIGame/YooAsset/Setup Bundle Collector")]
public static string Setup()
{
Directory.CreateDirectory(CollectPath);
EnsurePlaceholderDllBytes();
var setting = BundleCollectorSettingData.Setting;
setting.ShowPackageView = true;
setting.UniqueBundleName = false;
var package = setting.Packages.FirstOrDefault(p => p.PackageName == PackageName);
if (package == null)
package = BundleCollectorSettingData.CreatePackage(PackageName);
package.PackageDesc = "启动必需:热更 DLL 等";
package.EnableAddressable = true;
package.SupportExtensionless = true;
package.LocationToLower = false;
package.IncludeAssetGUID = false;
package.AutoCollectShaders = false;
package.IgnoreRuleName = nameof(RawFileIgnoreRule);
BundleCollectorSettingData.ModifyPackage(package);
var group = package.Groups.FirstOrDefault(g => g.GroupName == "HotUpdateDll");
if (group == null)
group = BundleCollectorSettingData.CreateGroup(package, "HotUpdateDll");
group.GroupDesc = "HybridCLR 热更程序集";
group.ActiveRuleName = nameof(EnableGroup);
group.AssetTags = "aot";
BundleCollectorSettingData.ModifyGroup(package, group);
var collector = group.Collectors.FirstOrDefault(c => c.CollectPath == CollectPath);
if (collector == null)
{
collector = new BundleCollector
{
CollectPath = CollectPath,
CollectorGUID = AssetDatabase.AssetPathToGUID(CollectPath),
CollectorType = ECollectorType.MainAssetCollector,
AddressRuleName = nameof(AddressByFileName),
PackRuleName = nameof(PackRawFile),
FilterRuleName = nameof(CollectAll),
AssetTags = "aot",
};
BundleCollectorSettingData.CreateCollector(group, collector);
}
else
{
collector.CollectorGUID = AssetDatabase.AssetPathToGUID(CollectPath);
collector.CollectorType = ECollectorType.MainAssetCollector;
collector.AddressRuleName = nameof(AddressByFileName);
collector.PackRuleName = nameof(PackRawFile);
collector.FilterRuleName = nameof(CollectAll);
collector.AssetTags = "aot";
BundleCollectorSettingData.ModifyCollector(group, collector);
}
package.CheckConfigError();
BundleCollectorSettingData.SaveFile();
return $"ok package={PackageName} collect={CollectPath} dll={DllBytesPath} setting={AssetDatabase.GetAssetPath(setting)}";
}
public static void EnsurePlaceholderDllBytes()
{
if (File.Exists(DllBytesPath))
return;
Directory.CreateDirectory(Path.GetDirectoryName(DllBytesPath) ?? CollectPath);
// 占位:真实内容由 BuildAndStageHotUpdate 覆盖
File.WriteAllBytes(DllBytesPath, System.Text.Encoding.UTF8.GetBytes("PLACEHOLDER_HOTUPDATE_DLL"));
AssetDatabase.ImportAsset(DllBytesPath, ImportAssetOptions.ForceUpdate);
}
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 011b17d87b02c034abdc5cad22378c26
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: