using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using BepInEx;
using HarmonyLib;
using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: AssemblyCopyright("Copyright (c) 2026 Loki Moon Labs")]
[assembly: CompilationRelaxations(8)]
[assembly: AssemblyTitle("Chinese to English Mod Translator")]
[assembly: AssemblyDescription("Offline English display translations for Dyson Sphere Program mods.")]
[assembly: AssemblyCompany("Loki Moon Labs")]
[assembly: AssemblyProduct("Chinese to English Mod Translator")]
[assembly: AssemblyVersion("1.0.0.0")]
namespace ChineseToEnglishModTranslator;
public static class InstalledTranslations
{
private sealed class Entry
{
public string Source;
public string Mode;
public string English;
}
private static IEnumerable<TypeDefinition> Types(IEnumerable<TypeDefinition> types)
{
foreach (TypeDefinition type in types)
{
yield return type;
foreach (TypeDefinition item in Types((IEnumerable<TypeDefinition>)type.NestedTypes))
{
yield return item;
}
}
}
private static string Fingerprint(string value, SHA256 sha)
{
return BitConverter.ToString(sha.ComputeHash(Encoding.UTF8.GetBytes(value))).Replace("-", "").ToLowerInvariant();
}
private static string Escape(string value)
{
return value.Replace("\\", "\\\\").Replace("\t", "\\t").Replace("\r", "\\r")
.Replace("\n", "\\n");
}
private static Dictionary<string, List<Entry>> ReadIndex()
{
Dictionary<string, List<Entry>> dictionary = new Dictionary<string, List<Entry>>(StringComparer.OrdinalIgnoreCase);
using Stream stream = typeof(InstalledTranslations).Assembly.GetManifestResourceStream("ChineseToEnglishModTranslator.installed-translations.tsv");
if (stream == null)
{
throw new InvalidDataException("Installed-mod translation index is missing.");
}
using StreamReader streamReader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
string text;
while ((text = streamReader.ReadLine()) != null)
{
if (text.Length != 0 && text[0] != '#')
{
string[] array = text.Split(new char[1] { '\t' });
if (array.Length != 4 || Path.GetFileName(array[0]) != array[0] || array[1].Length != 64 || (array[2] != "hash" && array[2] != "literal"))
{
throw new InvalidDataException("Invalid installed-mod translation index.");
}
if (!dictionary.TryGetValue(array[0], out var value))
{
value = new List<Entry>();
dictionary.Add(array[0], value);
}
value.Add(new Entry
{
Source = array[1],
Mode = array[2],
English = array[3]
});
}
}
return dictionary;
}
public static void Merge(IDictionary<string, string> entries, string pluginsPath, Action<string> warn)
{
//IL_0142: Unknown result type (might be due to invalid IL or missing references)
//IL_0149: Expected O, but got Unknown
//IL_0187: Unknown result type (might be due to invalid IL or missing references)
//IL_018c: Unknown result type (might be due to invalid IL or missing references)
//IL_01b1: Unknown result type (might be due to invalid IL or missing references)
//IL_01b6: Unknown result type (might be due to invalid IL or missing references)
if (string.IsNullOrEmpty(pluginsPath) || !Directory.Exists(pluginsPath))
{
return;
}
Dictionary<string, List<Entry>> dictionary = ReadIndex();
Dictionary<string, string> dictionary2 = new Dictionary<string, string>(StringComparer.Ordinal);
HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal);
string[] files;
try
{
files = Directory.GetFiles(pluginsPath, "*.dll", SearchOption.AllDirectories);
}
catch (Exception ex)
{
warn?.Invoke("Installed translations could not be inspected; bundled entries remain available. " + ex.Message);
return;
}
foreach (string item in files.OrderBy<string, string>((string result) => result, StringComparer.OrdinalIgnoreCase))
{
if (!dictionary.TryGetValue(Path.GetFileName(item), out var value))
{
continue;
}
try
{
HashSet<string> hashSet2 = new HashSet<string>(value.Select((Entry e) => e.Source), StringComparer.Ordinal);
foreach (Entry item2 in value)
{
if (item2.Mode == "hash")
{
hashSet2.Add(item2.English);
}
}
Dictionary<string, string> dictionary3 = new Dictionary<string, string>(StringComparer.Ordinal);
using (SHA256 sha = SHA256.Create())
{
ReaderParameters val = new ReaderParameters();
val.ReadSymbols = false;
AssemblyDefinition val2 = AssemblyDefinition.ReadAssembly(item, val);
try
{
foreach (TypeDefinition item3 in Types((IEnumerable<TypeDefinition>)val2.MainModule.Types))
{
Enumerator<MethodDefinition> enumerator4 = item3.Methods.GetEnumerator();
try
{
while (enumerator4.MoveNext())
{
MethodDefinition current4 = enumerator4.Current;
if (!current4.HasBody)
{
continue;
}
Enumerator<Instruction> enumerator5 = current4.Body.Instructions.GetEnumerator();
try
{
while (enumerator5.MoveNext())
{
Instruction current5 = enumerator5.Current;
string text = current5.Operand as string;
if (!string.IsNullOrEmpty(text) && text.Length <= 8192)
{
string text2 = Fingerprint(text, sha);
if (hashSet2.Contains(text2))
{
dictionary3[text2] = text;
}
}
}
}
finally
{
((IDisposable)enumerator5/*cast due to .constrained prefix*/).Dispose();
}
}
}
finally
{
((IDisposable)enumerator4/*cast due to .constrained prefix*/).Dispose();
}
}
}
finally
{
((IDisposable)val2)?.Dispose();
}
}
foreach (Entry item4 in value)
{
if (!dictionary3.TryGetValue(item4.Source, out var value2))
{
continue;
}
string value3;
if (item4.Mode == "hash")
{
if (!dictionary3.TryGetValue(item4.English, out value3))
{
continue;
}
value3 = Escape(value3);
}
else
{
value3 = item4.English;
}
string s = Escape(value2) + "\t" + value3 + "\n";
Dictionary<string, string> dictionary4 = TranslationCatalog.ReadMappings(new StringReader(s), null, warn);
if (dictionary4.TryGetValue(value2, out var value4) && !hashSet.Contains(value2))
{
if (dictionary2.TryGetValue(value2, out var value5) && value5 != value4)
{
dictionary2.Remove(value2);
hashSet.Add(value2);
}
else
{
dictionary2[value2] = value4;
}
}
}
}
catch (Exception ex2)
{
warn?.Invoke("Could not read local translations from " + Path.GetFileName(item) + "; other translations remain available. " + ex2.Message);
}
}
foreach (KeyValuePair<string, string> item5 in dictionary2)
{
if (!entries.TryGetValue(item5.Key, out var _))
{
entries[item5.Key] = item5.Value;
}
}
}
}
[BepInProcess("DSPGAME.exe")]
[BepInIncompatibility("local.dsp.modenglish")]
[BepInPlugin("com.lokimoonlabs.chinesetoenglishmodtranslator", "Chinese to English Mod Translator", "1.0.0")]
public sealed class Plugin : BaseUnityPlugin
{
public const string Guid = "com.lokimoonlabs.chinesetoenglishmodtranslator";
private Harmony harmony;
private bool ready;
private void Awake()
{
//IL_007c: Unknown result type (might be due to invalid IL or missing references)
//IL_0086: Expected O, but got Unknown
//IL_00b9: Unknown result type (might be due to invalid IL or missing references)
//IL_00bf: Expected O, but got Unknown
//IL_013b: Unknown result type (might be due to invalid IL or missing references)
//IL_0142: Expected O, but got Unknown
try
{
string extraFile = Path.Combine(Path.GetDirectoryName(typeof(Plugin).Assembly.Location), "translations.tsv");
TranslationRuntime.Catalog = TranslationCatalog.Load(extraFile, Paths.PluginPath, delegate(string message)
{
((BaseUnityPlugin)this).Logger.LogWarning((object)message);
});
TranslationRuntime.Vanilla = VanillaEnglish.Load(Paths.GameRootPath, delegate(string message)
{
((BaseUnityPlugin)this).Logger.LogWarning((object)message);
});
DisplayPatches.ValidateTargets();
harmony = new Harmony("com.lokimoonlabs.chinesetoenglishmodtranslator");
foreach (MethodBase item in DisplayPatches.Targets())
{
Harmony obj = harmony;
HarmonyMethod val = new HarmonyMethod(typeof(DisplayPatches).GetMethod("TranslateReads"));
val.priority = 0;
obj.Patch(item, (HarmonyMethod)null, (HarmonyMethod)null, val, (HarmonyMethod)null, (HarmonyMethod)null);
}
foreach (MethodBase item2 in VanillaPatches.Targets(TranslationRuntime.Vanilla))
{
Harmony obj2 = harmony;
HarmonyMethod val2 = new HarmonyMethod(typeof(VanillaPatches).GetMethod((item2.DeclaringType == typeof(Localization)) ? "TranslatePostfix" : "ProtoPostfix"));
val2.priority = 0;
obj2.Patch(item2, (HarmonyMethod)null, val2, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
}
TranslationRuntime.Enabled = true;
Localization.OnLanguageChange += RefreshVisibleText;
ready = true;
((BaseUnityPlugin)this).Logger.LogInfo((object)("English display translations ready: " + TranslationRuntime.Catalog.Count + " phrases; both DSP text rendering paths and text sizing covered."));
((BaseUnityPlugin)this).Logger.LogInfo((object)("Original English vanilla text ready: " + TranslationRuntime.Vanilla.BindingCount + " bindings; ID protection " + (TranslationRuntime.Vanilla.CanProtectIds ? "active" : "disabled") + "; " + TranslationRuntime.Vanilla.KeyCount + " original language keys."));
RefreshVisibleText();
}
catch (Exception ex)
{
TranslationRuntime.Enabled = false;
if (harmony != null)
{
harmony.UnpatchSelf();
}
((BaseUnityPlugin)this).Logger.LogError((object)("English display translation was not enabled: " + ex));
}
}
private void RefreshVisibleText()
{
Text[] array = Resources.FindObjectsOfTypeAll<Text>();
foreach (Text val in array)
{
if (!((Object)(object)val == (Object)null) && TranslationCatalog.ContainsChinese(val.text))
{
((Graphic)val).SetVerticesDirty();
((Graphic)val).SetLayoutDirty();
}
}
}
private void OnDestroy()
{
TranslationRuntime.Enabled = false;
if (ready)
{
Localization.OnLanguageChange -= RefreshVisibleText;
}
if (harmony != null)
{
harmony.UnpatchSelf();
}
}
}
public static class TranslationRuntime
{
public static volatile bool Enabled;
public static TranslationCatalog Catalog;
public static VanillaEnglish Vanilla;
public static string Render(string source, bool english, bool editable)
{
if (!Enabled || !english || editable || Catalog == null)
{
return source;
}
string source2 = ((Vanilla == null) ? source : Vanilla.ProtectDisplay(source));
return Catalog.Translate(source2);
}
public static string ForText(string source, Text owner)
{
if (object.ReferenceEquals(owner, null) || !Enabled || Catalog == null || !TranslationCatalog.ContainsChinese(source))
{
return source;
}
try
{
if (!Localization.isENUS)
{
return source;
}
string text = Render(source, english: true, editable: false);
if (object.ReferenceEquals(text, source) || text == source)
{
return source;
}
if ((Object)(object)owner == (Object)null || (Object)(object)((Component)owner).GetComponentInParent<InputField>() != (Object)null)
{
return source;
}
return text;
}
catch
{
return source;
}
}
}
public static class DisplayPatches
{
private static readonly string[] Names = new string[4] { "OnDirectPopulateMesh", "OnPopulateMesh", "get_preferredWidth", "get_preferredHeight" };
private static readonly FieldInfo TextField = AccessTools.Field(typeof(Text), "m_Text");
private static readonly MethodInfo TextGetter = AccessTools.PropertyGetter(typeof(Text), "text");
private static readonly MethodInfo Translator = typeof(TranslationRuntime).GetMethod("ForText");
public static IEnumerable<MethodBase> Targets()
{
try
{
string[] names = Names;
foreach (string name in names)
{
MethodInfo target = AccessTools.DeclaredMethod(typeof(Text), name, (Type[])null, (Type[])null);
if (target == null || target.GetMethodBody() == null)
{
throw new MissingMethodException(typeof(Text).FullName, name);
}
yield return target;
}
}
finally
{
}
}
private static bool IsTextRead(CodeInstruction instruction)
{
if (!CodeInstructionExtensions.LoadsField(instruction, TextField, false))
{
return CodeInstructionExtensions.Calls(instruction, TextGetter);
}
return true;
}
public static void ValidateTargets()
{
if (TextField == null || TextGetter == null || Translator == null)
{
throw new MissingMemberException("DSP UI text accessors changed.");
}
foreach (MethodBase item in Targets())
{
if (!PatchProcessor.GetOriginalInstructions(item, (ILGenerator)null).Any(IsTextRead))
{
throw new InvalidOperationException("No display text reads in " + item.Name);
}
}
}
public static IEnumerable<CodeInstruction> TranslateReads(IEnumerable<CodeInstruction> instructions, MethodBase __originalMethod)
{
//IL_002e: Unknown result type (might be due to invalid IL or missing references)
//IL_0038: Expected O, but got Unknown
//IL_0043: Unknown result type (might be due to invalid IL or missing references)
//IL_004d: Expected O, but got Unknown
List<CodeInstruction> list = new List<CodeInstruction>();
int num = 0;
foreach (CodeInstruction instruction in instructions)
{
list.Add(instruction);
if (IsTextRead(instruction))
{
list.Add(new CodeInstruction(OpCodes.Ldarg_0, (object)null));
list.Add(new CodeInstruction(OpCodes.Call, (object)Translator));
num++;
}
}
if (num == 0)
{
throw new InvalidOperationException("No display text reads in " + __originalMethod.Name);
}
return list;
}
}
public sealed class TranslationCatalog
{
private sealed class Template
{
public Regex Pattern;
public string Prefix;
public string Target;
public Dictionary<string, string> Groups;
public int LiteralLength;
public string Apply(string source)
{
if (source.Length > 8192 || !source.StartsWith(Prefix, StringComparison.Ordinal))
{
return null;
}
Match match;
try
{
match = Pattern.Match(source);
}
catch (RegexMatchTimeoutException)
{
return null;
}
if (!match.Success)
{
return null;
}
return FormatToken.Replace(Target, (Match token) => match.Groups[Groups[token.Value]].Value);
}
}
private const int MaxCacheEntries = 1024;
private const int MaxTextLength = 32768;
private readonly Dictionary<string, string> exact;
private readonly Dictionary<char, List<KeyValuePair<string, string>>> byFirstChar;
private readonly List<Template> templates = new List<Template>();
private readonly Dictionary<string, string> cache = new Dictionary<string, string>(StringComparer.Ordinal);
private readonly object cacheLock = new object();
private static readonly Regex FormatToken = new Regex("(?<!\\{)\\{\\d+(?:,[^{}:]+)?(?::[^{}]+)?\\}(?!\\})", RegexOptions.CultureInvariant);
private static readonly Regex RichTag = new Regex("<[A-Za-z/][^>]*>", RegexOptions.CultureInvariant);
public int Count { get; private set; }
public int TemplateCount => templates.Count;
public TranslationCatalog(IDictionary<string, string> entries)
{
exact = new Dictionary<string, string>(StringComparer.Ordinal);
byFirstChar = new Dictionary<char, List<KeyValuePair<string, string>>>();
foreach (KeyValuePair<string, string> entry in entries)
{
if (!Validate(entry.Key, entry.Value, out var error))
{
throw new ArgumentException(error);
}
exact.Add(entry.Key, entry.Value);
if (!RichTag.IsMatch(entry.Key))
{
if (!byFirstChar.TryGetValue(entry.Key[0], out var value))
{
value = new List<KeyValuePair<string, string>>();
byFirstChar.Add(entry.Key[0], value);
}
value.Add(entry);
}
Template template = MakeTemplate(entry.Key, entry.Value);
if (template != null)
{
templates.Add(template);
}
Count++;
}
foreach (List<KeyValuePair<string, string>> value2 in byFirstChar.Values)
{
value2.Sort(delegate(KeyValuePair<string, string> a, KeyValuePair<string, string> b)
{
int num = b.Key.Length.CompareTo(a.Key.Length);
return (num == 0) ? StringComparer.Ordinal.Compare(a.Key, b.Key) : num;
});
}
templates.Sort((Template a, Template b) => b.LiteralLength.CompareTo(a.LiteralLength));
}
private static Template MakeTemplate(string source, string target)
{
MatchCollection matchCollection = FormatToken.Matches(source);
if (matchCollection.Count == 0 || matchCollection.Count > 8 || source.Contains("{{") || source.Contains("}}"))
{
return null;
}
for (int i = 1; i < matchCollection.Count; i++)
{
if (matchCollection[i].Index == matchCollection[i - 1].Index + matchCollection[i - 1].Length)
{
return null;
}
}
Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.Ordinal);
StringBuilder stringBuilder = new StringBuilder("\\A");
int num = 0;
int num2 = source.Length;
foreach (Match item in matchCollection)
{
stringBuilder.Append(Regex.Escape(source.Substring(num, item.Index - num)));
if (dictionary.TryGetValue(item.Value, out var value))
{
stringBuilder.Append("\\k<" + value + ">");
}
else
{
value = "v" + dictionary.Count;
dictionary.Add(item.Value, value);
string text = (Regex.IsMatch(item.Value, ":[NnFfPpDdEeGgRrCcXx][0-9]*\\}$", RegexOptions.CultureInvariant) ? "[+\\-\\u2212]?[0-9][0-9a-fA-F.,'\\u2019\\u00a0\\u202f %$\\u20ac\\u00a3\\-+Ee]{0,127}?" : "[^<>\\r\\n;\\uFF1b\\u3400-\\u4dbf\\u4e00-\\u9fff]{1,512}?");
stringBuilder.Append("(?<" + value + ">" + text + ")");
}
num2 -= item.Length;
num = item.Index + item.Length;
}
stringBuilder.Append(Regex.Escape(source.Substring(num))).Append("\\z");
Template template = new Template();
template.Pattern = new Regex(stringBuilder.ToString(), RegexOptions.CultureInvariant, TimeSpan.FromMilliseconds(10.0));
template.Prefix = source.Substring(0, matchCollection[0].Index);
template.Target = target;
template.Groups = dictionary;
template.LiteralLength = num2;
return template;
}
private static bool Han(char c)
{
if (c < '㐀' || c > '䶿')
{
if (c >= '一')
{
return c <= '鿿';
}
return false;
}
return true;
}
public static bool ContainsChinese(string value)
{
if (value == null)
{
return false;
}
foreach (char c in value)
{
if (Han(c))
{
return true;
}
}
return false;
}
private static string[] Tokens(string value)
{
return (from Match m in FormatToken.Matches(value)
select m.Value).OrderBy<string, string>((string s) => s, StringComparer.Ordinal).ToArray();
}
private static bool Validate(string source, string target, out string error)
{
error = null;
if (string.IsNullOrEmpty(source) || string.IsNullOrWhiteSpace(target))
{
error = "Source and English translation must be nonempty.";
}
else if (source.Length > 8192 || target.Length > 8192)
{
error = "Translation entry is too long.";
}
else if (!ContainsChinese(source))
{
error = "Source must contain Chinese text; existing English is left alone.";
}
else if (ContainsChinese(target))
{
error = "English translation still contains Chinese text.";
}
else if (!(from Match m in RichTag.Matches(source)
select m.Value).SequenceEqual(from Match m in RichTag.Matches(target)
select m.Value))
{
error = "Rich-text tags and their order must be preserved exactly.";
}
else if (RichTag.Matches(source).Cast<Match>().Any((Match m) => ContainsChinese(m.Value) || FormatToken.IsMatch(m.Value)))
{
error = "Tag attributes cannot be translation keys or variable fields.";
}
else if (!Tokens(source).SequenceEqual(Tokens(target)))
{
error = "Numbered format placeholders must be preserved.";
}
return error == null;
}
public static Dictionary<string, string> ReadMappings(TextReader reader, IDictionary<string, string> initial, Action<string> warn)
{
Dictionary<string, string> dictionary = ((initial == null) ? new Dictionary<string, string>(StringComparer.Ordinal) : new Dictionary<string, string>(initial, StringComparer.Ordinal));
int num = 0;
string text;
while ((text = reader.ReadLine()) != null)
{
num++;
if (!string.IsNullOrWhiteSpace(text) && !text.TrimStart(new char[0]).StartsWith("#", StringComparison.Ordinal))
{
int num2 = text.IndexOf('\t');
string error = null;
string text2 = null;
string text3 = null;
if (num2 < 1 || num2 == text.Length - 1)
{
error = "Expected source, a tab, and English text.";
}
else
{
text2 = Unescape(text.Substring(0, num2));
text3 = Unescape(text.Substring(num2 + 1));
Validate(text2, text3, out error);
}
if (error != null)
{
warn?.Invoke("Translation line " + num + ": " + error);
}
else
{
dictionary[text2] = text3;
}
}
}
return dictionary;
}
private static string Unescape(string value)
{
StringBuilder stringBuilder = new StringBuilder(value.Length);
for (int i = 0; i < value.Length; i++)
{
if (value[i] == '\\' && i + 1 < value.Length)
{
switch (value[i + 1])
{
case 'n':
stringBuilder.Append('\n');
i++;
continue;
case 'r':
stringBuilder.Append('\r');
i++;
continue;
case 't':
stringBuilder.Append('\t');
i++;
continue;
case '\\':
stringBuilder.Append('\\');
i++;
continue;
}
}
stringBuilder.Append(value[i]);
}
return stringBuilder.ToString();
}
public static TranslationCatalog Load(string extraFile, Action<string> warn)
{
return Load(extraFile, null, warn);
}
public static TranslationCatalog Load(string extraFile, string pluginsPath, Action<string> warn)
{
Dictionary<string, string> dictionary;
using (Stream stream = typeof(TranslationCatalog).Assembly.GetManifestResourceStream("ChineseToEnglishModTranslator.translations.tsv"))
{
if (stream == null)
{
throw new InvalidOperationException("Bundled English translations are missing.");
}
using StreamReader reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
dictionary = ReadMappings(reader, null, delegate(string message)
{
throw new InvalidOperationException(message);
});
}
InstalledTranslations.Merge(dictionary, pluginsPath, warn);
if (!string.IsNullOrEmpty(extraFile) && File.Exists(extraFile))
{
try
{
using StreamReader reader2 = new StreamReader(extraFile, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
dictionary = ReadMappings(reader2, dictionary, warn);
}
catch (Exception ex)
{
warn?.Invoke("Could not read extra translations; using the bundled English text. " + ex.Message);
}
}
return new TranslationCatalog(dictionary);
}
private string TranslateTemplate(string source)
{
string text = null;
foreach (Template template in templates)
{
string text2 = template.Apply(source);
if (text2 != null)
{
if (text != null && text != text2)
{
return source;
}
text = text2;
}
}
return text;
}
private void TranslateSegment(string source, int start, int end, StringBuilder output)
{
string text = source.Substring(start, end - start);
if (exact.TryGetValue(text, out var value))
{
output.Append(value);
return;
}
value = TranslateTemplate(text);
if (value != null)
{
output.Append(value);
return;
}
int start2 = start;
for (int i = start; i < end; i++)
{
if (source[i] == '\r' || source[i] == '\n' || source[i] == ';')
{
TranslateChunk(source, start2, i, output);
output.Append(source[i]);
start2 = i + 1;
}
}
TranslateChunk(source, start2, end, output);
}
private void TranslateChunk(string source, int start, int end, StringBuilder output)
{
string text = source.Substring(start, end - start);
if (exact.TryGetValue(text, out var value))
{
output.Append(value);
return;
}
value = TranslateTemplate(text);
if (value != null)
{
output.Append(value);
return;
}
int num = start;
while (num < end)
{
KeyValuePair<string, string>? keyValuePair = null;
if (byFirstChar.TryGetValue(source[num], out var value2))
{
foreach (KeyValuePair<string, string> item in value2)
{
int num2 = num + item.Key.Length;
if (num2 <= end && string.CompareOrdinal(source, num, item.Key, 0, item.Key.Length) == 0 && (num <= start || !Han(item.Key[0]) || !Han(source[num - 1])) && (num2 >= end || !Han(item.Key[item.Key.Length - 1]) || !Han(source[num2])))
{
keyValuePair = item;
break;
}
}
}
if (keyValuePair.HasValue)
{
output.Append(keyValuePair.Value.Value);
num += keyValuePair.Value.Key.Length;
}
else
{
output.Append(source[num++]);
}
}
}
public string Translate(string source)
{
if (string.IsNullOrEmpty(source) || source.Length > 32768 || !ContainsChinese(source))
{
return source;
}
if (exact.TryGetValue(source, out var value))
{
return value;
}
lock (cacheLock)
{
if (cache.TryGetValue(source, out value))
{
return value;
}
}
value = TranslateTemplate(source);
if (value == null)
{
StringBuilder stringBuilder = new StringBuilder(source.Length + 32);
int num = 0;
while (num < source.Length)
{
int num2 = -1;
int num3 = -1;
for (int i = num; i < source.Length - 1; i++)
{
if (source[i] == '<' && (source[i + 1] == '/' || (source[i + 1] >= 'A' && source[i + 1] <= 'Z') || (source[i + 1] >= 'a' && source[i + 1] <= 'z')))
{
int num4 = source.IndexOf('>', i + 1);
if (num4 >= 0)
{
num2 = i;
num3 = num4;
break;
}
}
}
TranslateSegment(source, num, (num2 < 0) ? source.Length : num2, stringBuilder);
if (num2 < 0)
{
break;
}
stringBuilder.Append(source, num2, num3 - num2 + 1);
num = num3 + 1;
}
value = stringBuilder.ToString();
if (value == source)
{
value = source;
}
}
lock (cacheLock)
{
if (cache.Count >= 1024)
{
cache.Clear();
}
cache[source] = value;
return value;
}
}
}
public sealed class VanillaEnglish
{
private sealed class NativeText
{
public string Name;
public string Description;
public string Conclusion;
}
private const string ExpectedGameHash = "ef314425dade51f529f184f07be136cd3c2660a6a81515603c7226fbe7ab1d8c";
private readonly Dictionary<string, Dictionary<int, NativeText>> byType = new Dictionary<string, Dictionary<int, NativeText>>(StringComparer.Ordinal);
private int bindingCount;
private readonly Dictionary<string, string> byKey = new Dictionary<string, string>(StringComparer.Ordinal);
private readonly Dictionary<string, string> display = new Dictionary<string, string>(StringComparer.Ordinal);
private readonly HashSet<string> ambiguousDisplay = new HashSet<string>(StringComparer.Ordinal);
private readonly HashSet<string> members = new HashSet<string>(StringComparer.Ordinal);
private readonly Dictionary<string, string> names = new Dictionary<string, string>(StringComparer.Ordinal);
private readonly HashSet<string> ambiguousNames = new HashSet<string>(StringComparer.Ordinal);
private TranslationCatalog inlineNames;
public bool CanProtectIds { get; private set; }
public int BindingCount => bindingCount;
public int KeyCount => byKey.Count;
public IEnumerable<string> Members => members.ToArray();
private static string Unescape(string value)
{
StringBuilder stringBuilder = new StringBuilder(value.Length);
for (int i = 0; i < value.Length; i++)
{
if (value[i] == '\\' && i + 1 < value.Length)
{
char value2;
switch (value[i + 1])
{
case 'n':
value2 = '\n';
break;
case 'r':
value2 = '\r';
break;
case 't':
value2 = '\t';
break;
case 'v':
value2 = '\v';
break;
case 'f':
value2 = '\f';
break;
case '\\':
value2 = '\\';
break;
default:
stringBuilder.Append(value[i]);
continue;
}
stringBuilder.Append(value2);
i++;
}
else
{
stringBuilder.Append(value[i]);
}
}
return stringBuilder.ToString();
}
private static Dictionary<string, string> ReadLanguage(string directory)
{
Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.Ordinal);
if (!Directory.Exists(directory))
{
return dictionary;
}
string[] array = new string[6] { "base", "combat", "creation", "prototype", "keys", "dictionary" };
foreach (string text in array)
{
string path = Path.Combine(directory, text + ".txt");
if (!File.Exists(path))
{
continue;
}
using StreamReader streamReader = new StreamReader(path, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
string text2;
while ((text2 = streamReader.ReadLine()) != null)
{
string[] array2 = text2.Split(new char[1] { '\t' }, 4);
if (array2.Length == 4 && array2[0].Length != 0)
{
string key = Unescape(array2[0]);
if (!dictionary.ContainsKey(key))
{
dictionary.Add(key, Unescape(array2[3]));
}
}
}
}
return dictionary;
}
private void AddDisplay(string source, string english)
{
if (!string.IsNullOrEmpty(source) && TranslationCatalog.ContainsChinese(source) && !ambiguousDisplay.Contains(source))
{
if (display.TryGetValue(source, out var value) && value != english)
{
display.Remove(source);
ambiguousDisplay.Add(source);
}
else
{
display[source] = english;
}
}
}
private void AddInlineName(string source, string english)
{
if (!string.IsNullOrEmpty(source) && TranslationCatalog.ContainsChinese(source) && !ambiguousNames.Contains(source) && source.IndexOfAny(new char[4] { '<', '>', '{', '}' }) < 0 && english.IndexOfAny(new char[4] { '<', '>', '{', '}' }) < 0)
{
if (names.TryGetValue(source, out var value) && value != english)
{
names.Remove(source);
ambiguousNames.Add(source);
}
else
{
names[source] = english;
}
}
}
public static VanillaEnglish Load(string gameRoot, Action<string> warn)
{
VanillaEnglish vanillaEnglish = new VanillaEnglish();
try
{
string text = typeof(Proto).Assembly.Location;
if (string.IsNullOrEmpty(text) || !File.Exists(text))
{
text = Path.Combine(gameRoot, "DSPGAME_Data", "Managed", "Assembly-CSharp.dll");
}
using Stream inputStream = File.OpenRead(text);
using SHA256 sHA = SHA256.Create();
vanillaEnglish.CanProtectIds = BitConverter.ToString(sHA.ComputeHash(inputStream)).Replace("-", "").Equals("ef314425dade51f529f184f07be136cd3c2660a6a81515603c7226fbe7ab1d8c", StringComparison.OrdinalIgnoreCase);
}
catch (Exception ex)
{
warn?.Invoke("Could not verify vanilla ID index: " + ex.Message);
}
if (!vanillaEnglish.CanProtectIds)
{
warn?.Invoke("The game assembly differs from the verified vanilla ID index. Key-based English protection remains available; rebuild the ID index for this game update.");
}
Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.Ordinal);
Dictionary<string, string> dictionary2 = new Dictionary<string, string>(StringComparer.Ordinal);
try
{
dictionary = ReadLanguage(Path.Combine(gameRoot, "Locale", "1033"));
}
catch (Exception ex2)
{
warn?.Invoke("Could not read the game's English files; unavailable entries will be left unchanged. " + ex2.Message);
}
try
{
dictionary2 = ReadLanguage(Path.Combine(gameRoot, "Locale", "2052"));
}
catch (Exception ex3)
{
warn?.Invoke("Chinese display aliases are unavailable; ID and key protection remain active. " + ex3.Message);
}
using (Stream stream = typeof(VanillaEnglish).Assembly.GetManifestResourceStream("ChineseToEnglishModTranslator.vanilla.tsv"))
{
if (stream == null)
{
throw new InvalidOperationException("Verified vanilla text index is missing.");
}
using StreamReader streamReader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
string text2;
while ((text2 = streamReader.ReadLine()) != null)
{
if (text2.Length == 0 || text2[0] == '#')
{
continue;
}
string[] array = text2.Split(new char[1] { '\t' });
if (array.Length != 4 || !int.TryParse(array[1], out var result))
{
throw new InvalidDataException("Invalid vanilla text index.");
}
string text3 = array[0];
string text4 = array[2];
string text5 = Unescape(array[3]);
if (!dictionary.TryGetValue(text5, out var value) || string.IsNullOrEmpty(value) || TranslationCatalog.ContainsChinese(value) || (!vanillaEnglish.CanProtectIds && !dictionary.ContainsKey(text5)))
{
continue;
}
if (!vanillaEnglish.byType.TryGetValue(text3, out var value2))
{
value2 = new Dictionary<int, NativeText>();
vanillaEnglish.byType.Add(text3, value2);
}
if (!value2.TryGetValue(result, out var value3))
{
value3 = new NativeText();
value2.Add(result, value3);
}
switch (text4)
{
case "name":
if (value3.Name != null)
{
throw new InvalidDataException("Duplicate vanilla name.");
}
value3.Name = value;
break;
case "description":
if (value3.Description != null)
{
throw new InvalidDataException("Duplicate vanilla description.");
}
value3.Description = value;
break;
case "conclusion":
if (value3.Conclusion != null)
{
throw new InvalidDataException("Duplicate vanilla conclusion.");
}
value3.Conclusion = value;
break;
default:
throw new InvalidDataException("Unknown vanilla text member.");
}
vanillaEnglish.bindingCount++;
vanillaEnglish.byKey[text5] = value;
vanillaEnglish.members.Add(text3 + "|" + text4);
vanillaEnglish.AddDisplay(text5, value);
if (dictionary2.TryGetValue(text5, out var value4))
{
vanillaEnglish.AddDisplay(value4, value);
}
if (text4 == "name")
{
vanillaEnglish.AddInlineName(text5, value);
if (value4 != null)
{
vanillaEnglish.AddInlineName(value4, value);
}
}
}
}
if (vanillaEnglish.bindingCount == 0)
{
warn?.Invoke("No original English entries were available in the game locale files; vanilla text will be left unchanged.");
}
vanillaEnglish.inlineNames = new TranslationCatalog(vanillaEnglish.names);
return vanillaEnglish;
}
public string Protect(string type, int id, string member, string original, bool english)
{
if (!english || !CanProtectIds)
{
return original;
}
if (!byType.TryGetValue(type, out var value) || !value.TryGetValue(id, out var value2))
{
return original;
}
return (string)((member switch
{
"conclusion" => value2.Conclusion,
"description" => value2.Description,
"name" => value2.Name,
_ => null,
}) ?? original);
}
public string ProtectKey(string key, string original, bool english)
{
if (!english || key == null)
{
return original;
}
if (!byKey.TryGetValue(key, out var value))
{
return original;
}
return value;
}
public string ProtectDisplay(string source)
{
if (source == null)
{
return null;
}
if (display.TryGetValue(source, out var value))
{
return value;
}
if (inlineNames != null)
{
return inlineNames.Translate(source);
}
return source;
}
}
public static class VanillaPatches
{
public static IEnumerable<MethodBase> Targets(VanillaEnglish catalog)
{
MethodInfo translate = AccessTools.DeclaredMethod(typeof(Localization), "Translate", new Type[1] { typeof(string) }, (Type[])null);
if (translate == null || translate.ReturnType != typeof(string))
{
throw new MissingMethodException("Localization.Translate(string)");
}
yield return translate;
if (!catalog.CanProtectIds)
{
yield break;
}
yield return AccessTools.PropertyGetter(typeof(Proto), "name");
foreach (string entry in catalog.Members.OrderBy<string, string>((string s) => s, StringComparer.Ordinal))
{
string[] parts = entry.Split(new char[1] { '|' });
if (!(parts[1] == "name"))
{
Type type = typeof(Proto).Assembly.GetType(parts[0], throwOnError: true);
MethodInfo getter = AccessTools.DeclaredPropertyGetter(type, parts[1]);
if (getter == null || getter.ReturnType != typeof(string))
{
throw new MissingMethodException(type.FullName, "get_" + parts[1]);
}
yield return getter;
}
}
}
public static void TranslatePostfix(string __0, ref string __result)
{
try
{
VanillaEnglish vanilla = TranslationRuntime.Vanilla;
if (TranslationRuntime.Enabled && vanilla != null)
{
__result = vanilla.ProtectKey(__0, __result, Localization.isENUS);
}
}
catch
{
}
}
public static void ProtoPostfix(Proto __instance, MethodBase __originalMethod, ref string __result)
{
try
{
VanillaEnglish vanilla = TranslationRuntime.Vanilla;
if (TranslationRuntime.Enabled && vanilla != null && __instance != null)
{
__result = vanilla.Protect(((object)__instance).GetType().Name, __instance.ID, (__originalMethod.Name == "get_name") ? "name" : ((__originalMethod.Name == "get_description") ? "description" : "conclusion"), __result, Localization.isENUS);
}
}
catch
{
}
}
}