using System; using System.Collections.Generic; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Text; using System.Text.RegularExpressions; using BepInEx; using BepInEx.Logging; using HarmonyLib; using TMPro; using UnityEngine; using UnityEngine.SceneManagement; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: AssemblyVersion("0.0.0.0")] namespace RTLC.GWYF.TextRuntime; [BepInPlugin("rtlc.gwyf.text", "RTLC.GWYF Text Runtime", "0.3.0")] public sealed class Plugin : BaseUnityPlugin { internal static ManualLogSource Log; private Harmony _harmony; private void Awake() { //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; LogRtlcBanner(((BaseUnityPlugin)this).Logger); RuntimeTranslator.Load(Path.GetDirectoryName(((BaseUnityPlugin)this).Info.Location), ((BaseUnityPlugin)this).Logger, force: true); _harmony = new Harmony("dimalooper.gwyf.russianruntime"); _harmony.PatchAll(); ((BaseUnityPlugin)this).Logger.LogInfo((object)"Runtime text hooks installed."); } private static void LogRtlcBanner(ManualLogSource logger) { logger.LogInfo((object)""); logger.LogInfo((object)"RRRRR TTTTT L CCCCC"); logger.LogInfo((object)"R R T L C "); logger.LogInfo((object)"RRRRR T L C "); logger.LogInfo((object)"R R T L C "); logger.LogInfo((object)"R R T LLLLL CCCCC"); logger.LogInfo((object)"RTLC.GWYF runtime localization pack"); logger.LogInfo((object)"© Плагин является интеллектуальной собственностью RTLC Team. Любое копирование, изменение и распространение материалов без письменного разрешения запрещено. Все права защищены."); logger.LogInfo((object)""); } private void OnDestroy() { if (_harmony != null) { _harmony.UnpatchSelf(); } } } internal static class RuntimeTranslator { private sealed class RegexEntry { public Regex Regex; public string Replacement; } private static readonly Dictionary Exact = new Dictionary(StringComparer.Ordinal); private static readonly Dictionary LoadingQuoteTranslations = new Dictionary(StringComparer.Ordinal); private static readonly List Regexes = new List(); private static string _loadedPath; private static DateTime _loadedWriteTimeUtc; public static bool Load(string pluginDirectory, ManualLogSource log, bool force) { string text = ResolveTranslationPath(pluginDirectory); if (text == null) { if (force) { log.LogWarning((object)"Translate.txt was not found in BepInEx/config/Translation/ru/Text or fallback paths."); } return false; } DateTime lastWriteTimeUtc = File.GetLastWriteTimeUtc(text); if (!force && string.Equals(_loadedPath, text, StringComparison.OrdinalIgnoreCase) && lastWriteTimeUtc == _loadedWriteTimeUtc) { return false; } Dictionary dictionary = new Dictionary(StringComparer.Ordinal); Dictionary dictionary2 = new Dictionary(StringComparer.Ordinal); List list = new List(); int num = 0; int num2 = 0; int num3 = 0; string[] array = File.ReadAllLines(text, Encoding.UTF8); for (int i = 0; i < array.Length; i++) { string text2 = array[i]; if (string.IsNullOrWhiteSpace(text2)) { continue; } string text3 = text2.TrimStart(new char[0]); if (text3.StartsWith("#", StringComparison.Ordinal) || text3.StartsWith("//", StringComparison.Ordinal)) { continue; } int num4 = FindDelimiter(text2); if (num4 <= 0) { continue; } string text4 = text2.Substring(0, num4); string text5 = text2.Substring(num4 + 1); if (text4.StartsWith("r:\"", StringComparison.Ordinal) && text4.EndsWith("\"", StringComparison.Ordinal) && text4.Length >= 4) { string pattern = text4.Substring(3, text4.Length - 4).Replace("\\=", "="); try { list.Add(new RegexEntry { Regex = new Regex(pattern, RegexOptions.CultureInvariant), Replacement = UnescapeText(text5) }); num2++; } catch (Exception ex) { log.LogWarning((object)("Bad regex translation at line " + (i + 1) + ": " + ex.Message)); } } else { string key = UnescapeDelimiterEscapes(text4); string text6 = UnescapeText(text4); string translation = UnescapeDelimiterEscapes(text5); string translation2 = UnescapeText(text5); AddExactTranslation(dictionary, dictionary2, key, translation); AddExactTranslation(dictionary, dictionary2, text6, translation2); if (IsLoadingQuoteKey(text6)) { num3++; } num++; } } Exact.Clear(); LoadingQuoteTranslations.Clear(); foreach (KeyValuePair item in dictionary) { Exact[item.Key] = item.Value; } foreach (KeyValuePair item2 in dictionary2) { LoadingQuoteTranslations[item2.Key] = item2.Value; } Regexes.Clear(); Regexes.AddRange(list); _loadedPath = text; _loadedWriteTimeUtc = lastWriteTimeUtc; log.LogInfo((object)("Loaded runtime translations: exact=" + num + ", regex=" + num2 + ", loadingQuotes=" + num3 + " from " + text)); return true; } private static string ResolveTranslationPath(string pluginDirectory) { string gameRootPath = Paths.GameRootPath; string[] array = new string[4] { Path.Combine(Paths.ConfigPath, "Translation", "ru", "Text", "Translate.txt"), Path.Combine(gameRootPath, "BepInEx", "config", "Translation", "ru", "Text", "Translate.txt"), Path.Combine(gameRootPath, "BepInEx", "Translation", "ru", "Text", "Translate.txt"), Path.Combine(pluginDirectory ?? string.Empty, "Translate.txt") }; for (int i = 0; i < array.Length; i++) { if (File.Exists(array[i])) { return array[i]; } } return null; } public static string Translate(string text) { if (string.IsNullOrEmpty(text)) { return text; } if (TryLookupExact(text, out var value)) { return value; } for (int i = 0; i < Regexes.Count; i++) { RegexEntry regexEntry = Regexes[i]; if (regexEntry.Regex.IsMatch(text)) { return regexEntry.Regex.Replace(text, regexEntry.Replacement); } } return text; } public static string TranslateLoadingQuote(string text) { string text2 = Translate(text); if (!string.Equals(text2, text, StringComparison.Ordinal)) { return text2; } if (TryLookupRawTranslation(text, out var value, out var lineNumber)) { Exact[text] = value; if (IsLoadingQuoteKey(text)) { LoadingQuoteTranslations[text] = value; } Plugin.Log.LogInfo((object)("Recovered loading quote translation from Translate.txt line " + lineNumber + ".")); return value; } return text; } public static string TranslatePreserveOuterWhitespace(string text) { if (string.IsNullOrEmpty(text)) { return text; } int i; for (i = 0; i < text.Length && char.IsWhiteSpace(text[i]); i++) { } int num = text.Length - 1; while (num >= i && char.IsWhiteSpace(text[num])) { num--; } if (i == 0 && num == text.Length - 1) { return Translate(text); } if (num < i) { return text; } string text2 = text.Substring(i, num - i + 1); if (TryLookupExact(text2, out var value)) { return text.Substring(0, i) + value + text.Substring(num + 1); } for (int j = 0; j < Regexes.Count; j++) { RegexEntry regexEntry = Regexes[j]; if (regexEntry.Regex.IsMatch(text2)) { return text.Substring(0, i) + regexEntry.Regex.Replace(text2, regexEntry.Replacement) + text.Substring(num + 1); } } return text; } private static bool TryLookupExact(string text, out string value) { if (Exact.TryGetValue(text, out value)) { return true; } if (LoadingQuoteTranslations.TryGetValue(text, out value)) { return true; } if (text.IndexOf('\r') >= 0) { string text2 = text.Replace("\r\n", "\n").Replace("\r", "\n"); if (!string.Equals(text2, text, StringComparison.Ordinal) && Exact.TryGetValue(text2, out value)) { return true; } if (!string.Equals(text2, text, StringComparison.Ordinal) && LoadingQuoteTranslations.TryGetValue(text2, out value)) { return true; } } value = null; return false; } private static bool TryLookupRawTranslation(string text, out string value, out int lineNumber) { value = null; lineNumber = 0; if (string.IsNullOrEmpty(_loadedPath) || !File.Exists(_loadedPath)) { return false; } string[] array = File.ReadAllLines(_loadedPath, Encoding.UTF8); for (int i = 0; i < array.Length; i++) { string text2 = array[i]; if (string.IsNullOrWhiteSpace(text2)) { continue; } string text3 = text2.TrimStart(new char[0]); if (text3.StartsWith("#", StringComparison.Ordinal) || text3.StartsWith("//", StringComparison.Ordinal)) { continue; } int num = FindDelimiter(text2); if (num <= 0) { continue; } string text4 = text2.Substring(0, num); if (!text4.StartsWith("r:\"", StringComparison.Ordinal) || !text4.EndsWith("\"", StringComparison.Ordinal) || text4.Length < 4) { string a = UnescapeDelimiterEscapes(text4); string a2 = UnescapeText(text4); if (string.Equals(a, text, StringComparison.Ordinal)) { value = UnescapeDelimiterEscapes(text2.Substring(num + 1)); lineNumber = i + 1; return true; } if (string.Equals(a2, text, StringComparison.Ordinal)) { value = UnescapeText(text2.Substring(num + 1)); lineNumber = i + 1; return true; } } } return false; } private static bool IsLoadingQuoteKey(string text) { if (text.IndexOf("\n\n-", StringComparison.Ordinal) < 0) { return text.IndexOf("\\n\\n-", StringComparison.Ordinal) >= 0; } return true; } private static void AddExactTranslation(Dictionary exact, Dictionary loadingQuoteTranslations, string key, string translation) { exact[key] = translation; if (IsLoadingQuoteKey(key)) { loadingQuoteTranslations[key] = translation; } } private static int FindDelimiter(string line) { for (int i = 0; i < line.Length; i++) { if (line[i] == '=' && (i == 0 || line[i - 1] != '\\')) { return i; } } return -1; } private static string UnescapeText(string text) { if (text.IndexOf('\\') < 0) { return text; } return text.Replace("\\r", "\r").Replace("\\n", "\n").Replace("\\t", "\t") .Replace("\\=", "="); } private static string UnescapeDelimiterEscapes(string text) { return text.Replace("\\=", "="); } } internal static class RuntimeTextTracker { private sealed class TrackedText { public WeakReference Target; public string Original; } private static readonly Dictionary Tracked = new Dictionary(); public static void Apply(TMP_Text target, string original) { if (!((Object)(object)target == (Object)null) && !string.IsNullOrEmpty(original)) { string text = RuntimeTranslator.Translate(original); if (!string.Equals(text, original, StringComparison.Ordinal)) { target.text = text; Track(target, original); } } } public static void ApplyAlways(TMP_Text target, string original) { if (!((Object)(object)target == (Object)null) && !string.IsNullOrEmpty(original)) { target.text = RuntimeTranslator.Translate(original); Track(target, original); } } public static void TrackOriginal(TMP_Text target, string original) { if (!((Object)(object)target == (Object)null) && !string.IsNullOrEmpty(original)) { Track(target, original); } } public static void RefreshTrackedTexts() { List list = null; foreach (KeyValuePair item in Tracked) { object? target = item.Value.Target.Target; TMP_Text val = (TMP_Text)((target is TMP_Text) ? target : null); if ((Object)(object)val == (Object)null) { if (list == null) { list = new List(); } list.Add(item.Key); } else { string text = RuntimeTranslator.Translate(item.Value.Original); if (!string.Equals(val.text, text, StringComparison.Ordinal)) { val.text = text; } } } if (list != null) { for (int i = 0; i < list.Count; i++) { Tracked.Remove(list[i]); } } } public static void ScanVisibleTexts() { //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) TMP_Text[] array = Resources.FindObjectsOfTypeAll(); foreach (TMP_Text val in array) { if ((Object)(object)val == (Object)null || !((Behaviour)val).isActiveAndEnabled || (Object)(object)((Component)val).gameObject == (Object)null) { continue; } Scene scene = ((Component)val).gameObject.scene; if (((Scene)(ref scene)).IsValid() && ((Scene)(ref scene)).isLoaded) { int instanceID = ((Object)val).GetInstanceID(); if (!Tracked.ContainsKey(instanceID)) { Apply(val, val.text); } } } } private static void Track(TMP_Text target, string original) { int instanceID = ((Object)target).GetInstanceID(); Tracked[instanceID] = new TrackedText { Target = new WeakReference(target), Original = original }; } } internal static class ReflectionCache { public static readonly FieldInfo InteractionItemNameText = AccessTools.Field(typeof(InteractionUIPanel), "itemNameText"); public static readonly FieldInfo LoadingQuoteTargetText = AccessTools.Field(typeof(LoadingQuotes), "targetText"); public static readonly FieldInfo LoadingQuoteQuotes = AccessTools.Field(typeof(LoadingQuotes), "quotes"); public static readonly FieldInfo ChallengeNameText = AccessTools.Field(typeof(ChallengeEntryUI), "challengeNameText"); public static readonly FieldInfo ChallengeProgressText = AccessTools.Field(typeof(ChallengeEntryUI), "progressText"); public static readonly FieldInfo RerollCostText = AccessTools.Field(typeof(RerollButton), "rerollCostText"); public static readonly FieldInfo PhoneBoothScreenText = AccessTools.Field(typeof(PhoneBooth), "screenText"); } [HarmonyPatch(typeof(LoadingQuotes), "ShowRandomQuote")] internal static class LoadingQuotes_ShowRandomQuote_Patch { private static readonly HashSet MissingQuotesLogged = new HashSet(StringComparer.Ordinal); private static bool Prefix(LoadingQuotes __instance) { object? value = ReflectionCache.LoadingQuoteTargetText.GetValue(__instance); TMP_Text val = (TMP_Text)((value is TMP_Text) ? value : null); string[] array = ReflectionCache.LoadingQuoteQuotes.GetValue(__instance) as string[]; if ((Object)(object)val == (Object)null || array == null || array.Length == 0) { return true; } string text = array[Random.Range(0, array.Length)]; string text2 = RuntimeTranslator.TranslateLoadingQuote(text); if (string.Equals(text2, text, StringComparison.Ordinal) && MissingQuotesLogged.Add(text)) { Plugin.Log.LogWarning((object)("Untranslated loading quote key: len=" + text.Length + ", text=" + EscapeForLog(text) + ", chars=" + GetCharCodes(text))); } val.text = text2; RuntimeTextTracker.TrackOriginal(val, text); return false; } private static string EscapeForLog(string text) { return text.Replace("\r", "\\r").Replace("\n", "\\n").Replace("\t", "\\t"); } private static string GetCharCodes(string text) { StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < text.Length; i++) { if (i > 0) { stringBuilder.Append(','); } stringBuilder.Append((int)text[i]); } return stringBuilder.ToString(); } } [HarmonyPatch(typeof(InteractionUIPanel), "SetItemNameText")] internal static class InteractionUIPanel_SetItemNameText_Patch { private static void Postfix(InteractionUIPanel __instance, string itemName) { object? value = ReflectionCache.InteractionItemNameText.GetValue(__instance); TMP_Text val = (TMP_Text)((value is TMP_Text) ? value : null); if ((Object)(object)val != (Object)null) { RuntimeTextTracker.Apply(val, itemName); } } } [HarmonyPatch(typeof(InteractableBase), "get_InteractableName")] internal static class InteractableBase_GetInteractableName_Patch { private static void Postfix(ref string __result) { __result = RuntimeTranslator.Translate(__result); } } [HarmonyPatch(typeof(InteractableBase), "get_TooltipMessage")] internal static class InteractableBase_GetTooltipMessage_Patch { private static void Postfix(ref string __result) { __result = RuntimeTranslator.Translate(__result); } } [HarmonyPatch(typeof(KeyButtonManager), "CreateTextElement")] internal static class KeyButtonManager_CreateTextElement_Patch { private static void Prefix(ref string text) { text = RuntimeTranslator.TranslatePreserveOuterWhitespace(text); } } [HarmonyPatch(typeof(ItemDescriptionSettings), "GetDescription")] internal static class ItemDescriptionSettings_GetDescription_Patch { private static void Postfix(ref string __result) { __result = RuntimeTranslator.Translate(__result); } } [HarmonyPatch(typeof(ItemDescriptionDisplayManager), "UpdateDescriptionText")] internal static class ItemDescriptionDisplayManager_UpdateDescriptionText_Patch { private static void Postfix(GameObject canvas) { if (!((Object)(object)canvas == (Object)null)) { TMP_Text componentInChildren = canvas.GetComponentInChildren(true); if ((Object)(object)componentInChildren != (Object)null) { RuntimeTextTracker.Apply(componentInChildren, componentInChildren.text); } } } } [HarmonyPatch(typeof(Challenge), "GetProcessedDescription")] internal static class Challenge_GetProcessedDescription_Patch { private static void Postfix(ref string __result) { __result = RuntimeTranslator.Translate(__result); } } [HarmonyPatch(typeof(Challenge), "GetProgressText")] internal static class Challenge_GetProgressText_Patch { private static void Postfix(ref string __result) { __result = RuntimeTranslator.Translate(__result); } } [HarmonyPatch(typeof(ChallengeEntryUI), "SetData")] internal static class ChallengeEntryUI_SetData_Patch { private static void Postfix(ChallengeEntryUI __instance) { object? value = ReflectionCache.ChallengeNameText.GetValue(__instance); TMP_Text val = (TMP_Text)((value is TMP_Text) ? value : null); if ((Object)(object)val != (Object)null) { RuntimeTextTracker.Apply(val, val.text); } object? value2 = ReflectionCache.ChallengeProgressText.GetValue(__instance); TMP_Text val2 = (TMP_Text)((value2 is TMP_Text) ? value2 : null); if ((Object)(object)val2 != (Object)null) { RuntimeTextTracker.Apply(val2, val2.text); } } } [HarmonyPatch(typeof(RerollButton), "OnRerollCostChanged")] internal static class RerollButton_OnRerollCostChanged_Patch { private static void Postfix(RerollButton __instance) { object? value = ReflectionCache.RerollCostText.GetValue(__instance); TMP_Text val = (TMP_Text)((value is TMP_Text) ? value : null); if ((Object)(object)val != (Object)null) { RuntimeTextTracker.Apply(val, val.text); } } } [HarmonyPatch(typeof(PhoneBooth), "UserCode_RpcOnRerollActive")] internal static class PhoneBooth_UserCode_RpcOnRerollActive_Patch { private static void Postfix(PhoneBooth __instance) { object? value = ReflectionCache.PhoneBoothScreenText.GetValue(__instance); TMP_Text val = (TMP_Text)((value is TMP_Text) ? value : null); if ((Object)(object)val != (Object)null) { RuntimeTextTracker.Apply(val, val.text); } } }