/* * @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 TMPro; 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 = false; 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 = false; private bool _isTransformStr = false; [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() { 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 导回成功"); EditorUtility.DisplayDialog("成功", "一键导出导回完成!", "确定"); } catch (Exception ex) { Debug.LogError($"[一键导出导回] 失败: {ex.Message}"); EditorUtility.DisplayDialog("错误", $"操作失败: {ex.Message}", "确定"); } finally { EditorUtility.ClearProgressBar(); } } private void InitializeAsset() { var path = "Assets/Resources/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(); 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/Resources/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); } } var path = "Assets/Resources/DataAssets/"; string[] assetPaths = Directory.GetFiles(path, "*.asset", SearchOption.AllDirectories); foreach (var assetPath in assetPaths) { var asset = AssetDatabase.LoadAssetAtPath(assetPath); if (!asset) continue; ScriptableObject newAsset = Object.Instantiate(asset); var regex = new Regex(@"\*\*<(.+?)>\*\*"); TraverseObject(newAsset, regex); SaveExportAsset(asset.name, newAsset); } _asset.RefreshDict(); 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(); string filePath = "../Tools/MultilingualTxt.txt"; 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("../Tools/MultilingualTxt.txt", Encoding.Default, true)) { context = reader.ReadToEnd(); } var lines = context.Split("!@#$%"); foreach (string line in lines) { if (string.IsNullOrWhiteSpace(line)) continue; string[] cells = line.Split("%$#@!"); if (cells.Length == 0) continue; var id = uint.Parse(cells[0]); MultilingualItem item; if (_asset.ItemDict.TryGetValue(id, out var value)) item = value; else { item = new MultilingualItem(); _asset.Items.Add(item); } item.ID = id; 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; } if (cells.Length >= 5) { var str = RemoveCsvQuotes(cells[4]); if (!string.IsNullOrEmpty(str)) item.EN = str; } if (cells.Length >= 6) { var str = RemoveCsvQuotes(cells[5]); if (!string.IsNullOrEmpty(str)) item.JP = str; } if (cells.Length >= 7) { var str = RemoveCsvQuotes(cells[6]); if (!string.IsNullOrEmpty(str)) item.KR = str; } 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 && item.IsSpecialTerm) { // 编辑器侧 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; } } _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(); } protected virtual void OnEnable() { } private void OnDisable() { } private void OnGUI() { if (!_asset) { var path = $"Assets/Resources/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/Resources/Prefab 路径下的所有 prefab 并保存 var prefabList = new List(); var prefabPath = $"Assets/Resources/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/Resources/Prefab 路径下的所有 prefab 并保存 var prefabList = new List(); var prefabPath = $"Assets/Resources/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(); _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 (_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 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("../Tools/MultilingualTxt.txt", Encoding.Default, true)) { context = reader.ReadToEnd(); } var lines = context.Split("!@#$%"); foreach (string line in lines) { if (string.IsNullOrWhiteSpace(line)) continue; // 跳过空行 string[] cells = line.Split("%$#@!"); if (cells.Length == 0) continue; var id = uint.Parse(cells[0]); MultilingualItem item; if (_asset.ItemDict.TryGetValue(id, out var value)) item = value; else { item = new MultilingualItem(); _asset.Items.Add(item); } item.ID = id; 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; } if (cells.Length >= 5) { var str = RemoveCsvQuotes(cells[4]); if (!string.IsNullOrEmpty(str)) item.EN = str; } if (cells.Length >= 6) { var str = RemoveCsvQuotes(cells[5]); if (!string.IsNullOrEmpty(str)) item.JP = str; } if (cells.Length >= 7) { var str = RemoveCsvQuotes(cells[6]); if (!string.IsNullOrEmpty(str)) item.KR = str; } 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 && item.IsSpecialTerm) { 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; } } _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(); } private void AssetExportToExcel(bool isFilter = false) { DuplicateRemoval(); _zhStrDict.Clear(); _activeSet.Clear(); _specialTermSet.Clear(); _isProperNounDict.Clear(); _isDialogueDict.Clear(); _isDeprecatedDict.Clear(); _speakerDict.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 = GameObject.Find("UICanvas"); if (!uiObj) { Debug.LogError($"找不到UI根节点"); return; } // 匹配 **<...>** 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"); 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); // 先处理 Assets/Resources/Prefab 路径下的所有 prefab 并保存 var prefabList = new List(); var prefabPath = $"Assets/Resources/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 var path = $"Assets/Resources/DataAssets/"; string[] assetPaths = Directory.GetFiles(path, "*.asset", SearchOption.AllDirectories); 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); } 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(); 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(); string filePath = "../Tools/MultilingualTxt.txt"; 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] 开头 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不是数字,内容: {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 { subId = _idIndex; _zhStrDict[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); } 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 void SaveExportAsset(string name, ScriptableObject target) { if (target == null) return; // 处理目标路径 string targetPath = $"Assets/Resources/Export/{name}.asset"; if (AssetDatabase.LoadAssetAtPath(targetPath) != null) { AssetDatabase.DeleteAsset(targetPath); } // 保存新实例 AssetDatabase.CreateAsset(target, targetPath); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); } private void TraverseObject(object asset, Regex regex) { 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(); if (attr != null) { if (value is string s) { var str = s.Trim().Replace("\r\n", "\n"); if (string.IsNullOrEmpty(str)) continue; 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]] = assetType.Name + " : " + field.Name; _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; 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]] = assetType.Name + " : " + field.Name; _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; if (value is IEnumerable enumerable) { foreach (object item in enumerable) { TraverseObject(item, regex); // 递归处理集合项 } } // 如果是自定义对象(非基础类型),递归处理 else if (!value.GetType().IsPrimitive) TraverseObject(value, regex); } } private void TransformObject(object asset) { 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(); 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; if (value is IEnumerable enumerable) { foreach (object item in enumerable) { TransformObject(item); // 递归处理集合项 } } // 如果是自定义对象(非基础类型),递归处理 else if (!value.GetType().IsPrimitive) TransformObject(value); } } public void WriteToExcel() { var pythonScript = $"../Tools/ExportStringToExcel.py"; ProcessStartInfo start = new ProcessStartInfo { FileName = "python", Arguments = $"\"{pythonScript}\"", RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true, StandardErrorEncoding = Encoding.UTF8, StandardOutputEncoding = Encoding.UTF8, }; using (var process = Process.Start(start)) { string output = process.StandardOutput.ReadToEnd(); string error = process.StandardError.ReadToEnd(); // 获取错误信息 process.WaitForExit(); Debug.Log($"Exit Code: {process.ExitCode}"); // 打印退出码 Debug.Log($"Output: {output}"); Debug.Log($"Error: {error}"); // 打印错误信息 } } public void GetExcelData() { var pythonScript = $"../Tools/PrintExcelString.py"; ProcessStartInfo start = new ProcessStartInfo { FileName = "python", Arguments = $"\"{pythonScript}\"", RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true, StandardErrorEncoding = Encoding.UTF8, StandardOutputEncoding = Encoding.UTF8, }; using (var process = Process.Start(start)) { string output = process.StandardOutput.ReadToEnd(); string error = process.StandardError.ReadToEnd(); // 获取错误信息 process.WaitForExit(); Debug.Log($"Exit Code: {process.ExitCode}"); // 打印退出码 Debug.Log($"Output: {output}"); Debug.Log($"Error: {error}"); // 打印错误信息 } } 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/Resources/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; } } }