Decompiled source of Crawl AnyLangLocalization v0.14.5
BepInEx/plugins/CrawlLocalization/CrawlLocalization.dll
Decompiled 4 days ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; using System.Text.RegularExpressions; using BepInEx; using BepInEx.Configuration; using HarmonyLib; using UnityEngine; using UnityEngine.SceneManagement; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("Crawl Localization")] [assembly: AssemblyDescription("Menu localization and per-language font replacement for Crawl")] [assembly: AssemblyFileVersion("0.14.5.0")] [assembly: AssemblyVersion("0.14.5.0")] namespace CrawlLocalization; [HarmonyPatch(typeof(LevelTitle), "Start")] internal static class ChapterTitlePatch { private static void Postfix(TextMesh ___m_textLevel, TextMesh ___m_textFloor) { if (!((Object)(object)Plugin.Instance == (Object)null)) { Bind(___m_textLevel); Bind(___m_textFloor); } } private static void Bind(TextMesh mesh) { if (!((Object)(object)mesh == (Object)null)) { LocalizedMenuLabel orCreate = LocalizedMenuLabel.GetOrCreate(((Component)mesh).gameObject); orCreate.Standalone = true; orCreate.ResetSource(mesh.text); orCreate.Refresh(); } } } internal static class ChineseTextWrapper { private static readonly Regex Units = new Regex("</?(?:b|i|size|color|material)(?:=[^>]*)?>|<quad\\s[^>]*>|\\r\\n|[\\r\\n]|[A-Za-z0-9]+(?:[-'][A-Za-z0-9]+)*|.", RegexOptions.IgnoreCase); private const string Closing = ",。!?;:、)】》」』”’%%.,!?;:)"; private const string Opening = "(【《「『“‘("; private static bool IsTag(string unit) { if (unit.StartsWith("<", StringComparison.Ordinal)) { return unit.EndsWith(">", StringComparison.Ordinal); } return false; } internal static bool ContainsChinese(string text) { if (text == null) { return false; } foreach (char c in text) { if (c >= '㐀' && c <= '鿿') { return true; } } return false; } internal static string Wrap(string text, float width, Func<char, float> advance, Func<string, float> tagWidth = null) { if (string.IsNullOrEmpty(text) || width <= 0f) { return text; } List<string> list = new List<string>(); foreach (Match item in Units.Matches(text)) { string value = item.Value; if (list.Count > 0 && !IsTag(list[list.Count - 1]) && value.Length == 1 && ",。!?;:、)】》」』”’%%.,!?;:)".IndexOf(value[0]) >= 0 && list[list.Count - 1] != "\n" && list[list.Count - 1] != "\r" && list[list.Count - 1] != "\r\n") { list[list.Count - 1] += value; } else if (list.Count > 0 && list[list.Count - 1].Length == 1 && "(【《「『“‘(".IndexOf(list[list.Count - 1][0]) >= 0 && !IsTag(value) && value != "\n" && value != "\r" && value != "\r\n") { list[list.Count - 1] += value; } else { list.Add(value); } } StringBuilder stringBuilder = new StringBuilder(); float num = 0f; foreach (string item2 in list) { switch (item2) { case "\r\n": case "\r": case "\n": stringBuilder.Append('\n'); num = 0f; continue; } if (IsTag(item2)) { float num2 = tagWidth?.Invoke(item2) ?? 0f; if (num2 > 0f && num > 0f && num + num2 > width) { stringBuilder.Append('\n'); num = 0f; } stringBuilder.Append(item2); num += num2; continue; } float num3 = 0f; string text2 = item2; foreach (char arg in text2) { num3 += advance(arg); } if (num > 0f && num + num3 > width) { stringBuilder.Append('\n'); num = 0f; } if (num == 0f && item2 == " ") { continue; } text2 = item2; foreach (char c in text2) { float num4 = advance(c); if (num > 0f && num + num4 > width) { stringBuilder.Append('\n'); num = 0f; } stringBuilder.Append(c); num += num4; } } return stringBuilder.ToString(); } } internal sealed class FontChoice { internal Font Font; internal int Size; internal FontStyle Style; internal float Scale = 1f; } internal sealed class FontService : IDisposable { private readonly Action<string> warn; private readonly Action<string> info; private readonly string combinedFile; private readonly Dictionary<string, Font> fonts = new Dictionary<string, Font>(StringComparer.Ordinal); private readonly Dictionary<string, AssetBundle> bundles = new Dictionary<string, AssetBundle>(StringComparer.OrdinalIgnoreCase); private readonly HashSet<string> privateFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase); private readonly List<Font> ownedFonts = new List<Font>(); private static readonly Regex RichTags = new Regex("</?(?:b|i|size|color|material)(?:=[^>]*)?>|<quad\\s[^>]*>", RegexOptions.IgnoreCase); private const uint PrivateFont = 16u; internal bool HasCombinedFont => combinedFile != null; [DllImport("gdi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] private static extern int AddFontResourceEx(string file, uint flags, IntPtr reserved); [DllImport("gdi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] private static extern bool RemoveFontResourceEx(string file, uint flags, IntPtr reserved); internal FontService(Action<string> warn, Action<string> info, string root = null) { this.warn = warn; this.info = info; if (root != null) { string path = Path.Combine(Path.Combine(root, "Fonts"), "crawl-combined.bundle"); if (File.Exists(path)) { combinedFile = path; } } } internal bool TryChoose(LanguagePack language, Font original, int size, FontStyle style, string text, out FontChoice choice) { //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_011c: Unknown result type (might be due to invalid IL or missing references) //IL_0091: Unknown result type (might be due to invalid IL or missing references) //IL_013b: Unknown result type (might be due to invalid IL or missing references) //IL_013d: Unknown result type (might be due to invalid IL or missing references) //IL_00b0: Unknown result type (might be due to invalid IL or missing references) //IL_00b2: Unknown result type (might be due to invalid IL or missing references) //IL_00d9: Unknown result type (might be due to invalid IL or missing references) //IL_01a9: Unknown result type (might be due to invalid IL or missing references) //IL_01ca: Unknown result type (might be due to invalid IL or missing references) //IL_01cc: Unknown result type (might be due to invalid IL or missing references) //IL_01d4: Unknown result type (might be due to invalid IL or missing references) choice = null; if (TryCombined(((Object)(object)original == (Object)null) ? null : ((Object)original).name, size, style, text, gui: false, out choice)) { return true; } if (language.Fonts.TryGetValue(((Object)(object)original == (Object)null) ? "" : ((Object)original).name, out var value) || language.Fonts.TryGetValue("*", out value)) { Font val = Load(value); int num = ((value.Kind == "system") ? 32 : SamplingSize(language, size, text)); if (Covers(val, text, num, style)) { choice = new FontChoice { Font = val, Size = num, Style = style, Scale = value.Scale * ((value.Kind == "system") ? HeightRatio(original, size, style, val, num, text) : ((float)size / (float)num)) }; return true; } warn("Mapped font unavailable or missing glyphs: " + value.Name + "; trying original/system fonts."); } int num2 = SamplingSize(language, size, text); if (Covers(original, text, num2, style)) { choice = new FontChoice { Font = original, Size = num2, Style = style, Scale = (float)size / (float)num2 }; return true; } string[] fallbackFonts = language.FallbackFonts; foreach (string text2 in fallbackFonts) { Font val2 = Load(new FontRule { Kind = "system", File = "", Name = text2.Trim(), Scale = 1f }); if (Covers(val2, text, 32, style)) { choice = new FontChoice { Font = val2, Size = 32, Style = style, Scale = HeightRatio(original, size, style, val2, 32, text) }; return true; } } warn("No font covers translation: " + text + "; retaining original menu text."); return false; } internal bool TryChooseMirror(LanguagePack language, Font original, int size, FontStyle style, string text, out FontChoice choice) { //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) //IL_009b: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Unknown result type (might be due to invalid IL or missing references) choice = null; if (TryCombined(((Object)(object)original == (Object)null) ? null : ((Object)original).name, size, style, text, gui: false, out choice)) { return true; } if (!language.Fonts.TryGetValue(((Object)(object)original == (Object)null) ? "" : ((Object)original).name, out var value) && !language.Fonts.TryGetValue("*", out value)) { return false; } Font font = Load(value); int num = SamplingSize(language, size, text); if (!Covers(font, text, num, style)) { return false; } choice = new FontChoice { Font = font, Size = num, Style = style, Scale = (float)size / (float)num }; return true; } internal bool TryChooseUntranslated(LanguagePack language, Font original, int size, FontStyle style, string text, out FontChoice choice) { //IL_0038: Unknown result type (might be due to invalid IL or missing references) choice = null; if (!HasCombinedFont || text == null) { return false; } for (int i = 0; i < text.Length; i++) { if (text[i] > '\u007f') { return TryCombined(((Object)(object)original == (Object)null) ? null : ((Object)original).name, size, style, text, gui: false, out choice); } } return false; } private bool TryCombined(string name, int size, FontStyle style, string text, bool gui, out FontChoice choice) { //IL_00d7: Unknown result type (might be due to invalid IL or missing references) //IL_00f7: Unknown result type (might be due to invalid IL or missing references) //IL_00f8: Unknown result type (might be due to invalid IL or missing references) choice = null; if (!HasCombinedFont || string.IsNullOrEmpty(name)) { return false; } if (Array.IndexOf(new string[10] { "CrawlLarge", "CrawlSmall", "CrawlMedium", "CrawlTiny", "CrawlInvLarge", "CrawlTextBodyThin", "CrawlTextBodyWide", "CrawlTextBodyTeensy", "04B_03-White", "04B_04-Black" }, name) < 0) { return false; } Font font = Load(new FontRule { Kind = "bundle", File = combinedFile, Name = "assets/fonts/" + name.ToLowerInvariant() + ".ttf", Scale = 1f }); int num = (gui ? size : HanSamplingSize(size, text)); if (!Covers(font, text, num, style)) { return false; } choice = new FontChoice { Font = font, Size = num, Style = style, Scale = (float)size / (float)num }; return true; } internal bool TryChooseGui(LanguagePack language, Font original, int size, FontStyle style, string text, out Font font) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_00b7: Unknown result type (might be due to invalid IL or missing references) font = original; if (TryCombined("CrawlTextBodyThin", size, style, text, gui: true, out var choice)) { font = choice.Font; return true; } if (language.Fonts.TryGetValue("CrawlOnlineOverlay", out var value)) { Font val = Load(value); if (Covers(val, text, size, style)) { font = val; return true; } } if (Covers(original, text, size, style)) { return true; } string[] fallbackFonts = language.FallbackFonts; foreach (string text2 in fallbackFonts) { Font val2 = Load(new FontRule { Kind = "system", File = "", Name = text2.Trim(), Scale = 1f }); if (Covers(val2, text, size, style)) { font = val2; return true; } } return false; } internal static int SamplingSize(LanguagePack language, int size, string text) { if (!language.UsesChineseTextRules) { return size; } return HanSamplingSize(size, text); } private static int HanSamplingSize(int size, string text) { if (size <= 0 || size >= 64 || text == null) { return size; } foreach (char c in text) { if (c >= '㐀' && c <= '鿿') { return size * ((160 + size - 1) / size); } } return size; } private Font Load(FontRule rule) { string text = rule.Kind + "|" + rule.File + "|" + rule.Name; if (fonts.TryGetValue(text, out var value)) { return value; } value = null; try { if (rule.Kind == "bundle") { if (!bundles.TryGetValue(rule.File, out var value2)) { if (!File.Exists(rule.File)) { throw new FileNotFoundException("Font bundle not found.", rule.File); } value2 = AssetBundle.LoadFromFile(rule.File); bundles.Add(rule.File, value2); } if ((Object)(object)value2 == (Object)null) { throw new InvalidOperationException("Cannot load bundle; use a Windows bundle compatible with Unity 5.4.2."); } value = value2.LoadAsset<Font>(rule.Name); if ((Object)(object)value == (Object)null) { throw new InvalidOperationException("Font asset not found: " + rule.Name); } } else { if (rule.Kind == "ttf" && !privateFiles.Contains(rule.File)) { if (Environment.OSVersion.Platform != PlatformID.Win32NT) { throw new PlatformNotSupportedException("Private TTF loading requires Windows. Use a compatible bundle on other platforms."); } if (!File.Exists(rule.File)) { throw new FileNotFoundException("Font file not found.", rule.File); } if (AddFontResourceEx(rule.File, 16u, IntPtr.Zero) == 0) { throw new InvalidOperationException("Windows rejected font file (error " + Marshal.GetLastWin32Error() + ")."); } privateFiles.Add(rule.File); } if (rule.Kind == "system" && !Array.Exists(Font.GetOSInstalledFontNames(), (string name) => string.Equals(name, rule.Name, StringComparison.OrdinalIgnoreCase))) { throw new InvalidOperationException("System font is not installed: " + rule.Name); } value = Font.CreateDynamicFontFromOSFont(rule.Name, 32); if ((Object)(object)value == (Object)null) { throw new InvalidOperationException("Unity could not create font: " + rule.Name); } Object.DontDestroyOnLoad((Object)(object)value); ownedFonts.Add(value); } } catch (Exception ex) { warn("Font load failed [" + text + "]: " + ex.Message); } fonts[text] = value; if ((Object)(object)value != (Object)null) { info(((rule.Kind == "bundle") ? "Loaded embedded font: " : "Created font by family (file identity unverified): ") + text); } return value; } private bool Covers(Font font, string text, int size, FontStyle style) { //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Unknown result type (might be due to invalid IL or missing references) try { if ((Object)(object)font == (Object)null || (Object)(object)font.material == (Object)null) { return false; } text = RichTags.Replace(text ?? "", ""); if (font.dynamic) { font.RequestCharactersInTexture(text, size, style); } string text2 = text; CharacterInfo val = default(CharacterInfo); foreach (char c in text2) { if (!char.IsWhiteSpace(c) && !char.IsControl(c) && (!font.GetCharacterInfo(c, ref val, size, style) || ((CharacterInfo)(ref val)).glyphWidth <= 0 || ((CharacterInfo)(ref val)).glyphHeight <= 0)) { return false; } } return true; } catch (Exception ex) { warn("Font glyph check failed; trying next font: " + ex.Message); return false; } } private static float HeightRatio(Font original, int originalSize, FontStyle style, Font replacement, int size, string text) { //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Unknown result type (might be due to invalid IL or missing references) float num = (float)originalSize * 0.7f; CharacterInfo val = default(CharacterInfo); if ((Object)(object)original != (Object)null) { if (original.dynamic) { original.RequestCharactersInTexture("A", originalSize, style); } if (original.GetCharacterInfo('A', ref val, originalSize, style)) { num = Mathf.Max(1, ((CharacterInfo)(ref val)).maxY - ((CharacterInfo)(ref val)).minY); } } string text2 = RichTags.Replace(text, ""); foreach (char c in text2) { if (c > '\u007f' && !char.IsWhiteSpace(c) && replacement.GetCharacterInfo(c, ref val, size, style)) { return num / (float)Mathf.Max(1, ((CharacterInfo)(ref val)).maxY - ((CharacterInfo)(ref val)).minY); } } return num / Mathf.Max(1f, (float)size * 0.7f); } public void Dispose() { foreach (Font ownedFont in ownedFonts) { if ((Object)(object)ownedFont != (Object)null) { Object.Destroy((Object)(object)ownedFont); } } foreach (AssetBundle value in bundles.Values) { if ((Object)(object)value != (Object)null) { value.Unload(true); } } foreach (string privateFile in privateFiles) { RemoveFontResourceEx(privateFile, 16u, IntPtr.Zero); } fonts.Clear(); bundles.Clear(); ownedFonts.Clear(); privateFiles.Clear(); } } [HarmonyPatch(typeof(GUIActionIndicator), "SetText")] internal static class GameplayActionPatch { private static void Postfix(string text, TextMesh ___m_textMesh) { if ((Object)(object)___m_textMesh == (Object)null || (Object)(object)Plugin.Instance == (Object)null) { return; } LocalizedMenuLabel localizedMenuLabel = ((Component)___m_textMesh).GetComponent<LocalizedMenuLabel>(); if ((Object)(object)localizedMenuLabel == (Object)null) { if (!Plugin.Instance.Language.TryTranslate(text, out var _)) { return; } localizedMenuLabel = LocalizedMenuLabel.GetOrCreate(((Component)___m_textMesh).gameObject); } localizedMenuLabel.Skip = false; localizedMenuLabel.RefreshDirect(text, null); } } [HarmonyPatch] internal static class GameplayStaticTextPatch { private static IEnumerable<MethodBase> TargetMethods() { Type[] array = new Type[5] { typeof(PropPortal), typeof(PropSign), typeof(PropStoreTeleporter), typeof(PropActionable), typeof(GUIGame) }; foreach (Type type in array) { yield return AccessTools.Method(type, "Start", (Type[])null, (Type[])null); } } private static void Postfix(MonoBehaviour __instance) { if ((Object)(object)Plugin.Instance != (Object)null) { MenuTextMeshes.Scan(((Component)__instance).gameObject); } } } [HarmonyPatch(typeof(GUIPlayer), "Start")] internal static class GameplayRewardPatch { private static void Postfix(GUIPlayer __instance, MenuFlashEffect ___m_favourTallyContainer) { if ((Object)(object)Plugin.Instance == (Object)null) { return; } if ((Object)(object)___m_favourTallyContainer != (Object)null) { TextMesh[] componentsInChildren = ((Component)___m_favourTallyContainer).GetComponentsInChildren<TextMesh>(true); foreach (TextMesh val in componentsInChildren) { string text = null; if (string.Equals(val.text, "gold", StringComparison.OrdinalIgnoreCase)) { text = "@gameplay.reward-unit:gold"; } else if (string.Equals(val.text, "REWARD", StringComparison.OrdinalIgnoreCase)) { text = "REWARD"; } if (text != null) { LocalizedMenuLabel.GetOrCreate(((Component)val).gameObject).RefreshDirect(text, null); } } } MenuTextMeshes.Scan(((Component)__instance).gameObject); } } [HarmonyPatch(typeof(GUIGame), "ShowKillStreak")] internal static class GameplayStreakPatch { private static void Postfix(GameObject ___m_killStreak) { if (!((Object)(object)Plugin.Instance == (Object)null) && !((Object)(object)___m_killStreak == (Object)null)) { TextMesh component = ___m_killStreak.GetComponent<TextMesh>(); if ((Object)(object)component != (Object)null) { LocalizedMenuLabel.GetOrCreate(((Component)component).gameObject).RefreshDirect(component.text, null); } } } } internal static class HeroStatsDisplay { internal static void Refresh(TextMesh mesh, string lookup, string fallback) { if (!((Object)(object)mesh == (Object)null) && !((Object)(object)Plugin.Instance == (Object)null)) { LocalizedMenuLabel.GetOrCreate(((Component)mesh).gameObject).RefreshDirect(lookup, fallback); } } } [HarmonyPatch(typeof(GUIHeroStats), "UpdateText")] internal static class HeroStatsNamesPatch { private static void Postfix(PlayerGameData ___m_player, TextMesh ___m_weaponNoSplit, TextMesh ___m_weaponSplit1, TextMesh ___m_weaponSplit2, TextMesh ___m_spell) { if ((Object)(object)Plugin.Instance == (Object)null || ___m_player == null || ___m_player.GetWeaponData() == null) { return; } if ((Object)(object)___m_weaponNoSplit != (Object)null && ((Component)___m_weaponNoSplit).gameObject.activeSelf) { HeroStatsDisplay.Refresh(___m_weaponNoSplit, ___m_weaponNoSplit.text, null); } else { if ((Object)(object)___m_weaponSplit1 != (Object)null) { HeroStatsDisplay.Refresh(___m_weaponSplit1, "@shop.prefix:" + ___m_weaponSplit1.text, null); } if ((Object)(object)___m_weaponSplit2 != (Object)null) { HeroStatsDisplay.Refresh(___m_weaponSplit2, ___m_weaponSplit2.text, null); } } if ((Object)(object)___m_spell != (Object)null) { HeroStatsDisplay.Refresh(___m_spell, ___m_spell.text, null); } } } [HarmonyPatch(typeof(GUIHeroStats), "UpdateArtifactIcons")] internal static class HeroStatsArtifactsPatch { private static void Prefix(int ___m_numArtifacts, out int __state) { __state = ___m_numArtifacts; } private static void Postfix(bool onlyIfCountChanged, int __state, int ___m_numArtifacts, PlayerGameData ___m_player, List<GUIArtifactIcon> ___m_artifactIcons) { //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_003b: Invalid comparison between Unknown and I4 if ((Object)(object)Plugin.Instance == (Object)null || ___m_player == null || ___m_artifactIcons == null || (onlyIfCountChanged && __state == ___m_numArtifacts)) { return; } int num = 0; foreach (Powerup powerup in ___m_player.GetPowerups()) { if ((int)powerup.GetPurchasableType() == 2) { if (num >= ___m_numArtifacts || num >= ___m_artifactIcons.Count) { break; } GUIArtifactIcon val = ___m_artifactIcons[num++]; if (!((Object)(object)val == (Object)null)) { string lookup = (string.IsNullOrEmpty(powerup.m_displayNameHud) ? powerup.m_displayName : powerup.m_displayNameHud); HeroStatsDisplay.Refresh(val.m_text, lookup, powerup.m_displayName); } } } } } internal sealed class FontRule { internal string Kind; internal string File; internal string Name; internal float Scale; } internal sealed class LanguagePack { internal string Id; internal string[] FallbackFonts = new string[0]; internal readonly Dictionary<string, FontRule> Fonts = new Dictionary<string, FontRule>(StringComparer.Ordinal); private readonly Dictionary<string, string> exact = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); private readonly List<NumericTranslation> numeric = new List<NumericTranslation>(); internal readonly HashSet<string> TextMeshPaths = new HashSet<string>(StringComparer.Ordinal); internal bool UsesChineseTextRules { get { if (Id != null) { if (!string.Equals(Id, "zh", StringComparison.OrdinalIgnoreCase) && !Id.StartsWith("zh-", StringComparison.OrdinalIgnoreCase)) { return Id.StartsWith("zh_", StringComparison.OrdinalIgnoreCase); } return true; } return false; } } internal int Count => exact.Count + numeric.Count; internal static string NormalizeId(string id) { id = (id ?? "").Trim(); if (string.Equals(id, "zh-CN", StringComparison.OrdinalIgnoreCase) || string.Equals(id, "zh-Hans", StringComparison.OrdinalIgnoreCase)) { return "zh-Hans"; } if (string.Equals(id, "zh-TW", StringComparison.OrdinalIgnoreCase) || string.Equals(id, "zh-Hant", StringComparison.OrdinalIgnoreCase)) { return "zh-Hant"; } if (string.Equals(id, "en", StringComparison.OrdinalIgnoreCase)) { return "en"; } if (!Regex.IsMatch(id, "\\A[A-Za-z0-9][A-Za-z0-9_-]*\\z")) { return "en"; } return id; } internal static string ResolveFile(string root, string relative) { if (string.IsNullOrEmpty(relative) || Path.IsPathRooted(relative)) { throw new FormatException("Expected a relative file path."); } string text = Path.GetFullPath(root).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); char directorySeparatorChar = Path.DirectorySeparatorChar; string text2 = text + directorySeparatorChar; string fullPath = Path.GetFullPath(Path.Combine(text2, relative)); if (!fullPath.StartsWith(text2, StringComparison.OrdinalIgnoreCase)) { throw new FormatException("File must stay inside the plugin folder."); } return fullPath; } internal static LanguagePack Load(string root, string id, Action<string> warn) { LanguagePack pack = new LanguagePack { Id = NormalizeId(id) }; if (pack.Id == "en") { return pack; } string path = Path.Combine(Path.Combine(root, "Languages"), pack.Id); string[] array = new string[10] { "translations.tsv", "vault-translations.tsv", "deity-translations.tsv", "chapter-translations.tsv", "shop-translations.tsv", "gameplay-translations.tsv", "progression-translations.tsv", "shop-il-translations.tsv", "crawlonline-translations.tsv", "tutorial-translations.tsv" }; foreach (string text in array) { string path2 = Path.Combine(path, text); if (text != "translations.tsv" && !File.Exists(path2)) { continue; } ReadLines(path2, warn, delegate(string line) { string[] array2 = line.Split(new char[1] { '\t' }); if ((array2.Length != 2 && array2.Length != 3) || array2[0].Length == 0 || (array2[1].Length == 0 && !array2[0].StartsWith("@shop.il:", StringComparison.Ordinal))) { throw new FormatException("Expected source<TAB>translation[<TAB>comment]."); } if (!(array2[1] == "@TODO")) { string text2 = NormalizeText(Unescape(array2[0])); string text3 = Unescape(array2[1]); NumericTranslation numericTranslation = NumericTranslation.Create(text2, text3); if (text2.StartsWith("@shop.", StringComparison.Ordinal) || text2.StartsWith("@progression.", StringComparison.Ordinal) || text2.StartsWith("@online:", StringComparison.Ordinal)) { numericTranslation = null; } if (numericTranslation != null) { foreach (NumericTranslation item in pack.numeric) { if (string.Equals(item.Source, text2, StringComparison.OrdinalIgnoreCase)) { throw new FormatException("Duplicate source: " + text2); } } pack.numeric.Add(numericTranslation); } else { if (pack.exact.ContainsKey(text2)) { throw new FormatException("Duplicate source: " + text2); } pack.exact.Add(text2, text3); } } }); } ReadLines(Path.Combine(path, "fonts.tsv"), warn, delegate(string line) { string[] array2 = line.Split(new char[1] { '\t' }); if (array2.Length != 5 || array2[0].Length == 0 || array2[3].Length == 0 || !float.TryParse(array2[4], NumberStyles.Float, CultureInfo.InvariantCulture, out var result) || float.IsNaN(result) || float.IsInfinity(result) || result <= 0f || result > 10f) { throw new FormatException("Expected original<TAB>kind<TAB>file<TAB>family/asset<TAB>scale (0..10)."); } if (array2[1] != "ttf" && array2[1] != "bundle" && array2[1] != "system") { throw new FormatException("Font kind must be ttf, bundle or system."); } string file = ((array2[1] == "system") ? "" : ResolveFile(root, array2[2])); if (pack.Fonts.ContainsKey(array2[0])) { throw new FormatException("Duplicate font mapping: " + array2[0]); } pack.Fonts.Add(array2[0], new FontRule { Kind = array2[1], File = file, Name = array2[3], Scale = result }); }); ReadLines(Path.Combine(path, "language.ini"), warn, delegate(string line) { int num = line.IndexOf('='); if (num <= 0) { throw new FormatException("Expected key=value."); } string text2 = line.Substring(0, num).Trim(); if (text2 == "FallbackFonts") { pack.FallbackFonts = line.Substring(num + 1).Split(new char[1] { ';' }, StringSplitOptions.RemoveEmptyEntries); } else if (text2 != "Name") { throw new FormatException("Unknown setting: " + text2); } }); string path3 = Path.Combine(path, "textmesh-paths.txt"); if (File.Exists(path3)) { ReadLines(path3, warn, delegate(string line) { pack.TextMeshPaths.Add(line.Trim()); }); } return pack; } internal static string[] ReadTextLines(string path) { using FileStream stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); using StreamReader streamReader = new StreamReader(stream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true)); List<string> list = new List<string>(); string item; while ((item = streamReader.ReadLine()) != null) { list.Add(item); } return list.ToArray(); } private static void ReadLines(string path, Action<string> warn, Action<string> read) { try { string[] array = ReadTextLines(path); for (int i = 0; i < array.Length; i++) { if (array[i].Trim().Length != 0 && !array[i].StartsWith("#", StringComparison.Ordinal)) { try { read(array[i]); } catch (Exception ex) { warn(path + ":" + (i + 1) + ": " + ex.Message); } } } } catch (Exception ex2) { warn("Cannot read " + path + ": " + ex2.Message + "; keeping original text/font where needed."); } } internal bool TryTranslate(string source, out string translated) { translated = null; if (source == null) { return false; } source = NormalizeText(source); if (exact.TryGetValue(source, out translated)) { return true; } foreach (NumericTranslation item in numeric) { if (item.TryTranslate(source, out translated)) { return true; } } return false; } internal static string NormalizeText(string text) { return text.Replace("\r\n", "\n").Replace('\r', '\n'); } private static string Unescape(string value) { StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < value.Length; i++) { if (value[i] != '\\') { stringBuilder.Append(value[i]); continue; } if (++i == value.Length) { throw new FormatException("Trailing backslash; escape literal backslashes."); } switch (value[i]) { case 'n': stringBuilder.Append('\n'); break; case 'r': stringBuilder.Append('\r'); break; case 't': stringBuilder.Append('\t'); break; case '\\': stringBuilder.Append('\\'); break; default: throw new FormatException("Unsupported escape sequence."); } } return stringBuilder.ToString(); } } [HarmonyPatch(typeof(MenuTextMenuItem), "Initialise")] internal static class MenuInitialisePatch { private static void Prefix(MenuTextMenuItem __instance, MenuTextMenuItemData data) { if (!((Object)(object)Plugin.Instance == (Object)null) && data != null) { LocalizedMenuLabel orCreate = LocalizedMenuLabel.GetOrCreate(((Component)__instance).gameObject); if (!((Object)(object)orCreate == (Object)null)) { orCreate.Skip = data.m_message == "MsgCharSelect" || data.m_message == "MsgNameSelect"; orCreate.ResetSource(data.m_text); } } } private static void Postfix(MenuTextMenuItem __instance) { MenuTextMeshes.Scan(((Component)__instance).gameObject); } } [HarmonyPatch(typeof(MenuItemTextButton), "Initialise")] internal static class TextButtonInitialisePatch { private static void Postfix(MenuItemTextButton __instance) { LocalizedMenuLabel component = ((Component)__instance).GetComponent<LocalizedMenuLabel>(); if ((Object)(object)component != (Object)null) { component.Refresh(); } } } [HarmonyPatch(typeof(GUIText), "SetText")] internal static class MenuDisplayPatch { private static void Prefix(GUIText __instance, ref string text) { if (!((Object)(object)Plugin.Instance == (Object)null)) { LocalizedMenuLabel localizedMenuLabel = ((Component)__instance).GetComponent<LocalizedMenuLabel>(); if ((Object)(object)localizedMenuLabel == (Object)null && (Object)(object)((Component)__instance).GetComponent<MenuTextMenuItem>() != (Object)null) { localizedMenuLabel = LocalizedMenuLabel.GetOrCreate(((Component)__instance).gameObject); } if ((Object)(object)localizedMenuLabel == (Object)null) { localizedMenuLabel = MenuTextMeshes.Bind(((Component)__instance).gameObject, refresh: false); } if ((Object)(object)localizedMenuLabel != (Object)null) { text = localizedMenuLabel.Translate(text); } } } private static void Postfix(GUIText __instance) { LocalizedMenuLabel component = ((Component)__instance).GetComponent<LocalizedMenuLabel>(); if ((Object)(object)component != (Object)null) { component.CaptureRendered(); } } } [HarmonyPatch(typeof(MenuFlashEffect), "StartFlash")] internal static class MenuFlashPatch { private static void Prefix(MenuFlashEffect __instance) { LocalizedMenuLabel component = ((Component)__instance).GetComponent<LocalizedMenuLabel>(); if ((Object)(object)component != (Object)null) { component.Refresh(); } } } [HarmonyPatch(typeof(GUIText), "SetFontScale")] internal static class MenuFontScalePatch { private static void Prefix(GUIText __instance, ref float scale) { LocalizedMenuLabel component = ((Component)__instance).GetComponent<LocalizedMenuLabel>(); if (!((Object)(object)component == (Object)null)) { component.BaseScale = scale; scale *= component.ScaleFactor; } } } public sealed class LocalizedMenuLabel : MonoBehaviour { internal static Func<string, Func<LanguagePack, string>> ExternalProvider; private Func<LanguagePack, string> boundExternalProvider; internal bool Skip; internal bool Standalone; internal bool FontOnly; internal Func<LanguagePack, string> TextProvider; internal float BaseScale; internal float ScaleFactor = 1f; private TextMesh mesh; private Renderer textRenderer; private Font originalFont; private Material originalMaterial; private Material localMaterial; private int originalSize; private FontStyle originalStyle; private string source; private string display; private string rendered; private string directLookup; private string directFallback; private bool usesDirectLookup; private bool mirrored; internal static LocalizedMenuLabel GetOrCreate(GameObject obj, bool fontOnly = false) { //IL_009a: Unknown result type (might be due to invalid IL or missing references) //IL_009f: Unknown result type (might be due to invalid IL or missing references) LocalizedMenuLabel component = obj.GetComponent<LocalizedMenuLabel>(); if ((Object)(object)component != (Object)null) { if (component.FontOnly && !fontOnly) { component.FontOnly = false; component.Skip = false; } return component; } TextMesh component2 = obj.GetComponent<TextMesh>(); if ((Object)(object)component2 == (Object)null) { return null; } component = obj.AddComponent<LocalizedMenuLabel>(); component.mesh = component2; component.FontOnly = fontOnly; component.textRenderer = ((Component)component2).GetComponent<Renderer>(); component.originalFont = component2.font; component.originalMaterial = (((Object)(object)component.textRenderer == (Object)null) ? null : component.textRenderer.sharedMaterial); component.originalSize = component2.fontSize; component.originalStyle = component2.fontStyle; component.BaseScale = component2.characterSize; Font.textureRebuilt += component.OnTextureRebuilt; Plugin.Instance.Labels.Add(component); return component; } private void BindExternal(string text) { if (FontOnly || Skip || ExternalProvider == null) { return; } Func<LanguagePack, string> func = ExternalProvider(text); if (func != null) { TextProvider = func; boundExternalProvider = func; } else if (boundExternalProvider != null && text != source && text != display) { if ((object)TextProvider == boundExternalProvider) { TextProvider = null; } boundExternalProvider = null; } } internal void ResetSource(string text) { BindExternal(text); source = text; display = null; } internal void RefreshDirect(string lookup, string fallback) { FontOnly = false; string text = mesh.text; if (usesDirectLookup && source == text && directLookup == lookup && directFallback == fallback && rendered != null) { mesh.text = rendered; return; } directLookup = lookup; directFallback = fallback; if (!usesDirectLookup) { TextProvider = TranslateDirect; usesDirectLookup = true; } ResetSource(text); Refresh(); } private string TranslateDirect(LanguagePack pack) { if (pack.TryTranslate(directLookup, out var translated)) { return translated; } if (directFallback == null || !pack.TryTranslate(directFallback, out translated)) { return null; } return translated; } internal string Translate(string text) { //IL_00cd: Unknown result type (might be due to invalid IL or missing references) //IL_013b: Unknown result type (might be due to invalid IL or missing references) //IL_0113: Unknown result type (might be due to invalid IL or missing references) BindExternal(text); if (display == null || text != display) { source = text; } try { string translated = null; Plugin instance = Plugin.Instance; int size = ((originalSize > 0) ? originalSize : (((Object)(object)originalFont != (Object)null && originalFont.fontSize > 0) ? originalFont.fontSize : 16)); bool flag = false; if (!Skip && !FontOnly) { translated = ((TextProvider == null) ? null : TextProvider(instance.Language)); flag = translated != null || instance.Language.TryTranslate(source, out translated); } if (flag && instance.Fonts.TryChoose(instance.Language, originalFont, size, originalStyle, translated, out var choice)) { Apply(choice); display = translated; return display; } if (mirrored ? instance.Fonts.TryChooseMirror(instance.Language, originalFont, size, originalStyle, source, out choice) : instance.Fonts.TryChooseUntranslated(instance.Language, originalFont, size, originalStyle, source, out choice)) { Apply(choice); display = source; return source; } } catch (Exception ex) { Plugin.Instance.Warn("Menu localization failed for " + source + ": " + ex.Message); } Restore(restoreText: false); display = source; return source; } internal void CaptureMirrorSource() { //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) mirrored = true; originalFont = mesh.font; originalSize = mesh.fontSize; originalStyle = mesh.fontStyle; BaseScale = mesh.characterSize; originalMaterial = (((Object)(object)textRenderer == (Object)null) ? null : textRenderer.sharedMaterial); if ((Object)(object)localMaterial != (Object)null && (Object)(object)originalMaterial != (Object)null) { localMaterial.shader = originalMaterial.shader; localMaterial.CopyPropertiesFromMaterial(originalMaterial); } FontOnly = true; Standalone = false; ResetSource(mesh.text); Refresh(); EnsureMirrorAtlas(); } private void EnsureMirrorAtlas() { //IL_00be: Unknown result type (might be due to invalid IL or missing references) //IL_00c3: Unknown result type (might be due to invalid IL or missing references) if (mirrored && !((Object)(object)mesh == (Object)null) && !((Object)(object)mesh.font == (Object)null) && !((Object)(object)mesh.font.material == (Object)null) && !((Object)(object)textRenderer == (Object)null) && ((Object)(object)textRenderer.sharedMaterial == (Object)null || (Object)(object)textRenderer.sharedMaterial.mainTexture != (Object)(object)mesh.font.material.mainTexture)) { Apply(new FontChoice { Font = mesh.font, Size = mesh.fontSize, Style = mesh.fontStyle, Scale = ScaleFactor }); } } private void Apply(FontChoice choice) { //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_00f3: Unknown result type (might be due to invalid IL or missing references) //IL_00fd: Expected O, but got Unknown ScaleFactor = choice.Scale; mesh.font = choice.Font; mesh.fontSize = choice.Size; mesh.fontStyle = choice.Style; mesh.characterSize = BaseScale * ScaleFactor; if ((Object)(object)textRenderer == (Object)null) { return; } if ((Object)(object)choice.Font == (Object)(object)originalFont && (!mirrored || ((Object)(object)originalMaterial != (Object)null && (Object)(object)originalMaterial.mainTexture == (Object)(object)choice.Font.material.mainTexture))) { textRenderer.sharedMaterial = originalMaterial; return; } if ((Object)(object)localMaterial == (Object)null) { localMaterial = new Material(((Object)(object)originalMaterial != (Object)null) ? originalMaterial : choice.Font.material); } textRenderer.sharedMaterial = localMaterial; UpdateAtlas(); } private void OnTextureRebuilt(Font font) { if (!((Object)(object)mesh == (Object)null) && !((Object)(object)font != (Object)(object)mesh.font)) { if (mirrored) { EnsureMirrorAtlas(); } if ((Object)(object)font != (Object)(object)originalFont || mirrored) { UpdateAtlas(); } } } private void UpdateAtlas() { if (!((Object)(object)mesh == (Object)null) && !((Object)(object)mesh.font == (Object)null) && !((Object)(object)mesh.font.material == (Object)null) && !((Object)(object)localMaterial == (Object)null)) { Texture mainTexture = mesh.font.material.mainTexture; if ((Object)(object)mainTexture != (Object)null) { mainTexture.filterMode = (FilterMode)0; } localMaterial.mainTexture = mainTexture; if ((Object)(object)textRenderer != (Object)null && (Object)(object)textRenderer.sharedMaterial != (Object)null && (Object)(object)textRenderer.sharedMaterial != (Object)(object)originalMaterial) { textRenderer.sharedMaterial.mainTexture = mainTexture; } } } internal void Refresh() { if (source != null && !((Object)(object)mesh == (Object)null)) { GUIText component = ((Component)this).GetComponent<GUIText>(); if ((Object)(object)component != (Object)null) { component.SetText(source); } else { mesh.text = Translate(source); } if (!((Component)this).gameObject.activeInHierarchy) { mesh.text = display ?? source; } CaptureRendered(); } } internal void CaptureRendered() { if ((Object)(object)mesh != (Object)null) { rendered = mesh.text; } } private void LateUpdate() { RefreshChangedText(); } internal void RefreshChangedText() { if (Standalone && !((Object)(object)mesh == (Object)null) && !(mesh.text == rendered)) { mesh.text = Translate(mesh.text); CaptureRendered(); } } internal void Restore(bool restoreText) { //IL_0038: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)mesh == (Object)null) { return; } mesh.font = originalFont; mesh.fontSize = originalSize; mesh.fontStyle = originalStyle; mesh.characterSize = BaseScale; ScaleFactor = 1f; if ((Object)(object)textRenderer != (Object)null) { textRenderer.sharedMaterial = originalMaterial; } if (restoreText && source != null) { GUIText component = ((Component)this).GetComponent<GUIText>(); if ((Object)(object)component != (Object)null) { component.SetText(source); } mesh.text = source; } } private void OnDestroy() { Font.textureRebuilt -= OnTextureRebuilt; if ((Object)(object)Plugin.Instance != (Object)null) { Plugin.Instance.Labels.Remove(this); } if ((Object)(object)localMaterial != (Object)null) { Object.Destroy((Object)(object)localMaterial); } } } internal static class MenuTextMeshes { internal static LocalizedMenuLabel Bind(GameObject obj, bool refresh) { Plugin instance = Plugin.Instance; if ((Object)(object)instance == (Object)null || (Object)(object)obj.GetComponent<MenuTextMenuItem>() != (Object)null) { return null; } LocalizedMenuLabel component = obj.GetComponent<LocalizedMenuLabel>(); List<string> list = new List<string>(); Transform val = obj.transform; while ((Object)(object)val != (Object)null) { list.Insert(0, ((Object)val).name); val = val.parent; } bool flag = MenuTextPolicy.MatchesPath(instance.Language.TextMeshPaths, string.Join("/", list.ToArray())); if (!flag && !instance.Fonts.HasCombinedFont) { return component; } if ((Object)(object)component != (Object)null && (!component.FontOnly || !flag)) { return component; } TextMesh component2 = obj.GetComponent<TextMesh>(); if ((Object)(object)component2 == (Object)null) { return null; } LocalizedMenuLabel orCreate = LocalizedMenuLabel.GetOrCreate(obj, !flag); orCreate.Standalone = true; orCreate.ResetSource(component2.text); if (refresh) { orCreate.Refresh(); } return orCreate; } internal static void Scan(GameObject root) { TextMesh[] componentsInChildren = root.GetComponentsInChildren<TextMesh>(true); for (int i = 0; i < componentsInChildren.Length; i++) { Bind(((Component)componentsInChildren[i]).gameObject, refresh: true); } } internal static void ScanLoadedScenes() { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000a: Unknown result type (might be due to invalid IL or missing references) for (int i = 0; i < SceneManager.sceneCount; i++) { Scene sceneAt = SceneManager.GetSceneAt(i); if (((Scene)(ref sceneAt)).isLoaded) { GameObject[] rootGameObjects = ((Scene)(ref sceneAt)).GetRootGameObjects(); for (int j = 0; j < rootGameObjects.Length; j++) { Scan(rootGameObjects[j]); } } } } } [HarmonyPatch(typeof(MenuStateMachine), "SetState", new Type[] { typeof(int) })] internal static class MenuStateTextPatch { private static void Postfix(MenuStateMachine __instance) { MenuTextMeshes.Scan(((Component)__instance).gameObject); } } [HarmonyPatch(typeof(GUIText), "OnEnable")] internal static class MenuStaticTextEnablePatch { private static void Postfix(GUIText __instance) { MenuTextMeshes.Bind(((Component)__instance).gameObject, refresh: true); } } [HarmonyPatch(typeof(MenuOptions), "SetItemTextReplayTutorial")] internal static class ReplayTutorialDisplayPatch { private static bool Prefix(MenuTextMenu ___m_menuOptions) { if ((Object)(object)___m_menuOptions == (Object)null) { return true; } bool startTutorialHACK = Singleton<SystemUnlocks>.Get.GetStartTutorialHACK(); ___m_menuOptions.SetItemString(4, MenuTextPolicy.ReplayTutorialText(startTutorialHACK, SystemTutorial.GetShouldStartTutorial())); return false; } } internal static class MenuTextPolicy { internal static string ReplayTutorialText(bool requested, bool allowed) { if (requested) { if (!allowed) { return "Replay Tutorial: ON (unavailable)"; } return "Replay Tutorial: ON"; } return "Replay Tutorial: OFF"; } internal static bool MatchesPath(IEnumerable<string> paths, string hierarchy) { hierarchy = hierarchy.Replace("(Clone)", ""); hierarchy = Regex.Replace(hierarchy, "(?<=MenuJoinPlayer)[1-4](?=/)", "*"); foreach (string path in paths) { if (hierarchy == path || hierarchy.EndsWith("/" + path, StringComparison.Ordinal)) { return true; } } return false; } } [HarmonyPatch(typeof(TextWrapper), "WrapText")] internal static class MenuChineseWrapPatch { private static bool Prefix(string input, float width, TextMesh ___textMesh, ref string __result) { //IL_0135: Unknown result type (might be due to invalid IL or missing references) //IL_010e: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)Plugin.Instance == (Object)null || !Plugin.Instance.Language.UsesChineseTextRules || (Object)(object)___textMesh == (Object)null || !ChineseTextWrapper.ContainsChinese(input) || (Object)(object)((Component)___textMesh).GetComponent<LocalizedMenuLabel>() == (Object)null || ((Component)___textMesh).GetComponent<LocalizedMenuLabel>().FontOnly || ((Component)___textMesh).GetComponent<LocalizedMenuLabel>().Skip || ((Object)(object)((Component)___textMesh).GetComponentInParent<MenuLibrary>() == (Object)null && (Object)(object)((Component)___textMesh).GetComponentInParent<MenuDeitySelectPlayer>() == (Object)null && (Object)(object)((Component)___textMesh).GetComponentInParent<GUIStore>() == (Object)null && (Object)(object)((Component)___textMesh).GetComponentInParent<MenuPrompt>() == (Object)null)) { return true; } TextMesh mesh = ___textMesh; Font font = mesh.font; if ((Object)(object)font == (Object)null) { return true; } int size = ((mesh.fontSize > 0) ? mesh.fontSize : font.fontSize); if (font.dynamic) { font.RequestCharactersInTexture(input, size, mesh.fontStyle); } float scale = mesh.characterSize * 0.1f * Mathf.Abs(((Component)mesh).transform.lossyScale.x); CharacterInfo val = default(CharacterInfo); __result = ChineseTextWrapper.Wrap(input, width, (char ch) => (!font.GetCharacterInfo(ch, ref val, size, mesh.fontStyle)) ? ((float)size * scale) : ((float)((CharacterInfo)(ref val)).advance * scale)); return false; } } internal sealed class NumericTranslation { private static readonly Regex Tokens = new Regex("\\{([0-9])\\}"); internal readonly string Source; private readonly string target; private readonly Regex pattern; private NumericTranslation(string source, string target, string pattern) { Source = source; this.target = target; this.pattern = new Regex(pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); } internal static NumericTranslation Create(string source, string target) { MatchCollection matchCollection = Tokens.Matches(source); if (matchCollection.Count == 0) { return null; } HashSet<string> hashSet = new HashSet<string>(); StringBuilder stringBuilder = new StringBuilder("\\A"); int num = 0; foreach (Match item in matchCollection) { stringBuilder.Append(Regex.Escape(source.Substring(num, item.Index - num))); string value = item.Groups[1].Value; stringBuilder.Append(hashSet.Add(value) ? ("(?<n" + value + ">[0-9]+)") : ("\\k<n" + value + ">")); num = item.Index + item.Length; } stringBuilder.Append(Regex.Escape(source.Substring(num))).Append("\\z"); HashSet<string> hashSet2 = new HashSet<string>(); foreach (Match item2 in Tokens.Matches(target)) { hashSet2.Add(item2.Groups[1].Value); } if (!hashSet.SetEquals(hashSet2)) { throw new FormatException("Numeric translation must preserve all source placeholders without adding new ones."); } return new NumericTranslation(source, target, stringBuilder.ToString()); } internal bool TryTranslate(string source, out string translated) { Match match = pattern.Match(source); translated = null; if (match.Success) { translated = Tokens.Replace(target, (Match token) => match.Groups["n" + token.Groups[1].Value].Value); } return match.Success; } } internal static class OnlineIntegration { internal static readonly string[] MenuMethods = new string[11] { "ItemText", "ShowPlayOnline", "ShowHostType", "ShowServerList", "ShowServerListResults", "JoinEntry", "ShowLobby", "ConfirmCloseLobby", "ShowError", "LobbyText", "UpdatePanel" }; internal static readonly string[] OverlayMethods = new string[6] { "Draw", "StatusLine", "DrawWindow", "DrawIdle", "DrawHost", "DrawClient" }; internal static void Install(Harmony harmony, Action<string> warn) { //IL_012d: Unknown result type (might be due to invalid IL or missing references) //IL_013b: Expected O, but got Unknown //IL_00b8: Unknown result type (might be due to invalid IL or missing references) //IL_00c4: Expected O, but got Unknown Type type = AccessTools.TypeByName("CrawlOnline.UI.OnlineMenu"); if ((object)type == null) { return; } LocalizedMenuLabel.ExternalProvider = Provider; Type type2 = AccessTools.TypeByName("CrawlOnline.UI.Overlay"); Type[] array = new Type[2] { type, type2 }; foreach (Type type3 in array) { if ((object)type3 == null) { continue; } string[] array2 = (((object)type3 == type) ? MenuMethods : OverlayMethods); foreach (string text in array2) { MethodInfo methodInfo = AccessTools.Method(type3, text, (Type[])null, (Type[])null); if ((object)methodInfo == null) { warn("CrawlOnline UI method unavailable: " + type3.Name + "." + text); continue; } try { harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(typeof(OnlineIntegration), "Transpiler", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null); } catch (Exception ex) { warn("CrawlOnline localization unavailable for " + text + ": " + ex.Message); } } } MethodInfo methodInfo2 = AccessTools.Method(type, "ShowError", (Type[])null, (Type[])null); if ((object)methodInfo2 == null) { return; } try { harmony.Patch((MethodBase)methodInfo2, new HarmonyMethod(typeof(OnlineIntegration), "ErrorPrefix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } catch (Exception ex2) { warn("CrawlOnline error-message localization unavailable: " + ex2.Message); } } internal static Func<LanguagePack, string> Provider(string source) { OnlineRecipe recipe = OnlineText.Find(source); if (recipe != null) { return (LanguagePack p) => (!(p.Id == "en")) ? recipe.Render(p) : null; } return null; } private static void ErrorPrefix(ref string message) { message = OnlineText.Message(message); } private static MethodInfo Wrapper(MethodBase method) { Type declaringType = method.DeclaringType; string text = null; ParameterInfo[] parameters = method.GetParameters(); if ((object)declaringType == typeof(StringBuilder)) { if (method is ConstructorInfo && parameters.Length == 0) { text = "Create"; } if (method.Name == "Append" && parameters.Length == 1) { text = (((object)parameters[0].ParameterType == typeof(string)) ? "Append" : (((object)parameters[0].ParameterType == typeof(char)) ? "AppendChar" : null)); } if (method.Name == "ToString" && parameters.Length == 0) { text = "Finish"; } } if ((object)declaringType == typeof(object) && method.Name == "ToString" && parameters.Length == 0) { text = "ObjectString"; } if ((object)declaringType == typeof(string) && method.Name == "Concat") { if (parameters.Length == 1) { text = (((object)parameters[0].ParameterType == typeof(string[])) ? "ConcatStrings" : (((object)parameters[0].ParameterType == typeof(object[])) ? "ConcatArray" : null)); } else if ((object)parameters[0].ParameterType == typeof(string)) { text = ((parameters.Length == 2) ? "ConcatTwo" : ((parameters.Length == 3) ? "ConcatThree" : ((parameters.Length == 4) ? "ConcatFour" : null))); } else if (parameters.Length == 3) { text = "ConcatObjects"; } } if (text != null) { return AccessTools.Method(typeof(OnlineText), text, (Type[])null, (Type[])null); } if ((object)declaringType == typeof(TextMesh) && method.Name == "set_text") { return AccessTools.Method(typeof(OnlineOverlay), "SetText", (Type[])null, (Type[])null); } if ((object)declaringType == typeof(GUILayout) || (object)declaringType == typeof(GUI)) { Type[] array = new Type[parameters.Length]; for (int i = 0; i < parameters.Length; i++) { array[i] = parameters[i].ParameterType; } return typeof(OnlineOverlay).GetMethod((((object)declaringType == typeof(GUI)) ? "Gui" : "") + method.Name, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, array, null); } return null; } internal static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions) { foreach (CodeInstruction original in instructions) { CodeInstruction val = new CodeInstruction(original); MethodBase method = val.operand as MethodBase; if ((object)method != null && (val.opcode == OpCodes.Call || val.opcode == OpCodes.Callvirt || val.opcode == OpCodes.Newobj)) { MethodInfo methodInfo = Wrapper(method); if ((object)methodInfo != null) { val.opcode = OpCodes.Call; val.operand = methodInfo; } } yield return val; string text = null; if (original.opcode == OpCodes.Ldstr) { text = "Literal"; } if (original.operand is FieldInfo fieldInfo && (object)fieldInfo.FieldType == typeof(string) && (original.opcode == OpCodes.Ldfld || original.opcode == OpCodes.Ldsfld)) { string fullName = fieldInfo.DeclaringType.FullName; if ((fullName == "CrawlOnline.Online" && (fieldInfo.Name == "LastError" || fieldInfo.Name == "Status")) || (fullName == "CrawlOnline.Net.LobbyBrowser" && fieldInfo.Name == "Error")) { text = "Message"; } if (fullName == "CrawlOnline.Net.LobbyEntry" && fieldInfo.Name == "State") { text = "Literal"; } } if ((object)method != null && method.DeclaringType.FullName == "CrawlOnline.Net.SteamLobby" && method.Name == "TypeName") { text = "Literal"; } if ((object)method != null && method.DeclaringType.FullName == "CrawlOnline.Net.LobbyBrowser" && method.Name == "get_Error") { text = "Message"; } if ((object)method != null && (object)method.DeclaringType == typeof(List<string>.Enumerator) && method.Name == "get_Current") { text = "Roster"; } if (text != null) { yield return new CodeInstruction(OpCodes.Call, (object)AccessTools.Method(typeof(OnlineText), text, (Type[])null, (Type[])null)); } } } } internal static class OnlineMirrorFonts { private static FieldInfo puppetField; private static FieldInfo meshField; internal static void Install(Harmony harmony, Action<string> warn) { //IL_0095: Unknown result type (might be due to invalid IL or missing references) //IL_00aa: Unknown result type (might be due to invalid IL or missing references) //IL_00b7: Expected O, but got Unknown //IL_00b7: Expected O, but got Unknown Type type = AccessTools.TypeByName("CrawlOnline.Client.SceneMirror"); if ((object)type == null) { return; } Type nestedType = type.GetNestedType("Entity", BindingFlags.Public | BindingFlags.NonPublic); Type nestedType2 = type.GetNestedType("RendererPuppet", BindingFlags.Public | BindingFlags.NonPublic); MethodInfo methodInfo = AccessTools.Method(type, "ApplyRenderer", (Type[])null, (Type[])null); puppetField = (((object)nestedType == null) ? null : AccessTools.Field(nestedType, "Puppet")); meshField = (((object)nestedType2 == null) ? null : AccessTools.Field(nestedType2, "TM")); if ((object)methodInfo == null || (object)puppetField == null || (object)meshField == null) { warn("CrawlOnline mirror font fallback unavailable: unsupported renderer structure."); return; } try { harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(OnlineMirrorFonts), "Before", (Type[])null), new HarmonyMethod(typeof(OnlineMirrorFonts), "After", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } catch (Exception ex) { warn("CrawlOnline mirror font fallback unavailable: " + ex.Message); } } private static TextMesh Mesh(object entity) { if (entity == null || (Object)(object)Plugin.Instance == (Object)null) { return null; } object value = puppetField.GetValue(entity); if (value != null && meshField.DeclaringType.IsInstanceOfType(value)) { object? value2 = meshField.GetValue(value); return (TextMesh)((value2 is TextMesh) ? value2 : null); } return null; } private static void Before(object __0) { try { TextMesh val = Mesh(__0); if (!((Object)(object)val == (Object)null)) { LocalizedMenuLabel component = ((Component)val).GetComponent<LocalizedMenuLabel>(); if ((Object)(object)component != (Object)null) { component.Restore(restoreText: false); } } } catch (Exception ex) { if ((Object)(object)Plugin.Instance != (Object)null) { Plugin.Instance.Warn("Mirror font restore failed: " + ex.Message); } } } private static void After(object __0) { try { TextMesh val = Mesh(__0); if (!((Object)(object)val == (Object)null)) { LocalizedMenuLabel.GetOrCreate(((Component)val).gameObject, fontOnly: true).CaptureMirrorSource(); } } catch (Exception ex) { if ((Object)(object)Plugin.Instance != (Object)null) { Plugin.Instance.Warn("Mirror font fallback failed: " + ex.Message); } } } } internal static class OnlineOverlay { private sealed class StyleEntry { internal GUIStyle Original; internal GUIStyle Display; internal Font Font; } private static readonly List<StyleEntry> styles = new List<StyleEntry>(); internal static void Clear() { styles.Clear(); } private static string Prepare(string source, ref GUIStyle style) { //IL_0096: Unknown result type (might be due to invalid IL or missing references) //IL_0129: Unknown result type (might be due to invalid IL or missing references) //IL_0130: Expected O, but got Unknown Plugin instance = Plugin.Instance; OnlineRecipe onlineRecipe = OnlineText.Find(source); if ((Object)(object)instance == (Object)null) { return source; } string text = ((onlineRecipe == null || instance.Language.Id == "en") ? source : onlineRecipe.Render(instance.Language)); if (text == source && !instance.Fonts.HasCombinedFont) { return source; } int num = ((style.fontSize > 0) ? style.fontSize : 12); if (!instance.Fonts.TryChooseGui(instance.Language, style.font ?? GUI.skin.font, num, style.fontStyle, text, out var font)) { return source; } if (text == source && (Object)(object)font == (Object)(object)(style.font ?? GUI.skin.font)) { return text; } foreach (StyleEntry style2 in styles) { if (style2.Original == style && (Object)(object)style2.Font == (Object)(object)font) { style = style2.Display; return text; } } GUIStyle val = new GUIStyle(style); val.font = font; val.fontSize = num; styles.Add(new StyleEntry { Original = style, Display = val, Font = font }); style = val; return text; } internal static void Label(string text, params GUILayoutOption[] options) { GUIStyle style = GUI.skin.label; text = Prepare(text, ref style); GUILayout.Label(text, style, options); } internal static void Label(string text, GUIStyle style, params GUILayoutOption[] options) { text = Prepare(text, ref style); GUILayout.Label(text, style, options); } internal static bool Button(string text, params GUILayoutOption[] options) { GUIStyle style = GUI.skin.button; text = Prepare(text, ref style); return GUILayout.Button(text, style, options); } internal static bool Toggle(bool value, string text, params GUILayoutOption[] options) { GUIStyle style = GUI.skin.toggle; text = Prepare(text, ref style); return GUILayout.Toggle(value, text, style, options); } internal static bool Toggle(bool value, string text, GUIStyle style, params GUILayoutOption[] options) { text = Prepare(text, ref style); return GUILayout.Toggle(value, text, style, options); } internal static void GuiLabel(Rect position, string text, GUIStyle style) { //IL_000a: Unknown result type (might be due to invalid IL or missing references) text = Prepare(text, ref style); GUI.Label(position, text, style); } internal static Rect Window(int id, Rect rect, WindowFunction function, string text, params GUILayoutOption[] options) { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) GUIStyle style = GUI.skin.window; text = Prepare(text, ref style); return GUILayout.Window(id, rect, function, text, style, options); } internal static void SetText(TextMesh mesh, string source) { if (!((Object)(object)mesh == (Object)null)) { mesh.text = source; if (!((Object)(object)Plugin.Instance == (Object)null)) { LocalizedMenuLabel orCreate = LocalizedMenuLabel.GetOrCreate(((Component)mesh).gameObject); orCreate.FontOnly = OnlineText.Find(source) == null; orCreate.ResetSource(source); orCreate.Refresh(); } } } } internal sealed class OnlineRecipe { internal readonly string Source; private readonly bool literal; private readonly OnlineRecipe[] parts; internal OnlineRecipe(string source, bool literal, OnlineRecipe[] parts) { Source = new string((source ?? "").ToCharArray()); this.literal = literal; this.parts = parts; } internal string Render(LanguagePack pack) { if (pack.Id == "en") { return Source; } if (parts != null) { StringBuilder stringBuilder = new StringBuilder(); OnlineRecipe[] array = parts; foreach (OnlineRecipe onlineRecipe in array) { stringBuilder.Append(onlineRecipe.Render(pack)); } return stringBuilder.ToString(); } if (!literal || !pack.TryTranslate("@online:" + Source, out var translated)) { return Source; } return translated; } } internal static class OnlineText { private sealed class Trace { internal readonly List<OnlineRecipe> Parts = new List<OnlineRecipe>(); } private static readonly WeakIdentityMap<OnlineRecipe> strings = new WeakIdentityMap<OnlineRecipe>(); private static readonly WeakIdentityMap<Trace> builders = new WeakIdentityMap<Trace>(); internal static OnlineRecipe Find(string source) { return strings.Get(source); } private static OnlineRecipe Capture(string source) { return Find(source) ?? new OnlineRecipe(source, literal: false, null); } internal static string Literal(string source) { if (string.IsNullOrEmpty(source)) { return source; } string text = new string(source.ToCharArray()); strings.Set(text, new OnlineRecipe(source, literal: true, null)); return text; } private static void Remember(string source, OnlineRecipe[] parts) { if (!string.IsNullOrEmpty(source)) { StringBuilder stringBuilder = new StringBuilder(); foreach (OnlineRecipe onlineRecipe in parts) { stringBuilder.Append(onlineRecipe.Source); } if (stringBuilder.ToString() == source) { strings.Set(source, new OnlineRecipe(source, literal: false, parts)); } } } internal static StringBuilder Create() { StringBuilder stringBuilder = new StringBuilder(); builders.Set(stringBuilder, new Trace()); return stringBuilder; } internal static StringBuilder Append(StringBuilder b, string s) { b.Append(s); builders.Get(b)?.Parts.Add(Capture(s)); return b; } internal static StringBuilder AppendChar(StringBuilder b, char c) { b.Append(c); builders.Get(b)?.Parts.Add(Capture(c.ToString())); return b; } internal static string Finish(StringBuilder b) { string text = b.ToString(); Trace trace = builders.Get(b); if (trace != null) { Remember(text, trace.Parts.ToArray()); } return text; } internal static string ObjectString(object o) { if (o is StringBuilder b) { return Finish(b); } return o.ToString(); } private static string Join(string source, object[] args) { OnlineRecipe[] array = new OnlineRecipe[args.Length]; for (int i = 0; i < args.Length; i++) { object obj = args[i]; if (obj != null && !(obj is string) && !obj.GetType().IsPrimitive && !(obj is decimal)) { return source; } array[i] = Capture((obj == null) ? "" : Convert.ToString(obj, CultureInfo.CurrentCulture)); } Remember(source, array); return source; } internal static string ConcatTwo(string a, string b) { return Join(a + b, new object[2] { a, b }); } internal static string ConcatThree(string a, string b, string c) { return Join(a + b + c, new object[3] { a, b, c }); } internal static string ConcatFour(string a, string b, string c, string d) { return Join(a + b + c + d, new object[4] { a, b, c, d }); } internal static string ConcatStrings(string[] a) { return Join(string.Concat(a), a); } internal static string ConcatObjects(object a, object b, object c) { return Join(string.Concat(a, b, c), new object[3] { a, b, c }); } internal static string ConcatArray(object[] a) { return Join(string.Concat(a), a); } internal static string Roster(string source) { if (source == null) { return null; } Match match = Regex.Match(source, "\\A(.*)( [0-9]+ ms \\()([0-9]+)( controllers?)(\\))\\z", RegexOptions.Singleline); if (!match.Success) { return source; } return ConcatStrings(new string[5] { match.Groups[1].Value, match.Groups[2].Value, match.Groups[3].Value, Literal(match.Groups[4].Value), match.Groups[5].Value }); } internal static string Message(string source) { if (source == null || Find(source) != null) { return source; } string[] array = new string[7] { "Host failed: ", "UDP host failed: ", "Connect failed: ", "lobby creation failed: ", "could not enter lobby (", "Rejected: ", "Host closed the session: " }; foreach (string text in array) { if (source.StartsWith(text, StringComparison.Ordinal)) { string text2 = source.Substring(text.Length); if (text == "Rejected: " || text == "Host closed the session: ") { text2 = Message(text2); } return ConcatTwo(Literal(text), text2); } } if (source.StartsWith("Hosting (Steam, ", StringComparison.Ordinal) && source.EndsWith(" lobby)", StringComparison.Ordinal)) { return ConcatThree(Literal("Hosting (Steam, "), Literal(source.Substring(16, source.Length - 23)), Literal(" lobby)")); } if (source.StartsWith("Hosting (UDP port ", StringComparison.Ordinal) && source.EndsWith(")", StringComparison.Ordinal)) { return ConcatThree(Literal("Hosting (UDP port "), source.Substring(18, source.Length - 19), ")"); } if (source.StartsWith("Joining ", StringComparison.Ordinal) && source.EndsWith("...", StringComparison.Ordinal)) { return ConcatThree(Literal("Joining "), source.Substring(8, source.Length - 11), "..."); } if (source.StartsWith("Crawl Online version mismatch: host has ", StringComparison.Ordinal)) { int num = source.IndexOf(", you have ", "Crawl Online version mismatch: host has ".Length, StringComparison.Ordinal); if (num >= 0) { string text3 = source.Substring(num + 11); string text4 = ". Use the same version."; if (text3.EndsWith(text4, StringComparison.Ordinal)) { text3 = ConcatTwo(text3.Substring(0, text3.Length - text4.Length), Literal(text4)); } return ConcatFour(Literal("Crawl Online version mismatch: host has "), source.Substring("Crawl Online version mismatch: host has ".Length, num - "Crawl Online version mismatch: host has ".Length), Literal(", you have "), text3); } } return Literal(source); } } [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInPlugin("crawl.localization", "Crawl Localization", "0.14.5")] public sealed class Plugin : BaseUnityPlugin { public const string Id = "crawl.localization"; internal static Plugin Instance; internal LanguagePack Language; internal FontService Fonts; internal readonly HashSet<LocalizedMenuLabel> Labels = new HashSet<LocalizedMenuLabel>(); private readonly HashSet<string> warnings = new HashSet<string>(); private ConfigEntry<string> language; private ConfigEntry<KeyCode> reloadKey; private Harmony harmony; private string folder; private void Awake() { //IL_009b: Unknown result type (might be due to invalid IL or missing references) //IL_00a5: Expected O, but got Unknown Instance = this; folder = Path.GetDirectoryName(((BaseUnityPlugin)this).Info.Location); language = ((BaseUnityPlugin)this).Config.Bind<string>("General", "Language", "zh-Hans", "Language folder under Languages, e.g. zh-Hans, es or ru. Use en for original text. zh-TW selects zh-Hant. Missing entries or packs keep original game text."); reloadKey = ((BaseUnityPlugin)this).Config.Bind<KeyCode>("General", "ReloadKey", (KeyCode)289, "Reload configuration and language tables. Restart to replace font files."); Fonts = new FontService(Warn, delegate(string message) { ((BaseUnityPlugin)this).Logger.LogInfo((object)message); }, folder); LoadLanguage(); harmony = new Harmony("crawl.localization"); harmony.PatchAll(typeof(Plugin).Assembly); OnlineIntegration.Install(harmony, Warn); OnlineMirrorFonts.Install(harmony, Warn); SceneManager.sceneLoaded += OnSceneLoaded; } private void Start() { MenuTextMeshes.ScanLoadedScenes(); } private void OnSceneLoaded(Scene scene, LoadSceneMode mode) { MenuTextMeshes.ScanLoadedScenes(); } private void Update() { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) if ((int)reloadKey.Value == 0 || !Input.GetKeyDown(reloadKey.Value)) { return; } try { ((BaseUnityPlugin)this).Config.Reload(); LoadLanguage(); MenuTextMeshes.ScanLoadedScenes(); foreach (LocalizedMenuLabel item in new List<LocalizedMenuLabel>(Labels)) { if ((Object)(object)item != (Object)null) { item.Refresh(); } } } catch (Exception ex) { Warn("Reload failed: " + ex.Message); } } private void LoadLanguage() { LanguagePack languagePack = (Language = LanguagePack.Load(folder, language.Value, Warn)); ((BaseUnityPlugin)this).Logger.LogInfo((object)("Language=" + languagePack.Id + "; menu translations=" + languagePack.Count + "; font mappings=" + languagePack.Fonts.Count)); } internal void Warn(string message) { if (warnings.Add(message)) { ((BaseUnityPlugin)this).Logger.LogWarning((object)message); } } private void OnDestroy() { SceneManager.sceneLoaded -= OnSceneLoaded; if (harmony != null) { harmony.UnpatchSelf(); } foreach (LocalizedMenuLabel item in new List<LocalizedMenuLabel>(Labels)) { if ((Object)(object)item != (Object)null) { item.Restore(restoreText: true); Object.Destroy((Object)(object)item); } } LocalizedMenuLabel.ExternalProvider = null; OnlineOverlay.Clear(); if (Fonts != null) { Fonts.Dispose(); } Instance = null; } } [HarmonyPatch(typeof(SystemMenu), "ShowPrompt")] internal static class PromptSourcePatch { [ThreadStatic] internal static PromptSource Current; private static void Prefix(string title, string text, out PromptSource __state) { __state = new PromptSource(title, text, Current); Current = __state; } private static Exception Finalizer(Exception __exception, PromptSource __state) { if (__state != null) { Current = __state.Previous; } return __exception; } } internal sealed class PromptSource { internal readonly string Title; internal readonly string Body; internal readonly string DisplayTitle; internal readonly PromptSource Previous; internal readonly Func<LanguagePack, string> External; internal bool Consumed; internal PromptSource(string title, string body, PromptSource previous) { Title = title; Body = body; Previous = previous; DisplayTitle = title?.ToUpper().Replace(' ', '_'); External = ((LocalizedMenuLabel.ExternalProvider == null) ? null : LocalizedMenuLabel.ExternalProvider(title)); } internal string Translate(LanguagePack pack) { if (pack.Id == "en") { return null; } if (External != null && pack.TryTranslate("@online:title:" + Title, out var translated)) { return Format(translated); } if (External == null && pack.TryTranslate("@progression.prompt-title:" + Title, out translated)) { return Format(translated); } translated = ((External == null) ? null : External(pack)); if (translated == null && !pack.TryTranslate(Title, out translated)) { return null; } if (translated == Title) { return null; } return Format(translated); } internal static string Format(string value) { return value?.Replace(' ', '_'); } } [HarmonyPatch(typeof(MenuPrompt), "Initialise")] internal static class PausePromptPatch { private static void Prefix(string title, string text, GUIText ___m_textTitle, GUIText ___m_textText) { if ((Object)(object)Plugin.Instance == (Object)null) { return; } PromptSource promptSource = PromptSourcePatch.Current; if (promptSource != null && !promptSource.Consumed && promptSource.DisplayTitle == title && promptSource.Body == text) { promptSource.Consumed = true; } else { promptSource = null; } string title2 = ((promptSource == null) ? title : promptSource.Title); PausePlayerText snapshot = null; bool joined = false; if (title != null && title.StartsWith("_P", StringComparison.Ordinal) && (title.EndsWith("_DROPPED_IN_", StringComparison.Ordinal) || title.EndsWith("_DROPPED_OUT_", StringComparison.Ordinal))) { joined = title.EndsWith("_DROPPED_IN_", StringComparison.Ordinal); for (int i = 0; i < SystemPlayers.GetMaxPlayers(); i++) { PlayerData player = SystemPlayers.GetPlayer(i); if (player == null) { continue; } Deity val = ((joined && player.GameData != null) ? player.GameData.GetDeity() : null); if (!joined || val != null) { PausePlayerText pausePlayerText = new PausePlayerText(i + 1, player.Name, player.Level.ToString(), (val == null) ? "" : val.GetName(), (val == null) ? "" : val.GetText()); if (pausePlayerText.Matches(title2, text, joined)) { snapshot = pausePlayerText; break; } } } } Func<LanguagePack, string> provider = ((promptSource == null) ? null : new Func<LanguagePack, string>(promptSource.Translate)); if (snapshot != null) { provider = (LanguagePack pack) => PromptSource.Format(snapshot.Title(pack, joined)); } Bind(___m_textTitle, title, provider); Bind(___m_textText, text, (snapshot == null) ? null : ((Func<LanguagePack, string>)((LanguagePack pack) => snapshot.Body(pack, joined)))); } private static void Bind(GUIText gui, string source, Func<LanguagePack, string> provider) { if (!((Object)(object)gui == (Object)null)) { LocalizedMenuLabel orCreate = LocalizedMenuLabel.GetOrCreate(((Component)gui).gameObject); if (!((Object)(object)orCreate == (Object)null)) { orCreate.TextProvider = provider; orCreate.ResetSource(source); } } } } [HarmonyPatch(typeof(MenuEvolvePlayer), "Initialise")] internal static class EvolutionBindPatch { private static void Postfix(MenuEvolvePlayer __instance) { if ((Object)(object)Plugin.Instance != (Object)null) { MenuTextMeshes.Scan(((Component)__instance).gameObject); } } } [HarmonyPatch(typeof(MenuEvolvePlayer), "UpdateMenuDescriptionText")] internal static class EvolutionDescriptionPatch { private static void Postfix(TextMesh ___m_textMenuDescription) { if (!((Object)(object)Plugin.Instance == (Object)null) && !((Object)(object)___m_textMenuDescription == (Object)null)) { LocalizedMenuLabel localizedMenuLabel = MenuTextMeshes.Bind(((Component)___m_textMenuDescription).gameObject, refresh: true); if ((Object)(object)localizedMenuLabel != (Object)null) { localizedMenuLabel.RefreshChangedText(); } } } } [HarmonyPatch(typeof(MenuWin), "Start")] internal static class ResultsTextPatch { private static void Postfix(MenuWin __instance, bool ___m_heroWon, TextMesh ___m_winText, TextMesh ___m_bossKillerText) { if ((Object)(object)Plugin.Instance == (Object)null) { return; } if (___m_heroWon && (Object)(object)___m_bossKillerText != (Object)null) { LocalizedMenuLabel.GetOrCreate(((Component)___m_bossKillerText).gameObject).RefreshDirect(___m_bossKillerText.text.Trim(), null); } MenuTextMeshes.Scan(((Component)__instance).gameObject); if ((Object)(object)___m_winText != (Object)null) { LocalizedMenuLabel localizedMenuLabel = MenuTextMeshes.Bind(((Component)___m_winText).gameObject, refresh: true); if ((Object)(object)localizedMenuLabel != (Object)null) { localizedMenuLabel.RefreshChangedText(); } } } } [HarmonyPatch(typeof(MenuTextMenuItem), "Start")] internal static class BossEntranceTextPatch { private static void Postfix(MenuTextMenuItem __instance) { if (!((Object)(object)Plugin.Instance == (Object)null)) { string text = ((Object)((Component)__instance).gameObject).name.Replace("(Clone)", ""); if ((!(text != "EffectBossName") || !(text != "EffectBossAttemptText")) && !((Object)(object)((Component)__instance).GetComponent<TextMesh>() == (Object)null)) { LocalizedMenuLabel orCreate = LocalizedMenuLabel.GetOrCreate(((Component)__instance).gameObject); orCreate.Standalone = true; orCreate.RefreshChangedText(); } } } } internal sealed class PausePlayerText { internal readonly string Number; internal readonly string Name; internal readonly string Level; internal readonly string DeityName; internal readonly string DeityText; internal PausePlayerText(int number, string name, string level, string deityName, string deityText) { Number = number.ToString(CultureInfo.InvariantCulture); Name = name; Level = level; DeityName = deityName; DeityText = deityText; } internal string OriginalTitle(bool joined) { return "_P" + Number + "_'" + Name.ToUpper() + "'_DROPPED_" + (joined ? "IN_" : "OUT_"); } internal string OriginalBody(bool joined) { if (!joined) { return "Player " + Number + " (" + Name + ") will be replaced with a bot."; } return "Joined as Player " + Number + " with alias: '" + Name + "'\n\nHero level: " + Level + "\nWorships: " + DeityName + "\n(" + DeityText + ")"; } internal bool Matches(string title, string body, bool joined) { if (title == OriginalTitle(joined)) { return body == OriginalBody(joined); } return false; } internal string Title(LanguagePack pack, bool joined) { return Format(pack, joined ? "@progression.join-title:{0}:{1}" : "@progression.leave-title:{0}:{1}", Number, Name.ToUpper()); } internal string Body(LanguagePack pack, bool joined) { if (!joined) { return Format(pack, "@progression.leave-body:{0}:{1}", Number, Name); } return Format(pack, "@progression.join-body:{0}:{1}:{2}:{3}:{4}", Number, Name, Level, Term(pack, DeityName), Term(pack, DeityText)); } private static string Term(LanguagePack pack, string source) { if (!pack.TryTranslate(source, out var translated)) { return source; } return translated; } private static string Format(LanguagePack pack, string key, params object[] args) { if (!pack.TryTranslate(key, out var translated)) { return null; } try { return string.Format(CultureInfo.InvariantCulture, translated, args); } catch (FormatException) { return null; } } } internal static class ShopDisplay { private static LocalizedMenuLabel Bind(TextMesh mesh, string source) { if ((Object)(object)mesh == (Object)null || (Object)(object)Plugin.Instance == (Object)null) { return null; } ShopTextRecipe recipe = ShopText.Capture(source); LocalizedMenuLabel localizedMenuLabel = ((Component)mesh).GetComponent<LocalizedMenuLabel>(); bool flag = false; if (source != null) { for (int i = 0; i < source.Length; i++) { if (char.IsLetter(source[i])) { flag = true; break; } } } if ((Object)(object)localizedMenuLabel == (Object)null && !flag) { return null; } if ((Object)(object)localizedMenuLabel == (Object)null) { localizedMenuLabel = LocalizedMenuLabel.GetOrCreate(((Component)mesh).gameObject); } localizedMenuLabel.FontOnly = false; localizedMenuLabel.TextProvider = (LanguagePack pack) => recipe.Translate(pack); localizedMenuLabel.ResetSource(source); return localizedMenuLabel; } internal static void SetText(TextMesh mesh, string source) { mesh.text = source; LocalizedMenuLabel localizedMenuLabel = Bind(mesh, source); if ((Object)(object)localizedMenuLabel != (Object)null) { localizedMenuLabel.Refresh(); } } internal static void SetGuiText(GUIText gui, string source) { Bind(((Component)gui).GetComponent<TextMesh>(), source); gui.SetText(source); } } internal sealed class WeakIdentityMap<T> where T : class { private sealed class Entry { internal WeakReference Key; internal T Value; } private readonly Dictionary<int, List<Entry>> buckets = new Dictionary<int, List<Entry>>(); private int writes; internal T Get(object key) { if (key == null) { return null; } lock (buckets) { if (!buckets.TryGetValue(RuntimeHelpers.GetHashCode(key), out var value)) { return null; } foreach (Entry item in value) { if (item.Key.Target == key) { return item.Value; } } return null; } } internal void Set(object key, T value) { if (key == null) { return; } lock (buckets) { if (++writes % 128 == 0) { foreach (int item in new List<int>(buckets.Keys)) { buckets[item].RemoveAll((Entry e) => !e.Key.IsAlive); if (buckets[item].Count == 0) { buckets.Remove(item); } } } int hashCode = RuntimeHelpers.GetHashCode(key); if (!buckets.TryGetValue(hashCode, out var value2)) { value2 = new List<Entry>(); buckets.Add(hashCode, value2); } foreach (Entry item2 in value2) { if (item2.Key.Target == key) { item2.Value = value; return; } } value2.Add(new Entry { Key = new WeakReference(key), Value = value }); } } } internal sealed class ShopTextRecipe { internal readonly string Source; private readonly ShopTextRecipe[] parts; private readonly string format; private readonly object[] arguments; private readonly CultureInfo culture; private static readonly Regex Structural = new Regex("</?(?:b|i|size|color|material)(?:=[^>]*)?>|\\{[0-9]+(?:[^}]*)?\\}"); internal ShopTextRecipe(string source, ShopTextRecipe[] parts, string format, object[] arguments) { Source = new string((source ?? "").ToCharArray()); this.parts = parts; this.format = ((format == null) ? null : new string(format.ToCharArray())); this.arguments = arguments; culture = CultureInfo.CurrentCulture; } private static string Term(LanguagePack pack, string source, bool format) { if (pack.TryTranslate("@shop.il:" + source, out var translated) || pack.TryTranslate(source, out translated)) { return translated; } string[] array = new string[5] { "@shop.prefix:", "@shop.magic-name:", "@shop.status:", "@shop.magic:", "@shop.stat:" }; foreach (string text in array) { if (pack.TryTranslate(text + source, out translated)) { return translated; } } if (!format && !source.EndsWith(".", StringComparison.Ordinal) && pack.TryTranslate(source + ".", out translated)) { return translated; } string text2 = Structural.Replace(source, ""); for (int i = 0; i < text2.Length; i++) { if (char.IsLetter(text2[i])) { return null; } } return source; } internal string Translate(LanguagePack pack) { if (pack.Id == "en") { return null; } try { return Render(pack); } catch (FormatException) { return null; } } private string Render(LanguagePack pack) { if (pack.TryTranslate(Source, out var translated)) { return translated; } if (parts != null) { StringBuilder stringBuilder = new StringBuilder(); ShopTextRecipe[] array = parts; for (int i = 0; i < array.Length; i++) { string text = array[i].Render(pack); if (text == null) { return null; } stringBuilder.Append(text); } return stringBuilder.ToString(); } if (format != null) { string text2 = Term(pack, format, format: true); if (text2 == null) { return null; } object[] array2 = new object[arguments.Length]; for (int j = 0; j < array2.Length; j++) { ShopTextRecipe shopTextRecipe = arguments[j] as ShopTextRecipe; array2[j] = ((shopTextRecipe == null) ? arguments[j] : shopTextRecipe.Render(pack)); if (shopTextRecipe != null && array2[j] == null) { return null; } } return string.Format(culture, text2, array2); } return Term(pack, Source, format: false); } } internal static class ShopText { private sealed class BuilderTrace { internal readonly List<ShopTextRecipe> Parts = new List<ShopTextRecipe>(); internal bool Invalid; } private static readonly WeakIdentityMap<BuilderTrace> builders = new WeakIdentityMap<BuilderTrace>(); private static readonly WeakIdentityMap<ShopTextRecipe> strings = new WeakIdentityMap<ShopTextRecipe>(); internal static ShopTextRecipe Capture(string source) { return strings.Get(source) ?? new ShopTextRecipe(source, null, null, null); } private static ShopTextRecipe Sequence(string source, ShopTextRecipe[] parts) { StringBuilder stringBuilder = new StringBuilder(); foreach (ShopTextRecipe shopTextRecipe in parts) { stringBuilder.Append(shopTextRecipe.Source); } if (!(stringBuilder.ToString() == source)) { return null; } return new ShopTextRecipe(source, parts, null, null); } private static void Remember(string source, ShopTextRecipe recipe) { if (!string.IsNullOrEmpty(source) && recipe != null) { strings.Set(source, recipe); } } private static ShopTextRecipe FormatRecipe(string source, string format, object[] args) { object[] array = new object[args.Length]; for (int i = 0; i < args.Length; i++) { object obj = args[i]; if (obj is string) { array[i] = Capture((string)obj); continue; } if (obj == null || obj.GetType().IsPrimitive || obj is decimal) { array[i] = obj; continue; } return null; } return new ShopTextRecipe(source, null, format, array); } private static void Add(StringBuilder builder, ShopTextRecipe node) { BuilderTrace builderTrace = builders.Get(builder); if (builderTrace != null) { if (node == null) { builderTrace.Invalid = true; } else { builderTrace.Parts.Add(node); } } } internal static StringBuilder Create() { StringBuilder stringBuilder = new StringBuilder(); builders.Set(stringBuilder, new BuilderTrace()); return stringBuilder; } internal static StringBuilder Append(StringBuilder builder, string value) { builder.Append(value); try { Add(builder, Capture(value)); } catch (Exception) { Invalidate(builder); } return builder; } internal static StringBuilder AppendChar(StringBuilder builder, char value) { builder.Append(value); try { Add(builder, Capture(value.ToString())); } catch (Exception) { Invalidate(builder); } return builder; } private static void Invalidate(StringBuilder builder) { BuilderTrace builderTrace = builders.Get(builder); if (builderTrace != null) { builderTrace.Invalid = true; } } internal static StringBuilder AppendFormatOne(StringBuilder builder, string format, object arg) { return AppendFormatMany(builder, format, new object[1] { arg }); } internal static StringBuilder AppendFormatTwo(StringBuilder builder, string format, object a, object b) { return AppendFormatMany(builder, format, new object[2] { a, b }); } internal static StringBuilder AppendFormatMany(StringBuilder builder, string format, object[] args) { int length = builder.Length; builder.AppendFormat(format, args); try { Add(builder, FormatRecipe(builder.ToString(length, builder.Length - length), format, args)); } catch (Exception) { Invalidate(builder); } return builder; } internal static string Finish(StringBuilder builder) { string text = builder.ToString(); RecordFinished(builder, text); return text; } internal static string ObjectString(object value) { string text = value.ToString(); if (value is StringBuilder builder) { RecordFinished(builder, text); } return text; } private static void RecordFinished(StringBuilder builder, string source) { try { BuilderTrace builderTrace = builders.Get(builder); if (builderTrace != null && !builderTrace.Invalid) { Remember(source, Sequence(source, builderTrace.Parts.ToArray())); } } catch (Exception) { } } internal static string FormatOne(string format, object arg) { string text = string.Format(format, arg); try { Remember(text, FormatRecipe(text, format, new object[1] { arg })); } catch (Exception) { } return text; } internal static string ConcatTwo(string a, string b) { return Join(a + b, new object[2] { a, b }); } internal static string ConcatThree(string a, string b, string c) { return Join(a + b + c, new object[3] { a, b, c }); } internal static string ConcatObjects(object a, object b, object c) { return Join(string.Concat(a, b, c), new object[3] { a, b, c }); } internal static string ConcatArray(object[] args) { return Join(string.Concat(args), args); } private static string Join(string source, object[] args) { try { ShopTextRecipe[] array = new ShopTextRecipe[args.Length]; for (int i = 0; i < args.Length; i++) { object obj = args[i]; if (obj != null && !(obj is string) && !obj.GetType().IsPrimitive && !(obj is decimal)) { return source; } array[i] = Capture((obj == null) ? "" : Convert.ToString(obj, CultureInfo.CurrentCulture)); } Remember(source, Sequence(source, array)); } catch (Exception) { } return source; } } [HarmonyPatch] internal static class ShopTextIlPatch { private static readonly Dictionary<MethodBase, MethodInfo> replacements = CreateMap(); private static void Map(Dictionary<MethodBase, MethodInfo> map, Type type, string name, Type[] args, string replacement) { MethodBase key = ((name == ".ctor") ? ((MethodBase)type.GetConstructor(args)) : ((MethodBase)type.GetMethod(name, args))); map.Add(key, AccessTools.Method(typeof(ShopText), replacement, (Type[])null, (Type[])null)); } private static Dictionary<MethodBase, MethodInfo> CreateMap() { Dictionary<MethodBase, MethodInfo> dictionary = new Dictionary<MethodBase, MethodInfo>(); Map(dictionary, typeof(StringBuilder), ".ctor", Type.EmptyTypes, "Create"); Map(dictionary, typeof(StringBuilder), "Append", new Type[1] { typeof(string) }, "Append"); Map(dictionary, typeof(StringBuilder), "Append", new Type[1] { typeof(char) }, "AppendChar"); Map(dictionary, typeof(StringBuilder), "AppendFormat", new Type[2] { typeof(string), typeof(object) }, "AppendFormatOne"); Map(dictionary, typeof(StringBuilder), "AppendFormat", new Type[3] { typeof(string), typeof(object), typeof(object) }, "AppendFormatTwo"); Map(dictionary, typeof(StringBuilder), "AppendFormat", new Type[2] { typeof(string), typeof(object[]) }, "AppendFormatMany"); Map(dictionary, typeof(StringBuilder), "ToString", Type.EmptyTypes, "Finish"); Map(dictionary, typeof(object), "ToString", Type.EmptyTypes, "ObjectString"); Map(dictionary, typeof(string), "Format", new Type[2] { typeof(string), typeof(object) }, "FormatOne"); Map(dictionary, typeof(string), "Concat", new Type[2] { typeof(string), typeof(string) }, "ConcatTwo"); Map(dictionary, typeof(string), "Concat", new Type[3] { typeof(string), typeof(string), typeof(string) }, "ConcatThree"); Map(dictionary, typeof(string), "Concat", new Type[3] { typeof(object), typeof(object), typeof(object) }, "ConcatObjects"); Map(dictionary, typeof(string), "Concat", new Type[1] { typeof(object[]) }, "ConcatArray"); return dictionary; } private static IEnumerable<MethodBase> TargetMethods() { yield return AccessTools.Method(typeof(WeaponData), "GetDisplayName", (Type[])null, (Type[])null); yield return AccessTools.Method(typeof(WeaponData), "GetDescription", (Type[])null, (Type[])null); yield return AccessTools.Method(typeof(PropPurchasable), "ShowInfoBox", (Type[])null, (Type[])null); yield return AccessTools.Method(typeof(GUIStore), "Show", (Type[])null, (Type[])null); yield return AccessTools.Method(typeof(GUIStoreStatsDisplay), "Show", (Type[])null, (Type[])null); } internal static IEnumerable<CodeInstruction> Rewrite(IEnumerable<CodeInstruction> instructions, bool display) { foreach (CodeInstruction instruction in instructions) { MethodBase methodBase = instruction.operand as MethodBase; MethodInfo value = null; if ((object)methodBase != null && (instruction.opcode == OpCodes.Call || instruction.opcode == OpCodes.Callvirt || instruction.opcode == OpCodes.Newobj)) { replacements.TryGetValue(methodBase, out value); if (display && methodBase.Equals(AccessTools.PropertySetter(typeof(TextMesh), "text"))) { value = AccessTools.Method(typeof(ShopDisplay), "SetText", (Type[])null, (Type[])null); } if (display && methodBase.Equals(AccessTools.Method(typeof(GUIText), "SetText", (Type[])null, (Type[])null))) { value = AccessTools.Method(typeof(ShopDisplay), "SetGuiText", (Type[])null, (Type[])null); } } if ((object)value == null) { yield return instruction; continue; } CodeInstruction val = new CodeInstruction(instruction); val.opcode = OpCodes.Call; val.operand = value; yield return val; } } private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions, MethodBase __originalMethod) { return Rewrite(instructions, (object)__originalMethod.DeclaringType == typeof(GUIStore) || (object)__originalMethod.DeclaringType == typeof(GUIStoreStatsDisplay)); } } [HarmonyPatch(typeof(GuiTutorialMessageBox), "SetContent")] internal static class TutorialMessagePatch { private static void Prefix(GUIText ___m_textMessage) { Bind(___m_textMessage); } internal static void Bind(GUIText gui) { if (!((Object)(object)Plugin.Instance == (Object)null) && !((Object)(object)gui == (Object)null)) { LocalizedMenuLabel.GetOrCreate(((Component)gui).gameObject); } } } [HarmonyPatch(typeof(GuiTutorialMessageBox), "ShowButton")] internal static class TutorialButtonPatch { private static void Prefix(GUIText ___m_textButton) { TutorialMessagePatch.Bind(___m_textButton); } } [HarmonyPatch(typeof(TextWrapper), "WrapText")] internal static class TutorialWrapPatch { private static bool Prefix(string input, float width, TextMesh ___textMesh, ref string __result) { if ((Object)(object)Plugin.Instance == (Object)null || !Plugin.Instance.Language.UsesChineseTextRules || (Object)(object)___textMesh == (Object)null || (Object)(object)___textMesh.font == (Object)null || !ChineseTextWrapper.ContainsChinese(input) || (Object)(object)((Component)___textMesh).GetComponent<LocalizedMenuLabel>() == (Object)null || (Object)(object)((Component)___textMesh).GetComponentInParent<GuiTutorialMessageBox>() == (Object)null) { return true; } TutorialTextMetrics tutorialTextMetrics = new TutorialTextMetrics(___textMesh); __result = ChineseTextWrapper.Wrap(input, width, tutorialTextMetrics.Advance, tutorialTextMetrics.TagWidth); return false; } } internal sealed class TutorialTextMetrics { private static readonly Regex Size = new Regex("\\bsize\\s*=\\s*[\"']?([0-9]+(?:\\.[0-9]+)?)", RegexOptions.IgnoreCase); private readonly Font font; private readonly float scale; private readonly int baseSize; private readonly FontStyle baseStyle; private readonly Stack<int> sizes = new Stack<int>(); private int size; private int bold; private int italic; internal TutorialTextMetrics(TextMesh mesh) { //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Unknown result type (might be due to invalid IL or missing references) font = mesh.font; baseSize = (size = ((mesh.fontSize > 0) ? mesh.fontSize : font.fontSize)); baseStyle = mesh.fontStyle; scale = mesh.characterSize * 0.1f * Mathf.Abs(((Component)mesh).transform.lossyScale.x); } internal float Advance(char ch) { //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Invalid comparison between Unknown and I4 //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Invalid comparison between Unknown and I4 //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Invalid comparison between Unknown and I4 //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Invalid comparison between Unknown and I4 //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_0085: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Unkno