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:
@@ -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:
|
||||
Reference in New Issue
Block a user