534 lines
19 KiB
C#
534 lines
19 KiB
C#
/*
|
||
* @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;
|
||
}
|
||
}
|
||
}
|