using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text; using TH1_Logic.Editor.HybridCLR; using TH1_Logic.Editor.YooAssetTools; using TH1_Logic.Hotfix; using UnityEditor; using UnityEditor.Build; using UnityEditor.Build.Reporting; using UnityEngine; namespace TH1_Logic.Editor { public sealed class TH1MigrationBuildPanel : EditorWindow { private Vector2 _scroll; private bool _developmentBuild; private bool _runGenerateAll = true; private bool _showAdvancedActions; private List _statusItems = new List(); private string _lastAction = "Ready"; [MenuItem("Tools/TH1/iOS Migration/Build Panel")] public static void Open() { var window = GetWindow("TH1 Build Panel"); window.minSize = new Vector2(620, 520); window.RefreshStatus(); window.Show(); } private void OnEnable() { _developmentBuild = EditorUserBuildSettings.development; RefreshStatus(); } private void OnGUI() { DrawHeader(); DrawGuide(); using (new EditorGUILayout.HorizontalScope()) { if (GUILayout.Button("刷新状态", GUILayout.Height(26))) { RefreshStatus(); } if (GUILayout.Button("打开 StreamingAssets", GUILayout.Height(26))) { EditorUtility.RevealInFinder(Application.streamingAssetsPath); } } _scroll = EditorGUILayout.BeginScrollView(_scroll); DrawStatus(); EditorGUILayout.EndScrollView(); EditorGUILayout.Space(8); DrawActions(); } private void DrawHeader() { EditorGUILayout.LabelField("TH1 iOS / HybridCLR / YooAsset 出包面板", EditorStyles.boldLabel); EditorGUILayout.LabelField("当前平台", EditorUserBuildSettings.activeBuildTarget.ToString()); EditorGUILayout.LabelField("脚本后端", GetScriptingBackend(EditorUserBuildSettings.activeBuildTarget).ToString()); EditorGUILayout.LabelField("上次操作", _lastAction); _developmentBuild = EditorGUILayout.ToggleLeft("热更 DLL 使用 Development 编译(PC Smoke Test 建议开启;正式包可关闭)", _developmentBuild); _runGenerateAll = EditorGUILayout.ToggleLeft("一键准备前先执行 HybridCLR Generate All(迁移阶段建议保持开启)", _runGenerateAll); if (EditorApplication.isCompiling) { EditorGUILayout.HelpBox("Unity 正在编译脚本,等编译完成后再执行构建动作。", MessageType.Warning); } } private void DrawGuide() { var errorCount = CountStatus(MigrationStatusLevel.Error); var warningCount = CountStatus(MigrationStatusLevel.Warning); if (errorCount > 0) { EditorGUILayout.HelpBox( $"现在不能直接出包:还有 {errorCount} 个阻断项。你现在只需要点下面的大按钮“新手只点这个:准备当前平台出包”。", MessageType.Error); } else if (warningCount > 0) { EditorGUILayout.HelpBox( $"基础产物已经可用,但还有 {warningCount} 个提示项。建议点一次大按钮刷新所有产物后再出包。", MessageType.Warning); } else { EditorGUILayout.HelpBox( "当前平台出包准备完成:热更 DLL、AOT Metadata、YooAsset AB 都可用。现在可以 Build Player。", MessageType.Info); } EditorGUILayout.HelpBox( "日常规则:改了代码或资源后,打 PC/iOS 包前都点一次大按钮。编辑器内 Play 测试一般不用点。", MessageType.None); } private void DrawStatus() { foreach (var item in _statusItems) { EditorGUILayout.LabelField(item.Title, EditorStyles.boldLabel); EditorGUILayout.HelpBox(item.Message, ToMessageType(item.Level)); } } private void DrawActions() { using (new EditorGUI.DisabledScope(EditorApplication.isCompiling || EditorApplication.isPlayingOrWillChangePlaymode)) { if (GUILayout.Button("新手只点这个:准备当前平台出包(热更 DLL + AOT + AB)", GUILayout.Height(44))) { RunAction("Prepare Player Build", PreparePlayerBuild); } EditorGUILayout.Space(8); _showAdvancedActions = EditorGUILayout.Foldout(_showAdvancedActions, "高级/单项操作(平时不用)", true); if (!_showAdvancedActions) return; EditorGUILayout.LabelField("HybridCLR", EditorStyles.boldLabel); using (new EditorGUILayout.HorizontalScope()) { if (GUILayout.Button("配置 HybridCLR", GUILayout.Height(28))) { RunAction("Configure HybridCLR", TH1HybridCLRBuildTools.ConfigureHotfixSettings); } if (GUILayout.Button("Generate All", GUILayout.Height(28))) { RunAction("HybridCLR Generate All", TH1HybridCLRBuildTools.GenerateAll); } } if (GUILayout.Button("构建热更 DLL 并写入 StreamingAssets", GUILayout.Height(32))) { RunAction("Build Hotfix DLL", () => { if (!TH1HybridCLRBuildTools.BuildAndCopyHotfixArtifacts(_developmentBuild)) { throw new Exception("Build hotfix dll failed. See Console for details."); } }); } if (GUILayout.Button("测试 Hotfix 混淆", GUILayout.Height(32))) { RunAction("Test Hotfix Obfuscation", () => { if (!TH1HybridCLRBuildTools.BuildAndCopyHotfixArtifacts(_developmentBuild, true)) { throw new Exception("Test hotfix obfuscation failed. See Console for details."); } }); } EditorGUILayout.Space(6); EditorGUILayout.LabelField("YooAsset", EditorStyles.boldLabel); using (new EditorGUILayout.HorizontalScope()) { if (GUILayout.Button("配置 Collector", GUILayout.Height(28))) { RunAction("Configure YooAsset Collector", TH1YooAssetBuildTools.ConfigureDefaultPackageCollector); } if (GUILayout.Button("验证 Collector 地址", GUILayout.Height(28))) { RunAction("Validate YooAsset Collector", TH1YooAssetBuildTools.ValidateCollectorAddresses); } } if (GUILayout.Button("构建 YooAsset 内置 AB", GUILayout.Height(32))) { RunAction("Build YooAsset AB", TH1YooAssetBuildTools.BuildBuiltinDefaultPackage); } EditorGUILayout.Space(8); EditorGUILayout.HelpBox("上面的单项按钮用于排查问题。正常出包只点顶部的大按钮。", MessageType.None); } } private void PreparePlayerBuild() { TH1HybridCLRBuildTools.ConfigureHotfixSettings(); TH1YooAssetBuildTools.ConfigureDefaultPackageCollector(); if (_runGenerateAll) { TH1HybridCLRBuildTools.GenerateAll(); } if (!TH1HybridCLRBuildTools.BuildAndCopyHotfixArtifacts(_developmentBuild)) { throw new Exception("Build hotfix dll failed. See Console for details."); } TH1YooAssetBuildTools.BuildBuiltinDefaultPackage(); } private void RunAction(string title, System.Action action) { try { _lastAction = $"{title} running..."; Repaint(); AssetDatabase.SaveAssets(); action(); AssetDatabase.Refresh(); _lastAction = $"{title} OK"; Debug.Log($"[TH1.Migration] {title} OK"); } catch (Exception e) { var root = e is TargetInvocationException && e.InnerException != null ? e.InnerException : e; _lastAction = $"{title} failed: {root.Message}"; Debug.LogError($"[TH1.Migration] {title} failed:\n{root}"); } finally { RefreshStatus(); Repaint(); } } private void RefreshStatus() { _statusItems = TH1MigrationBuildStatus.Collect(EditorUserBuildSettings.activeBuildTarget); } private int CountStatus(MigrationStatusLevel level) { var count = 0; foreach (var item in _statusItems) { if (item.Level == level) count++; } return count; } private static MessageType ToMessageType(MigrationStatusLevel level) { return level switch { MigrationStatusLevel.Ok => MessageType.Info, MigrationStatusLevel.Warning => MessageType.Warning, MigrationStatusLevel.Error => MessageType.Error, _ => MessageType.None }; } private static ScriptingImplementation GetScriptingBackend(BuildTarget target) { return PlayerSettings.GetScriptingBackend(BuildPipeline.GetBuildTargetGroup(target)); } } public sealed class TH1MigrationBuildPreprocessor : IPreprocessBuildWithReport { public int callbackOrder => -1000; public void OnPreprocessBuild(BuildReport report) { if (TH1MigrationBuildValidationGate.IsSuppressed) return; var errors = TH1MigrationBuildStatus.GetBuildBlockingMessages(report.summary.platform); if (errors.Count == 0) return; throw new BuildFailedException( "[TH1.Migration] 出包前准备未完成,请打开 Tools/TH1/iOS Migration/Build Panel 执行一键准备出包。\n" + string.Join("\n", errors)); } } internal static class TH1MigrationBuildValidationGate { private static int _suppressCount; public static bool IsSuppressed => _suppressCount > 0; public static IDisposable Suppress() { _suppressCount++; return new SuppressScope(); } private sealed class SuppressScope : IDisposable { private bool _disposed; public void Dispose() { if (_disposed) return; _disposed = true; _suppressCount = Math.Max(0, _suppressCount - 1); } } } internal static class TH1MigrationBuildStatus { public static List Collect(BuildTarget target) { return Collect(target, TH1HybridCLRBuildTools.IsHotfixObfuscationEnabled(), true); } public static List Collect(BuildTarget target, bool expectObfuscatedHotfix) { return Collect(target, expectObfuscatedHotfix, true); } public static List Collect(BuildTarget target, bool expectObfuscatedHotfix, bool checkYooAssetAb) { var items = new List(); AddHotfixStatus(items, target, expectObfuscatedHotfix); AddAotMetadataStatus(items, target); if (checkYooAssetAb) { AddYooAssetStatus(items); } else { items.Add(new MigrationStatusItem( "4. YooAsset 内置 AB", MigrationStatusLevel.Warning, "已按一体化出包选项跳过检查,将沿用 StreamingAssets 中已有的 YooAsset AB。")); } return items; } public static List GetBuildBlockingMessages(BuildTarget target) { return GetBuildBlockingMessages(target, TH1HybridCLRBuildTools.IsHotfixObfuscationEnabled(), true); } public static List GetBuildBlockingMessages(BuildTarget target, bool expectObfuscatedHotfix) { return GetBuildBlockingMessages(target, expectObfuscatedHotfix, true); } public static List GetBuildBlockingMessages(BuildTarget target, bool expectObfuscatedHotfix, bool checkYooAssetAb) { var errors = new List(); foreach (var item in Collect(target, expectObfuscatedHotfix, checkYooAssetAb)) { if (item.Level == MigrationStatusLevel.Error) { errors.Add($"- {item.Title}: {item.Message}"); } } return errors; } private static void AddHotfixStatus(List items, BuildTarget target, bool expectObfuscatedHotfix) { var source = TH1HybridCLRBuildTools.GetHotfixDllSourcePath(target); var obfuscated = TH1HybridCLRBuildTools.GetObfuscatedHotfixDllPath(target); var streaming = TH1HybridCLRBuildTools.GetStreamingHotfixDllPath(); var sourceOk = TH1HybridCLRBuildTools.IsUsableHotfixDll(source, out var sourceMessage); items.Add(new MigrationStatusItem( "1. 代码热更 DLL 输出", sourceOk ? MigrationStatusLevel.Ok : MigrationStatusLevel.Error, $"{sourceMessage}\n这是 HybridCLR 针对当前平台编出来的 TH1.Hotfix.dll。")); var expected = source; var expectedOk = sourceOk; var expectedLabel = "未混淆输出"; if (expectObfuscatedHotfix) { var obfuscatedOk = TH1HybridCLRBuildTools.IsUsableHotfixDll(obfuscated, out var obfuscatedMessage); items.Add(new MigrationStatusItem( "1.5 OPS 混淆后 DLL 输出", obfuscatedOk ? MigrationStatusLevel.Ok : MigrationStatusLevel.Error, $"{obfuscatedMessage}\n这是打包会优先拷贝的 TH1.Hotfix.dll。")); expected = obfuscated; expectedOk = obfuscatedOk; expectedLabel = "混淆输出"; } var streamingOk = TH1HybridCLRBuildTools.IsUsableHotfixDll(streaming, out var streamingMessage); var streamingLevel = streamingOk ? MigrationStatusLevel.Ok : MigrationStatusLevel.Error; var streamingDetails = $"{streamingMessage}\n{streaming}"; if (streamingOk && FileContainsAscii(streaming, "UnityEditor")) { streamingLevel = MigrationStatusLevel.Error; streamingDetails += "\n该 DLL 疑似引用 UnityEditor,不能进 PC/iOS 包。请用面板重新构建目标平台热更 DLL。"; } else if (streamingOk && expectedOk) { var sourceInfo = new FileInfo(expected); var streamingInfo = new FileInfo(streaming); if (streamingInfo.LastWriteTimeUtc.AddSeconds(1) < sourceInfo.LastWriteTimeUtc || streamingInfo.Length != sourceInfo.Length) { streamingLevel = MigrationStatusLevel.Error; streamingDetails += $"\n它和{expectedLabel}不一致,请重新执行大按钮。"; } } items.Add(new MigrationStatusItem("2. 打包会携带的热更 DLL", streamingLevel, streamingDetails)); } private static void AddAotMetadataStatus(List items, BuildTarget target) { var streamingDir = TH1HybridCLRBuildTools.GetStreamingAotMetadataDir(); var missing = new List(); foreach (var fileName in HotfixManifest.AotMetadataAssemblyFileNames) { var path = Path.Combine(streamingDir, fileName); if (!File.Exists(path) || new FileInfo(path).Length == 0) { missing.Add(fileName); } } var backend = PlayerSettings.GetScriptingBackend(BuildPipeline.GetBuildTargetGroup(target)); if (missing.Count > 0) { var level = backend == ScriptingImplementation.IL2CPP ? MigrationStatusLevel.Error : MigrationStatusLevel.Warning; items.Add(new MigrationStatusItem( "3. AOT Metadata", level, $"缺少: {string.Join(", ", missing)}\n{streamingDir}")); return; } items.Add(new MigrationStatusItem("3. AOT Metadata", MigrationStatusLevel.Ok, $"OK\n{streamingDir}")); } private static void AddYooAssetStatus(List items) { var packageRoot = TH1YooAssetBuildTools.GetStreamingPackageRoot(); var manifestVersion = TH1YooAssetBuildTools.GetStreamingManifestVersionPath(); if (!File.Exists(manifestVersion)) { items.Add(new MigrationStatusItem( "4. YooAsset 内置 AB", MigrationStatusLevel.Error, $"缺少 DefaultPackage manifest,请构建 YooAsset 内置 AB。\n{manifestVersion}")); return; } var packageFileCount = CountFiles(packageRoot); var latestPackageWrite = GetLatestWriteTimeUtc(packageRoot); var resourceRoot = ToAbsoluteProjectPath(TH1YooAssetBuildTools.BundleResourceRoot); var latestResourceWrite = GetLatestWriteTimeUtc(resourceRoot); var level = MigrationStatusLevel.Ok; var message = $"OK, files={packageFileCount}, latest={FormatTime(latestPackageWrite)}\n{packageRoot}"; if (packageFileCount == 0) { level = MigrationStatusLevel.Error; message = $"manifest 存在,但包内没有可用文件,请重新构建 YooAsset 内置 AB。\n{packageRoot}"; } if (latestResourceWrite.HasValue && latestPackageWrite.HasValue && latestPackageWrite.Value.AddSeconds(1) < latestResourceWrite.Value) { level = MigrationStatusLevel.Error; message += $"\nBundleResources 最近变更时间 {FormatTime(latestResourceWrite)} 晚于 AB,请重新构建 YooAsset 内置 AB。"; } items.Add(new MigrationStatusItem("4. YooAsset 内置 AB", level, message)); } private static int CountFiles(string root) { if (!Directory.Exists(root)) return 0; var count = 0; foreach (var file in Directory.EnumerateFiles(root, "*", SearchOption.AllDirectories)) { if (file.EndsWith(".meta", StringComparison.OrdinalIgnoreCase)) continue; count++; } return count; } private static DateTime? GetLatestWriteTimeUtc(string root) { if (!Directory.Exists(root)) return null; DateTime? latest = Directory.GetLastWriteTimeUtc(root); foreach (var directory in Directory.EnumerateDirectories(root, "*", SearchOption.AllDirectories)) { var writeTime = Directory.GetLastWriteTimeUtc(directory); if (!latest.HasValue || writeTime > latest.Value) latest = writeTime; } foreach (var file in Directory.EnumerateFiles(root, "*", SearchOption.AllDirectories)) { if (file.EndsWith(".meta", StringComparison.OrdinalIgnoreCase)) continue; var writeTime = File.GetLastWriteTimeUtc(file); if (!latest.HasValue || writeTime > latest.Value) latest = writeTime; } return latest; } private static string ToAbsoluteProjectPath(string projectRelativePath) { var projectRoot = Directory.GetParent(Application.dataPath)?.FullName ?? Application.dataPath; return Path.Combine(projectRoot, projectRelativePath.Replace('/', Path.DirectorySeparatorChar)); } private static string FormatTime(DateTime? utcTime) { return utcTime.HasValue ? utcTime.Value.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss") : "missing"; } private static bool FileContainsAscii(string path, string marker) { var bytes = File.ReadAllBytes(path); var markerBytes = Encoding.ASCII.GetBytes(marker); for (var i = 0; i <= bytes.Length - markerBytes.Length; i++) { var matched = true; for (var j = 0; j < markerBytes.Length; j++) { if (bytes[i + j] == markerBytes[j]) continue; matched = false; break; } if (matched) return true; } return false; } } internal enum MigrationStatusLevel { Ok, Warning, Error } internal readonly struct MigrationStatusItem { public readonly string Title; public readonly MigrationStatusLevel Level; public readonly string Message; public MigrationStatusItem(string title, MigrationStatusLevel level, string message) { Title = title; Level = level; Message = message; } } }