using System; using System.Collections.Generic; using System.Reflection; using HarmonyLib; using Il2CppInterop.Runtime; using Il2CppInterop.Runtime.InteropTypes; using TMPro; using UnityEngine.UI; internal static class ClientTextRewrites { private static readonly (string From, string To)[] Rules = { ("https://recroom.zendesk.com/home", "https://support.radicade.app/"), ("recroom.zendesk.com", "support.radicade.app"), ("https://www.recroom.com/privacy-policy/", "https://radicade.app/privacy-policy/"), ("https://recroom.com/terms-of-service", "https://radicade.app/terms-of-service/"), ("https://www.recroom.com/community/", "https://radicade.app/community/"), }; private static readonly (string From, string To)[] ExactRules = { ("This account has been locked for security reasons. Contact us at recroom.zendesk.com.", LockedAccountSentence), ("This account has been locked for security reasons. Contact us at support.radicade.app.", LockedAccountSentence), }; private const string GenericLoginFailure = "Login failed. Please try again."; private const string LockedAccountErrorCode = "ACCOUNT_PERMALOCKED"; internal const string LockedAccountSentence = "This account has been locked for security reasons. Contact us at https://support.radicade.app."; private static bool _rewriting; internal static void Apply(Harmony harmony) { var mode = AsciiLower((Environment.GetEnvironmentVariable("RADICADE_CLIENT_TEXT") ?? string.Empty).Trim()); if (mode == "0" || mode == "off") { return; } bool onEnable = mode.Length == 0 || mode == "1" || mode == "all" || mode == "onenable"; bool onSet = mode.Length == 0 || mode == "1" || mode == "all" || mode == "settext"; var seen = new HashSet(); var names = new List(); if (onEnable) { Hook(harmony, seen, names, AccessTools.Method(typeof(TextMeshProUGUI), "OnEnable"), nameof(GraphicEvent), "TextMeshProUGUI.OnEnable"); Hook(harmony, seen, names, AccessTools.Method(typeof(Text), "OnEnable"), nameof(GraphicEvent), "UI.Text.OnEnable"); } if (onSet) { Hook(harmony, seen, names, AccessTools.Method(typeof(TMP_Text), "set_text"), nameof(GraphicEvent), "TMP_Text.set_text"); Hook(harmony, seen, names, AccessTools.Method(typeof(TMP_Text), "SetTextInternal"), nameof(GraphicEvent), "TMP_Text.SetTextInternal"); Hook(harmony, seen, names, AccessTools.Method(typeof(Text), "set_text"), nameof(GraphicEvent), "UI.Text.set_text"); } if (names.Count == 0) RadicadeEacBypassPlugin.LogSource.LogWarning("[ClientText] no text hooks could be installed (mode=" + (mode.Length == 0 ? "all" : mode) + "); the watch UI keeps its original text."); } private static void Hook(Harmony harmony, HashSet seen, List names, MethodBase target, string handler, string label) { if (target == null) { RadicadeEacBypassPlugin.LogSource.LogWarning("[ClientText] hook target missing: " + label); return; } if (!seen.Add(target)) return; try { var method = new HarmonyMethod(typeof(ClientTextRewrites) .GetMethod(handler, BindingFlags.Static | BindingFlags.NonPublic)); harmony.Patch(target, postfix: method); names.Add(label); } catch (Exception ex) { RadicadeEacBypassPlugin.LogSource.LogWarning("[ClientText] could not hook " + label + ": " + ex.GetType().Name + ": " + ex.Message); } } private static void GraphicEvent(object __instance) { try { if (__instance is not Il2CppObjectBase obj) return; if (obj.TryCast() is { } tmp) { Fix(tmp); return; } if (obj.TryCast() is { } txt) { Fix(txt); } } catch { } } private static void Fix(TMP_Text instance) { try { if (instance == null || _rewriting) return; var current = instance.text; var rewritten = Rewrite(current); if (ReferenceEquals(current, rewritten) || string.Equals(rewritten, current, StringComparison.Ordinal)) return; _rewriting = true; try { instance.text = rewritten; } finally { _rewriting = false; } } catch { } } private static void Fix(Text instance) { try { if (instance == null || _rewriting) return; var current = instance.text; var rewritten = Rewrite(current); if (ReferenceEquals(current, rewritten) || string.Equals(rewritten, current, StringComparison.Ordinal)) return; _rewriting = true; try { instance.text = rewritten; } finally { _rewriting = false; } } catch { } } private static string Rewrite(string value) { if (string.IsNullOrEmpty(value)) return value; if (HubSocketAuth.LastLoginRefusalCode == LockedAccountErrorCode && IndexOfAsciiIgnoreCase(value, GenericLoginFailure, 0) >= 0) { var locked = Replace(value, GenericLoginFailure, LockedAccountSentence); return locked; } if (IndexOfAsciiIgnoreCase(value, "recroom", 0) < 0 && IndexOfAsciiIgnoreCase(value, "radicade", 0) < 0) return value; var result = value; foreach (var (from, to) in ExactRules) if (IndexOfAsciiIgnoreCase(result, from, 0) >= 0) result = Replace(result, from, to); foreach (var (from, to) in Rules) if (IndexOfAsciiIgnoreCase(result, from, 0) >= 0) result = Replace(result, from, to); return result; } private static string Replace(string source, string from, string to) { var sb = new System.Text.StringBuilder(source.Length); var i = 0; while (i < source.Length) { var hit = IndexOfAsciiIgnoreCase(source, from, i); if (hit < 0) { sb.Append(source, i, source.Length - i); break; } sb.Append(source, i, hit - i).Append(to); i = hit + from.Length; } return sb.ToString(); } private static int IndexOfAsciiIgnoreCase(string haystack, string needle, int start) { if (haystack == null || needle == null) return -1; var n = needle.Length; var h = haystack.Length; if (n == 0) return start <= h ? start : -1; if (n > h) return -1; var last = h - n; for (var i = (start < 0 ? 0 : start); i <= last; i++) { var j = 0; while (j < n && LowerAscii(haystack[i + j]) == LowerAscii(needle[j])) j++; if (j == n) return i; } return -1; } private static char LowerAscii(char c) => c >= 'A' && c <= 'Z' ? (char)(c + 32) : c; private static string AsciiLower(string s) { for (var i = 0; i < s.Length; i++) if (s[i] >= 'A' && s[i] <= 'Z') return string.Create(s.Length, s, (span, state) => { for (var k = 0; k < state.Length; k++) span[k] = LowerAscii(state[k]); }); return s; } }