using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; using Logic.Config; using TH1_Logic.Editor.HybridCLR; using TH1_Logic.Editor.YooAssetTools; using UnityEditor; using UnityEditor.Build; using UnityEditor.Build.Reporting; using UnityEngine; namespace TH1_Logic.Editor { public sealed class TH1UnifiedBuildWindow : EditorWindow { private const string VersionConfigPath = "Assets/BundleResources/DataAssets/VersionConfig.asset"; private const string OpsObfuscatorSettingsPath = "Assets/OPS/Obfuscator/Settings/Obfuscator_Settings.json"; private const string ProductName = "TOHOTOPIA"; private const string ExeName = "TOHOTOPIA.exe"; private const string OpsObfuscationEnabledLabel = "开启"; private const string OpsObfuscationDisabledLabel = "关闭"; private static readonly Regex OpsObfuscationRegex = new Regex( "(?\"Key\"\\s*:\\s*\"Global_Enable_Obfuscation\"\\s*,\\s*\"Value\"\\s*:\\s*\")(?True|False)(?\")", RegexOptions.Multiline); private static readonly string[] BaseSharedDefines = { "UNITY", "ENABLE_VIEW", "NODECANVAS" }; private static readonly string[] ControlledDefines = { "UNITY", "ENABLE_VIEW", "NODECANVAS", "STEAMWORKS_NET", "STEAM_CHANNEL", "USE_INPUT", "ENABLE_SPEEDUP", "ENABLE_TRAIN", "ENABLE_AIMODEL", "GAME_AUTO_DEBUG", "CHECK_ACTIONDEFFERENCE", "STEAM_TEST", "TH1_PLATFORM_PC", "TH1_PLATFORM_IOS" }; private VersionConfig _versionConfig; private Vector2 _scroll; private int _versionIndex; private uint _major; private uint _minor; private uint _patch; private uint _fourth; private BuildPlatformProfile _platform = BuildPlatformProfile.PC; private PackageProfile _package = PackageProfile.Debug; private bool _prepareBeforeBuild = true; private bool _buildPlayer = true; private bool _cleanOutput; private bool _runMultilingualExportImport = true; private bool _showAdvanced; private bool _enableSpeedup; private bool _enableTrain; private bool _enableAiModel; private bool _gameAutoDebug; private bool _checkActionDifference; private bool _steamTest; private bool _actionScheduled; private string _lastAction = "Ready"; private string _stageDetail = string.Empty; private string _stageError = string.Empty; private readonly List _stageItems = new List(); [MenuItem("Tools/TH1/Unified Build Window")] [MenuItem("Tools/TH1/一体化出包工具")] public static void Open() { var window = GetWindow("TH1 Unified Build"); window.minSize = new Vector2(680, 620); window.Show(); } private void OnEnable() { LoadVersionConfig(); ResetReleaseTogglesIfNeeded(); } private void OnGUI() { LoadVersionConfig(); DrawHeader(); _scroll = EditorGUILayout.BeginScrollView(_scroll); DrawVersionSection(); EditorGUILayout.Space(8); DrawProfileSection(); EditorGUILayout.Space(8); DrawSpecialDefinesSection(); EditorGUILayout.Space(8); DrawActions(); EditorGUILayout.EndScrollView(); } private void DrawHeader() { EditorGUILayout.LabelField("TH1 PC / iOS 一体化出包", EditorStyles.boldLabel); EditorGUILayout.LabelField("当前 Unity 平台", EditorUserBuildSettings.activeBuildTarget.ToString()); EditorGUILayout.LabelField("上次操作", _lastAction); if (EditorApplication.isCompiling) { EditorGUILayout.HelpBox("Unity 正在编译脚本,等编译结束后再执行切平台或构建。", MessageType.Warning); } EditorGUILayout.HelpBox( "流程:选版本 -> 选平台 -> 选 Debug/Release -> 勾特殊功能 -> 一键执行。Debug 包会关闭 OPS 混淆;Release 包会关闭特殊功能宏并开启 OPS 混淆;iOS 配置不会写入 STEAM_CHANNEL/STEAMWORKS_NET。", MessageType.Info); } private void DrawVersionSection() { EditorGUILayout.LabelField("版本", EditorStyles.boldLabel); if (_versionConfig == null) { EditorGUILayout.HelpBox($"找不到或无法创建 {VersionConfigPath}", MessageType.Error); return; } using (new EditorGUILayout.HorizontalScope()) { _major = (uint)Mathf.Max(0, EditorGUILayout.IntField((int)_major, GUILayout.Width(46))); EditorGUILayout.LabelField(".", GUILayout.Width(10)); _minor = (uint)Mathf.Max(0, EditorGUILayout.IntField((int)_minor, GUILayout.Width(46))); EditorGUILayout.LabelField(".", GUILayout.Width(10)); _patch = (uint)Mathf.Max(0, EditorGUILayout.IntField((int)_patch, GUILayout.Width(46))); EditorGUILayout.LabelField("+", GUILayout.Width(10)); _fourth = (uint)Mathf.Clamp(EditorGUILayout.IntField((int)_fourth, GUILayout.Width(46)), 0, 25); var versionId = _major * 1000000 + _minor * 10000 + _patch * 100 + _fourth; using (new EditorGUI.DisabledScope(_versionConfig.GetVersionInfo(versionId) != null)) { if (GUILayout.Button("创建版本", GUILayout.Width(96))) { _versionConfig.CreateNewVersion(_major, _minor, _patch, _fourth); SortVersions(); _versionIndex = FindVersionIndex(versionId); SaveVersionConfig(); } } } if (_versionConfig.Versions == null || _versionConfig.Versions.Count == 0) { EditorGUILayout.HelpBox("还没有版本号,请先创建一个。", MessageType.Warning); return; } SortVersions(); var labels = _versionConfig.Versions.Select(v => v.FullVersion).ToArray(); _versionIndex = Mathf.Clamp(_versionIndex, 0, labels.Length - 1); _versionIndex = EditorGUILayout.Popup("打包版本", _versionIndex, labels); var selected = GetSelectedVersion(); if (selected != null) { selected.Description = EditorGUILayout.TextArea(selected.Description, GUILayout.MinHeight(90)); } } private void DrawProfileSection() { EditorGUILayout.LabelField("平台和包类型", EditorStyles.boldLabel); var nextPlatform = (BuildPlatformProfile)EditorGUILayout.EnumPopup("平台", _platform); var nextPackage = (PackageProfile)EditorGUILayout.EnumPopup("包类型", _package); if (nextPlatform != _platform || nextPackage != _package) { _platform = nextPlatform; _package = nextPackage; ResetReleaseTogglesIfNeeded(); } _prepareBeforeBuild = EditorGUILayout.ToggleLeft("构建前执行 HybridCLR + YooAsset 准备流程", _prepareBeforeBuild); _buildPlayer = EditorGUILayout.ToggleLeft("准备完成后 Build Player", _buildPlayer); _cleanOutput = EditorGUILayout.ToggleLeft("构建前清空本次输出目录", _cleanOutput); EditorGUILayout.HelpBox("建议多语言导表/导回后再打包。勾选后,一键流程会先执行多语言导出导回,把 DataAssets 同步到 Export,再继续构建 AB 和 Player。", MessageType.Warning); _runMultilingualExportImport = EditorGUILayout.ToggleLeft("一键流程中执行多语言导出导回(DataAssets -> Export)", _runMultilingualExportImport); var group = GetBuildTargetGroup(_platform); EditorGUILayout.LabelField("目标平台组", group.ToString()); EditorGUILayout.LabelField("目标 BuildTarget", GetBuildTarget(_platform).ToString()); EditorGUILayout.LabelField("脚本后端", GetScriptingBackend(_platform, _package).ToString()); EditorGUILayout.LabelField("输出目录", GetOutputPathPreview()); } private void DrawSpecialDefinesSection() { EditorGUILayout.LabelField("特殊功能", EditorStyles.boldLabel); using (new EditorGUI.DisabledScope(_package == PackageProfile.Release)) { _enableSpeedup = EditorGUILayout.ToggleLeft("加速模式 ENABLE_SPEEDUP", _enableSpeedup); _enableTrain = EditorGUILayout.ToggleLeft("训练模式 ENABLE_TRAIN", _enableTrain); _enableAiModel = EditorGUILayout.ToggleLeft("AI 模型 ENABLE_AIMODEL", _enableAiModel); _gameAutoDebug = EditorGUILayout.ToggleLeft("自动战斗 GAME_AUTO_DEBUG", _gameAutoDebug); _checkActionDifference = EditorGUILayout.ToggleLeft("MapData 变化检查 CHECK_ACTIONDEFFERENCE", _checkActionDifference); _steamTest = EditorGUILayout.ToggleLeft("Steam 测试窗口 STEAM_TEST", _platform == BuildPlatformProfile.PC && _steamTest); } if (_package == PackageProfile.Release) { EditorGUILayout.HelpBox("发布包会强制关闭上面所有特殊功能宏,并在应用配置时开启 OPS 混淆。", MessageType.None); } DrawOpsObfuscationStatus(); } private void DrawActions() { EditorGUILayout.LabelField("执行", EditorStyles.boldLabel); DrawStageProgress(); EditorGUILayout.Space(4); using (new EditorGUI.DisabledScope(_actionScheduled || EditorApplication.isCompiling || EditorApplication.isPlayingOrWillChangePlaymode || GetSelectedVersion() == null)) { if (GUILayout.Button("一键应用配置并出包", GUILayout.Height(42))) { ScheduleAction("Apply And Build", () => { if (!ConfirmSelectedVersion("一键应用配置并出包")) return false; ApplyAndRun(); return true; }); } using (new EditorGUILayout.HorizontalScope()) { if (GUILayout.Button("只应用配置", GUILayout.Height(30))) { ScheduleAction("Apply Profile", () => { if (!ConfirmSelectedVersion("只应用配置")) return false; ApplySelectedProfile(); return true; }); } if (GUILayout.Button("只准备热更/AB", GUILayout.Height(30))) { ScheduleAction("Prepare Assets", PrepareAssetsWithStages); } } _showAdvanced = EditorGUILayout.Foldout(_showAdvanced, "高级单项", true); if (_showAdvanced) { if (GUILayout.Button("多语言导出导回(DataAssets -> Export)", GUILayout.Height(30))) { ScheduleAction("Multilingual Export/Import", () => { ResetBuildStages(); RunBuildStage("多语言导出导回", "扫描 DataAssets/UI/Prefab,刷新 Export 和多语言 Excel。", RunMultilingualExportImport); }); } using (new EditorGUILayout.HorizontalScope()) { if (GUILayout.Button("HybridCLR Generate All")) { ScheduleAction("HybridCLR Generate All", TH1HybridCLRBuildTools.GenerateAll); } if (GUILayout.Button("Build Hotfix DLL")) { ScheduleAction("Build Hotfix DLL", () => { if (!TH1HybridCLRBuildTools.BuildAndCopyHotfixArtifacts(IsDevelopmentBuild())) throw new Exception("Build hotfix dll failed. See Console for details."); }); } } using (new EditorGUILayout.HorizontalScope()) { if (GUILayout.Button("Build YooAsset AB")) { ScheduleAction("Build YooAsset AB", TH1YooAssetBuildTools.BuildBuiltinDefaultPackage); } if (GUILayout.Button("打开输出目录")) { EditorUtility.RevealInFinder(Path.GetDirectoryName(GetOutputPath())); } } using (new EditorGUILayout.HorizontalScope()) { if (GUILayout.Button("按当前包类型设置 OPS 混淆")) { ScheduleAction("Apply OPS Obfuscation", ApplyOpsObfuscationSettings); } if (GUILayout.Button("查看 OPS 配置文件")) { EditorUtility.RevealInFinder(GetOpsSettingsFilePath()); } } } } } private void DrawStageProgress() { if (_stageItems.Count == 0) { EditorGUILayout.HelpBox("一键流程会按阶段执行:应用配置 -> 多语言导出导回 -> HybridCLR/YooAsset 准备 -> Build Player。", MessageType.None); return; } EditorGUILayout.LabelField("流程进度", EditorStyles.boldLabel); foreach (var item in _stageItems) { var messageType = item.State == BuildStageState.Failed ? MessageType.Error : item.State == BuildStageState.Running ? MessageType.Info : item.State == BuildStageState.Skipped ? MessageType.Warning : MessageType.None; var line = $"{GetStageMarker(item.State)} {item.Title}"; if (!string.IsNullOrEmpty(item.Detail)) { line += $"\n{item.Detail}"; } EditorGUILayout.HelpBox(line, messageType); } if (!string.IsNullOrEmpty(_stageError)) { EditorGUILayout.HelpBox(_stageError, MessageType.Error); } else if (!string.IsNullOrEmpty(_stageDetail)) { EditorGUILayout.HelpBox(_stageDetail, MessageType.None); } } private void ApplyAndRun() { ResetBuildStages(); var selected = GetSelectedVersion(); var version = selected?.FullVersion ?? "NoVersion"; RunBuildStage("应用配置", $"版本 {version},平台 {_platform},包类型 {_package},同步宏和 OPS 设置。", ApplySelectedProfile); if (_runMultilingualExportImport) { RunBuildStage("多语言导出导回", "扫描 DataAssets/UI/Prefab,刷新 Export 和多语言 Excel。", RunMultilingualExportImport); } else { SkipBuildStage("多语言导出导回", "未勾选。请确认 Export 下的多语言和配置资产已经是本次版本。"); } if (_prepareBeforeBuild) { RunHybridClrAndYooAssetStages(); } else { SkipPrepareStages("未勾选构建前准备流程。"); } if (_buildPlayer) { RunBuildStage("Build Player", $"输出目录:{GetOutputPathPreview()}", BuildPlayer); } else { SkipBuildStage("Build Player", "未勾选准备完成后 Build Player。"); } _stageDetail = "一键流程完成。"; Repaint(); } private void PrepareAssetsWithStages() { ResetBuildStages(); SkipBuildStage("应用配置", "只准备热更/AB,不修改版本、宏和 PlayerSettings。"); SkipBuildStage("多语言导出导回", "只准备热更/AB,不执行多语言导出导回。"); RunHybridClrAndYooAssetStages(); SkipBuildStage("Build Player", "只准备热更/AB,不执行 Build Player。"); _stageDetail = "热更/AB 准备完成。"; Repaint(); } private void RunHybridClrAndYooAssetStages() { RunBuildStage("配置 HybridCLR", "刷新热更程序集、AOT 元数据和 HybridCLR 设置。", TH1HybridCLRBuildTools.ConfigureHotfixSettings); RunBuildStage("配置 YooAsset Collector", "确保 DefaultPackage 收集整个 Assets/BundleResources。", TH1YooAssetBuildTools.ConfigureDefaultPackageCollector); RunBuildStage("HybridCLR Generate All", "生成 HybridCLR 必要代码和裁剪引用。", TH1HybridCLRBuildTools.GenerateAll); RunBuildStage("构建 Hotfix DLL/AOT", "编译热更 DLL 并写入 StreamingAssets。", () => { if (!TH1HybridCLRBuildTools.BuildAndCopyHotfixArtifacts(IsDevelopmentBuild())) { throw new BuildFailedException("Build hotfix dll failed. See Console for details."); } }); RunBuildStage("构建 YooAsset AB", "构建内置 DefaultPackage AssetBundle。", TH1YooAssetBuildTools.BuildBuiltinDefaultPackage); RunBuildStage("检查构建阻断项", "确认热更 DLL、AOT Metadata、YooAsset Manifest 都已就绪。", CheckBuildBlockers); } private void SkipPrepareStages(string detail) { SkipBuildStage("配置 HybridCLR", detail); SkipBuildStage("配置 YooAsset Collector", detail); SkipBuildStage("HybridCLR Generate All", detail); SkipBuildStage("构建 Hotfix DLL/AOT", detail); SkipBuildStage("构建 YooAsset AB", detail); SkipBuildStage("检查构建阻断项", detail); } private void RunMultilingualExportImport() { Logic.Editor.MultilingualEditorWindow.RunOneClickExportAndImportForBuild((message, progress) => UpdateRunningStage("多语言导出导回", message, progress)); } private static void CheckBuildBlockers() { var blockers = TH1MigrationBuildStatus.GetBuildBlockingMessages(EditorUserBuildSettings.activeBuildTarget); if (blockers.Count == 0) return; throw new BuildFailedException( "[TH1.UnifiedBuild] Build preparation still has blocking errors:\n" + string.Join("\n", blockers)); } private void ResetBuildStages() { _stageItems.Clear(); _stageError = string.Empty; _stageDetail = "准备开始一键流程。"; AddBuildStage("应用配置"); AddBuildStage("多语言导出导回"); AddBuildStage("配置 HybridCLR"); AddBuildStage("配置 YooAsset Collector"); AddBuildStage("HybridCLR Generate All"); AddBuildStage("构建 Hotfix DLL/AOT"); AddBuildStage("构建 YooAsset AB"); AddBuildStage("检查构建阻断项"); AddBuildStage("Build Player"); Repaint(); } private void AddBuildStage(string title) { _stageItems.Add(new BuildStageItem(title)); } private void RunBuildStage(string title, string detail, System.Action action) { var stage = FindBuildStage(title); stage.State = BuildStageState.Running; stage.Detail = detail; _stageDetail = $"{title}: {detail}"; _stageError = string.Empty; _lastAction = $"{title} running..."; UpdateProgressBar(stage, 0.05f); Debug.Log($"[TH1.UnifiedBuild] Stage start: {title} - {detail}"); Repaint(); try { AssetDatabase.SaveAssets(); action(); AssetDatabase.Refresh(); stage.State = BuildStageState.Done; stage.Detail = "完成"; _stageDetail = $"{title} 完成。"; _lastAction = $"{title} OK"; UpdateProgressBar(stage, 1f); Debug.Log($"[TH1.UnifiedBuild] Stage OK: {title}"); } catch (Exception e) { var root = UnwrapException(e); stage.State = BuildStageState.Failed; stage.Detail = root.Message; _stageError = $"{title} 失败:{root.Message}"; _lastAction = $"{title} failed: {root.Message}"; UpdateProgressBar(stage, 1f); Debug.LogError($"[TH1.UnifiedBuild] Stage failed: {title}\n{root}"); throw; } finally { EditorUtility.ClearProgressBar(); Repaint(); } } private void UpdateRunningStage(string title, string detail, float localProgress) { var stage = FindBuildStage(title); stage.State = BuildStageState.Running; stage.Detail = detail; _stageDetail = $"{title}: {detail}"; UpdateProgressBar(stage, localProgress); Repaint(); } private void SkipBuildStage(string title, string detail) { var stage = FindBuildStage(title); stage.State = BuildStageState.Skipped; stage.Detail = detail; Debug.Log($"[TH1.UnifiedBuild] Stage skipped: {title} - {detail}"); Repaint(); } private BuildStageItem FindBuildStage(string title) { var item = _stageItems.FirstOrDefault(stage => stage.Title == title); if (item != null) return item; item = new BuildStageItem(title); _stageItems.Add(item); return item; } private void UpdateProgressBar(BuildStageItem stage, float localProgress) { var index = Mathf.Max(0, _stageItems.IndexOf(stage)); var progress = _stageItems.Count == 0 ? Mathf.Clamp01(localProgress) : Mathf.Clamp01((index + Mathf.Clamp01(localProgress)) / _stageItems.Count); EditorUtility.DisplayProgressBar("TH1 Unified Build", $"{stage.Title}\n{stage.Detail}", progress); } private static Exception UnwrapException(Exception e) { return e is System.Reflection.TargetInvocationException && e.InnerException != null ? e.InnerException : e; } private static string GetStageMarker(BuildStageState state) { switch (state) { case BuildStageState.Running: return ">"; case BuildStageState.Done: return "OK"; case BuildStageState.Skipped: return "SKIP"; case BuildStageState.Failed: return "FAIL"; default: return "..."; } } private void ApplySelectedProfile() { var selected = GetSelectedVersion(); if (selected == null) throw new InvalidOperationException("No build version selected."); SwitchBuildTargetIfNeeded(); ApplyVersion(selected); ApplyDefines(); ApplyPlayerSettings(); ApplyOpsObfuscationSettings(); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); } private void ApplyVersion(VersionInfo selected) { _versionConfig.CurVersionId = selected.VersionId; PlayerSettings.productName = ProductName; PlayerSettings.bundleVersion = selected.FullVersion; SaveVersionConfig(); } private void ApplyDefines() { var group = GetBuildTargetGroup(_platform); var defines = new HashSet(GetDefines(group), StringComparer.Ordinal); foreach (var symbol in ControlledDefines) { defines.Remove(symbol); } foreach (var symbol in BaseSharedDefines) { defines.Add(symbol); } if (_platform == BuildPlatformProfile.PC) { defines.Add("TH1_PLATFORM_PC"); defines.Add("STEAMWORKS_NET"); defines.Add("STEAM_CHANNEL"); } else { defines.Add("TH1_PLATFORM_IOS"); } if (_package == PackageProfile.Debug) { defines.Add("USE_INPUT"); AddSpecialDefines(defines); } PlayerSettings.SetScriptingDefineSymbolsForGroup(group, string.Join(";", defines.OrderBy(s => s))); } private void AddSpecialDefines(HashSet defines) { if (_enableSpeedup) defines.Add("ENABLE_SPEEDUP"); if (_enableTrain) defines.Add("ENABLE_TRAIN"); if (_enableAiModel) defines.Add("ENABLE_AIMODEL"); if (_gameAutoDebug) defines.Add("GAME_AUTO_DEBUG"); if (_checkActionDifference) defines.Add("CHECK_ACTIONDEFFERENCE"); if (_platform == BuildPlatformProfile.PC && _steamTest) defines.Add("STEAM_TEST"); } private void ApplyPlayerSettings() { var group = GetBuildTargetGroup(_platform); PlayerSettings.SetScriptingBackend(group, GetScriptingBackend(_platform, _package)); EditorUserBuildSettings.development = IsDevelopmentBuild(); EditorUserBuildSettings.allowDebugging = IsDevelopmentBuild(); PlayerSettings.usePlayerLog = IsDevelopmentBuild(); PlayerSettings.enableInternalProfiler = IsDevelopmentBuild(); if (_platform == BuildPlatformProfile.iOS) { PlayerSettings.iOS.targetOSVersionString = "13.0"; } if (IsDevelopmentBuild()) { SetStackTrace(StackTraceLogType.ScriptOnly, StackTraceLogType.ScriptOnly, StackTraceLogType.ScriptOnly); } else { PlayerSettings.SetStackTraceLogType(LogType.Log, StackTraceLogType.None); PlayerSettings.SetStackTraceLogType(LogType.Warning, StackTraceLogType.None); PlayerSettings.SetStackTraceLogType(LogType.Error, StackTraceLogType.ScriptOnly); PlayerSettings.SetStackTraceLogType(LogType.Assert, StackTraceLogType.ScriptOnly); PlayerSettings.SetStackTraceLogType(LogType.Exception, StackTraceLogType.ScriptOnly); } } private void DrawOpsObfuscationStatus() { EditorGUILayout.Space(4); EditorGUILayout.LabelField("OPS 混淆", EditorStyles.boldLabel); var targetEnabled = ShouldEnableOpsObfuscation(); EditorGUILayout.LabelField("当前包类型目标", targetEnabled ? $"Release 包:{OpsObfuscationEnabledLabel}" : $"Debug 包:{OpsObfuscationDisabledLabel}"); if (TryReadOpsObfuscationEnabled(out var currentEnabled)) { EditorGUILayout.LabelField("当前配置文件状态", currentEnabled ? OpsObfuscationEnabledLabel : OpsObfuscationDisabledLabel); } else { EditorGUILayout.HelpBox($"无法读取 OPS 混淆状态:{OpsObfuscatorSettingsPath}", MessageType.Warning); } } private void ApplyOpsObfuscationSettings() { var enabled = ShouldEnableOpsObfuscation(); SetOpsObfuscationEnabled(enabled); Debug.Log($"[TH1.UnifiedBuild] OPS obfuscation {(enabled ? "enabled" : "disabled")} for {_package} package."); } private bool ShouldEnableOpsObfuscation() { return _package == PackageProfile.Release; } private static bool TryReadOpsObfuscationEnabled(out bool enabled) { enabled = false; var path = GetOpsSettingsFilePath(); if (!File.Exists(path)) return false; var json = File.ReadAllText(path); var matches = OpsObfuscationRegex.Matches(json); if (matches.Count != 1) return false; enabled = string.Equals(matches[0].Groups["value"].Value, "True", StringComparison.OrdinalIgnoreCase); return true; } private static void SetOpsObfuscationEnabled(bool enabled) { var path = GetOpsSettingsFilePath(); if (!File.Exists(path)) { throw new FileNotFoundException($"OPS obfuscator settings not found: {OpsObfuscatorSettingsPath}", path); } var json = File.ReadAllText(path); var matches = OpsObfuscationRegex.Matches(json); if (matches.Count != 1) { throw new InvalidOperationException($"Cannot find a unique Global_Enable_Obfuscation entry in {OpsObfuscatorSettingsPath}."); } var updated = OpsObfuscationRegex.Replace(json, match => $"{match.Groups["prefix"].Value}{(enabled ? "True" : "False")}{match.Groups["suffix"].Value}", 1); if (updated == json) return; File.WriteAllText(path, updated); AssetDatabase.ImportAsset(OpsObfuscatorSettingsPath); } private static void SetStackTrace(StackTraceLogType log, StackTraceLogType warning, StackTraceLogType error) { PlayerSettings.SetStackTraceLogType(LogType.Log, log); PlayerSettings.SetStackTraceLogType(LogType.Warning, warning); PlayerSettings.SetStackTraceLogType(LogType.Error, error); PlayerSettings.SetStackTraceLogType(LogType.Assert, error); PlayerSettings.SetStackTraceLogType(LogType.Exception, error); } private void BuildPlayer() { var outputPath = GetOutputPath(); var outputDir = _platform == BuildPlatformProfile.iOS ? outputPath : Path.GetDirectoryName(outputPath); if (string.IsNullOrEmpty(outputDir)) throw new BuildFailedException("Invalid output path."); if (_cleanOutput && Directory.Exists(outputDir)) { var fullOutputDir = Path.GetFullPath(outputDir); var packRoot = Path.GetFullPath(Path.Combine(GetProjectRoot(), "..", "Pack")); if (!fullOutputDir.StartsWith(packRoot, StringComparison.OrdinalIgnoreCase)) { throw new BuildFailedException($"Refuse to clean unexpected output dir: {fullOutputDir}"); } FileUtil.DeleteFileOrDirectory(fullOutputDir); } Directory.CreateDirectory(outputDir); var scenes = EditorBuildSettings.scenes .Where(scene => scene.enabled) .Select(scene => scene.path) .ToArray(); if (scenes.Length == 0) throw new BuildFailedException("No enabled scenes in EditorBuildSettings."); var options = new BuildPlayerOptions { scenes = scenes, target = GetBuildTarget(_platform), targetGroup = GetBuildTargetGroup(_platform), locationPathName = outputPath, options = GetBuildOptions() }; using (TH1MigrationBuildValidationGate.Suppress()) { var report = BuildPipeline.BuildPlayer(options); if (report.summary.result != BuildResult.Succeeded) { throw new BuildFailedException( $"Build failed: {report.summary.result}, errors={report.summary.totalErrors}, warnings={report.summary.totalWarnings}"); } } Debug.Log($"[TH1.UnifiedBuild] Build succeeded: {outputPath}"); } private BuildOptions GetBuildOptions() { if (!IsDevelopmentBuild()) return BuildOptions.None; return BuildOptions.Development | BuildOptions.AllowDebugging | BuildOptions.ConnectWithProfiler; } private string GetOutputPath() { var selected = GetSelectedVersion(); var version = selected?.FullVersion ?? "NoVersion"; var folder = $"{_platform}_{_package}_{version}"; var root = Path.GetFullPath(Path.Combine(GetProjectRoot(), "..", "Pack", folder)); if (_platform == BuildPlatformProfile.iOS) { return root; } return Path.Combine(root, ExeName); } private string GetOutputPathPreview() { var output = GetOutputPath(); return _platform == BuildPlatformProfile.iOS ? output : Path.GetDirectoryName(output); } private void SwitchBuildTargetIfNeeded() { var target = GetBuildTarget(_platform); var group = GetBuildTargetGroup(_platform); if (EditorUserBuildSettings.activeBuildTarget == target) return; if (!EditorUserBuildSettings.SwitchActiveBuildTarget(group, target)) { throw new BuildFailedException($"Switch build target failed: {target}"); } } private static BuildTarget GetBuildTarget(BuildPlatformProfile platform) { return platform == BuildPlatformProfile.iOS ? BuildTarget.iOS : BuildTarget.StandaloneWindows64; } private static BuildTargetGroup GetBuildTargetGroup(BuildPlatformProfile platform) { return platform == BuildPlatformProfile.iOS ? BuildTargetGroup.iOS : BuildTargetGroup.Standalone; } private static ScriptingImplementation GetScriptingBackend(BuildPlatformProfile platform, PackageProfile package) { if (platform == BuildPlatformProfile.iOS) return ScriptingImplementation.IL2CPP; return package == PackageProfile.Release ? ScriptingImplementation.IL2CPP : ScriptingImplementation.Mono2x; } private bool IsDevelopmentBuild() { return _package == PackageProfile.Debug; } private VersionInfo GetSelectedVersion() { if (_versionConfig?.Versions == null || _versionConfig.Versions.Count == 0) return null; _versionIndex = Mathf.Clamp(_versionIndex, 0, _versionConfig.Versions.Count - 1); return _versionConfig.Versions[_versionIndex]; } private void LoadVersionConfig() { if (_versionConfig != null) return; _versionConfig = AssetDatabase.LoadAssetAtPath(VersionConfigPath); if (_versionConfig == null) { var dir = Path.GetDirectoryName(VersionConfigPath); if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) Directory.CreateDirectory(dir); _versionConfig = CreateInstance(); AssetDatabase.CreateAsset(_versionConfig, VersionConfigPath); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); } SortVersions(); _versionIndex = _versionConfig.CurVersionId == 0 ? 0 : FindVersionIndex(_versionConfig.CurVersionId); } private void SaveVersionConfig() { EditorUtility.SetDirty(_versionConfig); AssetDatabase.SaveAssets(); } private void SortVersions() { if (_versionConfig?.Versions == null) return; _versionConfig.Versions = _versionConfig.Versions.OrderByDescending(v => v.VersionId).ToList(); } private int FindVersionIndex(uint versionId) { if (_versionConfig?.Versions == null) return 0; for (var i = 0; i < _versionConfig.Versions.Count; i++) { if (_versionConfig.Versions[i].VersionId == versionId) return i; } return 0; } private void ResetReleaseTogglesIfNeeded() { if (_package != PackageProfile.Release) return; _enableSpeedup = false; _enableTrain = false; _enableAiModel = false; _gameAutoDebug = false; _checkActionDifference = false; _steamTest = false; } private static IEnumerable GetDefines(BuildTargetGroup group) { return PlayerSettings.GetScriptingDefineSymbolsForGroup(group) .Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries) .Select(s => s.Trim()) .Where(s => !string.IsNullOrEmpty(s)); } private static string GetProjectRoot() { return Directory.GetParent(Application.dataPath)?.FullName ?? Application.dataPath; } private static string GetOpsSettingsFilePath() { return Path.Combine(GetProjectRoot(), OpsObfuscatorSettingsPath.Replace('/', Path.DirectorySeparatorChar)); } private void ScheduleAction(string title, System.Action action) { ScheduleAction(title, () => { action(); return true; }); } private void ScheduleAction(string title, System.Func action) { if (_actionScheduled) return; _actionScheduled = true; _lastAction = $"{title} queued..."; Repaint(); EditorApplication.delayCall += () => { try { RunAction(title, action); } finally { _actionScheduled = false; Repaint(); } }; } private bool ConfirmSelectedVersion(string actionName) { var selected = GetSelectedVersion(); if (selected == null) { EditorUtility.DisplayDialog("确认当前版本", "当前没有选中的打包版本。", "确定"); return false; } var multilingualLine = actionName.Contains("一键") ? $"多语言导出导回:{(_runMultilingualExportImport ? "开启" : "关闭")}" : "多语言导出导回:本操作不执行"; return EditorUtility.DisplayDialog( "确认当前出包版本", $"将执行:{actionName}\n\n当前版本:{selected.FullVersion}\nVersionId:{selected.VersionId}\n平台:{_platform}\n包类型:{_package}\n{multilingualLine}\n输出目录:{GetOutputPathPreview()}", "继续", "取消"); } private void RunAction(string title, System.Func action) { try { _lastAction = $"{title} running..."; Repaint(); if (!action()) { _lastAction = $"{title} cancelled"; Debug.Log($"[TH1.UnifiedBuild] {title} cancelled"); return; } _lastAction = $"{title} OK"; Debug.Log($"[TH1.UnifiedBuild] {title} OK"); } catch (Exception e) { _lastAction = $"{title} failed: {e.Message}"; Debug.LogError($"[TH1.UnifiedBuild] {title} failed:\n{e}"); } finally { EditorUtility.ClearProgressBar(); Repaint(); } } private enum BuildStageState { Pending, Running, Done, Skipped, Failed } private sealed class BuildStageItem { public readonly string Title; public string Detail; public BuildStageState State; public BuildStageItem(string title) { Title = title; Detail = string.Empty; State = BuildStageState.Pending; } } } public enum BuildPlatformProfile { PC, iOS } public enum PackageProfile { Debug, Release } }