TH1/Unity/Assets/Scripts/TH1_Logic/Multilingual/MultilingualManager.cs
2026-06-12 16:02:05 +08:00

534 lines
19 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日 星期一 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<MultilingualTextMono> _textComs;
// 原始翻译快照仅在游戏启动时拍照一次apply mod 之前的纯净数据)。
// 之后所有 mod apply / 重 apply 都基于"先 restore 再 apply",避免覆盖叠加。
// key = MultilingualItem.ID, value = 各语种字段的原始值
private Dictionary<uint, OriginalLangFields> _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<string>(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<MultilingualType>(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;
}
}
/// <summary>
/// 加载并应用所有创意工坊多语言 Mod按 Config.ModLanguageConfigs 的顺序。
/// 每次都先把 MultilingualData 还原到原始快照再 apply避免重复叠加。
/// </summary>
public void ApplyWorkshopMods()
{
RefreshMultilingualData();
if (_multilingualData == null) return;
SnapshotOriginalIfNeeded();
RestoreOriginal();
WorkshopModLoader.ApplyModsWithConfig(_multilingualData, ConfigManager.Instance.Config.ModLanguageConfigs);
_modsApplied = true;
}
/// <summary>
/// 玩家在 UI 里点击"保存并应用"调用:还原快照 → 按 Config 重 apply → 触发 TMP 重绘
/// </summary>
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<uint, OriginalLangFields>(_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);
}
/// <summary>
/// 按指定语言取多语言字符串(无视CurrentType)。用于"无论什么语言都要用XX语言显示"的字段,如原曲名只显示日文/中文原版。
/// </summary>
public string GetMultilingualText(uint id, MultilingualType type)
{
if (!RefreshMultilingualData()) return string.Empty;
return _multilingualData.GetMultilingualStr(id, type);
}
/// <summary>
/// 按指定语言取多语言字符串的安全版(传入数字ID字符串)。
/// </summary>
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<uint> GetMultilingualSubIdList(uint id)
{
if (!RefreshMultilingualData()) return new List<uint>();
return _multilingualData.GetSubStringIdRunning(id, _currentType);
}
public List<uint> GetMultilingualSubIdListSafe(string idString)
{
var result = new List<uint>();
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<string> paramList=null)
{
if (!textCom) return;
var multilingual = textCom.gameObject.GetComponent<MultilingualTextMono>();
if (!multilingual) multilingual = textCom.gameObject.AddComponent<MultilingualTextMono>();
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<string> paramList=null)
{
if (!textCom) return;
var multilingual = textCom.gameObject.GetComponent<MultilingualTextMono>();
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<MultilingualData>("Export/Multilingual");
return _multilingualData != null;
}
private void RefreshTextComs()
{
_textComs = Resources.FindObjectsOfTypeAll<MultilingualTextMono>()
.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;
}
}
/// <summary>
/// 原始翻译快照行:仅保存 mod 会覆盖的字段(与 WorkshopModLoader.SetItemStr 列表对齐)
/// </summary>
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;
}
}
}