1042 lines
41 KiB
C#
1042 lines
41 KiB
C#
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
|
||
}
|
||
}
|