diff --git a/.codex/skills/th1-base/SKILL.md b/.codex/skills/th1-base/SKILL.md index 2d96ab93d..d8185354a 100644 --- a/.codex/skills/th1-base/SKILL.md +++ b/.codex/skills/th1-base/SKILL.md @@ -1,6 +1,6 @@ --- name: th1-base -description: TH1 project baseline engineering guide for Unity client changes: assembly boundaries, HybridCLR hotfix rules, YooAsset/AssetBundle resource rules, MemoryPack/AOT serialization safety, build packaging flow, and routing to domain skills. Use before broad TH1 Unity code/resource/build changes, especially when touching Scripts, asmdefs, HybridCLR, hotfix DLLs, StreamingAssets, BundleResources, Resources.Load replacement, YooAsset, MemoryPack, generated config, build panels, or package verification. +description: TH1 project baseline engineering guide for Unity client changes: assembly boundaries, HybridCLR hotfix rules, YooAsset/AssetBundle resource rules, BundleResources DataAssets -> Export packaging sync, MemoryPack/AOT serialization safety, unified PC/iOS build packaging flow, and routing to domain skills. Use before broad TH1 Unity code/resource/build changes, especially when touching Scripts, asmdefs, HybridCLR, hotfix DLLs, StreamingAssets, BundleResources, Resources.Load replacement, YooAsset, MemoryPack, generated config, build panels, or package verification. --- # TH1 Base Engineering @@ -107,6 +107,7 @@ Rules: Resources are moving to AB-first loading: - Use `Assets/BundleResources` as the packaged resource root. +- TH1's default YooAsset collector should collect the whole `Assets/BundleResources` root and use `TH1AddressByBundleResourcesPath`, so an asset like `Assets/BundleResources/Export/VersionConfig.asset` is addressed as `Export/VersionConfig.asset`. - Prefer stable logical addresses. Do not make gameplay code depend on platform-specific bundle paths. - Replace `Resources.Load` through `TH1Resource.ResourceLoader`/resource cache wrappers, not scattered direct YooAsset calls. - Editor may run simulate mode; PC/iOS packages should run actual built-in AB package logic. @@ -120,18 +121,26 @@ Resources are moving to AB-first loading: ## Build And Packaging Flow -For migration-era PC IL2CPP smoke builds, use the project one-click flow where possible: +For PC/iOS packages, prefer `Tools/TH1/一体化出包工具` / `TH1UnifiedBuildWindow` where possible. + +Current one-click flow should be visible and stage-based: 1. Configure target platform/build settings. -2. Configure HybridCLR. -3. Run HybridCLR `GenerateAll`. -4. Build hotfix DLL and copy hotfix/AOT metadata to `StreamingAssets`. -5. Build YooAsset built-in `DefaultPackage`. -6. Build Windows IL2CPP player. -7. Launch player and scan the log for startup red errors. +2. Apply selected `VersionConfig` to `Assets/BundleResources/DataAssets/VersionConfig.asset`, `PlayerSettings.bundleVersion`, scripting defines, package type, and OPS obfuscation target. +3. Run multilingual export/import when packaging unless the user explicitly disables it. This copies/transforms `Assets/BundleResources/DataAssets/*` into `Assets/BundleResources/Export/*`, updates `Multilingual.asset`, and generates `MatchLevelData/ExportLevelData.bytes`. +4. Configure HybridCLR. +5. Configure YooAsset collector. +6. Run HybridCLR `GenerateAll`. +7. Build hotfix DLL and copy hotfix/AOT metadata to `StreamingAssets`. +8. Build YooAsset built-in `DefaultPackage`. +9. Check migration/build blockers. +10. Build the PC or iOS player. + +The window must show each phase and stop on the first phase failure. Do not rely only on a final Unity popup when the flow spans version config, multilingual export, HybridCLR, AB, and player build. Manual Unity packaging gotchas: +- If runtime version/localization/config looks stale, compare `Assets/BundleResources/DataAssets/*` against `Assets/BundleResources/Export/*`; the game usually reads `Export/...`, not raw `DataAssets/...`. - After scene list changes, use full `Build`/`Clean Build`; do not use `Build Scripts Only`. - If the one-click build panel reports success while AOT metadata is missing, trust the artifact check, not the old status text. Required AOT metadata must exist under `Assets/StreamingAssets/HybridCLR/AOTAssemblies`. - If you only need a runnable PC package, keep `Create Visual Studio Solution` off unless explicitly debugging the generated solution. diff --git a/.codex/skills/th1-ios-migration/SKILL.md b/.codex/skills/th1-ios-migration/SKILL.md index d0026eb89..626710803 100644 --- a/.codex/skills/th1-ios-migration/SKILL.md +++ b/.codex/skills/th1-ios-migration/SKILL.md @@ -1,6 +1,6 @@ --- name: th1-ios-migration -description: "TH1 project-specific iOS migration guide for same-mainline dual-platform support: HybridCLR hot update foundation, iOS/IL2CPP compile isolation, Steamworks/Steam SDK platform abstraction without Steam regressions, touch input adaptation, YooAsset AssetBundle/resource migration, iOS build settings, and verification that existing online Steam builds and gameplay behavior remain unaffected. Use whenever Codex works on TH1 iOS packaging, mobile porting, HybridCLR, YooAsset AB/resource loading, removing direct Steamworks references for iOS, platform services, touch controls, or build pipeline changes that must preserve the current Steam version." +description: "TH1 project-specific iOS migration guide for same-mainline dual-platform support: HybridCLR hot update foundation, iOS/IL2CPP compile isolation, Steamworks/Steam SDK platform abstraction without Steam regressions, touch input adaptation, YooAsset AssetBundle/resource migration, BundleResources DataAssets -> Export packaging sync, iOS build settings, unified PC/iOS build window flow, and verification that existing online Steam builds and gameplay behavior remain unaffected. Use whenever Codex works on TH1 iOS packaging, mobile porting, HybridCLR, YooAsset AB/resource loading, removing direct Steamworks references for iOS, platform services, touch controls, or build pipeline changes that must preserve the current Steam version." --- # TH1 iOS Migration @@ -173,6 +173,14 @@ Goal: move toward AB/resource hot update without destabilizing the Steam build. Use the existing YooAsset package and `AssetBundleCollectorSetting.asset` unless there is a clear reason not to. +Current TH1 built-in resource shape: + +- Packaged resource root is `Assets/BundleResources`. +- `TH1YooAssetBuildTools.ConfigureDefaultPackageCollector()` should collect the whole root for the default package. +- `TH1AddressByBundleResourcesPath` strips `Assets/BundleResources/`, so `Assets/BundleResources/Export/Multilingual.asset` becomes address `Export/Multilingual.asset`. +- `TH1Resource.ResourceLoader.Load("Export/VersionConfig")` and similar calls normalize addresses and try extension candidates. Do not change runtime callers to platform-specific paths when the collector/address rule is the real issue. +- Runtime config/localization/table data usually reads `Export/...`; raw `DataAssets/...` is the authoring source copied/transformed by the multilingual export/import flow. + Recommended sequence: 1. Finish a real `ResourceManager` wrapper around YooAsset package initialization. @@ -185,7 +193,9 @@ Recommended sequence: Resource rules: - Keep stable logical addresses. Do not make gameplay code depend on platform-specific paths. -- Do not modify export-flow outputs such as `Unity/Assets/Resources/Export/*`, `Tools/Multilingual.xlsx`, or `Tools/MultilingualTxt.txt` unless the user asked for export/import changes. +- Do not modify export-flow outputs such as `Unity/Assets/BundleResources/Export/*`, `Tools/Multilingual.xlsx`, or `Tools/MultilingualTxt.txt` unless the user asked for export/import changes. +- In the current migration, export-flow outputs live under `Assets/BundleResources/Export/*`. If a package shows the wrong version, stale text, or stale table config, first check whether `DataAssets -> Export` was refreshed before AB build. +- Before a release/debug package, run or explicitly skip with confirmation the multilingual export/import flow (`Tools/一键导出导回` or the checkbox in `TH1UnifiedBuildWindow`). It refreshes `Export/*`, `Multilingual.asset`, Excel/TXT intermediate files, and `MatchLevelData/ExportLevelData.bytes`. - Use iOS-specific texture/audio compression settings and bundle output. Do not reuse PC texture assumptions blindly. - Treat generated config/DataAsset loading as compatibility-sensitive; preserve existing table and localization behavior. - For Unity 2021+ / Unity 2022 with YooAsset 2.1.1, prefer SBP (`ScriptableBuildPipeline`) for built-in package builds. YooAsset's BBP path uses Unity's old `BuildPipeline.BuildAssetBundles` call and can trigger the internal assertion `m_InstanceIDToAssetBundleIndex.count(id) > 0`. @@ -209,6 +219,20 @@ Maintain one main branch with separate build profiles: - iOS build: iOS, IL2CPP, mobile platform services, no Steamworks compile dependency, touch enabled, iOS bundles. - Hot update build: platform-specific HybridCLR DLL/AOT metadata and YooAsset manifests. +Prefer `Tools/TH1/一体化出包工具` for manual PC/iOS packaging. The one-click path should be staged and stop on first failure: + +1. Apply version/platform/package defines and OPS obfuscation target. +2. Optional but recommended multilingual export/import (`DataAssets -> Export`). +3. Configure HybridCLR. +4. Configure YooAsset collector. +5. HybridCLR `GenerateAll`. +6. Build hotfix DLL/AOT metadata. +7. Build YooAsset built-in AB. +8. Check build blockers. +9. Build Player. + +The confirmation dialog should show the selected version and whether the multilingual export/import step is enabled. + Release branches may exist for stabilization only: - `release/steam-*` and `release/ios-*` may freeze and cherry-pick fixes. diff --git a/.codex/skills/th1-ios-migration/agents/openai.yaml b/.codex/skills/th1-ios-migration/agents/openai.yaml index 519298185..46e87a9f5 100644 --- a/.codex/skills/th1-ios-migration/agents/openai.yaml +++ b/.codex/skills/th1-ios-migration/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "TH1 iOS Migration" - short_description: "HybridCLR, iOS isolation, touch, YooAsset" - default_prompt: "Use $th1-ios-migration when making TH1 iOS migration changes that must keep the Steam build and existing gameplay behavior intact." + short_description: "HybridCLR、iOS隔离、YooAsset与统一出包" + default_prompt: "Use $th1-ios-migration when making TH1 iOS migration or PC/iOS packaging changes that must keep the Steam build and existing gameplay behavior intact." diff --git a/.codex/skills/th1-multilingual/SKILL.md b/.codex/skills/th1-multilingual/SKILL.md index 4d29d88bb..aa8ea87ce 100644 --- a/.codex/skills/th1-multilingual/SKILL.md +++ b/.codex/skills/th1-multilingual/SKILL.md @@ -1,6 +1,6 @@ --- name: th1-multilingual -description: TH1 project-specific multilingual/localization guide for Unity editor export/import, Multilingual.asset, Multilingual.xlsx, MultilingualTxt.txt, special-term syntax, ordered embedded references, duplicate ID prevention, active text scanning, Excel round-tripping, and translation data debugging. Use whenever Codex works on TH1 多语言导表/导回, localization Excel issues, duplicated rows/IDs, special-term parsing, ordered marker bugs, MultilingualEditorWindow, MultilingualData, or any bug that may create new localization IDs unexpectedly. +description: TH1 project-specific multilingual/localization guide for Unity editor export/import, BundleResources DataAssets -> Export runtime asset sync, Multilingual.asset, Multilingual.xlsx, MultilingualTxt.txt, special-term syntax, ordered embedded references, duplicate ID prevention, active text scanning, Excel round-tripping, and translation data debugging. Use whenever Codex works on TH1 多语言导表/导回, localization Excel issues, duplicated rows/IDs, special-term parsing, ordered marker bugs, MultilingualEditorWindow, MultilingualData, package/export stale version or text issues, or any bug that may create new localization IDs unexpectedly. --- # TH1 Multilingual @@ -26,7 +26,9 @@ Translation length is part of the project quality bar. Keep target-language text - `Tools/ExportStringToExcel.py` - `Tools/Multilingual.xlsx` - `Tools/MultilingualTxt.txt` -- `Unity/Assets/Resources/Export/Multilingual.asset` +- `Unity/Assets/BundleResources/Export/Multilingual.asset` +- `Unity/Assets/BundleResources/DataAssets/*` +- `Unity/Assets/BundleResources/Export/*` For the full data-flow contract, read `references/pipeline.md`. For duplicate IDs, bad markers, and validation commands, read `references/diagnostics.md`. @@ -62,6 +64,14 @@ Asset internal form: 7. Only allocate `_idIndex` when the canonical string is genuinely new. 8. Write Excel-visible text through `GetMultilingualStrEditor()`, which expands internal IDs back to readable terms. +In the current BundleResources/YooAsset flow, export is also the authoring-to-runtime sync step: + +- `Assets/BundleResources/DataAssets/*` is the authoring source for many ScriptableObject configs. +- `AssetExportToExcelInternal()` instantiates each DataAsset, traverses/transforms multilingual strings, and writes the runtime copy to `Assets/BundleResources/Export/{name}.asset`. +- Runtime table/config/localization code usually loads `Export/...` through `TH1Resource.ResourceLoader`, not raw `DataAssets/...`. +- `ExportMatchLevelData()` writes `Assets/BundleResources/MatchLevelData/ExportLevelData.bytes`; runtime match config uses this outside editor play mode. +- Before PC/iOS packaging, run `Tools/一键导出导回` or enable the `TH1UnifiedBuildWindow` checkbox for multilingual export/import before YooAsset AB build. If this is skipped, the built player can show a new title/bundle version while CrashSight/config/localization still report stale `Export` data. + ## Import Workflow 1. Convert Excel to `MultilingualTxt.txt` through `Tools/PrintExcelString.py`. diff --git a/.codex/skills/th1-multilingual/agents/openai.yaml b/.codex/skills/th1-multilingual/agents/openai.yaml index 23daf7b92..17813ccfa 100644 --- a/.codex/skills/th1-multilingual/agents/openai.yaml +++ b/.codex/skills/th1-multilingual/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "TH1 Multilingual" - short_description: "TH1 Unity 多语言导表导回特殊词和重复ID排查" - default_prompt: "Use TH1 multilingual rules to inspect, fix, or explain localization export/import issues." + short_description: "TH1 多语言导表导回、DataAssets到Export同步、重复ID排查" + default_prompt: "Use TH1 multilingual rules to inspect, fix, or explain localization export/import and DataAssets-to-Export sync issues." diff --git a/.codex/skills/th1-multilingual/references/diagnostics.md b/.codex/skills/th1-multilingual/references/diagnostics.md index 5f8efeed5..3954eb73b 100644 --- a/.codex/skills/th1-multilingual/references/diagnostics.md +++ b/.codex/skills/th1-multilingual/references/diagnostics.md @@ -38,7 +38,7 @@ import sys, io from pathlib import Path sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') ids = {'17228', '19977'} -text = Path('Unity/Assets/Resources/Export/Multilingual.asset').read_text(encoding='utf-8') +text = Path('Unity/Assets/BundleResources/Export/Multilingual.asset').read_text(encoding='utf-8') lines = text.splitlines() for idx, line in enumerate(lines): if line.strip() in [f'- ID: {i}' for i in ids]: @@ -59,13 +59,13 @@ Interpretation: Search an ID in exported assets to see who currently references it: ```powershell -rg -n "Desc: 19977|ActionName: 19977|TechDesc: 19977| 19977" Unity/Assets/Resources/Export -S +rg -n "Desc: 19977|ActionName: 19977|TechDesc: 19977| 19977" Unity/Assets/BundleResources/Export -S ``` For field-level reference context: ```powershell -rg -n "19977" Unity/Assets/Resources/Export/ActionDataAssets.asset -C 20 +rg -n "19977" Unity/Assets/BundleResources/Export/ActionDataAssets.asset -C 20 ``` If a new duplicate ID is already written into a DataAssets export, rerunning export after fixing canonicalization should normally reassign the source field back to the preserved ID. diff --git a/.codex/skills/th1-multilingual/references/pipeline.md b/.codex/skills/th1-multilingual/references/pipeline.md index d1f780cbd..728e9554f 100644 --- a/.codex/skills/th1-multilingual/references/pipeline.md +++ b/.codex/skills/th1-multilingual/references/pipeline.md @@ -14,8 +14,12 @@ - Embedded string resolve/unresolve. - Language fallback. - `MultilingualItem`. -- `Unity/Assets/Resources/Export/Multilingual.asset` +- `Unity/Assets/BundleResources/Export/Multilingual.asset` - Canonical ScriptableObject data. +- `Unity/Assets/BundleResources/DataAssets/*` + - Authoring ScriptableObject configs scanned during export. +- `Unity/Assets/BundleResources/Export/*` + - Runtime ScriptableObject copies produced by export. - `Tools/MultilingualTxt.txt` - Intermediate delimiter file. - `Tools/Multilingual.xlsx` @@ -85,8 +89,8 @@ The same row can therefore have different representations at different phases. A - Deduplicate by canonical raw `item.ZH`. 2. Initialize `_zhStrDict` from canonical `item.ZH`. 3. Scan active `TextMeshProUGUI` from scene `UICanvas`. -4. Scan prefabs under `Assets/Resources/Prefab/`. -5. Scan ScriptableObjects under `Assets/Resources/DataAssets/`. +4. Scan prefabs under `Assets/BundleResources/Prefab/`. +5. Scan ScriptableObjects under `Assets/BundleResources/DataAssets/`. 6. For each source string: - Trim and normalize newlines. - Optionally `TransformString`. @@ -98,6 +102,17 @@ The same row can therefore have different representations at different phases. A 8. Write `MultilingualTxt.txt` with `GetMultilingualStrEditor`, which expands IDs for Excel. 9. Run `ExportStringToExcel.py`. +## BundleResources / Packaging Notes + +The multilingual export flow also refreshes runtime config assets for AB builds: + +- `DataAssets/*` is authoring data. +- `SaveExportAsset(name, newAsset)` writes transformed runtime assets to `Assets/BundleResources/Export/{name}.asset`. +- `ConfigManager`, `Table`, `MultilingualManager`, achievements, AI config, and similar runtime paths generally load `Export/...` through `TH1Resource.ResourceLoader`. +- `ExportMatchLevelData()` writes `Assets/BundleResources/MatchLevelData/ExportLevelData.bytes`; runtime match config uses `MatchLevelData/ExportLevelData` outside editor play mode. +- `TH1YooAssetBuildTools` collects the whole `Assets/BundleResources` root, and `TH1AddressByBundleResourcesPath` strips that prefix. `Assets/BundleResources/Export/VersionConfig.asset` is therefore addressed as `Export/VersionConfig.asset`. +- Before building PC/iOS AB/player packages, run `Tools/一键导出导回` or the `TH1UnifiedBuildWindow` multilingual export/import checkbox. Otherwise a player can contain stale `Export` assets even when `DataAssets` and `PlayerSettings.bundleVersion` were changed. + ## Import Phases 1. Run `PrintExcelString.py`. diff --git a/Unity/Assets/Scripts/TH1_Logic/Editor/MultilingualEditorWindow.cs b/Unity/Assets/Scripts/TH1_Logic/Editor/MultilingualEditorWindow.cs index ac3dfd9dc..0795a1dec 100644 --- a/Unity/Assets/Scripts/TH1_Logic/Editor/MultilingualEditorWindow.cs +++ b/Unity/Assets/Scripts/TH1_Logic/Editor/MultilingualEditorWindow.cs @@ -92,28 +92,15 @@ namespace Logic.Editor [MenuItem("Tools/一键导出导回")] public static void OneClickExportAndImport() { - EditorUtility.DisplayProgressBar("一键导出导回", "正在导出到 Excel...", 0.3f); - try { - var window = new MultilingualEditorWindow(); - window.InitializeAsset(); - - // 步骤1: 执行导出 - window.AssetExportToExcelInternal(); - Debug.Log("[一键导出导回] 导出 Excel 成功"); - - EditorUtility.DisplayProgressBar("一键导出导回", "正在从 Excel 导回...", 0.7f); - - // 步骤2: 执行导回 - window.ExcelExportToAssetInternal(); - Debug.Log("[一键导出导回] Excel 导回成功"); - + RunOneClickExportAndImportForBuild((message, progress) => + EditorUtility.DisplayProgressBar("一键导出导回", message, progress)); EditorUtility.DisplayDialog("成功", "一键导出导回完成!", "确定"); } catch (Exception ex) { - Debug.LogError($"[一键导出导回] 失败: {ex.Message}"); + Debug.LogError($"[一键导出导回] 失败: {ex}"); EditorUtility.DisplayDialog("错误", $"操作失败: {ex.Message}", "确定"); } finally @@ -122,6 +109,24 @@ namespace Logic.Editor } } + public static void RunOneClickExportAndImportForBuild(System.Action progress = null) + { + var window = new MultilingualEditorWindow(); + + progress?.Invoke("正在初始化多语言资源...", 0.05f); + window.InitializeAsset(); + + progress?.Invoke("正在导出 DataAssets/UI/Prefab 到 Export 并生成 Excel...", 0.35f); + window.AssetExportToExcelInternal(); + Debug.Log("[一键导出导回] 导出 Excel 成功"); + + progress?.Invoke("正在从 Excel 导回 Multilingual.asset...", 0.75f); + window.ExcelExportToAssetInternal(); + Debug.Log("[一键导出导回] Excel 导回成功"); + + progress?.Invoke("多语言导出导回完成。", 1f); + } + private void InitializeAsset() { var path = "Assets/BundleResources/Export/Multilingual.asset"; diff --git a/Unity/Assets/Scripts/TH1_Logic/Editor/TH1UnifiedBuildWindow.cs b/Unity/Assets/Scripts/TH1_Logic/Editor/TH1UnifiedBuildWindow.cs index 338c486a4..5746837f4 100644 --- a/Unity/Assets/Scripts/TH1_Logic/Editor/TH1UnifiedBuildWindow.cs +++ b/Unity/Assets/Scripts/TH1_Logic/Editor/TH1UnifiedBuildWindow.cs @@ -2,6 +2,7 @@ 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; @@ -15,8 +16,14 @@ 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 = { @@ -51,10 +58,11 @@ namespace TH1_Logic.Editor private uint _patch; private uint _fourth; private BuildPlatformProfile _platform = BuildPlatformProfile.PC; - private PackageProfile _package = PackageProfile.Test; + 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; @@ -62,7 +70,11 @@ namespace TH1_Logic.Editor 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/一体化出包工具")] @@ -107,7 +119,7 @@ namespace TH1_Logic.Editor } EditorGUILayout.HelpBox( - "流程:选版本 -> 选平台 -> 选测试/发布 -> 勾特殊功能 -> 一键执行。发布包会关闭所有特殊功能宏;iOS 配置不会写入 STEAM_CHANNEL/STEAMWORKS_NET。", + "流程:选版本 -> 选平台 -> 选 Debug/Release -> 勾特殊功能 -> 一键执行。Debug 包会关闭 OPS 混淆;Release 包会关闭特殊功能宏并开启 OPS 混淆;iOS 配置不会写入 STEAM_CHANNEL/STEAMWORKS_NET。", MessageType.Info); } @@ -177,6 +189,8 @@ namespace TH1_Logic.Editor _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()); @@ -201,46 +215,70 @@ namespace TH1_Logic.Editor if (_package == PackageProfile.Release) { - EditorGUILayout.HelpBox("发布包会强制关闭上面所有特殊功能宏。OPS 混淆设置不会被这个窗口自动修改,需要单独确认后再处理。", MessageType.None); + EditorGUILayout.HelpBox("发布包会强制关闭上面所有特殊功能宏,并在应用配置时开启 OPS 混淆。", MessageType.None); } + + DrawOpsObfuscationStatus(); } private void DrawActions() { EditorGUILayout.LabelField("执行", EditorStyles.boldLabel); - using (new EditorGUI.DisabledScope(EditorApplication.isCompiling || EditorApplication.isPlayingOrWillChangePlaymode || GetSelectedVersion() == null)) + DrawStageProgress(); + EditorGUILayout.Space(4); + + using (new EditorGUI.DisabledScope(_actionScheduled || EditorApplication.isCompiling || EditorApplication.isPlayingOrWillChangePlaymode || GetSelectedVersion() == null)) { if (GUILayout.Button("一键应用配置并出包", GUILayout.Height(42))) { - RunAction("Apply And Build", ApplyAndRun); + ScheduleAction("Apply And Build", () => + { + if (!ConfirmSelectedVersion("一键应用配置并出包")) return false; + ApplyAndRun(); + return true; + }); } using (new EditorGUILayout.HorizontalScope()) { if (GUILayout.Button("只应用配置", GUILayout.Height(30))) { - RunAction("Apply Profile", ApplySelectedProfile); + ScheduleAction("Apply Profile", () => + { + if (!ConfirmSelectedVersion("只应用配置")) return false; + ApplySelectedProfile(); + return true; + }); } if (GUILayout.Button("只准备热更/AB", GUILayout.Height(30))) { - RunAction("Prepare Assets", () => TH1MigrationCommandLine.PrepareCurrentPlatform(IsDevelopmentBuild())); + 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")) { - RunAction("HybridCLR Generate All", TH1HybridCLRBuildTools.GenerateAll); + ScheduleAction("HybridCLR Generate All", TH1HybridCLRBuildTools.GenerateAll); } if (GUILayout.Button("Build Hotfix DLL")) { - RunAction("Build Hotfix DLL", () => + ScheduleAction("Build Hotfix DLL", () => { if (!TH1HybridCLRBuildTools.BuildAndCopyHotfixArtifacts(IsDevelopmentBuild())) throw new Exception("Build hotfix dll failed. See Console for details."); @@ -252,7 +290,7 @@ namespace TH1_Logic.Editor { if (GUILayout.Button("Build YooAsset AB")) { - RunAction("Build YooAsset AB", TH1YooAssetBuildTools.BuildBuiltinDefaultPackage); + ScheduleAction("Build YooAsset AB", TH1YooAssetBuildTools.BuildBuiltinDefaultPackage); } if (GUILayout.Button("打开输出目录")) @@ -260,22 +298,273 @@ namespace TH1_Logic.Editor 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() { - ApplySelectedProfile(); + 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) { - TH1MigrationCommandLine.PrepareCurrentPlatform(IsDevelopmentBuild()); + RunHybridClrAndYooAssetStages(); + } + else + { + SkipPrepareStages("未勾选构建前准备流程。"); } if (_buildPlayer) { - 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 "..."; } } @@ -288,6 +577,7 @@ namespace TH1_Logic.Editor ApplyVersion(selected); ApplyDefines(); ApplyPlayerSettings(); + ApplyOpsObfuscationSettings(); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); @@ -326,7 +616,7 @@ namespace TH1_Logic.Editor defines.Add("TH1_PLATFORM_IOS"); } - if (_package == PackageProfile.Test) + if (_package == PackageProfile.Debug) { defines.Add("USE_INPUT"); AddSpecialDefines(defines); @@ -373,6 +663,73 @@ namespace TH1_Logic.Editor } } + 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); @@ -486,7 +843,7 @@ namespace TH1_Logic.Editor private bool IsDevelopmentBuild() { - return _package == PackageProfile.Test; + return _package == PackageProfile.Debug; } private VersionInfo GetSelectedVersion() @@ -562,13 +919,75 @@ namespace TH1_Logic.Editor return Directory.GetParent(Application.dataPath)?.FullName ?? Application.dataPath; } - private void RunAction(string title, Action action) + 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(); - action(); + if (!action()) + { + _lastAction = $"{title} cancelled"; + Debug.Log($"[TH1.UnifiedBuild] {title} cancelled"); + return; + } + _lastAction = $"{title} OK"; Debug.Log($"[TH1.UnifiedBuild] {title} OK"); } @@ -579,9 +998,33 @@ namespace TH1_Logic.Editor } 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 @@ -592,7 +1035,7 @@ namespace TH1_Logic.Editor public enum PackageProfile { - Test, + Debug, Release } }