Decompiled source of Chinese to English Mod Translator v1.0.0

ChineseToEnglishModTranslator.dll

Decompiled 3 days ago
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
		{
		}
	}
}