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<string, string> Exact = new Dictionary<string, string>(StringComparer.Ordinal);
private static readonly Dictionary<string, string> LoadingQuoteTranslations = new Dictionary<string, string>(StringComparer.Ordinal);
private static readonly List<RegexEntry> Regexes = new List<RegexEntry>();
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<string, string> dictionary = new Dictionary<string, string>(StringComparer.Ordinal);
Dictionary<string, string> dictionary2 = new Dictionary<string, string>(StringComparer.Ordinal);
List<RegexEntry> list = new List<RegexEntry>();
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<string, string> item in dictionary)
{
Exact[item.Key] = item.Value;
}
foreach (KeyValuePair<string, string> 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<string, string> exact, Dictionary<string, string> 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<int, TrackedText> Tracked = new Dictionary<int, TrackedText>();
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<int> list = null;
foreach (KeyValuePair<int, TrackedText> 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<int>();
}
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<TMP_Text>();
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<string> MissingQuotesLogged = new HashSet<string>(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<TMP_Text>(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);
}
}
}