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