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:
@@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Build;
|
||||
using UnityEditor.Build.Reporting;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AOT.Editor.HybridCLR
|
||||
{
|
||||
/// <summary>Limit native WebGL build workers without changing player optimizations.</summary>
|
||||
[InitializeOnLoad]
|
||||
public sealed class WebGlBuildConcurrency : IPreprocessBuildWithReport, IPostprocessBuildWithReport
|
||||
{
|
||||
const string BeeVariable = "BEE_BUILD_THREADS";
|
||||
const string BinaryenVariable = "BINARYEN_CORES";
|
||||
const string WorkerLimit = "4";
|
||||
static bool ownsBee;
|
||||
static bool ownsBinaryen;
|
||||
|
||||
public int callbackOrder => int.MinValue;
|
||||
|
||||
static WebGlBuildConcurrency()
|
||||
{
|
||||
// Postprocess callbacks may be skipped when the native build fails.
|
||||
EditorApplication.update += RestoreWhenIdle;
|
||||
AssemblyReloadEvents.beforeAssemblyReload += Restore;
|
||||
}
|
||||
|
||||
public void OnPreprocessBuild(BuildReport report)
|
||||
{
|
||||
if (Application.platform != RuntimePlatform.WindowsEditor ||
|
||||
report.summary.platform != BuildTarget.WebGL) return;
|
||||
|
||||
ownsBee |= SetDefault(BeeVariable);
|
||||
ownsBinaryen |= SetDefault(BinaryenVariable);
|
||||
Debug.Log($"[WebGlBuildConcurrency] {BeeVariable}={Environment.GetEnvironmentVariable(BeeVariable)}, " +
|
||||
$"{BinaryenVariable}={Environment.GetEnvironmentVariable(BinaryenVariable)}");
|
||||
}
|
||||
|
||||
public void OnPostprocessBuild(BuildReport report) => Restore();
|
||||
|
||||
static bool SetDefault(string variable)
|
||||
{
|
||||
// Respect explicit values supplied by the developer or build machine.
|
||||
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(variable))) return false;
|
||||
Environment.SetEnvironmentVariable(variable, WorkerLimit);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void RestoreWhenIdle()
|
||||
{
|
||||
if (!BuildPipeline.isBuildingPlayer) Restore();
|
||||
}
|
||||
|
||||
static void Restore()
|
||||
{
|
||||
RestoreVariable(BeeVariable, ref ownsBee);
|
||||
RestoreVariable(BinaryenVariable, ref ownsBinaryen);
|
||||
}
|
||||
|
||||
static void RestoreVariable(string variable, ref bool owned)
|
||||
{
|
||||
if (!owned) return;
|
||||
if (Environment.GetEnvironmentVariable(variable) == WorkerLimit)
|
||||
Environment.SetEnvironmentVariable(variable, null);
|
||||
owned = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6bd16bdf1ab55704da188efd417e749a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -334,8 +334,8 @@ public static class SetupM3Character
|
||||
CollectorGUID = AssetDatabase.AssetPathToGUID(collectPath),
|
||||
CollectorType = ECollectorType.MainAssetCollector,
|
||||
AddressRuleName = nameof(AddressByFileName),
|
||||
PackRuleName = nameof(PackDirectory),
|
||||
FilterRuleName = nameof(CollectAll),
|
||||
PackRuleName = nameof(PackSeparately),
|
||||
FilterRuleName = nameof(CollectPrefab),
|
||||
AssetTags = "art",
|
||||
};
|
||||
BundleCollectorSettingData.CreateCollector(group, collector);
|
||||
@@ -344,14 +344,15 @@ public static class SetupM3Character
|
||||
{
|
||||
collector.CollectorGUID = AssetDatabase.AssetPathToGUID(collectPath);
|
||||
collector.AddressRuleName = nameof(AddressByFileName);
|
||||
collector.PackRuleName = nameof(PackDirectory);
|
||||
collector.FilterRuleName = nameof(CollectAll);
|
||||
collector.PackRuleName = nameof(PackSeparately);
|
||||
// 同名 FBX、材质和预制体不能都用文件名做地址;运行时只按预制体名加载。
|
||||
collector.FilterRuleName = nameof(CollectPrefab);
|
||||
BundleCollectorSettingData.ModifyCollector(group, collector);
|
||||
}
|
||||
|
||||
package.CheckConfigError();
|
||||
BundleCollectorSettingData.SaveFile();
|
||||
return $"package={ArtPackage} collect={collectPath} pack=PackDirectory";
|
||||
return $"package={ArtPackage} collect={collectPath} pack=PackSeparately";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using HotUpdate.UI;
|
||||
using UnityEditor;
|
||||
|
||||
namespace AIGame.EditorTools
|
||||
{
|
||||
/// <summary>UI 图集原图保持未压缩,其他图片按平台设置压缩格式。</summary>
|
||||
public sealed class TexturePlatformPostprocessor : AssetPostprocessor
|
||||
{
|
||||
private void OnPreprocessTexture()
|
||||
{
|
||||
// 只处理项目图片,不覆盖 Packages 中的包资源。
|
||||
if (!assetPath.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
|
||||
var importer = (TextureImporter)assetImporter;
|
||||
if (UiAtlasPaths.IsTexturePath(assetPath))
|
||||
{
|
||||
ConfigureUiAtlasSource(importer);
|
||||
return;
|
||||
}
|
||||
// Unity 的 Windows 纹理设置使用 Standalone 平台组。
|
||||
SetPlatformFormat(importer, "Standalone", TextureImporterFormat.DXT5);
|
||||
SetPlatformFormat(importer, "iPhone", TextureImporterFormat.ASTC_6x6);
|
||||
SetPlatformFormat(importer, "Android", TextureImporterFormat.ASTC_6x6);
|
||||
SetPlatformFormat(importer, "WebGL", TextureImporterFormat.ASTC_8x8);
|
||||
}
|
||||
|
||||
public static bool ConfigureUiAtlasSource(TextureImporter importer)
|
||||
{
|
||||
if (!UiAtlasPaths.IsTexturePath(importer.assetPath)) return false;
|
||||
// 源图使用无损 RGBA32,平台覆盖关闭;最终压缩由 SpriteAtlas 自己处理。
|
||||
bool changed = SetUncompressedSource(importer, "DefaultTexturePlatform");
|
||||
changed |= SetUncompressedSource(importer, "Standalone");
|
||||
changed |= SetUncompressedSource(importer, "iPhone");
|
||||
changed |= SetUncompressedSource(importer, "Android");
|
||||
changed |= SetUncompressedSource(importer, "WebGL");
|
||||
return changed;
|
||||
}
|
||||
|
||||
private static bool SetUncompressedSource(TextureImporter importer, string platform)
|
||||
{
|
||||
var settings = importer.GetPlatformTextureSettings(platform);
|
||||
if (!settings.overridden && settings.format == TextureImporterFormat.RGBA32
|
||||
&& settings.textureCompression == TextureImporterCompression.Uncompressed
|
||||
&& !settings.crunchedCompression) return false;
|
||||
settings.name = platform;
|
||||
settings.overridden = false;
|
||||
settings.format = TextureImporterFormat.RGBA32;
|
||||
settings.textureCompression = TextureImporterCompression.Uncompressed;
|
||||
settings.crunchedCompression = false;
|
||||
importer.SetPlatformTextureSettings(settings);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void SetPlatformFormat(
|
||||
TextureImporter importer, string platform, TextureImporterFormat format)
|
||||
{
|
||||
// 保留尺寸、缩放算法等原有设置,仅覆盖压缩相关选项。
|
||||
var settings = importer.GetPlatformTextureSettings(platform);
|
||||
settings.name = platform;
|
||||
settings.overridden = true;
|
||||
settings.format = format;
|
||||
settings.crunchedCompression = false;
|
||||
importer.SetPlatformTextureSettings(settings);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e93dc5cee8b081349a908c963e126ac8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -319,6 +319,8 @@ namespace AIGame.EditorTools
|
||||
try
|
||||
{
|
||||
Dictionary<string, GameObject> map = new Dictionary<string, GameObject>();
|
||||
var spritePaths = root["assets"]?["sprites"]?.ToObject<Dictionary<string, string>>()
|
||||
?? new Dictionary<string, string>();
|
||||
List<Action> deferred = new List<Action>();
|
||||
|
||||
foreach (JToken node in nodes)
|
||||
@@ -343,7 +345,7 @@ namespace AIGame.EditorTools
|
||||
|
||||
foreach (JToken comp in (node["components"] as JArray) ?? new JArray())
|
||||
{
|
||||
ApplyComponent(go, comp as JObject, map, deferred, Path.GetFileName(jsonPath));
|
||||
ApplyComponent(go, comp as JObject, map, deferred, Path.GetFileName(jsonPath), spritePaths);
|
||||
}
|
||||
go.SetActive(active);
|
||||
}
|
||||
@@ -524,7 +526,7 @@ namespace AIGame.EditorTools
|
||||
}
|
||||
|
||||
// ---------- 组件 ----------
|
||||
private static void ApplyComponent(GameObject go, JObject comp, Dictionary<string, GameObject> map, List<Action> deferred, string fileName)
|
||||
private static void ApplyComponent(GameObject go, JObject comp, Dictionary<string, GameObject> map, List<Action> deferred, string fileName, Dictionary<string, string> spritePaths)
|
||||
{
|
||||
if (comp == null)
|
||||
{
|
||||
@@ -564,7 +566,7 @@ namespace AIGame.EditorTools
|
||||
{
|
||||
Image img = GetOrAdd<Image>(go);
|
||||
string spriteName = (string)comp["sprite"];
|
||||
Sprite sprite = ResolveSprite(spriteName, fileName);
|
||||
Sprite sprite = ResolveSprite(spriteName, fileName, spritePaths);
|
||||
img.sprite = sprite;
|
||||
string imageType = (string)comp["imageType"];
|
||||
if (string.IsNullOrEmpty(imageType) && sprite != null
|
||||
@@ -591,6 +593,20 @@ namespace AIGame.EditorTools
|
||||
img.color = ParseColor((string)comp["color"], Color.white);
|
||||
break;
|
||||
}
|
||||
case "RawImage":
|
||||
{
|
||||
RawImage image = GetOrAdd<RawImage>(go);
|
||||
string materialPath = (string)comp["material"];
|
||||
if (!string.IsNullOrEmpty(materialPath))
|
||||
{
|
||||
image.material = AssetDatabase.LoadAssetAtPath<Material>(materialPath);
|
||||
if (image.material == null)
|
||||
throw new InvalidOperationException("RawImage material not found: " + materialPath);
|
||||
}
|
||||
image.color = ParseColor((string)comp["color"], Color.white);
|
||||
image.raycastTarget = comp["raycastTarget"] != null && (bool)comp["raycastTarget"];
|
||||
break;
|
||||
}
|
||||
case "TextMeshProUGUI":
|
||||
{
|
||||
TextMeshProUGUI t = GetOrAdd<TextMeshProUGUI>(go);
|
||||
@@ -622,9 +638,9 @@ namespace AIGame.EditorTools
|
||||
{
|
||||
btn.transition = Selectable.Transition.SpriteSwap;
|
||||
SpriteState state = btn.spriteState;
|
||||
state.highlightedSprite = ResolveSprite((string)comp["spriteHighlighted"], fileName);
|
||||
state.pressedSprite = ResolveSprite((string)comp["spritePressed"], fileName);
|
||||
state.disabledSprite = ResolveSprite((string)comp["spriteDisabled"], fileName);
|
||||
state.highlightedSprite = ResolveSprite((string)comp["spriteHighlighted"], fileName, spritePaths);
|
||||
state.pressedSprite = ResolveSprite((string)comp["spritePressed"], fileName, spritePaths);
|
||||
state.disabledSprite = ResolveSprite((string)comp["spriteDisabled"], fileName, spritePaths);
|
||||
btn.spriteState = state;
|
||||
}
|
||||
else
|
||||
@@ -1027,12 +1043,19 @@ namespace AIGame.EditorTools
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Sprite ResolveSprite(string spriteName, string fileName)
|
||||
private static Sprite ResolveSprite(string spriteName, string fileName, Dictionary<string, string> spritePaths)
|
||||
{
|
||||
if (string.IsNullOrEmpty(spriteName))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (spritePaths.TryGetValue(spriteName, out string declaredPath))
|
||||
{
|
||||
var declaredSprite = AssetDatabase.LoadAssetAtPath<Sprite>(declaredPath);
|
||||
if (declaredSprite == null)
|
||||
throw new InvalidOperationException(fileName + ": declared Sprite is missing: " + spriteName + " (" + declaredPath + ")");
|
||||
return declaredSprite;
|
||||
}
|
||||
string[] guids = AssetDatabase.FindAssets(spriteName, new[] { TextureRoot });
|
||||
foreach (string guid in guids)
|
||||
{
|
||||
|
||||
@@ -6,12 +6,13 @@ using YooAsset.Editor;
|
||||
|
||||
namespace AIGame.EditorTools
|
||||
{
|
||||
/// <summary>把 Candy UI Prefab 收进 Art;图片/字体通过 Prefab 依赖进入资源包。</summary>
|
||||
/// <summary>UI Prefab 与自动图集收进 Art;每个图集与原图绑定为同一资源包。</summary>
|
||||
public static class SetupUiBundleCollector
|
||||
{
|
||||
[MenuItem("Tools/UI/Setup UI Bundle Collector")]
|
||||
public static string Setup()
|
||||
{
|
||||
UiSpriteAtlasGenerator.GenerateAll();
|
||||
var package = BundleCollectorSettingData.Setting.Packages.FirstOrDefault(p => p.PackageName == "Art");
|
||||
if (package == null) throw new System.InvalidOperationException("Set up the Art package before UI collection.");
|
||||
var group = package.Groups.FirstOrDefault(g => g.GroupName == "UI");
|
||||
@@ -36,9 +37,29 @@ namespace AIGame.EditorTools
|
||||
};
|
||||
BundleCollectorSettingData.CreateCollector(group, collector);
|
||||
}
|
||||
ConfigureAtlasCollector(group, HotUpdate.UI.UiAtlasPaths.AtlasRoot,
|
||||
ECollectorType.MainAssetCollector, nameof(CollectUiAtlas), "ui");
|
||||
ConfigureAtlasCollector(group, HotUpdate.UI.UiAtlasPaths.TextureRoot,
|
||||
ECollectorType.DependAssetCollector, nameof(CollectUiAtlasTexture), "");
|
||||
package.CheckConfigError();
|
||||
BundleCollectorSettingData.SaveFile();
|
||||
return "Art/UI collects Candy UI by prefab name, including sprite/font dependencies.";
|
||||
return "Art/UI: UI prefabs by name; generated atlases by name; source sprites in the same bundle as their atlas.";
|
||||
}
|
||||
|
||||
private static void ConfigureAtlasCollector(BundleCollectorGroup group, string path,
|
||||
ECollectorType type, string filter, string tags)
|
||||
{
|
||||
var collector = group.Collectors.FirstOrDefault(c => c.CollectPath == path);
|
||||
bool created = collector == null;
|
||||
if (created) collector = new BundleCollector { CollectPath = path };
|
||||
collector.CollectorGUID = AssetDatabase.AssetPathToGUID(path);
|
||||
collector.CollectorType = type;
|
||||
collector.AddressRuleName = nameof(AddressByFileName);
|
||||
collector.PackRuleName = nameof(PackUiAtlas);
|
||||
collector.FilterRuleName = filter;
|
||||
collector.AssetTags = tags;
|
||||
if (created) BundleCollectorSettingData.CreateCollector(group, collector);
|
||||
else BundleCollectorSettingData.ModifyCollector(group, collector);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using HotUpdate.UI;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.U2D;
|
||||
using YooAsset.Editor;
|
||||
|
||||
namespace AIGame.EditorTools
|
||||
{
|
||||
/// <summary>检查实际构建产物,防止 SBP 的旧 Prefab 依赖缓存重新带入原图像素。</summary>
|
||||
public static class UiAtlasBuildValidation
|
||||
{
|
||||
public sealed class BundleCheck
|
||||
{
|
||||
public string FileName;
|
||||
public long Bytes;
|
||||
public int SourceTextureCount;
|
||||
}
|
||||
|
||||
public static BundleCheck[] Inspect(string packageDirectory)
|
||||
{
|
||||
string reportPath = Directory.GetFiles(packageDirectory, "*.report").Single();
|
||||
var report = JObject.Parse(File.ReadAllText(reportPath));
|
||||
string packageName = report["Summary"].Value<string>("BuildPackageName");
|
||||
bool uniqueNames = report["Summary"].Value<bool>("UniqueBundleName");
|
||||
var bundles = ((JArray)report["BundleInfos"]).ToDictionary(b => b.Value<string>("BundleName"));
|
||||
var checks = new List<BundleCheck>();
|
||||
foreach (string guid in AssetDatabase.FindAssets("l:" + UiSpriteAtlasGenerator.GeneratedLabel,
|
||||
new[] { UiAtlasPaths.AtlasRoot }))
|
||||
{
|
||||
string atlasPath = AssetDatabase.GUIDToAssetPath(guid);
|
||||
if (AssetDatabase.LoadAssetAtPath<SpriteAtlas>(atlasPath) == null) continue;
|
||||
string bundleName = new PackUiAtlas().GetPackRuleResult(
|
||||
new BundlePackRuleData(atlasPath, UiAtlasPaths.AtlasRoot, "UI", "")).GetBundleName(packageName, uniqueNames);
|
||||
if (!bundles.TryGetValue(bundleName, out var info))
|
||||
throw new InvalidOperationException("Generated UI atlas missing from build report: " + atlasPath);
|
||||
string fileName = info.Value<string>("FileName");
|
||||
string path = Path.Combine(packageDirectory, fileName);
|
||||
var bundle = AssetBundle.LoadFromFile(path);
|
||||
if (bundle == null) throw new IOException("Cannot validate UI atlas bundle: " + path);
|
||||
try
|
||||
{
|
||||
// 已剥离的源图仍有 Sprite 元数据,但不应再有可加载的原图 Texture2D。
|
||||
checks.Add(new BundleCheck { FileName = fileName, Bytes = new FileInfo(path).Length,
|
||||
SourceTextureCount = bundle.LoadAllAssets<Texture2D>().Length });
|
||||
}
|
||||
finally { bundle.Unload(true); }
|
||||
}
|
||||
if (checks.Count == 0) throw new InvalidOperationException("No generated UI atlas bundles validated.");
|
||||
return checks.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f0217b082b10a8846a737948f0b191c0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,49 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.IO;
|
||||
using HotUpdate.UI;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.U2D;
|
||||
using YooAsset.Editor;
|
||||
|
||||
namespace AIGame.EditorTools
|
||||
{
|
||||
[DisplayName("UI 图片与所属图集同包")]
|
||||
public sealed class PackUiAtlas : IBundlePackRule
|
||||
{
|
||||
public BundlePackRuleResult GetPackRuleResult(BundlePackRuleData data)
|
||||
{
|
||||
string path = data.AssetPath.Replace('\\', '/');
|
||||
if (UiAtlasPaths.IsTexturePath(path)) path = UiAtlasPaths.GetAtlasPathForTexture(path);
|
||||
else if (!path.StartsWith(UiAtlasPaths.AtlasRoot + "/", StringComparison.Ordinal)
|
||||
|| !path.EndsWith(".spriteatlas", StringComparison.Ordinal))
|
||||
throw new ArgumentException("Not a UI atlas input: " + path);
|
||||
return new BundlePackRuleResult(Path.ChangeExtension(path, null), DefaultBundlePackRule.AssetBundleFileExtension);
|
||||
}
|
||||
}
|
||||
|
||||
[DisplayName("自动生成的 UI 图集")]
|
||||
public sealed class CollectUiAtlas : IAssetFilterRule
|
||||
{
|
||||
public string FindAssetType => "SpriteAtlas";
|
||||
public bool IsCollectAsset(AssetFilterRuleData data)
|
||||
{
|
||||
var atlas = AssetDatabase.LoadAssetAtPath<SpriteAtlas>(data.AssetPath);
|
||||
return atlas != null && Array.IndexOf(AssetDatabase.GetLabels(atlas), UiSpriteAtlasGenerator.GeneratedLabel) >= 0;
|
||||
}
|
||||
}
|
||||
|
||||
[DisplayName("UI 图集原图")]
|
||||
public sealed class CollectUiAtlasTexture : IAssetFilterRule
|
||||
{
|
||||
public string FindAssetType => "Texture2D";
|
||||
public bool IsCollectAsset(AssetFilterRuleData data)
|
||||
{
|
||||
return UiAtlasPaths.IsTexturePath(data.AssetPath)
|
||||
&& AssetDatabase.LoadAssetAtPath<SpriteAtlas>(UiAtlasPaths.GetAtlasPathForTexture(data.AssetPath)) != null
|
||||
&& (AssetImporter.GetAtPath(data.AssetPath) as TextureImporter)?.textureType == TextureImporterType.Sprite;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d1a326e23fcef0d4b8e03135f02685ef
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,206 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using HotUpdate.UI;
|
||||
using UnityEditor;
|
||||
using UnityEditor.U2D;
|
||||
using UnityEngine;
|
||||
using UnityEngine.U2D;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace AIGame.EditorTools
|
||||
{
|
||||
/// <summary>移植 ClientTrunk SpriteAtalsGenerator 的按目录打图集规则,增量维护并保留 GUID。</summary>
|
||||
public static class UiSpriteAtlasGenerator
|
||||
{
|
||||
public const string GeneratedLabel = "AIGame.UI.GeneratedAtlas";
|
||||
private static bool s_Busy;
|
||||
|
||||
[MenuItem("Tools/UI/Generate Sprite Atlases")]
|
||||
public static void GenerateFromMenu()
|
||||
{
|
||||
Debug.Log("[UIAtlas] " + GenerateAll());
|
||||
}
|
||||
|
||||
public static string GenerateAll()
|
||||
{
|
||||
if (s_Busy) return "Generation already in progress.";
|
||||
if (EditorApplication.isPlayingOrWillChangePlaymode)
|
||||
throw new InvalidOperationException("Generate UI atlases in Edit Mode.");
|
||||
s_Busy = true;
|
||||
try
|
||||
{
|
||||
var groups = CollectTextures();
|
||||
EnsureFolder(UiAtlasPaths.AtlasRoot);
|
||||
// V1 matches the reference project and supports SpriteAtlas late binding in bundles.
|
||||
if (EditorSettings.spritePackerMode != SpritePackerMode.AlwaysOnAtlas)
|
||||
EditorSettings.spritePackerMode = SpritePackerMode.AlwaysOnAtlas;
|
||||
int changed = 0, deleted = 0;
|
||||
foreach (var group in groups)
|
||||
{
|
||||
string path = group.Key;
|
||||
var atlas = AssetDatabase.LoadAssetAtPath<SpriteAtlas>(path);
|
||||
bool created = atlas == null;
|
||||
if (created)
|
||||
{
|
||||
atlas = new SpriteAtlas();
|
||||
AssetDatabase.CreateAsset(atlas, path);
|
||||
}
|
||||
bool dirty = Configure(atlas, group.Value.ToArray()) || created;
|
||||
var labels = AssetDatabase.GetLabels(atlas);
|
||||
if (!labels.Contains(GeneratedLabel))
|
||||
{
|
||||
AssetDatabase.SetLabels(atlas, labels.Concat(new[] { GeneratedLabel }).ToArray());
|
||||
dirty = true;
|
||||
}
|
||||
if (!dirty) continue;
|
||||
EditorUtility.SetDirty(atlas);
|
||||
AssetDatabase.SaveAssetIfDirty(atlas);
|
||||
changed++;
|
||||
}
|
||||
foreach (string guid in AssetDatabase.FindAssets("l:" + GeneratedLabel, new[] { UiAtlasPaths.AtlasRoot }))
|
||||
{
|
||||
string path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
if (groups.ContainsKey(path) || !path.EndsWith(".spriteatlas", StringComparison.Ordinal)) continue;
|
||||
if (!AssetDatabase.DeleteAsset(path)) throw new IOException("Cannot remove stale UI atlas: " + path);
|
||||
deleted++;
|
||||
}
|
||||
return $"atlases={groups.Count}, textures={groups.Values.Sum(g => g.Count)}, updated={changed}, removed={deleted}";
|
||||
}
|
||||
finally { s_Busy = false; }
|
||||
}
|
||||
|
||||
public static SortedDictionary<string, List<Object>> CollectTextures()
|
||||
{
|
||||
var groups = new SortedDictionary<string, List<Object>>(StringComparer.Ordinal);
|
||||
var names = new Dictionary<string, HashSet<string>>(StringComparer.Ordinal);
|
||||
if (!AssetDatabase.IsValidFolder(UiAtlasPaths.TextureRoot)) return groups;
|
||||
foreach (string path in AssetDatabase.FindAssets("t:Texture2D", new[] { UiAtlasPaths.TextureRoot })
|
||||
.Select(AssetDatabase.GUIDToAssetPath).OrderBy(p => p, StringComparer.Ordinal))
|
||||
{
|
||||
var importer = AssetImporter.GetAtPath(path) as TextureImporter;
|
||||
if (importer == null || importer.textureType != TextureImporterType.Sprite)
|
||||
throw new InvalidOperationException("UI atlas input must be a Sprite: " + path);
|
||||
string atlasPath = UiAtlasPaths.GetAtlasPathForTexture(path);
|
||||
if (!groups.TryGetValue(atlasPath, out var textures))
|
||||
{
|
||||
groups.Add(atlasPath, textures = new List<Object>());
|
||||
names.Add(atlasPath, new HashSet<string>(StringComparer.Ordinal));
|
||||
}
|
||||
var sprites = AssetDatabase.LoadAllAssetsAtPath(path).OfType<Sprite>().ToArray();
|
||||
if (sprites.Length == 0) throw new InvalidOperationException("UI texture has no sprites: " + path);
|
||||
foreach (var sprite in sprites)
|
||||
if (!names[atlasPath].Add(sprite.name))
|
||||
throw new InvalidOperationException($"Duplicate sprite name '{sprite.name}' in {atlasPath}: {path}");
|
||||
textures.Add(AssetDatabase.LoadAssetAtPath<Texture2D>(path));
|
||||
}
|
||||
// Names are also YooAsset addresses; reject case-only collisions before writing assets.
|
||||
if (groups.Keys.Distinct(StringComparer.OrdinalIgnoreCase).Count() != groups.Count)
|
||||
throw new InvalidOperationException("UI atlas folder names differ only by case.");
|
||||
return groups;
|
||||
}
|
||||
|
||||
private static bool Configure(SpriteAtlas atlas, Object[] textures)
|
||||
{
|
||||
bool changed = false;
|
||||
if (!atlas.GetPackables().SequenceEqual(textures))
|
||||
{
|
||||
atlas.Remove(atlas.GetPackables());
|
||||
atlas.Add(textures);
|
||||
changed = true;
|
||||
}
|
||||
var packing = atlas.GetPackingSettings();
|
||||
var desiredPacking = packing;
|
||||
desiredPacking.enableRotation = false;
|
||||
desiredPacking.enableTightPacking = false;
|
||||
desiredPacking.enableAlphaDilation = true;
|
||||
desiredPacking.padding = 4;
|
||||
if (!packing.Equals(desiredPacking)) { atlas.SetPackingSettings(desiredPacking); changed = true; }
|
||||
var settings = atlas.GetTextureSettings();
|
||||
var desiredSettings = settings;
|
||||
desiredSettings.readable = false;
|
||||
desiredSettings.generateMipMaps = false;
|
||||
desiredSettings.sRGB = true;
|
||||
desiredSettings.filterMode = FilterMode.Bilinear;
|
||||
if (!settings.Equals(desiredSettings)) { atlas.SetTextureSettings(desiredSettings); changed = true; }
|
||||
// SBP 的 PackAllAtlases 需要启用此标志,才能建立打包纹理关联并剥离原图像素。
|
||||
// 资源仍由 YooAsset 收集到对应图集 bundle;运行时加载器保留延迟绑定兼容。
|
||||
if (!atlas.IsIncludeInBuild()) { atlas.SetIncludeInBuild(true); changed = true; }
|
||||
changed |= SetPlatform(atlas, "DefaultTexturePlatform", TextureImporterFormat.Automatic, false);
|
||||
changed |= SetPlatform(atlas, "Standalone", TextureImporterFormat.DXT5, true);
|
||||
changed |= SetPlatform(atlas, "iPhone", TextureImporterFormat.ASTC_6x6, true);
|
||||
changed |= SetPlatform(atlas, "Android", TextureImporterFormat.ASTC_6x6, true);
|
||||
changed |= SetPlatform(atlas, "WebGL", TextureImporterFormat.ASTC_8x8, true);
|
||||
return changed;
|
||||
}
|
||||
|
||||
private static bool SetPlatform(SpriteAtlas atlas, string platform, TextureImporterFormat format, bool overridden)
|
||||
{
|
||||
var settings = atlas.GetPlatformSettings(platform);
|
||||
if (settings.maxTextureSize == 2048 && settings.format == format && settings.overridden == overridden
|
||||
&& !settings.crunchedCompression && settings.compressionQuality == 100
|
||||
&& settings.textureCompression == TextureImporterCompression.CompressedHQ) return false;
|
||||
settings.name = platform;
|
||||
settings.overridden = overridden;
|
||||
settings.maxTextureSize = 2048;
|
||||
settings.format = format;
|
||||
settings.textureCompression = TextureImporterCompression.CompressedHQ;
|
||||
settings.compressionQuality = 100;
|
||||
settings.crunchedCompression = false;
|
||||
atlas.SetPlatformSettings(settings);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void EnsureFolder(string path)
|
||||
{
|
||||
if (AssetDatabase.IsValidFolder(path)) return;
|
||||
string parent = Path.GetDirectoryName(path).Replace('\\', '/');
|
||||
EnsureFolder(parent);
|
||||
AssetDatabase.CreateFolder(parent, Path.GetFileName(path));
|
||||
}
|
||||
}
|
||||
|
||||
[InitializeOnLoad]
|
||||
public sealed class UiSpriteAtlasPostprocessor : AssetPostprocessor
|
||||
{
|
||||
private static bool s_Pending;
|
||||
static UiSpriteAtlasPostprocessor() { Schedule(); }
|
||||
|
||||
private void OnPreprocessTexture()
|
||||
{
|
||||
if (!UiAtlasPaths.IsTexturePath(assetPath)) return;
|
||||
var importer = (TextureImporter)assetImporter;
|
||||
importer.textureType = TextureImporterType.Sprite;
|
||||
if (importer.spriteImportMode == SpriteImportMode.None) importer.spriteImportMode = SpriteImportMode.Single;
|
||||
importer.mipmapEnabled = false;
|
||||
importer.alphaIsTransparency = true;
|
||||
importer.npotScale = TextureImporterNPOTScale.None;
|
||||
}
|
||||
|
||||
private static void OnPostprocessAllAssets(string[] imported, string[] deleted, string[] moved, string[] movedFrom)
|
||||
{
|
||||
if (imported.Concat(deleted).Concat(moved).Concat(movedFrom)
|
||||
.Any(p => p == UiAtlasPaths.TextureRoot || UiAtlasPaths.IsTexturePath(p))) Schedule();
|
||||
}
|
||||
|
||||
private static void Schedule()
|
||||
{
|
||||
if (s_Pending) return;
|
||||
s_Pending = true;
|
||||
EditorApplication.update += UpdateAtlases;
|
||||
}
|
||||
|
||||
private static void UpdateAtlases()
|
||||
{
|
||||
if (EditorApplication.isCompiling || EditorApplication.isUpdating || EditorApplication.isPlayingOrWillChangePlaymode
|
||||
|| BuildPipeline.isBuildingPlayer) return;
|
||||
s_Pending = false;
|
||||
EditorApplication.update -= UpdateAtlases;
|
||||
try { UiSpriteAtlasGenerator.GenerateAll(); }
|
||||
catch (Exception ex) { Debug.LogException(ex); }
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2889ecd3f89a1a247aae474366e328b8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,8 +1,12 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using HybridCLR.Editor.Installer;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using WeChatWASM;
|
||||
@@ -16,7 +20,7 @@ namespace AOT.Editor.WeChat
|
||||
{
|
||||
const string BootScene = "Assets/Scenes/Boot.unity";
|
||||
const string RelativeDst = "Build/WeChat/Base";
|
||||
const string DataCdn = "https://www.xworld.ren/wechat/1.0.0/minigame/";
|
||||
const string DataCdn = "https://www.xworld.ren/game/Friends/webgl/";
|
||||
const int ShellHeapMb = 496;
|
||||
const string ResumeKey = "AIGame.WeChat.ExportBase.Resume";
|
||||
|
||||
@@ -92,6 +96,9 @@ namespace AOT.Editor.WeChat
|
||||
return Fail("微信转换失败 " + error + ",详见 Console");
|
||||
|
||||
string dst = Path.GetFullPath(Path.Combine(Application.dataPath, "..", RelativeDst));
|
||||
string projectError = PrepareDevToolsProject(dst);
|
||||
if (projectError != null)
|
||||
return Fail(projectError);
|
||||
string summary = WriteReport(dst);
|
||||
string minigame = Path.Combine(dst, "minigame");
|
||||
string result = "ok minigame=" + minigame + " " + summary;
|
||||
@@ -99,6 +106,94 @@ namespace AOT.Editor.WeChat
|
||||
return result;
|
||||
}
|
||||
|
||||
[MenuItem("AIGame/WeChat/Repair Exported DevTools Project")]
|
||||
public static string RepairExportedProject()
|
||||
{
|
||||
string dst = Path.GetFullPath(Path.Combine(Application.dataPath, "..", RelativeDst));
|
||||
string error = PrepareDevToolsProject(dst);
|
||||
if (error != null) return Fail(error);
|
||||
string result = "ok devtools=" + dst + " source=minigame/ " + WriteReport(dst);
|
||||
Debug.Log("[AIGame] " + result);
|
||||
return result;
|
||||
}
|
||||
|
||||
[MenuItem("AIGame/WeChat/Refresh Exported Config (No Build)")]
|
||||
public static string RefreshExportedConfig()
|
||||
{
|
||||
string dst = Path.GetFullPath(Path.Combine(Application.dataPath, "..", RelativeDst));
|
||||
string gamePath = Path.Combine(dst, "minigame", "game.js");
|
||||
if (!File.Exists(gamePath)) return Fail("尚未导出小游戏,请先执行 Export Base Package");
|
||||
string game = File.ReadAllText(gamePath);
|
||||
var cdnPattern = new Regex(@"\bDATA_CDN\s*:\s*(['""])[^'""]*\1");
|
||||
if (cdnPattern.Matches(game).Count != 1)
|
||||
return Fail("无法识别导出 game.js 的 DATA_CDN,未修改配置");
|
||||
string error = ApplyBaseConfig();
|
||||
if (error != null) return Fail(error);
|
||||
error = PrepareDevToolsProject(dst);
|
||||
if (error != null) return Fail(error);
|
||||
string updated = cdnPattern.Replace(game, _ => "DATA_CDN: " + JsonConvert.SerializeObject(DataCdn), 1);
|
||||
if (updated != game) File.WriteAllText(gamePath, updated, new UTF8Encoding(false));
|
||||
string result = "ok config refreshed (no WebGL build) " + WriteReport(dst);
|
||||
Debug.Log("[AIGame] " + result);
|
||||
return result;
|
||||
}
|
||||
|
||||
[MenuItem("AIGame/WeChat/Open Exported MiniGame Folder")]
|
||||
public static void OpenExportedMiniGameFolder()
|
||||
{
|
||||
string game = Path.GetFullPath(Path.Combine(Application.dataPath, "..", RelativeDst, "minigame", "game.json"));
|
||||
if (!File.Exists(game))
|
||||
{
|
||||
Fail("尚未导出小游戏,请先执行 Export Base Package");
|
||||
return;
|
||||
}
|
||||
EditorUtility.RevealInFinder(game);
|
||||
}
|
||||
|
||||
/// <summary>Allow importing either the export container or its minigame directory.</summary>
|
||||
public static string PrepareDevToolsProject(string dst)
|
||||
{
|
||||
string mini = Path.Combine(dst, "minigame");
|
||||
foreach (string required in new[] { "game.json", "game.js", "project.config.json" })
|
||||
if (!File.Exists(Path.Combine(mini, required)))
|
||||
return "小游戏导出不完整,缺少 " + Path.Combine(mini, required);
|
||||
|
||||
try
|
||||
{
|
||||
JObject.Parse(File.ReadAllText(Path.Combine(mini, "game.json")));
|
||||
string miniConfigPath = Path.Combine(mini, "project.config.json");
|
||||
string rootConfigPath = Path.Combine(dst, "project.config.json");
|
||||
JObject miniConfig = JObject.Parse(File.ReadAllText(miniConfigPath));
|
||||
JObject rootConfig = File.Exists(rootConfigPath)
|
||||
? JObject.Parse(File.ReadAllText(rootConfigPath)) : new JObject();
|
||||
string appid = (string)miniConfig["appid"];
|
||||
if (string.IsNullOrWhiteSpace(appid)) appid = (string)rootConfig["appid"];
|
||||
|
||||
// Carry SDK compiler options and symbol exclusions to the parent entry.
|
||||
// Keep other existing user fields and combine package ignore rules.
|
||||
rootConfig.Merge(miniConfig, new JsonMergeSettings { MergeArrayHandling = MergeArrayHandling.Union });
|
||||
rootConfig["compileType"] = "game";
|
||||
rootConfig["miniprogramRoot"] = "minigame/";
|
||||
if (!string.IsNullOrWhiteSpace(appid))
|
||||
{
|
||||
rootConfig["appid"] = appid;
|
||||
if (string.IsNullOrWhiteSpace((string)miniConfig["appid"]))
|
||||
{
|
||||
miniConfig["appid"] = appid;
|
||||
File.WriteAllText(miniConfigPath, miniConfig.ToString(Formatting.Indented), new UTF8Encoding(false));
|
||||
}
|
||||
}
|
||||
string rootJson = rootConfig.ToString(Formatting.Indented);
|
||||
if (!File.Exists(rootConfigPath) || File.ReadAllText(rootConfigPath) != rootJson)
|
||||
File.WriteAllText(rootConfigPath, rootJson, new UTF8Encoding(false));
|
||||
return null;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
return "小游戏项目配置失败: " + exception.Message;
|
||||
}
|
||||
}
|
||||
|
||||
static string ValidateBootOnly()
|
||||
{
|
||||
if (!File.Exists(BootScene))
|
||||
@@ -167,6 +262,14 @@ namespace AOT.Editor.WeChat
|
||||
text.AppendLine("minigameBytes: " + miniBytes);
|
||||
text.AppendLine("dataBytes: " + dataBytes);
|
||||
text.AppendLine("devtools: " + mini);
|
||||
text.AppendLine("devtoolsContainer: " + dst + " (miniprogramRoot=minigame/)");
|
||||
text.AppendLine("uploadSource: " + Path.Combine(dst, "webgl"));
|
||||
text.AppendLine("uploadContentsTo: " + DataCdn);
|
||||
text.AppendLine("uploadNote: copy the CONTENTS of webgl, preserving relative paths; export does not upload to the server.");
|
||||
string webgl = Path.Combine(dst, "webgl");
|
||||
if (Directory.Exists(webgl))
|
||||
foreach (string resource in Directory.GetFiles(webgl, "*.webgl.data.unityweb.bin.*"))
|
||||
text.AppendLine("startupData: " + Path.GetFileName(resource) + " -> " + DataCdn + Path.GetFileName(resource));
|
||||
if (Directory.Exists(mini))
|
||||
{
|
||||
text.AppendLine("largest:");
|
||||
|
||||
@@ -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