/* * @Author: 白哉 * @Description: * @Date: 2025年05月26日 星期一 14:05:31 * @Modify: */ using System; using System.Collections.Generic; using System.Linq; using Logic.CrashSight; using TMPro; using UnityEngine; using ConfigManager = TH1_Logic.Config.ConfigManager; namespace Logic.Multilingual { public class MultilingualManager { public static MultilingualManager Instance = new MultilingualManager(); public MultilingualType CurrentType => _currentType; private MultilingualData _multilingualData; private MultilingualType _currentType; private List _textComs; // 原始翻译快照:仅在游戏启动时拍照一次(apply mod 之前的纯净数据)。 // 之后所有 mod apply / 重 apply 都基于"先 restore 再 apply",避免覆盖叠加。 // key = MultilingualItem.ID, value = 各语种字段的原始值 private Dictionary _originalSnapshot; // 跟踪 export 数据是否已经被 mod 改写过:用于在切语言时判断是否需要重新 apply // (理论上只有 SaveAndApplyMods 会改写,所以这个标志只在那里翻转) private bool _modsApplied; public void Init() { RefreshMultilingualData(); SnapshotOriginalIfNeeded(); _currentType = ConfigManager.Instance.Config.MultilingualType; if (_currentType == MultilingualType.None) _currentType = GetSystemLanguage(); // 老存档迁移:第一次进入新版本时,把所有已安装 mod 按 targetLanguage 挂入 ModLanguageConfigs // (旧版本是"全自动 apply"模式,没有 ModLanguageConfigs;不迁移会导致老玩家 mod 失效) MigrateLegacyModConfigIfNeeded(); ApplyWorkshopMods(); ChangedMultilingual(_currentType); } // 一次性迁移:仅在 ModConfigMigrated == false 时执行 private void MigrateLegacyModConfigIfNeeded() { var config = ConfigManager.Instance.Config; if (config.ModConfigMigrated) return; try { var paths = new List(WorkshopModLoader.GetLocalModPaths()); paths.AddRange(WorkshopModLoader.GetSubscribedModPaths()); int migrated = 0; foreach (var folder in paths) { var info = WorkshopModExporter.ReadModInfo(folder); if (info == null) continue; if (!Enum.TryParse(info.targetLanguage, true, out var lang)) continue; if (lang == MultilingualType.None || lang == MultilingualType.Max) continue; config.AddModToLanguage(lang, folder); migrated++; } LogSystem.LogInfo($"[MultilingualManager] 老存档 Mod 配置迁移完成,挂载 {migrated} 个 mod"); } catch (Exception e) { LogSystem.LogError($"[MultilingualManager] 老存档 Mod 配置迁移失败: {e.Message}"); } finally { // 即使部分失败也置 true,避免每次启动都重试导致重复挂载 config.ModConfigMigrated = true; } } /// /// 加载并应用所有创意工坊多语言 Mod,按 Config.ModLanguageConfigs 的顺序。 /// 每次都先把 MultilingualData 还原到原始快照再 apply,避免重复叠加。 /// public void ApplyWorkshopMods() { RefreshMultilingualData(); if (_multilingualData == null) return; SnapshotOriginalIfNeeded(); RestoreOriginal(); WorkshopModLoader.ApplyModsWithConfig(_multilingualData, ConfigManager.Instance.Config.ModLanguageConfigs); _modsApplied = true; } /// /// 玩家在 UI 里点击"保存并应用"调用:还原快照 → 按 Config 重 apply → 触发 TMP 重绘 /// public void SaveAndApplyMods() { ApplyWorkshopMods(); ChangedMultilingual(_currentType); } // 仅在第一次(snapshot 还没建立时)拍照一次,避免重复触发或 apply 后再拍导致快照失真 private void SnapshotOriginalIfNeeded() { if (_originalSnapshot != null) return; if (_multilingualData == null) return; _multilingualData.RefreshDict(); if (_multilingualData.ItemDict == null) return; _originalSnapshot = new Dictionary(_multilingualData.ItemDict.Count); foreach (var kv in _multilingualData.ItemDict) { _originalSnapshot[kv.Key] = OriginalLangFields.From(kv.Value); } } // 把所有语种字段还原到原始值(仅 mod 会覆盖的字段) private void RestoreOriginal() { if (_originalSnapshot == null || _multilingualData == null) return; _multilingualData.RefreshDict(); if (_multilingualData.ItemDict == null) return; foreach (var kv in _originalSnapshot) { if (!_multilingualData.ItemDict.TryGetValue(kv.Key, out var item)) continue; kv.Value.RestoreTo(item); } _modsApplied = false; } #if UNITY_EDITOR public void SetMultilingualType(MultilingualType type) { _currentType = type; } #endif public string GetMultilingualText(uint id) { if (!RefreshMultilingualData()) return string.Empty; return _multilingualData.GetMultilingualStr(id, _currentType); } /// /// 按指定语言取多语言字符串(无视CurrentType)。用于"无论什么语言都要用XX语言显示"的字段,如原曲名只显示日文/中文原版。 /// public string GetMultilingualText(uint id, MultilingualType type) { if (!RefreshMultilingualData()) return string.Empty; return _multilingualData.GetMultilingualStr(id, type); } /// /// 按指定语言取多语言字符串的安全版(传入数字ID字符串)。 /// public string GetMultilingualTextSafe(string idString, MultilingualType type) { if (string.IsNullOrEmpty(idString)) return ""; if (uint.TryParse(idString, out uint id)) return GetMultilingualText(id, type); return idString; } public string GetMultilingualTextSafe(string idString) { if (string.IsNullOrEmpty(idString)) { LogSystem.LogError($"多语言ID为空"); return ""; } if (uint.TryParse(idString, out uint id)) { return GetMultilingualText(id); } LogSystem.LogError($"无法解析多语言ID: {idString}"); return idString; // 返回原始字符串作为fallback } public List GetMultilingualSubIdList(uint id) { if (!RefreshMultilingualData()) return new List(); return _multilingualData.GetSubStringIdRunning(id, _currentType); } public List GetMultilingualSubIdListSafe(string idString) { var result = new List(); if (string.IsNullOrEmpty(idString)) { LogSystem.LogError($"多语言ID为空"); return result; } if (uint.TryParse(idString, out uint id)) { return GetMultilingualSubIdList(id); } LogSystem.LogError($"无法解析多语言ID: {idString}"); return result; } public string GetMultilingualText(string str) { if (!uint.TryParse(str, out var id)) return ""; return GetMultilingualText(id); } public TMP_FontAsset GetMultilingualFont(uint fontId) { if (!RefreshMultilingualData()) return null; return _multilingualData.GetMultilingualFont(fontId, _currentType); } public void ChangedMultilingual(MultilingualType type) { _currentType = type; if (!RefreshMultilingualData()) { if (ConfigManager.Instance.Config != null) ConfigManager.Instance.Config.MultilingualType = _currentType; return; } RefreshTextComs(); foreach (var textCom in _textComs) textCom.OnMultilingualChanged(); ConfigManager.Instance.Config.MultilingualType = _currentType; } public uint GetFontGroupID(TMP_FontAsset font) { if (!RefreshMultilingualData()) return 0; return _multilingualData.GetFontGroupID(font); } // 这里的 ID 是 ID 字符串,paramList 是实际的字符串 public void SetUIText(TextMeshProUGUI textCom, string id, List paramList=null) { if (!textCom) return; var multilingual = textCom.gameObject.GetComponent(); if (!multilingual) multilingual = textCom.gameObject.AddComponent(); bool isNumeric = uint.TryParse(id, out uint realId); // 仅整数 if (!isNumeric) return; multilingual.ID = realId; multilingual.ParamList = paramList; multilingual.OnMultilingualChanged(); } // paramList 是实际的字符串 public void SetUIText(TextMeshProUGUI textCom, List paramList=null) { if (!textCom) return; var multilingual = textCom.gameObject.GetComponent(); if (!multilingual) return; multilingual.ParamList = paramList; multilingual.OnMultilingualChanged(); } private bool RefreshMultilingualData() { if (_multilingualData) return true; #if UNITY_EDITOR if (Application.isPlaying && !TH1Resource.ResourceManager.IsInitialized) return false; #else if (!TH1Resource.ResourceManager.IsInitialized) return false; #endif _multilingualData = TH1Resource.ResourceLoader.Load("Export/Multilingual"); return _multilingualData != null; } private void RefreshTextComs() { _textComs = Resources.FindObjectsOfTypeAll() .Where(textCom => textCom && textCom.gameObject.scene.IsValid()) .ToList(); } // 这里仅考虑了 PC public MultilingualType GetSystemLanguage() { // 获取系统语言 var systemLanguage = Application.systemLanguage; // 根据系统语言切换对应的多语言类型 var systemType = systemLanguage switch { SystemLanguage.Chinese => MultilingualType.ZH, SystemLanguage.ChineseSimplified => MultilingualType.ZH, SystemLanguage.ChineseTraditional => MultilingualType.TDZH, SystemLanguage.Japanese => MultilingualType.JP, SystemLanguage.Korean => MultilingualType.KR, SystemLanguage.Russian => MultilingualType.RU, SystemLanguage.Spanish => MultilingualType.ES, SystemLanguage.Portuguese => MultilingualType.PT, SystemLanguage.French => MultilingualType.FR, _ => MultilingualType.EN // 其他语言默认使用英语 }; return RefreshMultilingualData() ? _multilingualData.GetSystemLanguageTargetMultilingual(systemType) : systemType; } public MultilingualType GetCurLanguage() { return ConfigManager.Instance.Config.MultilingualType; } public MultilingualType GetLanguageByString(string str) { // 默认返回英语 if (string.IsNullOrEmpty(str)) return MultilingualType.EN; // 按照日 韩 繁体 中 英的顺序检测 // 检测日语(平假名、片假名、日文汉字范围) if (str.Any(c => (c >= '\u3040' && c <= '\u309F') || // 平假名 (c >= '\u30A0' && c <= '\u30FF'))) // 片假名 { return MultilingualType.JP; } // 检测韩语(韩文音节) if (str.Any(c => c >= '\uAC00' && c <= '\uD7AF')) { return MultilingualType.KR; } // 检测繁体中文(CJK兼容汉字、康熙部首、扩展区) if (str.Any(c => (c >= '\uF900' && c <= '\uFAFF') || // CJK兼容汉字 (c >= '\u2F00' && c <= '\u2FDF') || // 康熙部首 (c >= '\u3400' && c <= '\u4DBF'))) // CJK扩展A { return MultilingualType.TDZH; } // 检测简体中文(CJK统一汉字基本区) if (str.Any(c => c >= '\u4E00' && c <= '\u9FFF')) { return MultilingualType.ZH; } // 检测俄语(西里尔字母) if (str.Any(c => c >= '\u0400' && c <= '\u04FF')) { return MultilingualType.RU; } // 检测法语特有重音字符(如 œ, æ, ç 等) if (str.Any(c => c == '\u0153' || c == '\u0152' || // œ Œ c == '\u00E6' || c == '\u00C6' || // æ Æ c == '\u00E7' || c == '\u00C7')) // ç Ç { return MultilingualType.FR; } // 检测葡萄牙语特有重音字符(如 ã, õ, â, ê, ô 等) if (str.Any(c => c == '\u00E3' || c == '\u00C3' || // ã à c == '\u00F5' || c == '\u00D5')) // õ Õ { return MultilingualType.PT; } // 检测西班牙语特有字符(如 ñ, ¡, ¿ 等) if (str.Any(c => c == '\u00F1' || c == '\u00D1' || // ñ Ñ c == '\u00A1' || c == '\u00BF')) // ¡ ¿ { return MultilingualType.ES; } // 默认返回英语 return MultilingualType.EN; } } /// /// 原始翻译快照行:仅保存 mod 会覆盖的字段(与 WorkshopModLoader.SetItemStr 列表对齐) /// internal struct OriginalLangFields { public string ZH; public string TDZH; public string EN; public string JP; public string KR; public string RU; public string ES; public string PT; public string FR; public string DE; public string IDN; // 印尼语 (对应 enum.ID) public string TH; public string PL; public string VI; public string MS; public string UK; public string KZ; public string TR; public string IT; public string NL; public string FI; public string SV; public string NO; public string CS; public string HU; public string EL; public string RO; public string ET; public string LT; public string HR; public string SR; public string SL; public string SK; public string BE; public string HE; public string BG; public string UZ; public string KY; public string MN; public string AR; public string DA; public string TL; public string Custom; public static OriginalLangFields From(MultilingualItem item) { return new OriginalLangFields { ZH = item.ZH, TDZH = item.TDZH, EN = item.EN, JP = item.JP, KR = item.KR, RU = item.RU, ES = item.ES, PT = item.PT, FR = item.FR, DE = item.DE, IDN = item.IDN, TH = item.TH, PL = item.PL, VI = item.VI, MS = item.MS, UK = item.UK, KZ = item.KZ, TR = item.TR, IT = item.IT, NL = item.NL, FI = item.FI, SV = item.SV, NO = item.NO, CS = item.CS, HU = item.HU, EL = item.EL, RO = item.RO, ET = item.ET, LT = item.LT, HR = item.HR, SR = item.SR, SL = item.SL, SK = item.SK, BE = item.BE, HE = item.HE, BG = item.BG, UZ = item.UZ, KY = item.KY, MN = item.MN, AR = item.AR, DA = item.DA, TL = item.TL, Custom = item.Custom, }; } public void RestoreTo(MultilingualItem item) { item.ZH = ZH; item.TDZH = TDZH; item.EN = EN; item.JP = JP; item.KR = KR; item.RU = RU; item.ES = ES; item.PT = PT; item.FR = FR; item.DE = DE; item.IDN = IDN; item.TH = TH; item.PL = PL; item.VI = VI; item.MS = MS; item.UK = UK; item.KZ = KZ; item.TR = TR; item.IT = IT; item.NL = NL; item.FI = FI; item.SV = SV; item.NO = NO; item.CS = CS; item.HU = HU; item.EL = EL; item.RO = RO; item.ET = ET; item.LT = LT; item.HR = HR; item.SR = SR; item.SL = SL; item.SK = SK; item.BE = BE; item.HE = HE; item.BG = BG; item.UZ = UZ; item.KY = KY; item.MN = MN; item.AR = AR; item.DA = DA; item.TL = TL; item.Custom = Custom; } } }