Ship the WeChat hot-update catalogs and the camp, TideWhale T2, UI atlas, and animation-lab work.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
udx
2026-10-24 18:04:41 +08:00
co-authored by Cursor
parent 6ebc9401ce
commit 45e62132c8
1432 changed files with 2971856 additions and 2798 deletions
@@ -1,6 +1,7 @@
#if UNITY_EDITOR
using System;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
using YooAsset;
@@ -26,6 +27,7 @@ namespace AOT.Editor.YooAssetTools
{
SetupM3Character.SetupAll();
AIGame.EditorTools.SetupUiBundleCollector.Setup();
EnsureLoadableScenePrefabs();
var target = EditorUserBuildSettings.activeBuildTarget;
string packageVersion = DateTime.Now.ToString("yyyyMMdd.HHmmss");
@@ -33,15 +35,95 @@ namespace AOT.Editor.YooAssetTools
if (!buildResult.Success)
return $"fail: {buildResult.ErrorInfo}";
var atlasChecks = AIGame.EditorTools.UiAtlasBuildValidation.Inspect(buildResult.OutputPackageDirectory);
if (atlasChecks.Any(c => c.SourceTextureCount > 0))
{
// 更改图集绑定设置后,SBP 可能复用仍引用原图像素的旧 Prefab 依赖缓存。
Debug.LogWarning("[UIAtlas] Stale source textures found; purging SBP cache and rebuilding Art once.");
UnityEditor.Build.Pipeline.Utilities.BuildCache.PurgeCache(false);
packageVersion += ".atlasfix";
buildResult = BuildArtPackage(target, packageVersion);
if (!buildResult.Success) return $"fail: {buildResult.ErrorInfo}";
atlasChecks = AIGame.EditorTools.UiAtlasBuildValidation.Inspect(buildResult.OutputPackageDirectory);
var remaining = atlasChecks.Where(c => c.SourceTextureCount > 0).ToArray();
if (remaining.Length > 0)
return "fail: UI atlas bundles still contain source Texture2D data after cache rebuild: "
+ string.Join(", ", remaining.Select(c => c.FileName + " (" + c.SourceTextureCount + ")"));
}
Debug.Log("[UIAtlas] Validated " + atlasChecks.Length + " atlas bundles; no original Texture2D data.");
string packageDir = buildResult.OutputPackageDirectory;
var resMd5 = BuildAndStageHotUpdate.StampPackageIndex(packageDir, PackageName);
if (string.IsNullOrEmpty(resMd5))
return "fail: Art index missing";
string stageDir = Path.Combine(CdnRoot, Channel, AppVersion, PackageName);
StageDirectory(packageDir, stageDir);
BuildAndStageHotUpdate.StampPackageIndex(BuildAndStageHotUpdate.StreamingPackageDir(PackageName), PackageName);
// 合并 version.json packages 列表
MergeVersionPackages(packageVersion);
var packMd5 = BuildAndStageHotUpdate.StampPackageIndex(
BuildAndStageHotUpdate.StreamingPackageDir(BuildAndStageHotUpdate.PackageName),
BuildAndStageHotUpdate.PackageName);
if (string.IsNullOrEmpty(packMd5))
packMd5 = BuildAndStageHotUpdate.StampPackageIndex(
Path.Combine(CdnRoot, Channel, AppVersion, BuildAndStageHotUpdate.PackageName),
BuildAndStageHotUpdate.PackageName);
BuildAndStageHotUpdate.WriteVersionSet(packMd5, resMd5);
AssetDatabase.Refresh();
return $"ok ver={packageVersion} dir={packageDir} stage={stageDir}";
return $"ok res={resMd5} pack={packMd5} dir={packageDir} stage={stageDir}";
}
/// <summary>
/// 整棵 Scenes 目录不能按文件名做地址:同名模型、材质和嵌套预制体会冲突。
/// 只把运行时按名加载的 TownSquare 作为入口,其余场景资源随依赖打进包。
/// </summary>
public static string EnsureLoadableScenePrefabs()
{
const string broadPath = "Assets/Bundles/Scenes";
const string townSquare = "Assets/Bundles/Scenes/Town/TownSquare/TownSquare.prefab";
var package = BundleCollectorSettingData.Setting.Packages.Find(p => p.PackageName == PackageName);
if (package == null)
return "fail: Art package missing";
var group = package.Groups.Find(g => g.GroupName == "Scenes");
if (group == null)
group = BundleCollectorSettingData.CreateGroup(package, "Scenes");
group.ActiveRuleName = nameof(EnableGroup);
group.AssetTags = "art";
BundleCollectorSettingData.ModifyGroup(package, group);
var broad = group.Collectors.Find(c => c.CollectPath == broadPath);
if (broad != null)
BundleCollectorSettingData.RemoveCollector(group, broad);
var collector = group.Collectors.Find(c => c.CollectPath == townSquare);
if (collector == null)
{
collector = new BundleCollector
{
CollectPath = townSquare,
CollectorGUID = AssetDatabase.AssetPathToGUID(townSquare),
CollectorType = ECollectorType.MainAssetCollector,
AddressRuleName = nameof(AddressByFileName),
PackRuleName = nameof(PackSeparately),
FilterRuleName = nameof(CollectPrefab),
AssetTags = "art",
};
BundleCollectorSettingData.CreateCollector(group, collector);
}
else
{
collector.CollectorGUID = AssetDatabase.AssetPathToGUID(townSquare);
collector.CollectorType = ECollectorType.MainAssetCollector;
collector.AddressRuleName = nameof(AddressByFileName);
collector.PackRuleName = nameof(PackSeparately);
collector.FilterRuleName = nameof(CollectPrefab);
BundleCollectorSettingData.ModifyCollector(group, collector);
}
package.CheckConfigError();
BundleCollectorSettingData.SaveFile();
return "ok scenes=" + townSquare;
}
static BuildResult BuildArtPackage(BuildTarget target, string packageVersion)
@@ -61,7 +143,8 @@ namespace AOT.Editor.YooAssetTools
PackageVersion = packageVersion,
EnableSharePackRule = true,
VerifyBuildingResult = true,
FileNameStyle = EFileNameStyle.HashName,
// 输出为资源包原名加内容哈希,便于在 CDN 目录里按名称核对每个包的体积。
FileNameStyle = EFileNameStyle.BundleName_HashName,
BundledCopyOption = EBundledCopyOption.ClearAndCopyAll,
BundledCopyParams = string.Empty,
CompressOption = ECompressOption.LZ4,
@@ -88,36 +171,6 @@ namespace AOT.Editor.YooAssetTools
}
}
static void MergeVersionPackages(string resVersion)
{
var path = Path.Combine(CdnRoot, Channel, "version.json");
Directory.CreateDirectory(Path.GetDirectoryName(path) ?? CdnRoot);
var localCdn = $"http://127.0.0.1:8787/{Channel}/{AppVersion}/{BuildAndStageHotUpdate.PackageName}";
var json =
"{\n" +
$" \"appVersionMin\": \"{AppVersion}\",\n" +
$" \"appVersionMax\": \"{AppVersion}\",\n" +
$" \"resVersion\": \"{resVersion}\",\n" +
$" \"cdnRoot\": \"{localCdn}\",\n" +
" \"packages\": [ \"DefaultPackage\", \"Art\" ],\n" +
" \"forceUpdate\": false,\n" +
" \"notice\": \"\"\n" +
"}\n";
File.WriteAllText(path, json);
File.WriteAllText(Path.Combine(Application.streamingAssetsPath, "cdn", "version.json"), json);
var prod =
"{\n" +
$" \"appVersionMin\": \"{AppVersion}\",\n" +
$" \"appVersionMax\": \"{AppVersion}\",\n" +
$" \"resVersion\": \"{resVersion}\",\n" +
$" \"cdnRoot\": \"https://www.xworld.ren/{Channel}/{AppVersion}/{BuildAndStageHotUpdate.PackageName}\",\n" +
" \"packages\": [ \"DefaultPackage\", \"Art\" ],\n" +
" \"forceUpdate\": false,\n" +
" \"notice\": \"\"\n" +
"}\n";
File.WriteAllText(Path.Combine(CdnRoot, Channel, "version.prod.json"), prod);
}
}
}
#endif
@@ -1,6 +1,9 @@
#if UNITY_EDITOR
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text.RegularExpressions;
using AOT.Boot;
using HybridCLR.Editor;
using HybridCLR.Editor.Commands;
using UnityEditor;
@@ -18,6 +21,8 @@ namespace AOT.Editor.YooAssetTools
public const string PackageName = SetupBundleCollector.PackageName;
public const string AppVersion = "1.0.0";
public const string Channel = "wechat";
/// <summary>真机 version.prod.json 的资源根。最后一段必须是 DefaultPackage,Art 由客户端替换这一段。</summary>
public const string PublicCdnRoot = BootSettings.ProductionCdnRoot;
static string RepoRoot => Path.GetFullPath(Path.Combine(Application.dataPath, "..", ".."));
static string CdnRoot => Path.Combine(RepoRoot, "Tools", "cdn_root");
@@ -46,19 +51,21 @@ namespace AOT.Editor.YooAssetTools
string packageDir = buildResult.OutputPackageDirectory;
string stageDir = Path.Combine(CdnRoot, Channel, AppVersion, PackageName);
var packMd5 = StampPackageIndex(packageDir, PackageName);
if (string.IsNullOrEmpty(packMd5))
return "fail: DefaultPackage index missing";
StageDirectory(packageDir, stageDir);
StampPackageIndex(StreamingPackageDir(PackageName), PackageName);
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);
var resMd5 = StampPackageIndex(StreamingPackageDir("Art"), "Art");
if (string.IsNullOrEmpty(resMd5))
resMd5 = StampPackageIndex(Path.Combine(CdnRoot, Channel, AppVersion, "Art"), "Art");
WriteVersionSet(packMd5, resMd5);
WritePublishMeta(packageVersion, packageDir, stageDir, dllSrc);
WritePublishMeta(packMd5, packageDir, stageDir, dllSrc);
AssetDatabase.Refresh();
return $"ok ver={packageVersion} packageDir={packageDir} stage={stageDir} dllBytes={new FileInfo(SetupBundleCollector.DllBytesPath).Length}";
return $"ok pack={packMd5} res={resMd5} packageDir={packageDir} stage={stageDir} dllBytes={new FileInfo(SetupBundleCollector.DllBytesPath).Length}";
}
static BuildResult BuildRawPackage(BuildTarget target, string packageVersion)
@@ -100,30 +107,105 @@ namespace AOT.Editor.YooAssetTools
}
}
static void WriteVersionJson(string path, string resVersion, string cdnRoot)
public static string StreamingPackageDir(string packageName) =>
Path.Combine(Application.streamingAssetsPath, "yoo", packageName);
/// <summary>
/// 用清单 .bytes 的 MD5 作为包版本,并把清单和校验文件改名为 {包名}_{md5}.*。
/// </summary>
public static string StampPackageIndex(string directory, string packageName)
{
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);
if (string.IsNullOrEmpty(directory) || !Directory.Exists(directory))
return string.Empty;
var versionPath = Path.Combine(directory, packageName + ".version");
var recorded = File.Exists(versionPath) ? File.ReadAllText(versionPath).Trim() : string.Empty;
var bytesPath = string.IsNullOrEmpty(recorded)
? null
: Path.Combine(directory, $"{packageName}_{recorded}.bytes");
if (bytesPath == null || !File.Exists(bytesPath))
{
var matches = Directory.GetFiles(directory, packageName + "_*.bytes");
if (matches.Length != 1)
return string.Empty;
bytesPath = matches[0];
}
string md5;
using (var alg = MD5.Create())
using (var stream = File.OpenRead(bytesPath))
md5 = BitConverter.ToString(alg.ComputeHash(stream)).Replace("-", "").ToLowerInvariant();
MoveFile(bytesPath, Path.Combine(directory, $"{packageName}_{md5}.bytes"));
var oldHash = Path.ChangeExtension(bytesPath, ".hash");
if (File.Exists(oldHash))
MoveFile(oldHash, Path.Combine(directory, $"{packageName}_{md5}.hash"));
File.WriteAllText(versionPath, md5);
var catalog = Path.Combine(directory, "BuiltinCatalog.json");
if (File.Exists(catalog))
{
var text = File.ReadAllText(catalog);
text = Regex.Replace(text, "(\"PackageVersion\"\\s*:\\s*\")[^\"]*(\")", "${1}" + md5 + "$2");
File.WriteAllText(catalog, text);
}
return md5;
}
static void WritePublishMeta(string resVersion, string packageDir, string stageDir, string dllSrc)
static void MoveFile(string src, string dst)
{
if (!File.Exists(src))
return;
if (string.Equals(Path.GetFullPath(src), Path.GetFullPath(dst), StringComparison.OrdinalIgnoreCase))
return;
if (File.Exists(dst))
File.Delete(dst);
File.Move(src, dst);
if (File.Exists(src + ".meta"))
MoveFile(src + ".meta", dst + ".meta");
}
public static void WriteVersionSet(string packVersion, string resVersion)
{
var localCdn = $"http://127.0.0.1:8787/{Channel}/{AppVersion}/{PackageName}";
var local = VersionJson(packVersion, resVersion, localCdn);
var prod = VersionJson(packVersion, resVersion, PublicCdnRoot);
WriteText(Path.Combine(CdnRoot, Channel, "version.json"), local);
WriteText(Path.Combine(CdnRoot, Channel, "version.prod.json"), prod);
WriteText(Path.Combine(Application.streamingAssetsPath, "cdn", "version.json"), local);
}
static string VersionJson(string packVersion, string resVersion, string cdnRoot)
{
var packages = string.IsNullOrEmpty(resVersion)
? "[ \"DefaultPackage\" ]"
: "[ \"DefaultPackage\", \"Art\" ]";
return "{\n" +
$" \"appVersionMin\": \"{AppVersion}\",\n" +
$" \"appVersionMax\": \"{AppVersion}\",\n" +
$" \"resVersion\": \"{resVersion}\",\n" +
$" \"packVersion\": \"{packVersion}\",\n" +
$" \"cdnRoot\": \"{cdnRoot}\",\n" +
$" \"packages\": {packages},\n" +
" \"forceUpdate\": false,\n" +
" \"notice\": \"\"\n" +
"}\n";
}
static void WriteText(string path, string contents)
{
Directory.CreateDirectory(Path.GetDirectoryName(path) ?? ".");
File.WriteAllText(path, contents);
}
static void WritePublishMeta(string packVersion, 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" +
$" \"packVersion\": \"{packVersion}\",\n" +
$" \"appVersion\": \"{AppVersion}\",\n" +
$" \"packageDir\": \"{Escape(packageDir)}\",\n" +
$" \"stageDir\": \"{Escape(stageDir)}\",\n" +
@@ -18,8 +18,8 @@ namespace AOT.Editor.YooAssetTools
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");
versionUrl: BootSettings.ProductionVersionUrl,
defaultCdn: BootSettings.ProductionCdnRoot);
[MenuItem("AIGame/Boot/Mode/Offline Smoke (M1)")]
public static string SetOffline() => Apply(EPlayMode.OfflinePlayMode, editorDirect: false);
@@ -29,10 +29,10 @@ namespace AOT.Editor.YooAssetTools
versionUrl: "http://127.0.0.1:8787/wechat/version.json",
defaultCdn: "http://127.0.0.1:8787/wechat/1.0.0/DefaultPackage");
[MenuItem("AIGame/Boot/Server/Production game.xworld.ren")]
[MenuItem("AIGame/Boot/Server/Production BuddyDefense.xworld.ren 15888")]
public static string SetServerProd() => ApplyServer(
"https://game.xworld.ren",
"wss://game.xworld.ren/ws?pid={pid}&token={token}");
BootSettings.ProductionHttpBase,
BootSettings.ProductionWsTemplate);
[MenuItem("AIGame/Boot/Server/Local AIServer :8080")]
public static string SetServerLocal() => ApplyServer(