TH1/Unity/Assets/Scripts/TH1_Logic/Editor/MultilingualEditorWindow.cs
2026-05-17 22:08:17 +08:00

1827 lines
77 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

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

/*
* @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<string, uint> _zhStrDict = new Dictionary<string, uint>();
private Dictionary<uint, bool> _isProperNounDict = new Dictionary<uint, bool>();
private Dictionary<uint, bool> _isDialogueDict = new Dictionary<uint, bool>();
private Dictionary<uint, bool> _isDeprecatedDict = new Dictionary<uint, bool>();
private Dictionary<uint, string> _speakerDict = new Dictionary<uint, string>();
private HashSet<uint> _activeSet = new HashSet<uint>();
private HashSet<uint> _specialTermSet = new HashSet<uint>();
private Dictionary<uint, string> _descDict = new Dictionary<uint, string>();
private uint _idIndex;
private int _showIndex = 0;
private List<TMP_FontAsset> _assets = new List<TMP_FontAsset>();
private HashSet<char> characterSet = new HashSet<char>();
private HashSet<char> gameSet = new HashSet<char>();
private List<char> characterList = new List<char>();
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<MultilingualEditorWindow>();
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<MultilingualData>(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<Canvas>()?.gameObject;
if (!uiObj)
{
var sceneUICanvas = GameObject.Find("UICanvas");
uiObj = sceneUICanvas;
}
if (uiObj)
{
var regex = new Regex(@"\*\*<(.+?)>\*\*");
var coms = uiObj.GetComponentsInChildren<TextMeshProUGUI>(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<MultilingualTextMono>();
if (!textCom) textCom = com.gameObject.AddComponent<MultilingualTextMono>();
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<GameObject>();
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<GameObject>(prefabAssetPath);
if (!prefab) continue;
var prefabComs = prefab.GetComponentsInChildren<TextMeshProUGUI>(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<MultilingualTextMono>();
if (!textCom) textCom = com.gameObject.AddComponent<MultilingualTextMono>();
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<ScriptableObject>(assetPath);
if (!asset) continue;
ScriptableObject newAsset = Object.Instantiate(asset);
var regex = new Regex(@"\*\*<(.+?)>\*\*");
TraverseObject(newAsset, regex);
SaveExportAsset(asset.name, newAsset);
}
ExportMatchLevelData();
_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;
// 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;
}
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<MultilingualData>(path);
if (!_asset)
{
_asset = CreateInstance<MultilingualData>();
AssetDatabase.CreateAsset(_asset, path);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
}
}
if (_assets.Count == 0)
{
var pathList = new List<string>();
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<TMP_FontAsset>(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<MultilingualTextMono>(true).ToList();
foreach (var com in coms)
{
var textCom = com.gameObject.GetComponent<TextMeshProUGUI>();
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<GameObject>();
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<GameObject>(prefabAssetPath);
if (!prefab) continue;
var prefabComs = prefab.GetComponentsInChildren<MultilingualTextMono>(true).ToList();
foreach (var com in prefabComs)
{
var textCom = com.gameObject.GetComponent<TextMeshProUGUI>();
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<MultilingualTextMono>(true).ToList();
foreach (var com in coms)
{
com.FontBan = com.Ban;
}
EditorSceneManager.MarkSceneDirty(uiObj.scene);
// 先处理 Assets/Resources/Prefab 路径下的所有 prefab 并保存
var prefabList = new List<GameObject>();
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<GameObject>(prefabAssetPath);
if (!prefab) continue;
var prefabComs = prefab.GetComponentsInChildren<MultilingualTextMono>(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<MultilingualTextMono>(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<TextMeshProUGUI>();
textCom?.ForceMeshUpdate();
}
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
if (InspectorUtils.InspectorButtonWithTextWidth("添加字体组"))
{
_asset.FontGroups.Add(new MultilingualFontGroup());
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
_isTransformStr = EditorGUILayout.Toggle("是否批量替换 <color=***>", _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($"<b>系统语言{type} 对应游戏语言: </b>");
_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<MultilingualFontGroup>();
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($"<b>ID : {fontGroup.FontID}</b>");
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($"<b>{item.ID} : </b>");
InspectorUtils.InspectorTextWidthRich($" <b>中文:</b> {item.ZH}");
if (!string.IsNullOrEmpty(item.TDZH))
InspectorUtils.InspectorTextWidthRich($" <b>繁中:</b> {item.TDZH}");
if (!string.IsNullOrEmpty(item.EN))
InspectorUtils.InspectorTextWidthRich($" <b>英语:</b> {item.EN}");
if (!string.IsNullOrEmpty(item.JP))
InspectorUtils.InspectorTextWidthRich($" <b>日语:</b> {item.JP}");
if (!string.IsNullOrEmpty(item.KR))
InspectorUtils.InspectorTextWidthRich($" <b>韩语:</b> {item.KR}");
if (!string.IsNullOrEmpty(item.RU))
InspectorUtils.InspectorTextWidthRich($" <b>俄语:</b> {item.RU}");
if (!string.IsNullOrEmpty(item.ES))
InspectorUtils.InspectorTextWidthRich($" <b>西班牙语:</b> {item.ES}");
if (!string.IsNullOrEmpty(item.PT))
InspectorUtils.InspectorTextWidthRich($" <b>葡萄牙语:</b> {item.PT}");
if (!string.IsNullOrEmpty(item.FR))
InspectorUtils.InspectorTextWidthRich($" <b>法语:</b> {item.FR}");
var unicode = "";
foreach (var c in item.ZH) unicode += $"{(int)c:X4} ";
InspectorUtils.InspectorTextWidthRich($" <b>Unicode</b> {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<MultilingualItem>();
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;
// 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;
}
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)
{
if (!item.IsActive) continue;
// 中文先转换 **<str>** -> **<id>**
item.ZH = _asset.UnResolveEmbeddedStrings(item.ZH, MultilingualType.ZH);
// 其他语言对齐中文的 **<id>**
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();
_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 = GameObject.Find("UICanvas");
if (!uiObj)
{
Debug.LogError($"找不到UI根节点");
return;
}
// 匹配 **<...>**
var regex = new Regex(@"\*\*<(.+?)>\*\*");
var coms = uiObj.GetComponentsInChildren<TextMeshProUGUI>(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<MultilingualTextMono>();
if (!textCom) textCom = com.gameObject.AddComponent<MultilingualTextMono>();
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<GameObject>();
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<GameObject>(prefabAssetPath);
if (!prefab) continue;
var prefabComs = prefab.GetComponentsInChildren<TextMeshProUGUI>(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<MultilingualTextMono>();
if (!textCom) textCom = com.gameObject.AddComponent<MultilingualTextMono>();
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<ScriptableObject>(assetPath);
if (!asset) continue;
if (_isTransformStr)
{
TransformObject(asset);
EditorUtility.SetDirty(asset);
}
ScriptableObject newAsset = Object.Instantiate(asset);
TraverseObject(newAsset, regex);
SaveExportAsset(asset.name, newAsset);
}
ExportMatchLevelData();
var newStr = new Dictionary<string, uint>();
var replaceStr = new Dictionary<string, string>();
// 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] 或历史格式 [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);
if (prefix != null)
{
var canonical = $"**<![{prefix}]{strId}>**";
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 = $"**<![{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<ScriptableObject>(targetPath) != null)
{
AssetDatabase.DeleteAsset(targetPath);
}
// 保存新实例
AssetDatabase.CreateAsset(target, targetPath);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
}
private void ExportMatchLevelData()
{
const string sourcePath = "Assets/Resources/MatchLevelData/LevelData.bytes";
const string targetPath = "Assets/Resources/MatchLevelData/ExportLevelData.bytes";
if (!File.Exists(sourcePath))
{
Debug.LogWarning($"ExportMatchLevelData: 未找到关卡配置资源 {sourcePath}");
return;
}
try
{
var bytes = File.ReadAllBytes(sourcePath);
var levelData = MemoryPackSerializer.Deserialize<MatchLevelData>(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<MultilingualFieldAttribute>();
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;
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<string> 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]] = $"{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<MultilingualFieldAttribute>();
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<string> 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()
{
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<MultilingualData>(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;
// 正则表达式匹配 <color=yellow>内容</color> 或 <color=#FFFF00>内容</color>
// (?i) 忽略大小写
// 匹配 <color=yellow> 开始,然后匹配任意不包含 < 的字符(确保不嵌套),最后匹配 </color>
string pattern = @"(?i)<color=(?:yellow|#FFFF00)>([^<]*)</color>";
// 使用正则表达式进行替换
// 匹配到的内容保存在分组 1 中,即 $1
// 替换为 **<$1>**
string result = Regex.Replace(input, pattern, "**<$1>**");
// 处理特殊情况:如果正则表达式内部允许包含 <,说明可能存在嵌套
// 但是上面的 [^<]* 已经排除了内部包含 < 的情况,所以不会处理嵌套。
// 例如:<color=yellow>文本<color=red>红</color></color>
// 内部有 <color=red>[^<]* 无法匹配,因此整个表达式不会匹配,也就不会替换。
return result;
}
}
}