/* * @Author: 白哉 * @Description: * @Date: 2025年05月26日 星期一 17:05:14 * @Modify: */ using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Text.RegularExpressions; using Logic.CrashSight; using Logic.Multilingual; using MemoryPack; using TMPro; using TH1_Logic.MatchConfig; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using Debug = UnityEngine.Debug; using Object = UnityEngine.Object; namespace Logic.Editor { public class MultilingualEditorWindow : EditorWindow { // 滑条 private Vector2 _barPosition; // 背景 private GUIStyle _redBoxStyle; private GUIStyle _whiteBoxStyle; private MultilingualData _asset; private Dictionary _zhStrDict = new Dictionary(); private Dictionary _isProperNounDict = new Dictionary(); private Dictionary _isDialogueDict = new Dictionary(); private Dictionary _isDeprecatedDict = new Dictionary(); private Dictionary _speakerDict = new Dictionary(); private HashSet _activeSet = new HashSet(); private HashSet _specialTermSet = new HashSet(); private Dictionary _descDict = new Dictionary(); private uint _idIndex; private int _showIndex = 0; private List _assets = new List(); private HashSet characterSet = new HashSet(); private HashSet gameSet = new HashSet(); private List characterList = new List(); private MultilingualType _selectType = MultilingualType.ZH; private TextAsset _importedTxtFile; private string _txtFilePath; private bool _isActive = true; private bool _isProperNoun = false; private bool _isDialogue = false; private bool _isDeprecated = false; private bool _isENNoTranslate = true; private bool _isTDZHNoTranslate = false; private bool _isJPNoTranslate = false; private bool _isKRNoTranslate = false; private bool _isAnyNoTranslate = false; private bool _isSpecialTermSet = false; // 排除次要文案:勾选后导出会跳过 IsSecondary=true 的条目(版本说明 / 地理科普等) private bool _excludeSecondary = true; private bool _isTransformStr = false; private static string ProjectRootPath => Path.GetFullPath(Path.Combine(Application.dataPath, "..", "..")); private static string ToolsPath => Path.Combine(ProjectRootPath, "Tools"); private static string MultilingualTxtPath => Path.Combine(ToolsPath, "MultilingualTxt.txt"); private static string ExportStringToExcelScriptPath => Path.Combine(ToolsPath, "ExportStringToExcel.py"); private static string PrintExcelStringScriptPath => Path.Combine(ToolsPath, "PrintExcelString.py"); private const string DataAssetsPath = "Assets/BundleResources/DataAssets/"; private const string ExportAssetsPath = "Assets/BundleResources/Export/"; private static readonly SkillType[] RequiredSkillDataAssetTypes = { SkillType.AunnPetrifiedState, SkillType.AunnTwinBody, SkillType.AunnSharedHealth, SkillType.AunnTwinOperable, SkillType.SuikaMiniSpawnAfterMove, SkillType.SuikaMiniStack, SkillType.SuikaDamageHalveDropMini, SkillType.SuikaBigForm, SkillType.SuikaGiantForm, SkillType.SuikaFallingSplash, SkillType.SuikaMiniUnitMarker, SkillType.ReimuFantasyNature, SkillType.HakureiRoundShieldWall, SkillType.HakureiKarviEmbark, SkillType.ValhallaOath, SkillType.NoCounterAttack, SkillType.HakureiDragonshipRam, SkillType.CityRaided, SkillType.Berserk, SkillType.Disperse, SkillType.Item, SkillType.AlwaysHide, SkillType.ValkyrieMoveHeal, SkillType.ValkyrieProtectionCaster, SkillType.ValkyrieProtection, SkillType.SuikaThrowReady, SkillType.SuikaThrownUnit, SkillType.SuikaThrowUnitBuff, SkillType.AunnPetrification, SkillType.AunnPetrifiedDefenseAura, SkillType.AunnPortalState, SkillType.AunnHeroDamageBearer, SkillType.AunnHeroDamageBearerBuff, }; private static readonly string[] SerializedFieldLeakTokens = { "NotShow:", "ShowOnUnitMono:", "SkillIcon:", "HasShowList:", "SkillShowList:", "skillPriority:", "ReserveOnCarry:", "ReserveLeaveCarry:", "ReserveGiantUpgrade:", "ReserveCommonTransform:", "- SkillType:", "SkillViewType:", }; private static readonly Dictionary RequiredSkillIconGuids = new() { { SkillType.AunnPetrifiedState, "a994762e362347569aaac57eda527bbd" }, { SkillType.AunnTwinBody, "a557b1e216994dbc9aba71daa4b6cd7f" }, { SkillType.AunnSharedHealth, "d0c8d1956592444eb96e533d0b0ec728" }, { SkillType.AunnTwinOperable, "6b94561a0cb3413499cb61ee8788d4e0" }, { SkillType.AunnPetrification, "a994762e362347569aaac57eda527bbd" }, { SkillType.AunnPetrifiedDefenseAura, "6f47452c0c494c579c2d939697037cd6" }, { SkillType.AunnPortalState, "4e8a40912d3d450c92996e337cad1067" }, { SkillType.AunnHeroDamageBearer, "6f47452c0c494c579c2d939697037cd6" }, { SkillType.AunnHeroDamageBearerBuff, "6f47452c0c494c579c2d939697037cd6" }, }; private static readonly string[] KnownBadTextTokens = { "并非附近", "再附近", "鑾", "鍏", "涓嶆", "銆", "�", "Ã", "Â", }; [MenuItem("Tools/多语言编辑器")] private static void ShowWindow() { var window = CreateWindow(); window.titleContent = new GUIContent("多语言编辑器"); window.Show(); window.minSize = new Vector2(500, 600); } [MenuItem("Tools/一键导出导回")] public static void OneClickExportAndImport() { try { RunOneClickExportAndImportForBuild((message, progress) => EditorUtility.DisplayProgressBar("一键导出导回", message, progress)); EditorUtility.DisplayDialog("成功", "一键导出导回完成!", "确定"); } catch (Exception ex) { Debug.LogError($"[一键导出导回] 失败: {ex}"); EditorUtility.DisplayDialog("错误", $"操作失败: {ex.Message}", "确定"); } finally { EditorUtility.ClearProgressBar(); } } 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"; _asset = AssetDatabase.LoadAssetAtPath(path); if (!_asset) { throw new FileNotFoundException("未找到多语言资源文件: " + path); } } private void AssetExportToExcelInternal() { DuplicateRemoval(); _zhStrDict.Clear(); _activeSet.Clear(); _specialTermSet.Clear(); _isProperNounDict.Clear(); _isDialogueDict.Clear(); _isDeprecatedDict.Clear(); _speakerDict.Clear(); _descDict.Clear(); foreach (var item in _asset.Items) _zhStrDict[item.ZH] = item.ID; if (_asset.Items.Count != 0) _idIndex = _asset.Items[^1].ID + 1; else _idIndex = 1; var uiObj = Object.FindObjectOfType()?.gameObject; if (!uiObj) { var sceneUICanvas = GameObject.Find("UICanvas"); uiObj = sceneUICanvas; } if (uiObj) { var regex = new Regex(@"\*\*<(.+?)>\*\*"); var coms = uiObj.GetComponentsInChildren(true).ToList(); foreach (var com in coms) { if (!Regex.IsMatch(com.text, @"[\u4E00-\u9FFF\u3400-\u4DBFa-zA-Z]")) continue; com.text = com.text.Trim().Replace("\r\n", "\n"); var textCom = com.gameObject.GetComponent(); if (!textCom) textCom = com.gameObject.AddComponent(); if (textCom.NoExport) continue; var text = ExportSpecialTerm(com.text, regex); if (_zhStrDict.TryGetValue(text, out var value)) { textCom.ID = value; } else { textCom.ID = _idIndex; _zhStrDict[text] = _idIndex; _idIndex++; } _activeSet.Add(textCom.ID); _descDict[textCom.ID] = GetGameObjectPath(com.gameObject); EditorUtility.SetDirty(textCom); PrefabUtility.RecordPrefabInstancePropertyModifications(textCom); } EditorSceneManager.MarkSceneDirty(uiObj.scene); } var prefabList = new List(); var prefabPath = "Assets/BundleResources/Prefab/"; if (Directory.Exists(prefabPath)) { string[] prefabPaths = Directory.GetFiles(prefabPath, "*.prefab", SearchOption.AllDirectories); foreach (var prefabAssetPath in prefabPaths) { var prefab = AssetDatabase.LoadAssetAtPath(prefabAssetPath); if (!prefab) continue; var prefabComs = prefab.GetComponentsInChildren(true).ToList(); if (prefabComs.Count == 0) continue; var regex = new Regex(@"\*\*<(.+?)>\*\*"); foreach (var com in prefabComs) { if (!Regex.IsMatch(com.text, @"[\u4E00-\u9FFF\u3400-\u4DBFa-zA-Z]")) continue; com.text = com.text.Trim().Replace("\r\n", "\n"); var textCom = com.gameObject.GetComponent(); if (!textCom) textCom = com.gameObject.AddComponent(); if (textCom.NoExport) continue; var text = ExportSpecialTerm(com.text, regex); if (_zhStrDict.TryGetValue(text, out var value)) { textCom.ID = value; } else { textCom.ID = _idIndex; _zhStrDict[text] = _idIndex; _idIndex++; } _activeSet.Add(textCom.ID); _descDict[textCom.ID] = prefab.name + " : " + GetGameObjectPath(com.gameObject); } prefabList.Add(prefab); } } string[] assetPaths = GetDataAssetPathsForExport(); foreach (var assetPath in assetPaths) { var asset = AssetDatabase.LoadAssetAtPath(assetPath); if (!asset) continue; ValidateSourceDataAssetBeforeExport(asset, assetPath); ScriptableObject newAsset = Object.Instantiate(asset); var regex = new Regex(@"\*\*<(.+?)>\*\*"); TraverseObject(newAsset, regex); SaveExportAsset(asset.name, newAsset, asset, assetPath); } ExportMatchLevelData(); _asset.RefreshDict(true); foreach (var kv in _zhStrDict) { if (_asset.ItemDict.ContainsKey(kv.Value)) continue; var item = new MultilingualItem(); item.ID = kv.Value; item.ZH = kv.Key; _asset.Items.Add(item); } foreach (var item in _asset.Items) { item.IsSpecialTerm = _specialTermSet.Contains(item.ID); if (!_descDict.TryGetValue(item.ID, out var value)) continue; item.Desc = value; } _asset.Items = _asset.Items.OrderBy(i => i.ID).ToList(); _asset.RefreshDict(true); string filePath = MultilingualTxtPath; Directory.CreateDirectory(Path.GetDirectoryName(filePath)); if (!File.Exists(filePath)) { using (File.Create(filePath)) { } } using (StreamWriter sw = new StreamWriter(filePath, false, Encoding.UTF8)) { StringBuilder sb = new StringBuilder(); foreach (var item in _asset.Items) { if (!item.IsCustom) { if (_isProperNounDict.ContainsKey(item.ID)) item.IsProperNoun = _isProperNounDict[item.ID]; if (_isDialogueDict.ContainsKey(item.ID)) item.IsDialogue = _isDialogueDict[item.ID]; if (_isDeprecatedDict.ContainsKey(item.ID)) item.IsDeprecated = _isDeprecatedDict[item.ID]; if (_speakerDict.ContainsKey(item.ID)) item.DialogueSpeaker = _speakerDict[item.ID]; } var active = _activeSet.Contains(item.ID); var zh = _asset.GetMultilingualStrEditor(item.ID, MultilingualType.ZH); var tdzh = _asset.GetMultilingualStrEditor(item.ID, MultilingualType.TDZH); var en = _asset.GetMultilingualStrEditor(item.ID, MultilingualType.EN); var jp = _asset.GetMultilingualStrEditor(item.ID, MultilingualType.JP); var kr = _asset.GetMultilingualStrEditor(item.ID, MultilingualType.KR); sb.Append( $"{item.ID}%$#@!{active}%$#@!{zh}%$#@!{tdzh}%$#@!{en}%$#@!{jp}%$#@!{kr}" + $"%$#@!{item.IsSecondary}" + $"%$#@!{item.IsProperNoun}%$#@!{item.IsDialogue}%$#@!{item.DialogueSpeaker}" + $"%$#@!{item.IsDeprecated}%$#@!{item.IsCustom}%$#@!{item.IsSpecialTerm}" + $"%$#@!{item.Color}%$#@!{item.Icon}%$#@!{item.Desc}!@#$%"); } sw.Write(sb.ToString()); } WriteToExcel(); foreach (var prefab in prefabList) EditorUtility.SetDirty(prefab); EditorUtility.SetDirty(_asset); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); } private void ExcelExportToAssetInternal() { GetExcelData(); _asset.RefreshDict(); string context; using (var reader = new StreamReader(MultilingualTxtPath, Encoding.UTF8, true)) { context = reader.ReadToEnd(); } var lines = context.Split("!@#$%"); var importedRows = 0; var importedLanguageCells = 0; foreach (string line in lines) { if (string.IsNullOrWhiteSpace(line)) continue; string[] cells = line.Split("%$#@!"); if (cells.Length < 7) { throw new FormatException($"多语言导回失败:TXT列数不足,期望至少7列,实际{cells.Length}列。原始行: {line}"); } importedRows++; var idStr = RemoveCsvQuotes(cells[0])?.TrimStart('\uFEFF'); if (!uint.TryParse(idStr, out var id)) { throw new FormatException($"多语言导回失败:ID格式错误。原始行: {line}"); } MultilingualItem item; if (_asset.ItemDict.TryGetValue(id, out var value)) item = value; else { item = new MultilingualItem(); _asset.Items.Add(item); } item.ID = id; // cells[1] = 活跃文本(active),由"一键导出"时写入,导回时回填到IsActive if (cells.Length >= 2) { var activeStr = RemoveCsvQuotes(cells[1]); item.IsActive = MultilingualItem.ParseBoolStr(activeStr); if (!item.IsActive) continue; } if (cells.Length >= 3) { var str = RemoveCsvQuotes(cells[2]); if (!string.IsNullOrEmpty(str)) item.ZH = str; } if (cells.Length >= 4) { var str = RemoveCsvQuotes(cells[3]); if (!string.IsNullOrEmpty(str)) { item.TDZH = str; importedLanguageCells++; } } if (cells.Length >= 5) { var str = RemoveCsvQuotes(cells[4]); if (!string.IsNullOrEmpty(str)) { item.EN = str; importedLanguageCells++; } } if (cells.Length >= 6) { var str = RemoveCsvQuotes(cells[5]); if (!string.IsNullOrEmpty(str)) { item.JP = str; importedLanguageCells++; } } if (cells.Length >= 7) { var str = RemoveCsvQuotes(cells[6]); if (!string.IsNullOrEmpty(str)) { item.KR = str; importedLanguageCells++; } } if (cells.Length >= 8) { var str = RemoveCsvQuotes(cells[7]); if (!string.IsNullOrEmpty(str)) item.IsSecondary = MultilingualItem.ParseBoolStr(str); } if (cells.Length >= 9) { var str = RemoveCsvQuotes(cells[8]); if (!string.IsNullOrEmpty(str)) item.IsProperNoun = MultilingualItem.ParseBoolStr(str); } if (cells.Length >= 10) { var str = RemoveCsvQuotes(cells[9]); if (!string.IsNullOrEmpty(str)) item.IsDialogue = MultilingualItem.ParseBoolStr(str); } if (cells.Length >= 11) { var str = RemoveCsvQuotes(cells[10]); if (!string.IsNullOrEmpty(str)) item.DialogueSpeaker = str; } if (cells.Length >= 12) { var str = RemoveCsvQuotes(cells[11]); if (!string.IsNullOrEmpty(str)) item.IsDeprecated = MultilingualItem.ParseBoolStr(str); } if (cells.Length >= 13) { var str = RemoveCsvQuotes(cells[12]); if (!string.IsNullOrEmpty(str)) item.IsCustom = MultilingualItem.ParseBoolStr(str); } if (cells.Length >= 14) { // 编辑器侧 IsSpecialTerm 由 prefab 扫描时决定,TXT 中传入仅作回传校验 var str = RemoveCsvQuotes(cells[13]); if (!string.IsNullOrEmpty(str)) item.IsSpecialTerm = MultilingualItem.ParseBoolStr(str); } if (cells.Length >= 15) { var str = RemoveCsvQuotes(cells[14]); if (!string.IsNullOrEmpty(str)) { if (!str.StartsWith("#")) str = "#" + str; item.Color = str; } } if (cells.Length >= 16) { var str = RemoveCsvQuotes(cells[15]); if (!string.IsNullOrEmpty(str)) item.Icon = str; } } if (importedRows == 0) { throw new Exception($"多语言导回失败:{MultilingualTxtPath} 没有可导入行,请检查 Excel 转 TXT 是否成功。"); } _asset.RefreshDict(); foreach (var item in _asset.Items) { item.ZH = _asset.UnResolveEmbeddedStrings(item.ZH, MultilingualType.ZH); item.TDZH = _asset.AlignEmbeddedStringsToZH(item.ZH, item.TDZH, MultilingualType.TDZH, item.ID); item.EN = _asset.AlignEmbeddedStringsToZH(item.ZH, item.EN, MultilingualType.EN, item.ID); item.JP = _asset.AlignEmbeddedStringsToZH(item.ZH, item.JP, MultilingualType.JP, item.ID); item.KR = _asset.AlignEmbeddedStringsToZH(item.ZH, item.KR, MultilingualType.KR, item.ID); item.Refresh(); } EditorUtility.SetDirty(_asset); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); Debug.Log($"多语言Excel导回完成: rows={importedRows}, languageCells={importedLanguageCells}, txt={MultilingualTxtPath}"); } protected virtual void OnEnable() { } private void OnDisable() { } private void OnGUI() { if (!_asset) { var path = $"Assets/BundleResources/Export/Multilingual.asset"; _asset = AssetDatabase.LoadAssetAtPath(path); if (!_asset) { _asset = CreateInstance(); AssetDatabase.CreateAsset(_asset, path); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); } } if (_assets.Count == 0) { var pathList = new List(); pathList.Add($"Assets/Fonts/SourceHanSansCN-Bold SDF.asset"); pathList.Add($"Assets/Fonts/SourceHanSansCN-ExtraLight SDF.asset"); pathList.Add($"Assets/Fonts/SourceHanSansCN-Regular SDF.asset"); foreach (var path in pathList) { var asset = AssetDatabase.LoadAssetAtPath(path); if (asset) _assets.Add(asset); } } if (_redBoxStyle == null) { _redBoxStyle = InspectorUtils.GetHelpBoxStyle(); InspectorUtils.AddBorder(_redBoxStyle, new Color(0.5f, 0.4f, 0.4f, 0.6f)); } if (_whiteBoxStyle == null) { _whiteBoxStyle = InspectorUtils.GetHelpBoxStyle(); InspectorUtils.AddBorder(_whiteBoxStyle, new Color(1f, 1f, 1f, 0.2f)); } GUI.skin.button.wordWrap = true; _barPosition = EditorGUILayout.BeginScrollView(_barPosition); EditorGUILayout.BeginHorizontal(); if (InspectorUtils.InspectorButtonWithTextWidth("保存")) { EditorUtility.SetDirty(_asset); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); } if (InspectorUtils.InspectorButtonWithTextWidth("清空")) { _asset.Items.Clear(); _asset.RefreshDict(); } if (InspectorUtils.InspectorButtonWithTextWidth("导出 Excel")) { AssetExportToExcel(); } if (InspectorUtils.InspectorButtonWithTextWidth("Excel 导回")) { ExcelExportToAsset(); } if (InspectorUtils.InspectorButtonWithTextWidth("生成基础配置(中文字体)")) { var uiObj = GameObject.Find("UICanvas"); var coms = uiObj.GetComponentsInChildren(true).ToList(); foreach (var com in coms) { var textCom = com.gameObject.GetComponent(); if (!textCom) continue; if (com.GetMultiTextConfig(MultilingualType.ZH) == null) { var cfg = new MultiTextConfig(textCom, MultilingualType.ZH); com.TextCfg.Add(cfg); } //if (com.FontID == 0) com.FontID = _asset.GetFontGroupID(textCom.font); } EditorSceneManager.MarkSceneDirty(uiObj.scene); // 先处理 Assets/BundleResources/Prefab 路径下的所有 prefab 并保存 var prefabList = new List(); var prefabPath = $"Assets/BundleResources/Prefab/"; if (Directory.Exists(prefabPath)) { string[] prefabPaths = Directory.GetFiles(prefabPath, "*.prefab", SearchOption.AllDirectories); foreach (var prefabAssetPath in prefabPaths) { var prefab = AssetDatabase.LoadAssetAtPath(prefabAssetPath); if (!prefab) continue; var prefabComs = prefab.GetComponentsInChildren(true).ToList(); foreach (var com in prefabComs) { var textCom = com.gameObject.GetComponent(); if (!textCom) continue; if (com.GetMultiTextConfig(MultilingualType.ZH) == null) { var cfg = new MultiTextConfig(textCom, MultilingualType.ZH); com.TextCfg.Add(cfg); } //if (com.FontID == 0) com.FontID = _asset.GetFontGroupID(textCom.font); } prefabList.Add(prefab); } } foreach (var prefab in prefabList) EditorUtility.SetDirty(prefab); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); } /* if (InspectorUtils.InspectorButtonWithTextWidth("设置 font ban = ban")) { var uiObj = GameObject.Find("UICanvas"); var coms = uiObj.GetComponentsInChildren(true).ToList(); foreach (var com in coms) { com.FontBan = com.Ban; } EditorSceneManager.MarkSceneDirty(uiObj.scene); // 先处理 Assets/BundleResources/Prefab 路径下的所有 prefab 并保存 var prefabList = new List(); var prefabPath = $"Assets/BundleResources/Prefab/"; if (Directory.Exists(prefabPath)) { string[] prefabPaths = Directory.GetFiles(prefabPath, "*.prefab", SearchOption.AllDirectories); foreach (var prefabAssetPath in prefabPaths) { var prefab = AssetDatabase.LoadAssetAtPath(prefabAssetPath); if (!prefab) continue; var prefabComs = prefab.GetComponentsInChildren(true).ToList(); foreach (var com in prefabComs) { com.FontBan = com.Ban; } prefabList.Add(prefab); } } foreach (var prefab in prefabList) EditorUtility.SetDirty(prefab); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); } */ if (InspectorUtils.InspectorButtonWithTextWidth("清除预览")) { var uiObj = GameObject.Find("UICanvas"); var coms = uiObj.GetComponentsInChildren(true).ToList(); foreach (var com in coms) { var text = _asset.GetMultilingualStr(com.ID, MultilingualType.ZH); var font = _asset.GetMultilingualFont(com.FontID, MultilingualType.ZH); com.SetMultilingualText(text, MultilingualType.ZH, font); var textCom = com.GetComponent(); textCom?.ForceMeshUpdate(); } } EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(); if (InspectorUtils.InspectorButtonWithTextWidth("添加字体组")) { _asset.FontGroups.Add(new MultilingualFontGroup()); } EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(); _isTransformStr = EditorGUILayout.Toggle("是否批量替换 ", _isTransformStr); EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(); _selectType = (MultilingualType)EditorGUILayout.EnumPopup(_selectType, GUILayout.Width(200)); if (_selectType != MultilingualType.None) { if (InspectorUtils.InspectorButtonWithTextWidth($"检查字符集 {_selectType} 是否有新增")) { string path = $"Assets/Fonts/{_selectType}CharSet.txt"; if (!File.Exists(path)) { EditorUtility.DisplayDialog("字符集提示", "找不到字符集TXT", "确定"); } else { characterSet.Clear(); string content = System.IO.File.ReadAllText(path); foreach (var c in content) characterSet.Add(c); bool isNeedUpdate = false; foreach (var item in _asset.Items) { var str = item.GetStrByType(_selectType); if (string.IsNullOrEmpty(str)) continue; foreach (var c in str) { if (!characterSet.Contains(c)) isNeedUpdate = true; } } if (isNeedUpdate) EditorUtility.DisplayDialog("字符集提示", "有新增,请创建新的字符集TXT", "确定"); else EditorUtility.DisplayDialog("字符集提示", "无新增,无需理会", "确定"); } } InspectorUtils.InspectorTextWidthRich($"{_selectType} TXT并集:"); _importedTxtFile = (TextAsset)EditorGUILayout.ObjectField(_importedTxtFile, typeof(TextAsset), false, GUILayout.Width(300)); if (InspectorUtils.InspectorButtonWithTextWidth($"创建新的 {_selectType} 字符集TXT")) { OnBuildTxt(_selectType); } } EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(); EditorGUILayout.BeginVertical(_redBoxStyle); _isActive = EditorGUILayout.Toggle("筛选活跃文本", _isActive); _isProperNoun = EditorGUILayout.Toggle("筛选翻译专有名词", _isProperNoun); _isDialogue = EditorGUILayout.Toggle("筛选对话文本", _isDialogue); _isDeprecated = EditorGUILayout.Toggle("筛选已废弃文本", _isDeprecated); _isENNoTranslate = EditorGUILayout.Toggle("筛选英文未翻译文本", _isENNoTranslate); _isTDZHNoTranslate = EditorGUILayout.Toggle("筛选繁中未翻译文本", _isTDZHNoTranslate); _isJPNoTranslate = EditorGUILayout.Toggle("筛选日文未翻译文本", _isJPNoTranslate); _isKRNoTranslate = EditorGUILayout.Toggle("筛选韩文未翻译文本", _isKRNoTranslate); _isAnyNoTranslate = EditorGUILayout.Toggle("筛选任意未翻译文本", _isAnyNoTranslate); _isSpecialTermSet = EditorGUILayout.Toggle("筛选专有名词", _isSpecialTermSet); _excludeSecondary = EditorGUILayout.Toggle("排除次要文案", _excludeSecondary); if (InspectorUtils.InspectorButtonWithTextWidth("导出 Excel 筛选类型文本")) { AssetExportToExcel(true); } EditorGUILayout.EndVertical(); EditorGUILayout.Space(); EditorGUILayout.BeginVertical(_whiteBoxStyle); if (_asset.TargetTypes.Count != (int)MultilingualType.Max) { _asset.TargetTypes.Clear(); for (int i = (int)MultilingualType.ZH; i < (int)MultilingualType.Max; i++) _asset.TargetTypes.Add((MultilingualType)i); } for (int i = 0; i < _asset.TargetTypes.Count; i++) { EditorGUILayout.BeginHorizontal(); var type = (MultilingualType)(i + 1); InspectorUtils.InspectorTextWidthRich($"系统语言{type} 对应游戏语言: "); _asset.TargetTypes[i] = (MultilingualType)EditorGUILayout.EnumPopup(_asset.TargetTypes[i], GUILayout.Width(200)); EditorGUILayout.EndHorizontal(); } EditorGUILayout.EndVertical(); foreach (var asset in _assets) { EditorGUILayout.BeginHorizontal(); if (InspectorUtils.InspectorButtonWithTextWidth($"·")) Selection.activeObject = asset; EditorGUI.BeginDisabledGroup(true); // 开始禁用组 EditorGUILayout.ObjectField(asset, typeof(TMP_FontAsset), GUILayout.Width(400)); EditorGUI.EndDisabledGroup(); // 结束禁用组 EditorGUILayout.EndHorizontal(); } var deleteSet = new HashSet(); for (int i = 0; i < _asset.FontGroups.Count; i++) { _asset.FontGroups[i].FontID = (uint)i + 1; if (!ShowFontGroup(_asset.FontGroups[i])) continue; deleteSet.Add(_asset.FontGroups[i]); } foreach (var deleteGroup in deleteSet) _asset.FontGroups.Remove(deleteGroup); ShowAllMultilingualItem(); EditorGUILayout.EndScrollView(); } private void ShowAllMultilingualItem() { int maxIndex = (_asset.Items.Count - 1) / 10; _showIndex = Mathf.Clamp(_showIndex, 0, maxIndex); EditorGUILayout.BeginHorizontal(); if (_showIndex > 0 && InspectorUtils.InspectorButtonWithTextWidth("上一页")) _showIndex--; if (_showIndex < maxIndex && InspectorUtils.InspectorButtonWithTextWidth("下一页")) _showIndex++; EditorGUILayout.EndHorizontal(); for (int i = _showIndex * 10; i < (_showIndex + 1) * 10; i++) { if (i < 0 || i >= _asset.Items.Count) continue; ShowMultilingualItem(_asset.Items[i]); } } private bool ShowFontGroup(MultilingualFontGroup fontGroup) { var isDelete = false; EditorGUILayout.BeginVertical(EditorStyles.helpBox); EditorGUILayout.BeginHorizontal(); InspectorUtils.InspectorTextWidthRich($"ID : {fontGroup.FontID}"); if (InspectorUtils.InspectorButtonWithTextWidth("x")) isDelete = true; EditorGUILayout.EndHorizontal(); fontGroup.ZHFont = (TMP_FontAsset)EditorGUILayout.ObjectField(fontGroup.ZHFont, typeof(TMP_FontAsset), false, GUILayout.Width(400)); fontGroup.TDZHFont = (TMP_FontAsset)EditorGUILayout.ObjectField(fontGroup.TDZHFont, typeof(TMP_FontAsset), false, GUILayout.Width(400)); fontGroup.ENFont = (TMP_FontAsset)EditorGUILayout.ObjectField(fontGroup.ENFont, typeof(TMP_FontAsset), false, GUILayout.Width(400)); fontGroup.JPFont = (TMP_FontAsset)EditorGUILayout.ObjectField(fontGroup.JPFont, typeof(TMP_FontAsset), false, GUILayout.Width(400)); fontGroup.KRFont = (TMP_FontAsset)EditorGUILayout.ObjectField(fontGroup.KRFont, typeof(TMP_FontAsset), false, GUILayout.Width(400)); fontGroup.RUFont = (TMP_FontAsset)EditorGUILayout.ObjectField(fontGroup.RUFont, typeof(TMP_FontAsset), false, GUILayout.Width(400)); fontGroup.ESFont = (TMP_FontAsset)EditorGUILayout.ObjectField(fontGroup.ESFont, typeof(TMP_FontAsset), false, GUILayout.Width(400)); fontGroup.PTFont = (TMP_FontAsset)EditorGUILayout.ObjectField(fontGroup.PTFont, typeof(TMP_FontAsset), false, GUILayout.Width(400)); fontGroup.FRFont = (TMP_FontAsset)EditorGUILayout.ObjectField(fontGroup.FRFont, typeof(TMP_FontAsset), false, GUILayout.Width(400)); EditorGUILayout.EndVertical(); EditorGUILayout.Space(); return isDelete; } private void ShowMultilingualItem(MultilingualItem item) { EditorGUILayout.BeginVertical(EditorStyles.helpBox); InspectorUtils.InspectorTextWidthRich($"{item.ID} : "); InspectorUtils.InspectorTextWidthRich($" 中文: {item.ZH}"); if (!string.IsNullOrEmpty(item.TDZH)) InspectorUtils.InspectorTextWidthRich($" 繁中: {item.TDZH}"); if (!string.IsNullOrEmpty(item.EN)) InspectorUtils.InspectorTextWidthRich($" 英语: {item.EN}"); if (!string.IsNullOrEmpty(item.JP)) InspectorUtils.InspectorTextWidthRich($" 日语: {item.JP}"); if (!string.IsNullOrEmpty(item.KR)) InspectorUtils.InspectorTextWidthRich($" 韩语: {item.KR}"); if (!string.IsNullOrEmpty(item.RU)) InspectorUtils.InspectorTextWidthRich($" 俄语: {item.RU}"); if (!string.IsNullOrEmpty(item.ES)) InspectorUtils.InspectorTextWidthRich($" 西班牙语: {item.ES}"); if (!string.IsNullOrEmpty(item.PT)) InspectorUtils.InspectorTextWidthRich($" 葡萄牙语: {item.PT}"); if (!string.IsNullOrEmpty(item.FR)) InspectorUtils.InspectorTextWidthRich($" 法语: {item.FR}"); var unicode = ""; foreach (var c in item.ZH) unicode += $"{(int)c:X4} "; InspectorUtils.InspectorTextWidthRich($" Unicode: {unicode}"); EditorGUILayout.EndVertical(); EditorGUILayout.Space(); } private void DuplicateRemoval() { // 排序 asset.items 保证id从小到大 _asset.Items = _asset.Items.OrderBy(i => i.ID).ToList(); var regex = new Regex(@"\*\*<(.+?)>\*\*"); var zhIndex = new Dictionary(); _zhStrDict.Clear(); var deleteItem = new HashSet(); foreach (var item in _asset.Items) { item.Refresh(); if (_isTransformStr) { item.ZH = TransformString(item.ZH); item.TDZH = TransformString(item.TDZH); item.EN = TransformString(item.EN); item.JP = TransformString(item.JP); item.KR = TransformString(item.KR); } if (!string.IsNullOrEmpty(item.ZH) && !zhIndex.ContainsKey(item.ZH)) { zhIndex[item.ZH] = item.ID; } } foreach (var item in _asset.Items) { item.ZH = CanonicalizeExistingZh(item.ZH, zhIndex, regex); if (_zhStrDict.ContainsKey(item.ZH)) { deleteItem.Add(item); continue; } _zhStrDict[item.ZH] = item.ID; } foreach (var item in deleteItem) _asset.Items.Remove(item); _asset.RefreshDict(); } private string CanonicalizeExistingZh(string origin, Dictionary zhIndex, Regex regex) { var matches = regex.Matches(origin); if (matches.Count == 0) return origin; var replaced = origin; for (int i = matches.Count - 1; i >= 0; i--) { var match = matches[i]; var innerContent = match.Groups[1].Value; if (innerContent.Contains("**<") || innerContent.Contains(">**")) { LogSystem.LogError($"检测到嵌套的**<>**,内容: {origin}"); continue; } string prefix = null; string actualStr = innerContent; var prefixMatch = Regex.Match(innerContent, @"^!\[(\d+)\](.*)$", RegexOptions.Singleline); if (prefixMatch.Success) { prefix = prefixMatch.Groups[1].Value; actualStr = prefixMatch.Groups[2].Value; } else if (Regex.IsMatch(innerContent, @"^\[\d+\]", RegexOptions.Singleline)) { LogSystem.LogError($"**<>**内的顺序标记缺少!,请写成****,内容: {innerContent}"); continue; } if (!uint.TryParse(actualStr, out var subId)) { if (!zhIndex.TryGetValue(actualStr, out subId)) continue; } var replacement = prefix != null ? $"****" : $"**<{subId}>**"; replaced = replaced.Substring(0, match.Index) + replacement + replaced.Substring(match.Index + match.Length); } return replaced; } private string RemoveCsvQuotes(string field) { if (string.IsNullOrEmpty(field)) return field; // 去除首尾空格和换行符 field = field.Trim(); // 若字段以引号开头和结尾,则去除引号并处理内部转义 if (field.Length >= 2 && field.StartsWith("\"") && field.EndsWith("\"")) { field = field.Substring(1, field.Length - 2) // 去除首尾引号 .Replace("\"\"", "\""); // 将转义引号还原为单个引号 } return field; } private void ExcelExportToAsset() { GetExcelData(); _asset.RefreshDict(); string context; using (var reader = new StreamReader(MultilingualTxtPath, Encoding.UTF8, true)) { context = reader.ReadToEnd(); } var lines = context.Split("!@#$%"); var importedRows = 0; var importedLanguageCells = 0; foreach (string line in lines) { if (string.IsNullOrWhiteSpace(line)) continue; // 跳过空行 string[] cells = line.Split("%$#@!"); if (cells.Length < 7) { throw new FormatException($"多语言导回失败:TXT列数不足,期望至少7列,实际{cells.Length}列。原始行: {line}"); } importedRows++; var idStr = RemoveCsvQuotes(cells[0])?.TrimStart('\uFEFF'); if (!uint.TryParse(idStr, out var id)) { throw new FormatException($"多语言导回失败:ID格式错误。原始行: {line}"); } MultilingualItem item; if (_asset.ItemDict.TryGetValue(id, out var value)) item = value; else { item = new MultilingualItem(); _asset.Items.Add(item); } item.ID = id; // cells[1] = 活跃文本(active),由"一键导出"时写入,导回时回填到IsActive if (cells.Length >= 2) { var activeStr = RemoveCsvQuotes(cells[1]); item.IsActive = MultilingualItem.ParseBoolStr(activeStr); } if (cells.Length >= 3) { var str = RemoveCsvQuotes(cells[2]); if (!string.IsNullOrEmpty(str)) item.ZH = str; } if (cells.Length >= 4) { var str = RemoveCsvQuotes(cells[3]); if (!string.IsNullOrEmpty(str)) { item.TDZH = str; importedLanguageCells++; } } if (cells.Length >= 5) { var str = RemoveCsvQuotes(cells[4]); if (!string.IsNullOrEmpty(str)) { item.EN = str; importedLanguageCells++; } } if (cells.Length >= 6) { var str = RemoveCsvQuotes(cells[5]); if (!string.IsNullOrEmpty(str)) { item.JP = str; importedLanguageCells++; } } if (cells.Length >= 7) { var str = RemoveCsvQuotes(cells[6]); if (!string.IsNullOrEmpty(str)) { item.KR = str; importedLanguageCells++; } } if (cells.Length >= 8) { var str = RemoveCsvQuotes(cells[7]); if (!string.IsNullOrEmpty(str)) item.IsSecondary = MultilingualItem.ParseBoolStr(str); } if (cells.Length >= 9) { var str = RemoveCsvQuotes(cells[8]); if (!string.IsNullOrEmpty(str)) item.IsProperNoun = MultilingualItem.ParseBoolStr(str); } if (cells.Length >= 10) { var str = RemoveCsvQuotes(cells[9]); if (!string.IsNullOrEmpty(str)) item.IsDialogue = MultilingualItem.ParseBoolStr(str); } if (cells.Length >= 11) { var str = RemoveCsvQuotes(cells[10]); if (!string.IsNullOrEmpty(str)) item.DialogueSpeaker = str; } if (cells.Length >= 12) { var str = RemoveCsvQuotes(cells[11]); if (!string.IsNullOrEmpty(str)) item.IsDeprecated = MultilingualItem.ParseBoolStr(str); } if (cells.Length >= 13) { var str = RemoveCsvQuotes(cells[12]); if (!string.IsNullOrEmpty(str)) item.IsCustom = MultilingualItem.ParseBoolStr(str); } if (cells.Length >= 14) { var str = RemoveCsvQuotes(cells[13]); if (!string.IsNullOrEmpty(str)) item.IsSpecialTerm = MultilingualItem.ParseBoolStr(str); } if (cells.Length >= 15) { var str = RemoveCsvQuotes(cells[14]); if (!string.IsNullOrEmpty(str)) { // 确保有 # 前缀 if (!str.StartsWith("#")) str = "#" + str; item.Color = str; } } if (cells.Length >= 16) { var str = RemoveCsvQuotes(cells[15]); if (!string.IsNullOrEmpty(str)) item.Icon = str; } } if (importedRows == 0) { throw new Exception($"多语言导回失败:{MultilingualTxtPath} 没有可导入行,请检查 Excel 转 TXT 是否成功。"); } _asset.RefreshDict(); foreach (var item in _asset.Items) { if (!item.IsActive) continue; // 中文先转换 **** -> **** item.ZH = _asset.UnResolveEmbeddedStrings(item.ZH, MultilingualType.ZH); // 其他语言对齐中文的 **** item.TDZH = _asset.AlignEmbeddedStringsToZH(item.ZH, item.TDZH, MultilingualType.TDZH, item.ID); item.EN = _asset.AlignEmbeddedStringsToZH(item.ZH, item.EN, MultilingualType.EN, item.ID); item.JP = _asset.AlignEmbeddedStringsToZH(item.ZH, item.JP, MultilingualType.JP, item.ID); item.KR = _asset.AlignEmbeddedStringsToZH(item.ZH, item.KR, MultilingualType.KR, item.ID); item.Refresh(); } EditorUtility.SetDirty(_asset); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); Debug.Log($"多语言Excel导回完成: rows={importedRows}, languageCells={importedLanguageCells}, txt={MultilingualTxtPath}"); } private void AssetExportToExcel(bool isFilter = false) { DuplicateRemoval(); _zhStrDict.Clear(); _activeSet.Clear(); _specialTermSet.Clear(); _isProperNounDict.Clear(); _isDialogueDict.Clear(); _isDeprecatedDict.Clear(); _speakerDict.Clear(); _descDict.Clear(); foreach (var item in _asset.Items) _zhStrDict[item.ZH] = item.ID; if (_asset.Items.Count != 0) _idIndex = _asset.Items[^1].ID + 1; else _idIndex = 1; // 匹配 **<...>** var regex = new Regex(@"\*\*<(.+?)>\*\*"); var uiObj = GameObject.Find("UICanvas"); if (uiObj) { var coms = uiObj.GetComponentsInChildren(true).ToList(); foreach (var com in coms) { if (!Regex.IsMatch(com.text, @"[\u4E00-\u9FFF\u3400-\u4DBFa-zA-Z]")) continue; // 去除首尾空格和换行符 com.text = com.text.Trim().Replace("\r\n", "\n"); if(_isTransformStr) com.text = TransformString(com.text); var textCom = com.gameObject.GetComponent(); if (!textCom) textCom = com.gameObject.AddComponent(); if (textCom.NoExport) continue; var text = ExportSpecialTerm(com.text, regex); if (_zhStrDict.TryGetValue(text, out var value)) { textCom.ID = value; } else { textCom.ID = _idIndex; _zhStrDict[text] = _idIndex; _idIndex++; } _activeSet.Add(textCom.ID); _descDict[textCom.ID] = GetGameObjectPath(com.gameObject); if(_isTransformStr)EditorUtility.SetDirty(com); EditorUtility.SetDirty(textCom); if(_isTransformStr)PrefabUtility.RecordPrefabInstancePropertyModifications(com); PrefabUtility.RecordPrefabInstancePropertyModifications(textCom); } EditorSceneManager.MarkSceneDirty(uiObj.scene); } else { Debug.LogWarning("找不到UI根节点,仅导出Prefab和DataAssets文本"); } // 先处理 Assets/BundleResources/Prefab 路径下的所有 prefab 并保存 var prefabList = new List(); var prefabPath = $"Assets/BundleResources/Prefab/"; if (Directory.Exists(prefabPath)) { string[] prefabPaths = Directory.GetFiles(prefabPath, "*.prefab", SearchOption.AllDirectories); foreach (var prefabAssetPath in prefabPaths) { var prefab = AssetDatabase.LoadAssetAtPath(prefabAssetPath); if (!prefab) continue; var prefabComs = prefab.GetComponentsInChildren(true).ToList(); if (prefabComs.Count == 0) continue; foreach (var com in prefabComs) { if (!Regex.IsMatch(com.text, @"[\u4E00-\u9FFF\u3400-\u4DBFa-zA-Z]")) continue; // 去除首尾空格和换行符 com.text = com.text.Trim().Replace("\r\n", "\n"); if(_isTransformStr) com.text = TransformString(com.text); var textCom = com.gameObject.GetComponent(); if (!textCom) textCom = com.gameObject.AddComponent(); if (textCom.NoExport) continue; var text = ExportSpecialTerm(com.text, regex); if (_zhStrDict.TryGetValue(text, out var value)) { textCom.ID = value; } else { textCom.ID = _idIndex; _zhStrDict[text] = _idIndex; _idIndex++; } _activeSet.Add(textCom.ID); _descDict[textCom.ID] = prefab.name + " : " + GetGameObjectPath(com.gameObject); } prefabList.Add(prefab); } } // 最后处理 assets string[] assetPaths = GetDataAssetPathsForExport(); foreach (var assetPath in assetPaths) { var asset = AssetDatabase.LoadAssetAtPath(assetPath); if (!asset) continue; if (_isTransformStr) { TransformObject(asset); EditorUtility.SetDirty(asset); } ScriptableObject newAsset = Object.Instantiate(asset); TraverseObject(newAsset, regex); SaveExportAsset(asset.name, newAsset, asset, assetPath); } ExportMatchLevelData(); var newStr = new Dictionary(); var replaceStr = new Dictionary(); // foreach (var kv in _zhStrDict) // { // var original = kv.Key; // var id = kv.Value; // // var matches = regex.Matches(original); // if (matches.Count == 0) continue; // // var replaced = original; // bool isReplaced = false; // // 从后往前替换,避免索引偏移 // for (int i = matches.Count - 1; i >= 0; i--) // { // var match = matches[i]; // // **<>** 内的内容 // var innerContent = match.Groups[1].Value; // // // 检查嵌套:内部不应再包含 **<>** // if (innerContent.Contains("**<") || innerContent.Contains(">**")) // { // LogSystem.LogError($"检测到嵌套的**<>**,ID: {id},内容: {original}"); // continue; // } // // // 检查是否以 [n] 开头 // string prefix = null; // string actualStr = innerContent; // var prefixRegex = new Regex(@"^!\[([^\]]*)\](.*)$", RegexOptions.Singleline); // var prefixMatch = prefixRegex.Match(innerContent); // if (prefixMatch.Success) // { // var prefixValue = prefixMatch.Groups[1].Value; // // 检查 n 是否为数字 // if (!int.TryParse(prefixValue, out _)) // { // LogSystem.LogError($"**<>**内的前缀[n]中n不是数字,ID: {id},内容: {innerContent}"); // continue; // } // // prefix = prefixValue; // actualStr = prefixMatch.Groups[2].Value; // } // // // 已经是转化后的ID格式了,不需要再转化了 // if (uint.TryParse(actualStr, out var strId)) // { // _activeSet.Add(strId); // continue; // } // if (string.IsNullOrEmpty(actualStr)) continue; // // // 检查子字符串是否已存在 // uint subId; // if (_zhStrDict.TryGetValue(actualStr, out var existingId)) // { // subId = existingId; // } // else if (newStr.TryGetValue(actualStr, out var newExistingId)) // { // subId = newExistingId; // } // else // { // subId = _idIndex; // newStr[actualStr] = _idIndex; // _idIndex++; // } // // // 构建替换字符串 // string replacement; // if (prefix != null) // { // replacement = $"**<[{prefix}]{subId}>**"; // } // else // { // replacement = $"**<{subId}>**"; // } // // _specialTermSet.Add(subId); // _activeSet.Add(subId); // replaced = replaced.Substring(0, match.Index) + replacement + // replaced.Substring(match.Index + match.Length); // isReplaced = true; // } // if (isReplaced) replaceStr[replaced] = original; // } // // foreach (var kv in replaceStr) // { // var zh = kv.Value; // var id = _zhStrDict[zh]; // _zhStrDict.Remove(zh); // if (!_zhStrDict.TryAdd(kv.Key, id)) continue; // } // // foreach (var kv in newStr) // { // _zhStrDict[kv.Key] = kv.Value; // } _asset.RefreshDict(true); foreach (var kv in _zhStrDict) { if (_asset.ItemDict.ContainsKey(kv.Value)) continue; var item = new MultilingualItem(); item.ID = kv.Value; item.ZH = kv.Key; _asset.Items.Add(item); } foreach (var item in _asset.Items) { item.IsSpecialTerm = _specialTermSet.Contains(item.ID); if (!_descDict.TryGetValue(item.ID, out var value)) continue; item.Desc = value; } // 排序 asset.items 保证id从小到大 _asset.Items = _asset.Items.OrderBy(i => i.ID).ToList(); _asset.RefreshDict(true); string filePath = MultilingualTxtPath; Directory.CreateDirectory(Path.GetDirectoryName(filePath)); if (!File.Exists(filePath)) { using (File.Create(filePath)) { } // 立即释放句柄 } using (StreamWriter sw = new StreamWriter(filePath, false, Encoding.UTF8)) { StringBuilder sb = new StringBuilder(); foreach (var item in _asset.Items) { if (!item.IsCustom) { if (_isProperNounDict.ContainsKey(item.ID)) item.IsProperNoun = _isProperNounDict[item.ID]; if (_isDialogueDict.ContainsKey(item.ID)) item.IsDialogue = _isDialogueDict[item.ID]; if (_isDeprecatedDict.ContainsKey(item.ID)) item.IsDeprecated = _isDeprecatedDict[item.ID]; if (_speakerDict.ContainsKey(item.ID)) item.DialogueSpeaker = _speakerDict[item.ID]; } if (isFilter) { if (_isActive && !_activeSet.Contains(item.ID)) continue; if (_isProperNoun && !item.IsProperNoun) continue; if (_isDialogue && !item.IsDialogue) continue; if (_isDeprecated && !item.IsDeprecated) continue; if (_isENNoTranslate && item.IsTranslate(MultilingualType.EN)) continue; if (_isTDZHNoTranslate && item.IsTranslate(MultilingualType.TDZH)) continue; if (_isJPNoTranslate && item.IsTranslate(MultilingualType.JP)) continue; if (_isKRNoTranslate && item.IsTranslate(MultilingualType.KR)) continue; if (_isAnyNoTranslate && item.IsTranslate(MultilingualType.None)) continue; if (_isSpecialTermSet && !item.IsSpecialTerm) continue; if (_excludeSecondary && item.IsSecondary) continue; } var active = _activeSet.Contains(item.ID); var zh = _asset.GetMultilingualStrEditor(item.ID, MultilingualType.ZH); var tdzh = _asset.GetMultilingualStrEditor(item.ID, MultilingualType.TDZH); var en = _asset.GetMultilingualStrEditor(item.ID, MultilingualType.EN); var jp = _asset.GetMultilingualStrEditor(item.ID, MultilingualType.JP); var kr = _asset.GetMultilingualStrEditor(item.ID, MultilingualType.KR); sb.Append( $"{item.ID}%$#@!{active}%$#@!{zh}%$#@!{tdzh}%$#@!{en}%$#@!{jp}%$#@!{kr}" + $"%$#@!{item.IsSecondary}" + $"%$#@!{item.IsProperNoun}%$#@!{item.IsDialogue}%$#@!{item.DialogueSpeaker}" + $"%$#@!{item.IsDeprecated}%$#@!{item.IsCustom}%$#@!{item.IsSpecialTerm}" + $"%$#@!{item.Color}%$#@!{item.Icon}%$#@!{item.Desc}!@#$%"); } sw.Write(sb.ToString()); } WriteToExcel(); foreach (var prefab in prefabList) EditorUtility.SetDirty(prefab); EditorUtility.SetDirty(_asset); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); } private string ExportSpecialTerm(string origin, Regex regex) { var matches = regex.Matches(origin); if (matches.Count == 0) return origin; var replaced = origin; bool isReplaced = false; // 从后往前替换,避免索引偏移 for (int i = matches.Count - 1; i >= 0; i--) { var match = matches[i]; // **<>** 内的内容 var innerContent = match.Groups[1].Value; // 检查嵌套:内部不应再包含 **<>** if (innerContent.Contains("**<") || innerContent.Contains(">**")) { LogSystem.LogError($"检测到嵌套的**<>**,内容: {origin}"); continue; } // 检查是否以 ![n] 开头,仅数字 n 算顺序标记。 // 普通文本如 [马]职阶英雄 不能被当成顺序标记。 string prefix = null; string actualStr = innerContent; var prefixRegex = new Regex(@"^!\[(\d+)\](.*)$", RegexOptions.Singleline); var prefixMatch = prefixRegex.Match(innerContent); if (prefixMatch.Success) { var prefixValue = prefixMatch.Groups[1].Value; prefix = prefixValue; actualStr = prefixMatch.Groups[2].Value; } else if (Regex.IsMatch(innerContent, @"^\[\d+\]", RegexOptions.Singleline)) { LogSystem.LogError($"**<>**内的顺序标记缺少!,请写成****,内容: {innerContent}"); continue; } // 已经是转化后的ID格式了,不需要再转化了 if (uint.TryParse(actualStr, out var strId)) { _activeSet.Add(strId); if (prefix != null) { var canonical = $"****"; if (match.Value != canonical) { replaced = replaced.Substring(0, match.Index) + canonical + replaced.Substring(match.Index + match.Length); } } continue; } if (string.IsNullOrEmpty(actualStr)) continue; // 检查子字符串是否已存在 uint subId; if (_zhStrDict.TryGetValue(actualStr, out var existingId)) { subId = existingId; } else { subId = _idIndex; _zhStrDict[actualStr] = _idIndex; _idIndex++; } // 构建替换字符串 string replacement; if (prefix != null) { replacement = $"****"; } else { replacement = $"**<{subId}>**"; } _specialTermSet.Add(subId); _activeSet.Add(subId); replaced = replaced.Substring(0, match.Index) + replacement + replaced.Substring(match.Index + match.Length); } return replaced; } private string GetGameObjectPath(GameObject go) { if (go == null) return string.Empty; if (go.transform.parent == null) return go.name; return GetGameObjectPath(go.transform.parent.gameObject) + "/" + go.name; } private static string[] GetDataAssetPathsForExport() { AssetDatabase.SaveAssets(); AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate); if (!Directory.Exists(DataAssetsPath)) return Array.Empty(); var assetPaths = Directory.GetFiles(DataAssetsPath, "*.asset", SearchOption.AllDirectories) .Select(i => i.Replace('\\', '/')) .OrderBy(i => i, StringComparer.Ordinal) .ToArray(); foreach (var assetPath in assetPaths) { AssetDatabase.ImportAsset(assetPath, ImportAssetOptions.ForceSynchronousImport | ImportAssetOptions.ForceUpdate); } return assetPaths; } private static void ValidateSourceDataAssetBeforeExport(ScriptableObject asset, string assetPath) { if (asset is SkillDataAssets skillDataAssets) { ValidateSkillDataAssetIntegrity(skillDataAssets, assetPath); } } private void SaveExportAsset(string name, ScriptableObject target, ScriptableObject source, string sourcePath) { if (target == null) return; RepairExportAsset(source, target); // 处理目标路径 string targetPath = $"{ExportAssetsPath}{name}.asset"; ValidateExportAsset(source, target, sourcePath, targetPath); if (AssetDatabase.LoadAssetAtPath(targetPath) != null) { AssetDatabase.DeleteAsset(targetPath); } // 保存新实例 AssetDatabase.CreateAsset(target, targetPath); AssetDatabase.SaveAssets(); AssetDatabase.ImportAsset(targetPath, ImportAssetOptions.ForceSynchronousImport | ImportAssetOptions.ForceUpdate); var savedAsset = AssetDatabase.LoadAssetAtPath(targetPath); ValidateExportAsset(source, savedAsset, sourcePath, targetPath); AssetDatabase.Refresh(); } private static void RepairExportAsset(ScriptableObject source, ScriptableObject exported) { if (source is not SkillDataAssets sourceSkill || exported is not SkillDataAssets exportedSkill) return; if ((sourceSkill.SkillViewTypeColorList?.Count ?? 0) == 0) { sourceSkill.SkillViewTypeColorList = exportedSkill.SkillViewTypeColorList != null && exportedSkill.SkillViewTypeColorList.Count > 0 ? new List(exportedSkill.SkillViewTypeColorList) : CreateDefaultSkillViewTypeColors(); EditorUtility.SetDirty(sourceSkill); } if ((exportedSkill.SkillViewTypeColorList?.Count ?? 0) > 0) return; exportedSkill.SkillViewTypeColorList = sourceSkill.SkillViewTypeColorList != null && sourceSkill.SkillViewTypeColorList.Count > 0 ? new List(sourceSkill.SkillViewTypeColorList) : CreateDefaultSkillViewTypeColors(); } private static List CreateDefaultSkillViewTypeColors() { return new List { new SkillViewTypeColor { SkillViewType = SkillViewType.Normal, Color = new Color(0.16078432f, 0.16078432f, 0.3647059f, 1f) }, new SkillViewTypeColor { SkillViewType = SkillViewType.Special, Color = new Color(0.6324806f, 0f, 0.6745098f, 1f) }, new SkillViewTypeColor { SkillViewType = SkillViewType.Unique, Color = new Color(0.84313726f, 0.49411765f, 0f, 1f) }, new SkillViewTypeColor { SkillViewType = SkillViewType.Negative, Color = new Color(0.654088f, 0.06306553f, 0.0020567426f, 1f) }, new SkillViewTypeColor { SkillViewType = SkillViewType.Positive, Color = new Color(0.18039216f, 0.8f, 0.44313726f, 1f) }, }; } private static void ValidateExportAsset( ScriptableObject source, ScriptableObject exported, string sourcePath, string targetPath) { if (source is not SkillDataAssets sourceSkill || exported is not SkillDataAssets exportedSkill) return; ValidateSkillDataAssetIntegrity(sourceSkill, sourcePath); ValidateSkillDataAssetIntegrity(exportedSkill, targetPath); var sourceSkillCount = sourceSkill.SkillInfoList?.Count ?? 0; var exportedSkillCount = exportedSkill.SkillInfoList?.Count ?? 0; var sourceColorCount = sourceSkill.SkillViewTypeColorList?.Count ?? 0; var exportedColorCount = exportedSkill.SkillViewTypeColorList?.Count ?? 0; if (exportedColorCount == 0) { throw new InvalidOperationException( $"SkillDataAssets export has no SkillViewType colors. " + $"source={sourcePath} skills={sourceSkillCount}, colors={sourceColorCount}; " + $"export={targetPath} skills={exportedSkillCount}, colors={exportedColorCount}. " + "This would make skill backgrounds render black."); } if (sourceSkillCount == exportedSkillCount && sourceColorCount == exportedColorCount) return; throw new InvalidOperationException( $"SkillDataAssets export mismatch. " + $"source={sourcePath} skills={sourceSkillCount}, colors={sourceColorCount}; " + $"export={targetPath} skills={exportedSkillCount}, colors={exportedColorCount}. " + "Please save/reimport SkillDataAssets and rerun export before building."); } private static void ValidateSkillDataAssetIntegrity(SkillDataAssets skillDataAssets, string assetPath) { if (skillDataAssets.SkillInfoList == null || skillDataAssets.SkillInfoList.Count == 0) { throw new InvalidOperationException($"SkillDataAssets has no skills: {assetPath}"); } var seen = new HashSet(); foreach (var skillInfo in skillDataAssets.SkillInfoList) { if (!seen.Add(skillInfo.SkillType)) { throw new InvalidOperationException( $"SkillDataAssets has duplicate SkillType {skillInfo.SkillType} in {assetPath}."); } ValidateMultilingualSourceString( skillInfo.SkillName, assetPath, $"SkillInfoList[{skillInfo.SkillType}].SkillName"); ValidateMultilingualSourceString( skillInfo.SkillDesc, assetPath, $"SkillInfoList[{skillInfo.SkillType}].SkillDesc"); ValidateSkillIcon( skillInfo.SkillType, skillInfo.ShowOnUnitMono, skillInfo.SkillIcon, assetPath); if (skillInfo.SkillShowList == null) continue; for (var i = 0; i < skillInfo.SkillShowList.Count; i++) { var showPack = skillInfo.SkillShowList[i]; ValidateMultilingualSourceString( showPack.SkillName, assetPath, $"SkillInfoList[{skillInfo.SkillType}].SkillShowList[{i}].SkillName"); ValidateMultilingualSourceString( showPack.SkillDesc, assetPath, $"SkillInfoList[{skillInfo.SkillType}].SkillShowList[{i}].SkillDesc"); } } var missing = RequiredSkillDataAssetTypes.Where(skillType => !seen.Contains(skillType)).ToList(); if (missing.Count == 0) return; throw new InvalidOperationException( $"SkillDataAssets is missing required SkillType rows in {assetPath}: " + string.Join(", ", missing)); } private static void ValidateMultilingualSourceString(string value, string assetPath, string fieldDesc) { if (string.IsNullOrEmpty(value)) return; foreach (var token in SerializedFieldLeakTokens) { if (!value.Contains(token, StringComparison.Ordinal)) continue; throw new InvalidOperationException( $"Multilingual source text contains serialized field token '{token}' at {assetPath}: {fieldDesc}. " + "Fix the source DataAsset before exporting."); } foreach (var token in KnownBadTextTokens) { if (!value.Contains(token, StringComparison.Ordinal)) continue; throw new InvalidOperationException( $"Multilingual source text contains known-bad token '{token}' at {assetPath}: {fieldDesc}. " + "Fix the source DataAsset before exporting."); } } private static void ValidateSkillIcon(SkillType skillType, bool showOnUnitMono, Sprite skillIcon, string assetPath) { if (showOnUnitMono && skillIcon == null) { throw new InvalidOperationException( $"SkillDataAssets {skillType} is ShowOnUnitMono but has no SkillIcon in {assetPath}."); } if (!RequiredSkillIconGuids.TryGetValue(skillType, out var expectedGuid)) return; if (skillIcon == null) { throw new InvalidOperationException( $"SkillDataAssets {skillType} has no required SkillIcon in {assetPath}; expected guid {expectedGuid}."); } var iconPath = AssetDatabase.GetAssetPath(skillIcon); var actualGuid = AssetDatabase.AssetPathToGUID(iconPath); if (actualGuid == expectedGuid) return; throw new InvalidOperationException( $"SkillDataAssets {skillType} SkillIcon guid mismatch in {assetPath}: expected {expectedGuid}, actual {actualGuid} ({iconPath})."); } private void ExportMatchLevelData() { const string sourcePath = "Assets/BundleResources/MatchLevelData/LevelData.bytes"; const string targetPath = "Assets/BundleResources/MatchLevelData/ExportLevelData.bytes"; if (!File.Exists(sourcePath)) { Debug.LogWarning($"ExportMatchLevelData: 未找到关卡配置资源 {sourcePath}"); return; } try { var bytes = File.ReadAllBytes(sourcePath); var levelData = MemoryPackSerializer.Deserialize(bytes); if (levelData == null) { Debug.LogWarning("ExportMatchLevelData: 关卡配置反序列化结果为空"); return; } if (_isTransformStr) TransformObject(levelData, "MatchLevelData"); var regex = new Regex(@"\*\*<(.+?)>\*\*"); TraverseObject(levelData, regex, "MatchLevelData"); var folder = Path.GetDirectoryName(targetPath); if (!string.IsNullOrEmpty(folder) && !Directory.Exists(folder)) Directory.CreateDirectory(folder); File.WriteAllBytes(targetPath, MemoryPackSerializer.Serialize(levelData)); AssetDatabase.ImportAsset(targetPath); Debug.Log($"ExportMatchLevelData: 已导出 {targetPath}"); } catch (Exception e) { Debug.LogError($"ExportMatchLevelData: 导出失败 - {e.Message}"); } } private void TraverseObject(object asset, Regex regex, string descPrefix = null) { if (asset == null) return; var assetType = asset.GetType(); var fields = asset.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance); // 检查是否拥有 GetSpeaker() 方法并调用 var getSpeakerMethod = assetType.GetMethod("GetSpeaker", BindingFlags.Public | BindingFlags.Instance); string speaker = null; if (getSpeakerMethod != null && getSpeakerMethod.ReturnType == typeof(string)) { speaker = getSpeakerMethod.Invoke(asset, null) as string; } foreach (var field in fields) { var value = field.GetValue(asset); var attr = field.GetCustomAttribute(); var fieldDesc = GetFieldDesc(assetType, field, descPrefix); // 临时诊断:定位 CustomHint 是否被扫到 if (field.Name == "CustomHint" || field.Name == "CustomDesc") { Debug.Log($"[MultiLingExport-Traverse] field={field.Name} attr={(attr != null)} valueType={value?.GetType().Name ?? "null"} value='{value}' descPrefix='{descPrefix}'"); } if (attr != null) { if (value is string s) { var str = s.Trim().Replace("\r\n", "\n"); if (string.IsNullOrEmpty(str)) continue; ValidateMultilingualSourceString(str, assetType.Name, fieldDesc); str = ExportSpecialTerm(str, regex); if (_zhStrDict.TryGetValue(str, out var id)) { field.SetValue(asset, id.ToString()); } else { _zhStrDict[str] = _idIndex; field.SetValue(asset, _zhStrDict[str].ToString()); _idIndex++; } _activeSet.Add(_zhStrDict[str]); _descDict[_zhStrDict[str]] = fieldDesc; _isProperNounDict[_zhStrDict[str]] = attr.IsProperNoun; _isDialogueDict[_zhStrDict[str]] = attr.IsDialogue; _isDeprecatedDict[_zhStrDict[str]] = attr.IsDeprecated; if (attr.IsDialogue && !string.IsNullOrEmpty(speaker)) _speakerDict[_zhStrDict[str]] = speaker; continue; } if (value is List list) { for (int i = 0; i < list.Count; i++) { var str = list[i].Trim().Replace("\r\n", "\n"); if (string.IsNullOrEmpty(str)) continue; ValidateMultilingualSourceString(str, assetType.Name, $"{fieldDesc}[{i}]"); str = ExportSpecialTerm(str, regex); if (_zhStrDict.TryGetValue(str, out var id)) { list[i] = id.ToString(); } else { _zhStrDict[str] = _idIndex; list[i] = _zhStrDict[str].ToString(); _idIndex++; } _activeSet.Add(_zhStrDict[str]); _descDict[_zhStrDict[str]] = $"{fieldDesc}[{i}]"; _isProperNounDict[_zhStrDict[str]] = attr.IsProperNoun; _isDialogueDict[_zhStrDict[str]] = attr.IsDialogue; _isDeprecatedDict[_zhStrDict[str]] = attr.IsDeprecated; if (attr.IsDialogue && !string.IsNullOrEmpty(speaker)) _speakerDict[_zhStrDict[str]] = speaker; } field.SetValue(asset, list); continue; } } if (value == null) continue; TraverseChildValue(value, regex, fieldDesc); } } private void TraverseChildValue(object value, Regex regex, string descPrefix) { if (value == null) return; if (value is string) return; if (value is IDictionary dictionary) { foreach (DictionaryEntry entry in dictionary) { TraverseObject(entry.Value, regex, $"{descPrefix}[{entry.Key}]"); } return; } if (value is IEnumerable enumerable) { var index = 0; foreach (object item in enumerable) { if (TryGetKeyValuePairValue(item, out var key, out var entryValue)) TraverseObject(entryValue, regex, $"{descPrefix}[{key}]"); else TraverseObject(item, regex, $"{descPrefix}[{index}]"); index++; } return; } if (ShouldTraverse(value)) TraverseObject(value, regex, descPrefix); } private void TransformObject(object asset, string descPrefix = null) { if (asset == null) return; var assetType = asset.GetType(); var fields = asset.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance); // 检查是否拥有 GetSpeaker() 方法并调用 var getSpeakerMethod = assetType.GetMethod("GetSpeaker", BindingFlags.Public | BindingFlags.Instance); string speaker = null; if (getSpeakerMethod != null && getSpeakerMethod.ReturnType == typeof(string)) { speaker = getSpeakerMethod.Invoke(asset, null) as string; } foreach (var field in fields) { var value = field.GetValue(asset); var attr = field.GetCustomAttribute(); var fieldDesc = GetFieldDesc(assetType, field, descPrefix); if (attr != null) { if (value is string s) { var str = s.Trim().Replace("\r\n", "\n"); str = TransformString(str); if (string.IsNullOrEmpty(str)) continue; field.SetValue(asset, str); continue; } if (value is List list) { for (int i = 0; i < list.Count; i++) { var str = list[i].Trim().Replace("\r\n", "\n"); str = TransformString(str); if (string.IsNullOrEmpty(str)) continue; list[i] = str; } field.SetValue(asset, list); continue; } } if (value == null) continue; TransformChildValue(value, fieldDesc); } } private void TransformChildValue(object value, string descPrefix) { if (value == null) return; if (value is string) return; if (value is IDictionary dictionary) { foreach (DictionaryEntry entry in dictionary) { TransformObject(entry.Value, $"{descPrefix}[{entry.Key}]"); } return; } if (value is IEnumerable enumerable) { var index = 0; foreach (object item in enumerable) { if (TryGetKeyValuePairValue(item, out var key, out var entryValue)) TransformObject(entryValue, $"{descPrefix}[{key}]"); else TransformObject(item, $"{descPrefix}[{index}]"); index++; } return; } if (ShouldTraverse(value)) TransformObject(value, descPrefix); } private static string GetFieldDesc(Type assetType, FieldInfo field, string descPrefix) { return string.IsNullOrEmpty(descPrefix) ? assetType.Name + " : " + field.Name : descPrefix + " : " + field.Name; } private static bool ShouldTraverse(object value) { var type = value.GetType(); return !type.IsPrimitive && !type.IsEnum && type != typeof(string) && type != typeof(decimal); } private static bool TryGetKeyValuePairValue(object item, out object key, out object value) { key = null; value = null; if (item == null) return false; if (item is DictionaryEntry dictionaryEntry) { key = dictionaryEntry.Key; value = dictionaryEntry.Value; return true; } var itemType = item.GetType(); if (!itemType.IsGenericType || itemType.GetGenericTypeDefinition() != typeof(KeyValuePair<,>)) return false; key = itemType.GetProperty("Key")?.GetValue(item); value = itemType.GetProperty("Value")?.GetValue(item); return true; } public void WriteToExcel() { RunPythonTool(ExportStringToExcelScriptPath); } public void GetExcelData() { RunPythonTool(PrintExcelStringScriptPath); } private void RunPythonTool(string pythonScript) { if (!File.Exists(pythonScript)) { throw new FileNotFoundException($"Python工具不存在: {pythonScript}", pythonScript); } var pythonExecutable = ResolvePythonExecutable(); ProcessStartInfo start = new ProcessStartInfo { FileName = pythonExecutable, Arguments = $"\"{pythonScript}\"", WorkingDirectory = ToolsPath, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true, StandardErrorEncoding = Encoding.UTF8, StandardOutputEncoding = Encoding.UTF8, }; using (var process = Process.Start(start)) { if (process == null) { throw new Exception($"Python工具启动失败: {pythonScript}"); } string output = process.StandardOutput.ReadToEnd(); string error = process.StandardError.ReadToEnd(); // 获取错误信息 process.WaitForExit(); Debug.Log($"Python: {pythonExecutable}"); Debug.Log($"Exit Code: {process.ExitCode}"); // 打印退出码 Debug.Log($"Output: {output}"); Debug.Log($"Error: {error}"); // 打印错误信息 if (process.ExitCode != 0) { throw new Exception( $"Python工具执行失败: {pythonScript}\n" + $"请确认命令行 python 可以正常运行,并已安装 openpyxl:python -m pip install openpyxl\n" + $"{error}\n{output}"); } } } private static string ResolvePythonExecutable() { var candidates = new List(); var envPython = Environment.GetEnvironmentVariable("TH1_PYTHON"); if (!string.IsNullOrWhiteSpace(envPython)) candidates.Add(envPython); var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); var localPythonRoot = Path.Combine(localAppData, "Programs", "Python"); if (Directory.Exists(localPythonRoot)) { candidates.AddRange(Directory.GetDirectories(localPythonRoot, "Python*") .OrderByDescending(i => i) .Select(i => Path.Combine(i, "python.exe"))); } candidates.Add("python"); foreach (var candidate in candidates) { if (string.Equals(candidate, "python", StringComparison.OrdinalIgnoreCase)) return candidate; if (File.Exists(candidate)) return candidate; } return "python"; } private void OnBuildTxt(MultilingualType multilingualType) { characterSet.Clear(); gameSet.Clear(); characterList.Clear(); if (_importedTxtFile != null) { _txtFilePath = AssetDatabase.GetAssetPath(_importedTxtFile); foreach (var c in _importedTxtFile.text) characterSet.Add(c); } AddBasicCharacters(); ExtractFromI2Languages(multilingualType); foreach (var c in gameSet) characterList.Add(c); foreach (var c in characterSet) characterList.Add(c); ExportCharacterList(multilingualType); } private void AddBasicCharacters() { // 添加大写字母 for (char c = 'A'; c <= 'Z'; c++) { characterSet.Add(c); } // 添加小写字母 for (char c = 'a'; c <= 'z'; c++) { characterSet.Add(c); } // 添加数字 for (char c = '0'; c <= '9'; c++) { characterSet.Add(c); } // 添加常用标点符号 string punctuations = ",.!?;:\"'()[]{}+-*/=_<>@#$%^&|\\~|/\~{}[]【】「」『』《》〈〉:;“” ‘’"、,。?!…—--_+-=×÷"; foreach (char c in punctuations) { characterSet.Add(c); } } private void ExtractFromI2Languages(MultilingualType multilingualType) { var path = $"Assets/BundleResources/Export/Multilingual.asset"; var asset = AssetDatabase.LoadAssetAtPath(path); if (asset == null) { EditorUtility.DisplayDialog("错误", "未找到多语言资源文件,请确认路径是否正确。", "确定"); return; } foreach (var item in asset.Items) { var str = item.GetStrByType(multilingualType); if (string.IsNullOrEmpty(str)) continue; foreach (var c in str) { if (characterSet.Contains(c)) continue; gameSet.Add(c); } } } private void ExportCharacterList(MultilingualType multilingualType) { var path = $"Assets/Fonts/{multilingualType}CharSet.txt"; string directory = Path.GetDirectoryName(path); if (!Directory.Exists(directory)) if (directory != null) Directory.CreateDirectory(directory); File.WriteAllText(path, string.Join("", characterList), Encoding.UTF8); EditorUtility.DisplayDialog("成功", "字符集已导出!", "确定"); } // 字符串特定转换 private string TransformString(string input) { if (string.IsNullOrEmpty(input)) return input; // 正则表达式匹配 内容 或 内容 // (?i) 忽略大小写 // 匹配 开始,然后匹配任意不包含 < 的字符(确保不嵌套),最后匹配 string pattern = @"(?i)([^<]*)"; // 使用正则表达式进行替换 // 匹配到的内容保存在分组 1 中,即 $1 // 替换为 **<$1>** string result = Regex.Replace(input, pattern, "**<$1>**"); // 处理特殊情况:如果正则表达式内部允许包含 <,说明可能存在嵌套 // 但是上面的 [^<]* 已经排除了内部包含 < 的情况,所以不会处理嵌套。 // 例如:文本红 // 内部有 ,[^<]* 无法匹配,因此整个表达式不会匹配,也就不会替换。 return result; } } }