TH1/Unity/Assets/Scripts/TH1_Logic/Editor/TH1UnifiedBuildWindow.cs

1042 lines
41 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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(
"(?<prefix>\"Key\"\\s*:\\s*\"Global_Enable_Obfuscation\"\\s*,\\s*\"Value\"\\s*:\\s*\")(?<value>True|False)(?<suffix>\")",
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<BuildStageItem> _stageItems = new List<BuildStageItem>();
[MenuItem("Tools/TH1/Unified Build Window")]
[MenuItem("Tools/TH1/一体化出包工具")]
public static void Open()
{
var window = GetWindow<TH1UnifiedBuildWindow>("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<string>(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<string> 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<VersionConfig>(VersionConfigPath);
if (_versionConfig == null)
{
var dir = Path.GetDirectoryName(VersionConfigPath);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
Directory.CreateDirectory(dir);
_versionConfig = CreateInstance<VersionConfig>();
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<string> 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<bool> 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<bool> 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
}
}