496 lines
17 KiB
C#
496 lines
17 KiB
C#
/*
|
||
* @Author: 白哉
|
||
* @Description:
|
||
* @Date: 2025年05月26日 星期一 11:05:13
|
||
* @Modify:
|
||
*/
|
||
|
||
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.Text.RegularExpressions;
|
||
using Logic.CrashSight;
|
||
using TMPro;
|
||
using Unity.VisualScripting;
|
||
using UnityEngine;
|
||
|
||
|
||
namespace Logic.Multilingual
|
||
{
|
||
public enum MultilingualType
|
||
{
|
||
None,
|
||
ZH, // 简中
|
||
TDZH, // 繁中
|
||
EN, // 英文
|
||
JP, // 日语
|
||
KR, // 韩语
|
||
RU, // 俄语
|
||
ES, // 西班牙语
|
||
PT, // 葡萄牙语
|
||
FR, // 法语
|
||
Max,
|
||
}
|
||
|
||
|
||
public class MultilingualData : ScriptableObject
|
||
{
|
||
public List<MultilingualFontGroup> FontGroups = new List<MultilingualFontGroup>();
|
||
public List<MultilingualItem> Items = new List<MultilingualItem>();
|
||
public List<MultilingualType> TargetTypes = new List<MultilingualType>();
|
||
private Dictionary<uint, MultilingualItem> _itemDict;
|
||
public Dictionary<uint, MultilingualItem> ItemDict => _itemDict;
|
||
|
||
|
||
public string GetMultilingualStr(uint id, MultilingualType type)
|
||
{
|
||
if (_itemDict == null) RefreshDict();
|
||
if (_itemDict == null) return string.Empty;
|
||
if (!_itemDict.TryGetValue(id, out var item)) return string.Empty;
|
||
|
||
var ret = type switch
|
||
{
|
||
MultilingualType.ZH => item.ZH,
|
||
MultilingualType.TDZH => item.TDZH,
|
||
MultilingualType.EN => item.EN,
|
||
MultilingualType.JP => item.JP,
|
||
MultilingualType.KR => item.KR,
|
||
MultilingualType.RU => item.RU,
|
||
MultilingualType.ES => item.ES,
|
||
MultilingualType.PT => item.PT,
|
||
MultilingualType.FR => item.FR,
|
||
_ => string.Empty,
|
||
};
|
||
|
||
// TODO 临时换色
|
||
ret = ret.Replace("<color=yellow>", "<color=orange>");
|
||
// TODO 临时转义换色
|
||
ret = ret.Replace("**<", "<color=orange>");
|
||
ret = ret.Replace(">**", "</color>");
|
||
|
||
ret = ResolveEmbeddedStringsRunning(ret, type);
|
||
return ret;
|
||
}
|
||
|
||
public TMP_FontAsset GetMultilingualFont(uint fontId, MultilingualType type)
|
||
{
|
||
if (fontId == 0) return null;
|
||
foreach (var group in FontGroups)
|
||
{
|
||
if (group.FontID != fontId) continue;
|
||
return type switch
|
||
{
|
||
MultilingualType.ZH => group.ZHFont,
|
||
MultilingualType.TDZH => group.TDZHFont,
|
||
MultilingualType.EN => group.ENFont,
|
||
MultilingualType.JP => group.JPFont,
|
||
MultilingualType.KR => group.KRFont,
|
||
MultilingualType.RU => group.RUFont,
|
||
MultilingualType.ES => group.ESFont,
|
||
MultilingualType.PT => group.PTFont,
|
||
MultilingualType.FR => group.FRFont,
|
||
_ => null,
|
||
};
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
public uint GetFontGroupID(TMP_FontAsset font)
|
||
{
|
||
foreach (var group in FontGroups)
|
||
{
|
||
if (group.ZHFont == font) return group.FontID;
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
public void RefreshDict()
|
||
{
|
||
if (_itemDict == null) _itemDict = new Dictionary<uint, MultilingualItem>();
|
||
if (_itemDict.Count == Items.Count) return;
|
||
_itemDict.Clear();
|
||
foreach (var item in Items) _itemDict[item.ID] = item;
|
||
}
|
||
|
||
public MultilingualType GetSystemLanguageTargetMultilingual(MultilingualType type)
|
||
{
|
||
var index = (int)type - 1;
|
||
if (index >= TargetTypes.Count) return type;
|
||
return TargetTypes[index];
|
||
}
|
||
|
||
public string GetMultilingualStrEditor(uint id, MultilingualType type)
|
||
{
|
||
if (_itemDict == null) RefreshDict();
|
||
if (_itemDict == null) return string.Empty;
|
||
if (!_itemDict.TryGetValue(id, out var item)) return string.Empty;
|
||
|
||
var ret = type switch
|
||
{
|
||
MultilingualType.ZH => item.ZH,
|
||
MultilingualType.TDZH => item.TDZH,
|
||
MultilingualType.EN => item.EN,
|
||
MultilingualType.JP => item.JP,
|
||
MultilingualType.KR => item.KR,
|
||
MultilingualType.RU => item.RU,
|
||
MultilingualType.ES => item.ES,
|
||
MultilingualType.PT => item.PT,
|
||
MultilingualType.FR => item.FR,
|
||
_ => string.Empty,
|
||
};
|
||
|
||
ret = ResolveEmbeddedStrings(ret, type);
|
||
return ret;
|
||
}
|
||
|
||
// ID 转 String (运行时)
|
||
public string ResolveEmbeddedStringsRunning(string origin, MultilingualType type)
|
||
{
|
||
if (string.IsNullOrEmpty(origin)) return string.Empty;
|
||
|
||
var regex = new Regex(@"\[\[(?:\[(\d+)\])?(\d+)\]\]");
|
||
var result = regex.Replace(origin, m =>
|
||
{
|
||
var prefixGroup = m.Groups[1]; // [n] 中的 n
|
||
var idStr = m.Groups[2].Value; // id
|
||
|
||
RefreshDict();
|
||
if (!uint.TryParse(idStr, out var subId)) return m.Value;
|
||
if (_itemDict == null || !_itemDict.TryGetValue(subId, out var subItem)) return m.Value;
|
||
var str = subItem.GetStrByType(type);
|
||
|
||
// 检测嵌套:展开后的字符串不应再包含 [[...]]
|
||
if (Regex.IsMatch(str, @"\[\[.+?\]\]"))
|
||
{
|
||
LogSystem.LogError($"ResolveEmbeddedStrings: 检测到嵌套引用,ID: {subId},内容: {str}");
|
||
return m.Value;
|
||
}
|
||
|
||
return str;
|
||
});
|
||
|
||
return result;
|
||
}
|
||
|
||
// ID 转 String
|
||
public string ResolveEmbeddedStrings(string origin, MultilingualType type)
|
||
{
|
||
if (string.IsNullOrEmpty(origin)) return string.Empty;
|
||
|
||
var regex = new Regex(@"\[\[(?:\[(\d+)\])?(\d+)\]\]");
|
||
var result = regex.Replace(origin, m =>
|
||
{
|
||
var prefixGroup = m.Groups[1]; // [n] 中的 n
|
||
var idStr = m.Groups[2].Value; // id
|
||
|
||
RefreshDict();
|
||
if (!uint.TryParse(idStr, out var subId)) return m.Value;
|
||
if (_itemDict == null || !_itemDict.TryGetValue(subId, out var subItem)) return m.Value;
|
||
var str = subItem.GetStrByType(type);
|
||
|
||
// 检测嵌套:展开后的字符串不应再包含 [[...]]
|
||
if (Regex.IsMatch(str, @"\[\[.+?\]\]"))
|
||
{
|
||
LogSystem.LogError($"ResolveEmbeddedStrings: 检测到嵌套引用,ID: {subId},内容: {str}");
|
||
return m.Value;
|
||
}
|
||
|
||
if (prefixGroup.Success)
|
||
{
|
||
return $"[[[{prefixGroup.Value}]{str}]]";
|
||
}
|
||
else
|
||
{
|
||
return $"[[{str}]]";
|
||
}
|
||
});
|
||
|
||
return result;
|
||
}
|
||
|
||
// String 转 ID
|
||
public string UnResolveEmbeddedStrings(string origin, MultilingualType type)
|
||
{
|
||
if (string.IsNullOrEmpty(origin)) return string.Empty;
|
||
|
||
var regex = new Regex(@"\[\[(?:\[(\d+)\])?(.+?)\]\]");
|
||
var result = regex.Replace(origin, m =>
|
||
{
|
||
var prefixGroup = m.Groups[1]; // [n] 中的 n
|
||
var str = m.Groups[2].Value; // 内部字符串
|
||
|
||
// 已经是 id 则跳过
|
||
if (uint.TryParse(str, out _)) return m.Value;
|
||
|
||
// 检测嵌套:内部字符串不应再包含 [[...]]
|
||
if (Regex.IsMatch(str, @"\[\[.+?\]\]"))
|
||
{
|
||
LogSystem.LogError($"UnResolveEmbeddedStrings: 检测到嵌套引用,内容: {str}");
|
||
return m.Value;
|
||
}
|
||
|
||
RefreshDict();
|
||
// 在 _itemDict 中反查 id
|
||
uint id = 0;
|
||
bool found = false;
|
||
foreach (var kv in _itemDict)
|
||
{
|
||
if (kv.Value.GetStrByType(type) == str)
|
||
{
|
||
id = kv.Key;
|
||
found = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!found)
|
||
{
|
||
LogSystem.LogError($"UnResolveEmbeddedStrings: 找不到字符串对应的ID,字符串: origin:{origin} str:{str}");
|
||
return m.Value;
|
||
}
|
||
|
||
if (prefixGroup.Success)
|
||
{
|
||
return $"[[[{prefixGroup.Value}]{id}]]";
|
||
}
|
||
else
|
||
{
|
||
return $"[[{id}]]";
|
||
}
|
||
});
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将其他语言的嵌入字符串对齐到中文已转换好的ID格式
|
||
/// zhResolved: 中文已经通过 UnResolveEmbeddedStrings 转换后的字符串(包含 [[id]] 和 [[[n]id]])
|
||
/// otherOrigin: 其他语言的原始字符串(包含 [[str]] 和 [[[n]str]])
|
||
/// type: 当前语言类型(用于日志)
|
||
/// itemId: 当前item的ID(用于日志)
|
||
/// </summary>
|
||
public string AlignEmbeddedStringsToZH(string zhResolved, string otherOrigin, MultilingualType type,
|
||
uint itemId)
|
||
{
|
||
if (string.IsNullOrEmpty(otherOrigin)) return otherOrigin;
|
||
if (string.IsNullOrEmpty(zhResolved)) return otherOrigin;
|
||
|
||
// 解析中文已转换的嵌入引用
|
||
var zhRegex = new Regex(@"\[\[(?:\[(\d+)\])?(\d+)\]\]");
|
||
var zhMatches = zhRegex.Matches(zhResolved);
|
||
|
||
// 构建中文的 n->完整匹配 字典(带前缀n的)和 无前缀的有序列表
|
||
var zhPrefixDict = new Dictionary<string, string>(); // n -> [[[n]id]]
|
||
var zhNoPrefixList = new List<string>(); // [[id]] 有序列表
|
||
|
||
foreach (Match m in zhMatches)
|
||
{
|
||
var prefixGroup = m.Groups[1];
|
||
var idStr = m.Groups[2].Value;
|
||
|
||
if (prefixGroup.Success)
|
||
{
|
||
zhPrefixDict[prefixGroup.Value] = m.Value; // n -> [[[n]id]]
|
||
}
|
||
else
|
||
{
|
||
zhNoPrefixList.Add(m.Value); // [[id]]
|
||
}
|
||
}
|
||
|
||
// 解析其他语言的嵌入引用
|
||
var otherRegex = new Regex(@"\[\[(?:\[(\d+)\])?(.+?)\]\]");
|
||
var otherMatches = otherRegex.Matches(otherOrigin);
|
||
|
||
// 分类其他语言的匹配:带前缀n的 和 不带前缀的
|
||
var otherPrefixMatches = new List<Match>();
|
||
var otherNoPrefixMatches = new List<Match>();
|
||
|
||
foreach (Match m in otherMatches)
|
||
{
|
||
var prefixGroup = m.Groups[1];
|
||
var content = m.Groups[2].Value;
|
||
|
||
// 如果内容已经是纯数字(id),跳过
|
||
if (uint.TryParse(content, out _)) continue;
|
||
|
||
if (prefixGroup.Success)
|
||
{
|
||
otherPrefixMatches.Add(m);
|
||
}
|
||
else
|
||
{
|
||
otherNoPrefixMatches.Add(m);
|
||
}
|
||
}
|
||
|
||
// 从后往前替换,避免索引偏移
|
||
// 收集所有需要替换的操作
|
||
var replacements = new List<(int index, int length, string replacement)>();
|
||
|
||
// 1. 处理带前缀 [[[n]str]] 的情况:使用中文对应n的 [[[n]id]]
|
||
foreach (var m in otherPrefixMatches)
|
||
{
|
||
var n = m.Groups[1].Value;
|
||
if (zhPrefixDict.TryGetValue(n, out var zhReplacement))
|
||
{
|
||
replacements.Add((m.Index, m.Length, zhReplacement));
|
||
}
|
||
else
|
||
{
|
||
LogSystem.LogError(
|
||
$"AlignEmbeddedStringsToZH: 语言{type} ID:{itemId} 中文找不到前缀[{n}]对应的引用,删除此项: {m.Value}");
|
||
replacements.Add((m.Index, m.Length, string.Empty));
|
||
}
|
||
}
|
||
|
||
// 2. 处理不带前缀 [[str]] 的情况:按顺序对齐中文的 [[id]]
|
||
int zhCount = zhNoPrefixList.Count;
|
||
int otherCount = otherNoPrefixMatches.Count;
|
||
|
||
if (zhCount != otherCount)
|
||
{
|
||
LogSystem.LogError(
|
||
$"AlignEmbeddedStringsToZH: 语言{type} ID:{itemId} [[str]]数量不匹配,中文:{zhCount} 当前语言:{otherCount}");
|
||
}
|
||
|
||
for (int i = 0; i < otherNoPrefixMatches.Count; i++)
|
||
{
|
||
var m = otherNoPrefixMatches[i];
|
||
if (i < zhCount)
|
||
{
|
||
// 对齐中文的 [[id]]
|
||
replacements.Add((m.Index, m.Length, zhNoPrefixList[i]));
|
||
}
|
||
else
|
||
{
|
||
// 中文少,删除其他语言多余的 [[str]]
|
||
replacements.Add((m.Index, m.Length, string.Empty));
|
||
}
|
||
}
|
||
|
||
// 按索引从后往前排序并替换
|
||
replacements.Sort((a, b) => b.index.CompareTo(a.index));
|
||
|
||
var result = otherOrigin;
|
||
foreach (var (index, length, replacement) in replacements)
|
||
{
|
||
result = result.Substring(0, index) + replacement + result.Substring(index + length);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
}
|
||
|
||
|
||
[Serializable]
|
||
public class MultilingualItem
|
||
{
|
||
public uint ID;
|
||
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 bool IsProperNoun;
|
||
public bool IsDialogue;
|
||
public string DialogueSpeaker;
|
||
public bool IsDeprecated;
|
||
public bool IsCustom;
|
||
public bool IsSpecialTerm;
|
||
|
||
[NonSerialized]
|
||
public string Desc;
|
||
|
||
|
||
public MultilingualItem()
|
||
{
|
||
IsCustom = false;
|
||
}
|
||
|
||
public void Refresh()
|
||
{
|
||
ZH = ZH?.Replace("\r\n", "\n") ?? string.Empty;
|
||
TDZH = TDZH?.Replace("\r\n", "\n") ?? string.Empty;
|
||
EN = EN?.Replace("\r\n", "\n") ?? string.Empty;
|
||
JP = JP?.Replace("\r\n", "\n") ?? string.Empty;
|
||
KR = KR?.Replace("\r\n", "\n") ?? string.Empty;
|
||
RU = RU?.Replace("\r\n", "\n") ?? string.Empty;
|
||
ES = ES?.Replace("\r\n", "\n") ?? string.Empty;
|
||
PT = PT?.Replace("\r\n", "\n") ?? string.Empty;
|
||
FR = FR?.Replace("\r\n", "\n") ?? string.Empty;
|
||
DialogueSpeaker = DialogueSpeaker?.Replace("\r\n", "\n") ?? string.Empty;
|
||
Desc = Desc?.Replace("\r\n", "\n") ?? string.Empty;
|
||
}
|
||
|
||
public string GetStrByType(MultilingualType type)
|
||
{
|
||
return type switch
|
||
{
|
||
MultilingualType.ZH => ZH,
|
||
MultilingualType.TDZH => TDZH,
|
||
MultilingualType.EN => EN,
|
||
MultilingualType.JP => JP,
|
||
MultilingualType.KR => KR,
|
||
MultilingualType.RU => RU,
|
||
MultilingualType.ES => ES,
|
||
MultilingualType.PT => PT,
|
||
MultilingualType.FR => FR,
|
||
_ => string.Empty,
|
||
};
|
||
}
|
||
|
||
public bool IsTranslate(MultilingualType type)
|
||
{
|
||
if (type == MultilingualType.ZH) return true;
|
||
if (type == MultilingualType.None)
|
||
{
|
||
return !string.IsNullOrEmpty(TDZH) && !string.IsNullOrEmpty(EN) &&
|
||
!string.IsNullOrEmpty(JP) && !string.IsNullOrEmpty(KR) &&
|
||
!string.IsNullOrEmpty(RU) && !string.IsNullOrEmpty(ES) &&
|
||
!string.IsNullOrEmpty(PT) && !string.IsNullOrEmpty(FR);
|
||
}
|
||
|
||
return type switch
|
||
{
|
||
MultilingualType.TDZH => !string.IsNullOrEmpty(TDZH),
|
||
MultilingualType.EN => !string.IsNullOrEmpty(EN),
|
||
MultilingualType.JP => !string.IsNullOrEmpty(JP),
|
||
MultilingualType.KR => !string.IsNullOrEmpty(KR),
|
||
MultilingualType.RU => !string.IsNullOrEmpty(RU),
|
||
MultilingualType.ES => !string.IsNullOrEmpty(ES),
|
||
MultilingualType.PT => !string.IsNullOrEmpty(PT),
|
||
MultilingualType.FR => !string.IsNullOrEmpty(FR),
|
||
_ => false,
|
||
};
|
||
}
|
||
|
||
// 不分大小写将 True 或者 False 字符串转换为布尔值
|
||
public static bool ParseBoolStr(string str)
|
||
{
|
||
return string.Equals(str, "True", StringComparison.OrdinalIgnoreCase);
|
||
}
|
||
}
|
||
|
||
|
||
[Serializable]
|
||
public class MultilingualFontGroup
|
||
{
|
||
public uint FontID;
|
||
public TMP_FontAsset ZHFont;
|
||
public TMP_FontAsset TDZHFont;
|
||
public TMP_FontAsset ENFont;
|
||
public TMP_FontAsset JPFont;
|
||
public TMP_FontAsset KRFont;
|
||
public TMP_FontAsset RUFont;
|
||
public TMP_FontAsset ESFont;
|
||
public TMP_FontAsset PTFont;
|
||
public TMP_FontAsset FRFont;
|
||
}
|
||
} |