mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 15:09:26 -07:00
Radicade Public Code
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
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<MethodBase>();
|
||||
var names = new List<string>();
|
||||
|
||||
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<MethodBase> seen, List<string> 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<TMP_Text>() is { } tmp) { Fix(tmp); return; }
|
||||
if (obj.TryCast<Text>() 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using HarmonyLib;
|
||||
using UnityEngine;
|
||||
|
||||
internal static class ColliderCastGuardPatch
|
||||
{
|
||||
internal static void Apply(Harmony harmony)
|
||||
{
|
||||
if (!Enabled) return;
|
||||
int installed = 0;
|
||||
foreach (var method in TargetMethods())
|
||||
{
|
||||
try
|
||||
{
|
||||
Install(harmony, method);
|
||||
installed++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
RadicadeEacBypassPlugin.LogSource.LogError("[ColliderGuard] failed on " + Describe(method) + ": " + ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static void Install(Harmony harmony, MethodBase method)
|
||||
{
|
||||
var prefix = IsColliderFrame(method)
|
||||
? new HarmonyMethod(typeof(ColliderCastGuardPatch), nameof(NoteCollider)) : null;
|
||||
harmony.Patch(method, prefix: prefix,
|
||||
finalizer: new HarmonyMethod(typeof(ColliderCastGuardPatch), nameof(Swallow)));
|
||||
}
|
||||
private static readonly bool _disabled = IsDisabled();
|
||||
|
||||
private static int _swallows;
|
||||
|
||||
private static readonly Dictionary<MethodBase, string> LastCollider = new Dictionary<MethodBase, string>();
|
||||
|
||||
|
||||
private static readonly HashSet<MethodBase> ColliderFrames = new HashSet<MethodBase>();
|
||||
|
||||
private static readonly string[] ColliderParameterTypes = new string[2] { "AOCLEIAJHKP", "AKCGGCOFMHJ" };
|
||||
|
||||
private static long _inspections;
|
||||
|
||||
private static readonly string[] CastTypes = new string[3] { "BPJMFIFDPJO", "CBKENBPBBPK", "AOCLEIAJHKP" };
|
||||
|
||||
private const string RbexAssembly = "RecRoom.Rbex.Runtime";
|
||||
|
||||
private const string GameAssembly = "Assembly-CSharp";
|
||||
|
||||
private static readonly (string Assembly, string Type, string Method, int Args, string Role)[] Specs = new(string, string, string, int, string)[5]
|
||||
{
|
||||
("RecRoom.Rbex.Runtime", "NFMLBBHIOHJ", "JAMGDJKMPGM", 2, "locomotion: attach/detach a collider child"),
|
||||
("RecRoom.Rbex.Runtime", "NPDJILHGCBI", "JAMGDJKMPGM", 2, "locomotion: collider-child update (twin implementation)"),
|
||||
("RecRoom.Rbex.Runtime", "MOBAMIJAHPJ", "JDAFCHGGCBG", 1, "teardown: unregister a collider from its body"),
|
||||
("RecRoom.Rbex.Runtime", "CBKENBPBBPK", "JDIBOGNGEFK", 0, "teardown: body Dispose -> collider walk"),
|
||||
("Assembly-CSharp", "RecRoom.Core.Locomotion.PlayerMovement", "AuthorityUpdateMovement", 2, "locomotion: authority tick (backstop for every caller of the cast)")
|
||||
};
|
||||
|
||||
internal static bool Enabled => !_disabled;
|
||||
|
||||
[HarmonyPrefix]
|
||||
private static void NoteCollider(object __0, MethodBase __originalMethod)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (__0 == null || __originalMethod == null || !ColliderFrames.Contains(__originalMethod))
|
||||
{
|
||||
return;
|
||||
}
|
||||
long num = ++_inspections;
|
||||
if (num > 200 && num % 50 != 0L)
|
||||
{
|
||||
return;
|
||||
}
|
||||
string text = DescribeCollider(__0);
|
||||
if (text != null)
|
||||
{
|
||||
if (__originalMethod != null)
|
||||
{
|
||||
LastCollider[__originalMethod] = text;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private static string DescribeCollider(object collider)
|
||||
{
|
||||
string text = null;
|
||||
try
|
||||
{
|
||||
CBKENBPBBPK val = (CBKENBPBBPK)((collider is CBKENBPBBPK) ? collider : null);
|
||||
if (val != null)
|
||||
{
|
||||
text = NameOf(val.JPBNIBGFKJO);
|
||||
}
|
||||
else
|
||||
{
|
||||
BPJMFIFDPJO val2 = (BPJMFIFDPJO)((collider is BPJMFIFDPJO) ? collider : null);
|
||||
if (val2 != null)
|
||||
{
|
||||
text = NameOf(val2.JPBNIBGFKJO);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
string name;
|
||||
try
|
||||
{
|
||||
name = collider.GetType().Name;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (text != null)
|
||||
{
|
||||
return name + " on '" + text + "'";
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
private static string NameOf(GameObject gameObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
return gameObject == null ? null : gameObject.name;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsDisabled()
|
||||
{
|
||||
try
|
||||
{
|
||||
string environmentVariable = Environment.GetEnvironmentVariable("RADICADE_COLLIDER_GUARD");
|
||||
if (string.IsNullOrWhiteSpace(environmentVariable))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
bool result;
|
||||
switch (environmentVariable.Trim().ToLowerInvariant())
|
||||
{
|
||||
case "0":
|
||||
case "off":
|
||||
case "false":
|
||||
case "no":
|
||||
result = true;
|
||||
break;
|
||||
default:
|
||||
result = false;
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<MethodBase> TargetMethods()
|
||||
{
|
||||
List<MethodBase> list = new List<MethodBase>();
|
||||
(string, string, string, int, string)[] specs = Specs;
|
||||
for (int i = 0; i < specs.Length; i++)
|
||||
{
|
||||
(string, string, string, int, string) tuple = specs[i];
|
||||
foreach (MethodBase item in Resolve(tuple.Item1, tuple.Item2, tuple.Item3, tuple.Item4))
|
||||
{
|
||||
list.Add(item);
|
||||
if (IsColliderFrame(item))
|
||||
{
|
||||
ColliderFrames.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (list.Count == 0)
|
||||
{
|
||||
RadicadeEacBypassPlugin.LogSource.LogWarning((object)("[ColliderGuard] none of the " + Specs.Length + " known collider frames resolved - the physics cast failure is NOT guarded."));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private static bool IsColliderFrame(MethodBase method)
|
||||
{
|
||||
try
|
||||
{
|
||||
ParameterInfo[] parameters = method.GetParameters();
|
||||
if (parameters.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
string name = parameters[0].ParameterType.Name;
|
||||
string[] colliderParameterTypes = ColliderParameterTypes;
|
||||
foreach (string b in colliderParameterTypes)
|
||||
{
|
||||
if (string.Equals(name, b, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Type FindType(string assemblyName, string typeName)
|
||||
{
|
||||
try
|
||||
{
|
||||
Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
|
||||
foreach (Assembly assembly in assemblies)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.Equals(assembly.GetName().Name, assemblyName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Type type = assembly.GetType(typeName, throwOnError: false);
|
||||
if (type != null)
|
||||
{
|
||||
return type;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
try
|
||||
{
|
||||
return Type.GetType(typeName + ", " + assemblyName, throwOnError: false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
try
|
||||
{
|
||||
return Assembly.Load(new AssemblyName(assemblyName)).GetType(typeName, throwOnError: false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
RadicadeEacBypassPlugin.LogSource.LogWarning((object)("[ColliderGuard] " + assemblyName + " could not be loaded: " + ex.Message));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<MethodBase> Resolve(string assemblyName, string typeName, string methodName, int args)
|
||||
{
|
||||
var type = FindType(assemblyName, typeName);
|
||||
if (type == null) yield break;
|
||||
foreach (var method in type.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic))
|
||||
if (method.Name == methodName && method.GetParameters().Length == args)
|
||||
yield return method;
|
||||
}
|
||||
[HarmonyFinalizer]
|
||||
private static Exception Swallow(Exception __exception, MethodBase __originalMethod)
|
||||
{
|
||||
if (__exception == null || !Enabled)
|
||||
{
|
||||
return __exception;
|
||||
}
|
||||
if (!IsPhysicsCastFailure(__exception))
|
||||
{
|
||||
return __exception;
|
||||
}
|
||||
int num = ++_swallows;
|
||||
if (num <= 5 || num % 100 == 0)
|
||||
{
|
||||
RadicadeEacBypassPlugin.LogSource.LogWarning((object)("[ColliderGuard] caught a collider cast failure in " + Describe(__originalMethod) + " (#" + num + ")" + DescribeArgument(__originalMethod) + ": " + __exception.GetType().Name + ": " + __exception.Message + " - the failed operation was aborted; remaining work in that method did not run."));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string DescribeArgument(MethodBase method)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (method != null && LastCollider.TryGetValue(method, out var value))
|
||||
{
|
||||
return " collider=" + value;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static string Describe(MethodBase method)
|
||||
{
|
||||
try
|
||||
{
|
||||
return (method == null) ? "<unknown frame>" : (method.DeclaringType?.ToString() + "." + method.Name);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "<unknown frame>";
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsPhysicsCastFailure(Exception exception)
|
||||
{
|
||||
string text;
|
||||
try
|
||||
{
|
||||
text = exception.Message ?? string.Empty;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (text.IndexOf("Unable to cast object of type", StringComparison.OrdinalIgnoreCase) < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
string[] castTypes = CastTypes;
|
||||
foreach (string value in castTypes)
|
||||
{
|
||||
if (text.IndexOf(value, StringComparison.Ordinal) >= 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using HarmonyLib;
|
||||
|
||||
internal static class ConnectionGate
|
||||
{
|
||||
private const int WaitMilliseconds = 15000;
|
||||
|
||||
private static readonly ManualResetEventSlim Opened = new ManualResetEventSlim(false);
|
||||
|
||||
private static int _holds;
|
||||
|
||||
internal static void Install(Harmony harmony)
|
||||
{
|
||||
harmony.PatchAll(typeof(ConnectionGatePatch));
|
||||
}
|
||||
|
||||
internal static void Open()
|
||||
{
|
||||
Opened.Set();
|
||||
}
|
||||
|
||||
internal static void Hold()
|
||||
{
|
||||
if (Opened.IsSet) return;
|
||||
|
||||
var count = Interlocked.Increment(ref _holds);
|
||||
RadicadeEacBypassPlugin.LogSource.LogWarning("[Patch] a server request arrived before the patch list finished; holding it until it does (hold #"
|
||||
+ count + ").");
|
||||
|
||||
if (Opened.Wait(WaitMilliseconds)) return;
|
||||
|
||||
RadicadeEacBypassPlugin.LogSource.LogError("[Patch] the patch list did not finish within "
|
||||
+ (WaitMilliseconds / 1000) + "s; releasing this request and every later one so the client is not held forever.");
|
||||
Opened.Set();
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(BestHTTP.HTTPManager), "SendRequest", new[] { typeof(BestHTTP.HTTPRequest) })]
|
||||
internal static class ConnectionGatePatch
|
||||
{
|
||||
[HarmonyPriority(Priority.First)]
|
||||
internal static void Prefix()
|
||||
{
|
||||
ConnectionGate.Hold();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
internal static class CrashAddressMap
|
||||
{
|
||||
private const long DefaultTarget = 0x29a79ec;
|
||||
|
||||
internal static bool Enabled => !_disabled;
|
||||
|
||||
internal static long TargetRva => _target;
|
||||
|
||||
private static readonly bool _disabled = IsDisabled();
|
||||
|
||||
private static readonly long _target = ReadTarget();
|
||||
|
||||
private static bool IsDisabled()
|
||||
{
|
||||
try
|
||||
{
|
||||
var raw = Environment.GetEnvironmentVariable("RADICADE_ADDR_MAP");
|
||||
if (string.IsNullOrWhiteSpace(raw)) return false;
|
||||
return raw.Trim().ToLowerInvariant() is "0" or "off" or "false" or "no";
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
private static long ReadTarget()
|
||||
{
|
||||
try
|
||||
{
|
||||
var raw = Environment.GetEnvironmentVariable("RADICADE_ADDR_TARGET");
|
||||
if (string.IsNullOrWhiteSpace(raw)) return DefaultTarget;
|
||||
raw = raw.Trim();
|
||||
if (raw.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) raw = raw.Substring(2);
|
||||
return long.TryParse(raw, System.Globalization.NumberStyles.HexNumber,
|
||||
System.Globalization.CultureInfo.InvariantCulture, out var value) ? value : DefaultTarget;
|
||||
}
|
||||
catch { return DefaultTarget; }
|
||||
}
|
||||
|
||||
internal static void Start()
|
||||
{
|
||||
if (!Enabled) return;
|
||||
var thread = new System.Threading.Thread(Run) { IsBackground = true, Name = "RadicadeAddrMap" };
|
||||
thread.Start();
|
||||
}
|
||||
|
||||
private static void Run()
|
||||
{
|
||||
try { System.Threading.Thread.Sleep(5000); } catch { }
|
||||
|
||||
var log = RadicadeEacBypassPlugin.LogSource;
|
||||
|
||||
IntPtr baseAddress = IntPtr.Zero;
|
||||
var moduleSize = 0L;
|
||||
try
|
||||
{
|
||||
using (var self = System.Diagnostics.Process.GetCurrentProcess())
|
||||
{
|
||||
foreach (System.Diagnostics.ProcessModule module in self.Modules)
|
||||
{
|
||||
if (!string.Equals(module.ModuleName, "GameAssembly.dll", StringComparison.OrdinalIgnoreCase)) continue;
|
||||
baseAddress = module.BaseAddress;
|
||||
moduleSize = module.ModuleMemorySize;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.LogError("[AddrMap] could not locate GameAssembly.dll: " + ex.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (baseAddress == IntPtr.Zero || moduleSize <= 0)
|
||||
{
|
||||
log.LogError("[AddrMap] GameAssembly.dll is not loaded - nothing to map");
|
||||
return;
|
||||
}
|
||||
|
||||
var gameRoot = Path.GetDirectoryName(UnityEngine.Application.dataPath) ?? ".";
|
||||
var path = Path.Combine(gameRoot, "BepInEx", "radicade-addr-map.txt");
|
||||
|
||||
var baseValue = baseAddress.ToInt64();
|
||||
var mapped = 0;
|
||||
var images = 0;
|
||||
var unmappable = 0;
|
||||
var bestBelow = -1L;
|
||||
string bestBelowName = null;
|
||||
long bestAbove = -1;
|
||||
string bestAboveName = null;
|
||||
var failures = 0;
|
||||
|
||||
try
|
||||
{
|
||||
using (var writer = new StreamWriter(path, false))
|
||||
{
|
||||
writer.WriteLine("# il2cpp RVA -> method map, written by RadicadeAugustClient (run 24)");
|
||||
writer.WriteLine("# GameAssembly.dll base 0x" + baseValue.ToString("x") + ", mapped size 0x" + moduleSize.ToString("x"));
|
||||
writer.WriteLine("# rva<TAB>image<TAB>Namespace.Class::Method - rva is relative to the base above");
|
||||
|
||||
unsafe
|
||||
{
|
||||
var domain = Il2CppInterop.Runtime.IL2CPP.il2cpp_domain_get();
|
||||
if (domain == IntPtr.Zero)
|
||||
{
|
||||
log.LogError("[AddrMap] no il2cpp domain; nothing to map");
|
||||
return;
|
||||
}
|
||||
|
||||
uint assemblyCount = 0;
|
||||
var assemblies = Il2CppInterop.Runtime.IL2CPP.il2cpp_domain_get_assemblies(domain, ref assemblyCount);
|
||||
for (uint a = 0; a < assemblyCount; a++)
|
||||
{
|
||||
var image = Il2CppInterop.Runtime.IL2CPP.il2cpp_assembly_get_image(assemblies[a]);
|
||||
if (image == IntPtr.Zero) continue;
|
||||
images++;
|
||||
|
||||
string imageName;
|
||||
try { imageName = Marshal.PtrToStringAnsi(Il2CppInterop.Runtime.IL2CPP.il2cpp_image_get_name(image)) ?? "?"; }
|
||||
catch { imageName = "?"; }
|
||||
|
||||
var classCount = Il2CppInterop.Runtime.IL2CPP.il2cpp_image_get_class_count(image);
|
||||
for (uint c = 0; c < classCount; c++)
|
||||
{
|
||||
var klass = Il2CppInterop.Runtime.IL2CPP.il2cpp_image_get_class(image, c);
|
||||
if (klass == IntPtr.Zero) continue;
|
||||
|
||||
string className;
|
||||
try
|
||||
{
|
||||
var namespaze = Marshal.PtrToStringAnsi(Il2CppInterop.Runtime.IL2CPP.il2cpp_class_get_namespace(klass));
|
||||
var name = Marshal.PtrToStringAnsi(Il2CppInterop.Runtime.IL2CPP.il2cpp_class_get_name(klass));
|
||||
className = string.IsNullOrEmpty(namespaze) ? name : namespaze + "." + name;
|
||||
}
|
||||
catch { className = "?"; failures++; }
|
||||
|
||||
var iterator = IntPtr.Zero;
|
||||
IntPtr methodInfo;
|
||||
while ((methodInfo = Il2CppInterop.Runtime.IL2CPP.il2cpp_class_get_methods(klass, ref iterator)) != IntPtr.Zero)
|
||||
{
|
||||
long entry;
|
||||
try { entry = Marshal.ReadIntPtr(methodInfo).ToInt64(); }
|
||||
catch { entry = 0; }
|
||||
|
||||
if (entry <= baseValue || entry >= baseValue + moduleSize)
|
||||
{
|
||||
unmappable++;
|
||||
continue;
|
||||
}
|
||||
|
||||
var rva = entry - baseValue;
|
||||
string methodName;
|
||||
try { methodName = Marshal.PtrToStringAnsi(Il2CppInterop.Runtime.IL2CPP.il2cpp_method_get_name(methodInfo)) ?? "?"; }
|
||||
catch { methodName = "?"; failures++; }
|
||||
|
||||
writer.WriteLine("0x" + rva.ToString("x") + "\t" + imageName + "\t" + className + "::" + methodName);
|
||||
mapped++;
|
||||
|
||||
var full = className + "::" + methodName;
|
||||
if (rva <= _target && rva > bestBelow) { bestBelow = rva; bestBelowName = full; }
|
||||
if (rva > _target && (bestAbove < 0 || rva < bestAbove)) { bestAbove = rva; bestAboveName = full; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.LogError("[AddrMap] pass failed after " + mapped + " methods: " + ex);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (bestBelow < 0)
|
||||
{
|
||||
log.LogWarning("[AddrMap] no method starts below the target 0x" + _target.ToString("x")
|
||||
+ " - the crash is in code no managed method owns (il2cpp runtime or native plugin)."
|
||||
+ (bestAboveName != null ? " Nearest above: 0x" + bestAbove.ToString("x") + " " + bestAboveName : string.Empty));
|
||||
return;
|
||||
}
|
||||
|
||||
var span = bestAbove > bestBelow ? bestAbove - bestBelow : -1;
|
||||
log.LogWarning("[AddrMap] crash target 0x" + _target.ToString("x") + " -> containing method 0x" + bestBelow.ToString("x")
|
||||
+ " " + bestBelowName + " (+0x" + (_target - bestBelow).ToString("x") + " into it)"
|
||||
+ (span > 0 ? "; next method 0x" + bestAbove.ToString("x") + " " + bestAboveName + " (span 0x" + span.ToString("x") + ")" : string.Empty)
|
||||
+ (failures > 0 ? "; " + failures + " name reads failed" : string.Empty));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using HarmonyLib;
|
||||
using Il2CppInterop.Runtime;
|
||||
using RRUI;
|
||||
using RRUI.Data;
|
||||
using UnityEngine;
|
||||
|
||||
internal static class DynamicUiPrefabRepair
|
||||
{
|
||||
private static bool reportedMissing;
|
||||
|
||||
internal static void Apply(Harmony harmony)
|
||||
{
|
||||
harmony.Patch(AccessTools.PropertyGetter(typeof(DynamicUIConfig), "Instance"),
|
||||
postfix: new HarmonyMethod(typeof(DynamicUiPrefabRepair), nameof(Repair)));
|
||||
}
|
||||
|
||||
private static void Repair(DynamicUIConfig __result)
|
||||
{
|
||||
if (__result == null) return;
|
||||
var standard = __result.DynamicFloatPrefab_MultiSelectConfig;
|
||||
var pro = __result.DynamicProUXFloatPrefab_MultiSelectConfig;
|
||||
var fallback = Compatible(standard) ? standard : Compatible(pro) ? pro : null;
|
||||
if (fallback == null)
|
||||
{
|
||||
if (!reportedMissing)
|
||||
{
|
||||
reportedMissing = true;
|
||||
RadicadeEacBypassPlugin.LogSource.LogWarning(
|
||||
"[DynamicUI] No compatible multi-select float prefab is available; leaving the missing mapping visible.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
_ = __result.LandscapePrefabsByType;
|
||||
_ = __result.PortraitPrefabsByType;
|
||||
_ = __result.ProUXPrefabsByType;
|
||||
RepairMap(__result.landscapePrefabsByType, fallback, "landscape");
|
||||
RepairMap(__result.portraitPrefabsByType, fallback, "portrait");
|
||||
RepairMap(__result.proUXPrefabsByType, fallback, "ProUX");
|
||||
}
|
||||
|
||||
private static bool Compatible(GameObject prefab) => prefab != null &&
|
||||
prefab.GetComponentInChildren<DynamicFloatInputFieldModelController_MultiSelectConfig>(true) != null;
|
||||
|
||||
private static void RepairMap(Il2CppSystem.Collections.Generic.Dictionary<Il2CppSystem.Type, GameObject> map,
|
||||
GameObject fallback, string layout)
|
||||
{
|
||||
if (map == null) return;
|
||||
var key = Il2CppType.Of<JNHHEBGEAMP>();
|
||||
if (map.ContainsKey(key) && map[key] != null) return;
|
||||
map[key] = fallback;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using HarmonyLib;
|
||||
|
||||
internal static class InviteOnlineFriendsPatch
|
||||
{
|
||||
internal static void Apply(Harmony harmony)
|
||||
{
|
||||
harmony.Patch(AccessTools.PropertyGetter(typeof(ECFHJOKNJIJ), "OHKPFEMEPAE"),
|
||||
prefix: new HarmonyMethod(typeof(InviteOnlineFriendsPatch), nameof(EnableDialog)));
|
||||
harmony.Patch(AccessTools.PropertyGetter(typeof(ECFHJOKNJIJ), "EBCNNMAJGBN"),
|
||||
prefix: new HarmonyMethod(typeof(InviteOnlineFriendsPatch), nameof(IncomingDialog)));
|
||||
}
|
||||
|
||||
private static bool EnableDialog(ECFHJOKNJIJ __instance, ref bool __result)
|
||||
{
|
||||
return ReadServerOverride(__instance, "Radicade.Invites.ShowOnlineFriendsBeforeJoining", ref __result);
|
||||
}
|
||||
|
||||
private static bool IncomingDialog(ECFHJOKNJIJ __instance, ref bool __result)
|
||||
{
|
||||
return ReadServerOverride(__instance, "Radicade.Invites.UseRRUIDialog", ref __result);
|
||||
}
|
||||
|
||||
private static readonly System.Collections.Generic.HashSet<string> logged = new();
|
||||
|
||||
private static bool ReadServerOverride(ECFHJOKNJIJ instance, string key, ref bool result)
|
||||
{
|
||||
try
|
||||
{
|
||||
var configs = instance.CCNKOPKDIJI;
|
||||
if (configs == null || !configs.FNBNNDMHFCK) return true;
|
||||
if (!bool.TryParse(configs.KFNCGHMLMAI(key), out var value)) return true;
|
||||
result = value;
|
||||
return false;
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
if (logged.Add(key + ":error"))
|
||||
RadicadeEacBypassPlugin.LogSource.LogWarning("[InviteUI] config lookup failed: " + ex.Message);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,461 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using HarmonyLib;
|
||||
|
||||
internal static class KeepsakeToolPatch
|
||||
{
|
||||
private static readonly Type Palette =
|
||||
typeof(RecRoom.AGUI.ToolUIs.MakerPenPaletteMainScreen);
|
||||
|
||||
private static readonly Type Registry = typeof(PMJEHEPKBJL);
|
||||
|
||||
private static readonly Type ToolTypeEnum = typeof(AOCJJLANBMH);
|
||||
|
||||
private static readonly Type ToolConfig = typeof(RuntimeSpawnableToolConfig);
|
||||
|
||||
|
||||
private static readonly Dictionary<string, int> RepeatedLines = new Dictionary<string, int>();
|
||||
|
||||
private static string Mode
|
||||
=> (Environment.GetEnvironmentVariable("RADICADE_KEEPSAKE_TOOL")
|
||||
?? (RadicadeEacBypassPlugin.KeepsakeToolEnabled?.Value == true ? "on" : "skip")).Trim();
|
||||
|
||||
private static bool Requested => Mode.ToLowerInvariant() switch
|
||||
{
|
||||
"on" or "true" or "1" or "yes" or "enable" or "enabled" => true,
|
||||
"off" or "false" or "0" => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
internal static bool Forcing => Mode.ToLowerInvariant() switch
|
||||
{
|
||||
"on" or "true" or "1" or "yes" or "enable" or "enabled" => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
internal static void Apply(Harmony harmony)
|
||||
{
|
||||
if (!Requested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Palette == null || Registry == null || ToolTypeEnum == null)
|
||||
{
|
||||
RadicadeEacBypassPlugin.LogSource.LogWarning(
|
||||
"[Keepsake] palette/registry/tool-type type not found (" +
|
||||
$"palette={Palette?.FullName ?? "null"}, registry={Registry?.FullName ?? "null"}, " +
|
||||
$"toolType={ToolTypeEnum?.FullName ?? "null"}) - keepsake patch skipped");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Patch(harmony, Registry, "JBAINDGLMBO", new[] { ToolTypeEnum }, null, nameof(ConfigPostfix));
|
||||
|
||||
PatchByName(harmony, Registry, "LPBDCDIMBEN", nameof(PredicatePostfix));
|
||||
|
||||
|
||||
PatchByNameOnFamily(harmony, Palette, "LKGMIHJOIID", nameof(PredicatePostfix));
|
||||
PatchByNameOnFamily(harmony, Palette, "OLKPEGJNJGG", nameof(ConfigFilterPostfix));
|
||||
PatchByNameOnFamily(harmony, Palette, "IBHJBFJMBFB", nameof(ConfigFilterPostfix));
|
||||
|
||||
Patch(harmony, Palette, "OnUsedPaletteItem", null, nameof(UsedPalettePrefix), null);
|
||||
|
||||
Audit(harmony);
|
||||
}
|
||||
|
||||
private static void Audit(Harmony harmony)
|
||||
{
|
||||
var service = AccessTools.TypeByName("AMAOGLDGNLL");
|
||||
foreach (var name in new[]
|
||||
{
|
||||
"MJDEEABFHPJ", "NCAKOLMAAEA", "CAFCLGFGKAO", "KHPJGNADGEE", "NGJMABOMHHB",
|
||||
"FCOMKMPDEIO", "AEHHFFIKLG"
|
||||
})
|
||||
PatchByName(harmony, service, name, nameof(CallPostfix), 2);
|
||||
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("RADICADE_KEEPSAKE_AUDIT"), "1",
|
||||
StringComparison.Ordinal)) return;
|
||||
|
||||
Log("[Keepsake] RADICADE_KEEPSAKE_AUDIT=1: instrumenting the room/progression models too");
|
||||
foreach (var typeName in new[] { "RRUI.Data.RoomModel", "RRUI.Data.RoomListModel", "RRUI.Data.ProgressionEventsModel" })
|
||||
{
|
||||
var model = AccessTools.TypeByName(typeName);
|
||||
foreach (var name in new[]
|
||||
{
|
||||
"IsKeepsakeRoom", "GetCachedAreAllKeepsakesCollected",
|
||||
"GetTotalKeepsakeCountPerCategoryForRoom", "GetUncollectedKeepsakeCountPerCategoryForRoom",
|
||||
"GetTotalKeepsakeCountForRoom", "GetUncollectedKeepsakeCountForRoom",
|
||||
"IsKeepsakeRoomListUpsellItemOwned", "AHACLFABDEO", "CJPFGOBHGBB"
|
||||
})
|
||||
PatchByName(harmony, model, name, nameof(ModelPostfix), 4);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal static object KeepsakeMember()
|
||||
{
|
||||
try
|
||||
{
|
||||
return ToolTypeEnum == null ? null : Enum.Parse(ToolTypeEnum, "KEEPSAKE");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
RadicadeEacBypassPlugin.LogSource.LogWarning("[Keepsake] KEEPSAKE enum member missing: " + e.Message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool IsKeepsake(object toolType)
|
||||
=> toolType != null && KeepsakeMember()?.Equals(toolType) == true;
|
||||
|
||||
internal static void Log(string message)
|
||||
=> RadicadeEacBypassPlugin.LogSource.LogInfo(message);
|
||||
|
||||
private static void LogOnce(string message, int limit = 3)
|
||||
{
|
||||
var seen = RepeatedLines.TryGetValue(message, out var count) ? count + 1 : 1;
|
||||
RepeatedLines[message] = seen;
|
||||
if (seen <= limit) Log(message);
|
||||
else if (seen == limit + 1) Log($"[Keepsake] (suppressing further copies of: {message})");
|
||||
}
|
||||
|
||||
|
||||
private static void ConfigPostfix(object[] __args, RuntimeSpawnableToolConfig __result)
|
||||
{
|
||||
var toolType = __args.Length > 0 ? __args[0] : null;
|
||||
var keepsake = IsKeepsake(toolType);
|
||||
if (!keepsake && !IsRestricted(__result)) return;
|
||||
|
||||
|
||||
if (!keepsake || !Forcing) return;
|
||||
|
||||
var allowed = Field(__result, "spawnableToolTypesAllowedInRooms2");
|
||||
if (Contains(allowed, KeepsakeMember())) return;
|
||||
AddToCollection(allowed, KeepsakeMember());
|
||||
}
|
||||
|
||||
private static void PredicatePostfix(MethodBase __originalMethod, object[] __args, ref bool __result)
|
||||
{
|
||||
var toolType = FirstToolType(__args);
|
||||
var keepsake = IsKeepsake(toolType);
|
||||
|
||||
if (keepsake && Forcing && !__result)
|
||||
{
|
||||
__result = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static void CallPostfix(MethodBase __originalMethod, object[] __args, object __result)
|
||||
{
|
||||
}
|
||||
|
||||
private static void ModelPostfix(MethodBase __originalMethod, object[] __args, object __result)
|
||||
{
|
||||
}
|
||||
|
||||
private static string DescribeResult(object result)
|
||||
{
|
||||
if (result == null) return "(null)";
|
||||
var type = result.GetType();
|
||||
try
|
||||
{
|
||||
if (type == typeof(bool) || type == typeof(int) || type == typeof(long))
|
||||
return result.ToString();
|
||||
|
||||
var status = AccessTools.Property(type, "IsCompleted")?.GetValue(result) as bool?;
|
||||
if (status == true)
|
||||
{
|
||||
var value = AccessTools.Property(type, "Result")?.GetValue(result);
|
||||
return $"{type.Name}(completed) = {value}";
|
||||
}
|
||||
|
||||
if (status == false) return $"{type.Name}(pending)";
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return type.Name;
|
||||
}
|
||||
|
||||
private static void ConfigFilterPostfix(MethodBase __originalMethod, object[] __args, ref bool __result)
|
||||
{
|
||||
var config = __args.Length > 0 ? __args[0] : null;
|
||||
var name = Field(config, "UniqueName")?.ToString() ?? "(unnamed tool)";
|
||||
var keepsake = string.Equals(name, "KEEPSAKE", StringComparison.OrdinalIgnoreCase);
|
||||
if (!keepsake) return;
|
||||
|
||||
if (!Forcing || __result) return;
|
||||
|
||||
__result = true;
|
||||
}
|
||||
|
||||
private static void UsedPalettePrefix(object[] __args)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
private static object FirstToolType(object[] args)
|
||||
{
|
||||
if (args == null) return null;
|
||||
foreach (var arg in args)
|
||||
{
|
||||
if (arg == null) continue;
|
||||
if (ToolTypeEnum.IsInstanceOfType(arg) || arg.GetType() == ToolTypeEnum) return arg;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool IsRestricted(object config)
|
||||
=> Field(config, "AvailableInMakerPen") is bool makerPen && !makerPen;
|
||||
|
||||
private static string Frames(int take = 10)
|
||||
{
|
||||
try
|
||||
{
|
||||
var frames = new StackTrace(true).GetFrames();
|
||||
var sb = new StringBuilder();
|
||||
var taken = 0;
|
||||
foreach (var frame in frames)
|
||||
{
|
||||
var method = frame.GetMethod();
|
||||
var owner = method?.DeclaringType?.Name ?? "?";
|
||||
if (owner.Contains("KeepsakeToolPatch") || owner.Contains("Harmony")) continue;
|
||||
sb.Append(owner).Append('.').Append(method?.Name ?? "?").Append(" <- ");
|
||||
if (++taken >= take) break;
|
||||
}
|
||||
return sb.Length == 0 ? "(no frames)" : sb.ToString().TrimEnd(' ', '<', '-');
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return "(stack failed: " + e.GetType().Name + ")";
|
||||
}
|
||||
}
|
||||
|
||||
private static string DescribeList(object collection)
|
||||
{
|
||||
if (collection == null) return "(null)";
|
||||
var type = collection.GetType();
|
||||
|
||||
var count = 0;
|
||||
var keepsake = false;
|
||||
var names = new StringBuilder();
|
||||
try
|
||||
{
|
||||
var countValue = AccessTools.Property(type, "Count")?.GetValue(collection);
|
||||
count = countValue is int n ? n : 0;
|
||||
if (count == 0) return "0 item(s) []";
|
||||
|
||||
var indexer = type.GetProperty("Item");
|
||||
for (var i = 0; i < count && i < 12; i++)
|
||||
{
|
||||
var item = indexer?.GetValue(collection, new object[] { i });
|
||||
if (IsKeepsake(item)) keepsake = true;
|
||||
names.Append(item).Append(',');
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return $"(list read failed: {e.GetType().Name}: {e.Message})";
|
||||
}
|
||||
|
||||
return $"{count} item(s) [{names.ToString().TrimEnd(',')}]" + (keepsake ? " KEEPSAKE present" : "");
|
||||
}
|
||||
|
||||
private static bool Contains(object collection, object item)
|
||||
{
|
||||
if (collection == null || item == null) return false;
|
||||
try
|
||||
{
|
||||
var contains = AccessTools.Method(collection.GetType(), "Contains");
|
||||
return contains?.Invoke(collection, new[] { item }) is true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool AddToCollection(object collection, object item)
|
||||
{
|
||||
if (collection == null || item == null) return false;
|
||||
try
|
||||
{
|
||||
var type = collection.GetType();
|
||||
MethodInfo add = null;
|
||||
foreach (var candidate in type.GetMethods(BindingFlags.Public | BindingFlags.Instance))
|
||||
{
|
||||
if (candidate.Name != "Add" || candidate.GetParameters().Length != 1) continue;
|
||||
add = candidate;
|
||||
break;
|
||||
}
|
||||
|
||||
if (add == null) return false;
|
||||
add.Invoke(collection, new[] { item });
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
RadicadeEacBypassPlugin.LogSource.LogWarning(
|
||||
$"[Keepsake] could not add to {collection.GetType().Name}: {e.GetType().Name}: {e.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static string Describe(object config)
|
||||
{
|
||||
if (config == null) return "(null config)";
|
||||
|
||||
var sb = new StringBuilder(config.GetType().Name).Append('{');
|
||||
foreach (var name in new[]
|
||||
{
|
||||
"UniqueName", "AvailableInMakerPen", "AvailableToDevsOnly", "IsBetaItem",
|
||||
"AvailableInSpawner", "CanSaveIntoInventions", "LimitInventionSaveForJuniors"
|
||||
})
|
||||
{
|
||||
var value = Field(config, name);
|
||||
if (value != null) sb.Append(name).Append('=').Append(value).Append(' ');
|
||||
}
|
||||
|
||||
var allowed = Field(config, "spawnableToolTypesAllowedInRooms2");
|
||||
if (allowed != null)
|
||||
sb.Append("allowedInRooms=").Append(DescribeList(allowed)).Append(' ');
|
||||
|
||||
return sb.Append('}').ToString();
|
||||
}
|
||||
|
||||
private static object Field(object instance, string name)
|
||||
{
|
||||
if (instance == null) return null;
|
||||
try
|
||||
{
|
||||
var type = instance.GetType();
|
||||
var property = AccessTools.Property(type, name);
|
||||
if (property != null) return property.GetValue(instance);
|
||||
return AccessTools.Field(type, name)?.GetValue(instance);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static void PatchByNameOnFamily(Harmony harmony, Type root, string method, string postfix)
|
||||
{
|
||||
var patched = 0;
|
||||
foreach (var type in Family(root))
|
||||
{
|
||||
foreach (var found in type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic
|
||||
| BindingFlags.Static | BindingFlags.Instance))
|
||||
{
|
||||
if (found.Name != method) continue;
|
||||
if (Bind(harmony, found, null, postfix)) patched++;
|
||||
}
|
||||
}
|
||||
|
||||
if (patched == 0)
|
||||
RadicadeEacBypassPlugin.LogSource.LogWarning(
|
||||
$"[Keepsake] no method named {method} on {root.Name} or its nested types - not instrumented");
|
||||
}
|
||||
|
||||
private static void PatchByName(Harmony harmony, Type target, string method, string postfix,
|
||||
int maxArguments = 1)
|
||||
{
|
||||
if (target == null)
|
||||
{
|
||||
RadicadeEacBypassPlugin.LogSource.LogWarning(
|
||||
$"[Keepsake] type for {method} not found - not instrumented");
|
||||
return;
|
||||
}
|
||||
|
||||
var patched = 0;
|
||||
foreach (var found in target.GetMethods(BindingFlags.Public | BindingFlags.NonPublic
|
||||
| BindingFlags.Static | BindingFlags.Instance))
|
||||
{
|
||||
if (found.Name != method) continue;
|
||||
var parameters = found.GetParameters();
|
||||
if (parameters.Length > maxArguments) continue;
|
||||
if (parameters.Any(p => p.ParameterType.IsByRef || p.IsOut || p.ParameterType.IsPointer)) continue;
|
||||
if (maxArguments == 1 && (parameters.Length != 1 ||
|
||||
(!ToolTypeEnum.IsAssignableFrom(parameters[0].ParameterType)
|
||||
&& parameters[0].ParameterType != ToolTypeEnum))) continue;
|
||||
if (Bind(harmony, found, null, postfix)) patched++;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static IEnumerable<Type> Family(Type root)
|
||||
{
|
||||
yield return root;
|
||||
Type[] nested;
|
||||
try
|
||||
{
|
||||
nested = root.GetNestedTypes(BindingFlags.Public | BindingFlags.NonPublic);
|
||||
}
|
||||
catch
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (var type in nested)
|
||||
foreach (var deeper in Family(type))
|
||||
yield return deeper;
|
||||
}
|
||||
|
||||
private static void Patch(Harmony harmony, Type target, string method, Type[] args,
|
||||
string prefix, string postfix)
|
||||
{
|
||||
if (target == null)
|
||||
{
|
||||
RadicadeEacBypassPlugin.LogSource.LogWarning($"[Keepsake] type for {method} not found - not instrumented");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var found = args == null
|
||||
? AccessTools.Method(target, method)
|
||||
: AccessTools.Method(target, method, args);
|
||||
if (found == null)
|
||||
{
|
||||
RadicadeEacBypassPlugin.LogSource.LogWarning(
|
||||
$"[Keepsake] {target.Name}.{method} not found - not instrumented");
|
||||
return;
|
||||
}
|
||||
|
||||
Bind(harmony, found, prefix, postfix);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
RadicadeEacBypassPlugin.LogSource.LogWarning(
|
||||
$"[Keepsake] FAILED to instrument {target.Name}.{method}: {e.GetType().Name}: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool Bind(Harmony harmony, MethodBase found, string prefix, string postfix)
|
||||
{
|
||||
try
|
||||
{
|
||||
var self = typeof(KeepsakeToolPatch);
|
||||
const BindingFlags flags = BindingFlags.Static | BindingFlags.NonPublic;
|
||||
harmony.Patch(found,
|
||||
prefix: prefix == null ? null : new HarmonyMethod(self.GetMethod(prefix, flags)),
|
||||
postfix: postfix == null ? null : new HarmonyMethod(self.GetMethod(postfix, flags)));
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
RadicadeEacBypassPlugin.LogSource.LogWarning(
|
||||
$"[Keepsake] FAILED to instrument {found.DeclaringType?.Name}.{found.Name}: " +
|
||||
$"{e.GetType().Name}: {e.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
using HarmonyLib;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
internal static class LogFloodGate
|
||||
{
|
||||
internal const double WindowSeconds = 30.0;
|
||||
|
||||
internal static bool Enabled => !_disabled;
|
||||
|
||||
private static readonly bool _disabled = IsDisabled();
|
||||
private static readonly Dictionary<string, Entry> Seen = new Dictionary<string, Entry>();
|
||||
private static long _totalSuppressed;
|
||||
|
||||
private static bool _selfTestPending;
|
||||
private static int _selfTestFired;
|
||||
|
||||
private sealed class Entry
|
||||
{
|
||||
public double WindowStart;
|
||||
public int Suppressed;
|
||||
public int ReportedAt;
|
||||
}
|
||||
|
||||
private static bool IsDisabled()
|
||||
{
|
||||
try
|
||||
{
|
||||
var raw = Environment.GetEnvironmentVariable("RADICADE_LOG_GATE");
|
||||
if (string.IsNullOrWhiteSpace(raw)) return false;
|
||||
return raw.Trim().ToLowerInvariant() is "0" or "off" or "false" or "no";
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
internal static void Apply(Harmony harmony)
|
||||
{
|
||||
try
|
||||
{
|
||||
var target = AccessTools.Method(typeof(ODGGOALOLEG), "LogException",
|
||||
new[] { typeof(Il2CppSystem.Exception), typeof(UnityEngine.Object) });
|
||||
if (target == null)
|
||||
{
|
||||
Warn("[LogGate] ODGGOALOLEG.LogException(Il2CppSystem.Exception, UnityEngine.Object) not found - " +
|
||||
"not armed; the per-frame exception flood stays as-is.");
|
||||
return;
|
||||
}
|
||||
|
||||
var prefix = AccessTools.Method(typeof(LogFloodGate), nameof(Prefix));
|
||||
if (prefix == null)
|
||||
{
|
||||
Warn("[LogGate] patch method Prefix not found - not armed.");
|
||||
return;
|
||||
}
|
||||
|
||||
harmony.Patch(target, prefix: new HarmonyMethod(prefix));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Warn($"[LogGate] failed to arm ({e.GetType().Name}: {e.Message}); the exception flood stays as-is.");
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
internal static bool Prefix(Il2CppSystem.Exception __0)
|
||||
{
|
||||
if (!Enabled) return true;
|
||||
try
|
||||
{
|
||||
if (_selfTestPending && System.Threading.Interlocked.Exchange(ref _selfTestFired, 1) == 0)
|
||||
{
|
||||
_selfTestPending = false;
|
||||
RunSelfTest();
|
||||
}
|
||||
|
||||
var signature = Signature(__0);
|
||||
if (signature == null) return true;
|
||||
|
||||
var now = Time.realtimeSinceStartup;
|
||||
lock (Seen)
|
||||
{
|
||||
if (Seen.Count > 512) Evict(now);
|
||||
|
||||
if (!Seen.TryGetValue(signature, out var entry))
|
||||
{
|
||||
Seen[signature] = new Entry { WindowStart = now };
|
||||
return true;
|
||||
}
|
||||
|
||||
if (now - entry.WindowStart < WindowSeconds)
|
||||
{
|
||||
entry.Suppressed++;
|
||||
_totalSuppressed++;
|
||||
if (entry.Suppressed == 1)
|
||||
Warn($"[LogGate] repeats of this exception are now suppressed for {WindowSeconds:0}s at a time: {signature}");
|
||||
else if (entry.Suppressed - entry.ReportedAt >= 500)
|
||||
{
|
||||
entry.ReportedAt = entry.Suppressed;
|
||||
Warn($"[LogGate] {entry.Suppressed} identical repeats of this exception suppressed so far " +
|
||||
$"(session total {_totalSuppressed}): {signature}");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
var dropped = entry.Suppressed;
|
||||
entry.WindowStart = now;
|
||||
entry.Suppressed = 0;
|
||||
entry.ReportedAt = 0;
|
||||
if (dropped > 0)
|
||||
Warn($"[LogGate] {dropped} identical repeats were collapsed in the previous {WindowSeconds:0}s window " +
|
||||
$"(session total {_totalSuppressed}); logging this occurrence in full: {signature}");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static void Evict(double now)
|
||||
{
|
||||
var stale = new List<string>();
|
||||
foreach (var pair in Seen)
|
||||
if (now - pair.Value.WindowStart > 600.0)
|
||||
stale.Add(pair.Key);
|
||||
foreach (var key in stale)
|
||||
Seen.Remove(key);
|
||||
}
|
||||
|
||||
private static string Signature(Il2CppSystem.Exception exception)
|
||||
{
|
||||
string type, message, stack;
|
||||
try { type = exception.GetType().Name; } catch { type = "Exception"; }
|
||||
try { message = exception.Message; } catch { message = "<message unreadable>"; }
|
||||
try { stack = exception.StackTrace; } catch { stack = null; }
|
||||
if (string.IsNullOrEmpty(message) && string.IsNullOrEmpty(stack)) return null;
|
||||
return Trim(type + ": " + message, 150) + " @ " + FirstFrame(stack);
|
||||
}
|
||||
|
||||
private static string FirstFrame(string stack)
|
||||
{
|
||||
if (string.IsNullOrEmpty(stack)) return "<no stack>";
|
||||
var start = stack.IndexOf(" at ", StringComparison.Ordinal);
|
||||
if (start < 0)
|
||||
{
|
||||
var newline = stack.IndexOf('\n');
|
||||
return Trim(newline < 0 ? stack : stack.Substring(0, newline), 120);
|
||||
}
|
||||
var end = stack.IndexOf('\n', start);
|
||||
return Trim(end < 0 ? stack.Substring(start) : stack.Substring(start, end - start), 120);
|
||||
}
|
||||
|
||||
private static string Trim(string value, int max)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) return string.Empty;
|
||||
value = value.Replace('\r', ' ').Replace('\n', ' ').Trim();
|
||||
return value.Length <= max ? value : value.Substring(0, max) + "...";
|
||||
}
|
||||
|
||||
private static void Warn(string message)
|
||||
{
|
||||
try { RadicadeEacBypassPlugin.LogSource.LogWarning(message); } catch { }
|
||||
}
|
||||
|
||||
internal static void RequestSelfTestIfAsked()
|
||||
{
|
||||
try
|
||||
{
|
||||
var raw = Environment.GetEnvironmentVariable("RADICADE_LOG_GATE_SELFTEST");
|
||||
if (string.IsNullOrWhiteSpace(raw)) return;
|
||||
if (raw.Trim().ToLowerInvariant() is "0" or "off" or "false" or "no") return;
|
||||
|
||||
_selfTestPending = true;
|
||||
RadicadeEacBypassPlugin.LogSource.LogInfo(
|
||||
"[LogGate] self-test armed: the next exception through the game handler fires five identical ones to prove the gate (RADICADE_LOG_GATE_SELFTEST).");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Warn($"[LogGate] self-test could not be armed: {e.GetType().Name}: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void RunSelfTest()
|
||||
{
|
||||
try
|
||||
{
|
||||
var before = _totalSuppressed;
|
||||
var exception = new Il2CppSystem.Exception("radicade log gate self-test");
|
||||
const int shots = 5;
|
||||
for (var i = 0; i < shots; i++)
|
||||
UnityEngine.Debug.LogException(exception);
|
||||
|
||||
var collapsed = _totalSuppressed - before;
|
||||
Warn($"[LogGate] self-test: fired {shots} identical exceptions through the live game handler; {collapsed} were collapsed, " +
|
||||
$"{shots - collapsed} reached the log " +
|
||||
$"({(collapsed == shots - 1 ? "PASS" : "FAIL")} - the first of a burst always logs in full, the rest are suppressed).");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Warn($"[LogGate] self-test failed to run: {e.GetType().Name}: {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using HarmonyLib;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Drops known-noise log lines inside Unity's own logger, so they never reach the
|
||||
/// console or BepInEx's LogOutput.log. The game funnels both Debug.LogError and
|
||||
/// Debug.LogWarning through Engine.Logger.Log(LogType, object) - that is the
|
||||
/// single place where a message can be refused before anyone formats it.
|
||||
/// </summary>
|
||||
internal static class LogNoiseFilter
|
||||
{
|
||||
private const string SelfTestProbe =
|
||||
"The character used for Underline is not available in font asset [radicade-filter-self-test].";
|
||||
|
||||
internal static void Apply(Harmony harmony)
|
||||
{
|
||||
var prefix = AccessTools.Method(typeof(LogNoiseFilter), nameof(Prefix));
|
||||
if (prefix == null)
|
||||
{
|
||||
Diag.Warn("[LogFilter] the filter method was not found; known-noise log spam stays as-is.");
|
||||
return;
|
||||
}
|
||||
|
||||
var patched = 0;
|
||||
foreach (var target in Targets())
|
||||
{
|
||||
try
|
||||
{
|
||||
harmony.Patch(target, prefix: new HarmonyMethod(prefix));
|
||||
patched++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Diag.Warn("[LogFilter] could not patch " + target.DeclaringType?.Name + "." + target.Name +
|
||||
" (" + ex.GetType().Name + ": " + ex.Message + ").");
|
||||
}
|
||||
}
|
||||
|
||||
if (patched == 0)
|
||||
{
|
||||
Diag.Warn("[LogFilter] no Unity log funnel could be patched; the RigidbodyEx and font-underline spam stays as-is.");
|
||||
return;
|
||||
}
|
||||
|
||||
Diag.Info("[LogFilter] armed on " + patched + " Unity log funnel(s): " + LogNoiseRules.PatternCount +
|
||||
" known-noise pattern(s) are dropped before they are printed.");
|
||||
SelfTest();
|
||||
}
|
||||
|
||||
private static IEnumerable<MethodBase> Targets()
|
||||
{
|
||||
var log = AccessTools.Method(typeof(Logger), "Log",
|
||||
new[] { typeof(LogType), typeof(Il2CppSystem.Object) });
|
||||
if (log != null) yield return log;
|
||||
|
||||
var logWithContext = AccessTools.Method(typeof(Logger), "Log",
|
||||
new[] { typeof(LogType), typeof(Il2CppSystem.Object), typeof(UnityEngine.Object) });
|
||||
if (logWithContext != null) yield return logWithContext;
|
||||
}
|
||||
|
||||
private static bool Prefix(object[] __args)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (__args == null || __args.Length < 2) return true;
|
||||
var message = __args[1];
|
||||
if (message == null) return true;
|
||||
return !LogNoiseRules.ShouldDrop(message.ToString());
|
||||
}
|
||||
catch
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fires one probe line through Debug.LogWarning and checks that the rules saw
|
||||
/// it. If they did not, the filter is not wired to this build's logger and the
|
||||
/// log says so instead of pretending the spam is gone.
|
||||
/// </summary>
|
||||
private static void SelfTest()
|
||||
{
|
||||
try
|
||||
{
|
||||
var before = LogNoiseRules.DroppedCount;
|
||||
UnityEngine.Debug.LogWarning(SelfTestProbe);
|
||||
if (LogNoiseRules.DroppedCount > before)
|
||||
Diag.Info("[LogFilter] self-test PASS: a probe log line was intercepted inside Unity's logger.");
|
||||
else
|
||||
Diag.Warn("[LogFilter] self-test FAIL: a probe log line reached the log, so this build's spam is not being filtered.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Diag.Warn("[LogFilter] self-test could not run (" + ex.GetType().Name + ": " + ex.Message + ").");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
/// <summary>
|
||||
/// Log lines the client prints on every room load that say nothing a player can
|
||||
/// act on and bury the lines that matter. Pure rules only, so they can be
|
||||
/// exercised outside the game.
|
||||
/// </summary>
|
||||
internal static class LogNoiseRules
|
||||
{
|
||||
private static readonly string[] Noise =
|
||||
{
|
||||
// Every chair and couch the dorm/Rec Center spawns complains that its
|
||||
// RigidbodyEx has no entity component - dozens of lines per room load,
|
||||
// all identical in substance.
|
||||
"has an RigidbodyEx but no",
|
||||
|
||||
// TMP asks for an underline glyph the legacy Diggity font does not carry;
|
||||
// the text renders fine without it.
|
||||
"The character used for Underline is not available in font asset",
|
||||
};
|
||||
|
||||
private static int _dropped;
|
||||
|
||||
internal static int PatternCount => Noise.Length;
|
||||
|
||||
internal static int DroppedCount => Volatile.Read(ref _dropped);
|
||||
|
||||
internal static bool ShouldDrop(string message)
|
||||
{
|
||||
if (string.IsNullOrEmpty(message)) return false;
|
||||
|
||||
foreach (var pattern in Noise)
|
||||
{
|
||||
if (message.IndexOf(pattern, StringComparison.OrdinalIgnoreCase) < 0) continue;
|
||||
Interlocked.Increment(ref _dropped);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,1038 @@
|
||||
using BepInEx;
|
||||
using BepInEx.Logging;
|
||||
using BepInEx.Unity.IL2CPP;
|
||||
using HarmonyLib;
|
||||
using RecRoom.AntiCheat;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
internal sealed class FixedSetting<T>
|
||||
{
|
||||
internal T Value { get; }
|
||||
|
||||
internal FixedSetting(T value) => Value = value;
|
||||
}
|
||||
|
||||
[BepInPlugin("com.radicade.patch", "RadicadePatch", "3.0.0")]
|
||||
public sealed class RadicadeEacBypassPlugin : BasePlugin
|
||||
{
|
||||
internal static ManualLogSource LogSource;
|
||||
|
||||
internal const string ServerAddress = "https://ns.radicade.app/";
|
||||
internal const string ServerHost = "ns.radicade.app";
|
||||
|
||||
internal static FixedSetting<string> ServerUrl = new(ServerAddress);
|
||||
internal static FixedSetting<string> TlsAllowedHosts = new(ServerHost);
|
||||
internal static FixedSetting<bool> RealtimeLog = new(false);
|
||||
internal static FixedSetting<bool> KeepsakeToolEnabled = new(true);
|
||||
internal static Il2CppSystem.Threading.SynchronizationContext MainThreadContext;
|
||||
internal static string ImageKeyId;
|
||||
|
||||
static string ReadMetadataKey(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
return File.Exists(path)
|
||||
? ImageSignatureRequest.DetectKeyIdFromGameMetadata(File.ReadAllBytes(path))
|
||||
: null;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LogSource.LogWarning($"[ImageSig] key id detection failed for {Path.GetFileName(path)}: {e.GetType().Name}: {e.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void LogFixedConnection()
|
||||
{
|
||||
Log.LogInfo($"[TLS] fixed server {ServerUrl.Value}; AllowedHosts = '{TlsAllowedHosts.Value}'");
|
||||
if (!EmbeddedTlsPatches.IsHostAllowed(ServerHost))
|
||||
Log.LogError($"[TLS] built-in AllowedHosts '{TlsAllowedHosts.Value}' does not cover '{ServerHost}' - the embedded TLS stack will reject the server certificate.");
|
||||
}
|
||||
|
||||
void WarnIfMetadataPatchedForAnotherServer(Uri server)
|
||||
{
|
||||
try
|
||||
{
|
||||
var marker = Path.Combine(UnityEngine.Application.dataPath, "radicade-patch.json");
|
||||
if (!File.Exists(marker))
|
||||
{
|
||||
Log.LogWarning($"[Meta] no {marker}: this install was never metadata-patched, so the client still resolves the official hosts.");
|
||||
return;
|
||||
}
|
||||
|
||||
var text = File.ReadAllText(marker);
|
||||
var key = text.IndexOf("\"serverUrl\"", StringComparison.Ordinal);
|
||||
var colon = key < 0 ? -1 : text.IndexOf(':', key);
|
||||
var open = colon < 0 ? -1 : text.IndexOf('"', colon + 1);
|
||||
var close = open < 0 ? -1 : text.IndexOf('"', open + 1);
|
||||
if (close < 0)
|
||||
{
|
||||
Log.LogWarning($"[Meta] {marker} has no readable serverUrl field; cannot confirm the patched server matches {server}.");
|
||||
return;
|
||||
}
|
||||
|
||||
var patched = text[(open + 1)..close].TrimEnd('/');
|
||||
var wanted = server.GetLeftPart(UriPartial.Authority).TrimEnd('/');
|
||||
if (patched.Equals(wanted, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Log.LogInfo($"[Meta] metadata patched for {patched}; matches Connection:ServerUrl.");
|
||||
return;
|
||||
}
|
||||
|
||||
Log.LogWarning($"[Meta] this install's metadata was patched for '{patched}' but Connection:ServerUrl is '{wanted}'. "
|
||||
+ "The client's own literals still point at the patched address, so requests will not reach the configured server. "
|
||||
+ $"Re-patch from the pristine copy: python tools/patch-0831-client-metadata.py <game dir> --server {wanted} --apply");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.LogWarning($"[Meta] metadata marker check failed: {e.GetType().Name}: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public override void Load()
|
||||
{
|
||||
LogSource = Log;
|
||||
if (UnityEngine.Application.version != "20230831")
|
||||
throw new NotSupportedException("This plugin requires Rec Room 20230831.");
|
||||
|
||||
MainThreadContext = Il2CppSystem.Threading.SynchronizationContext.Current;
|
||||
if (MainThreadContext == null)
|
||||
Log.LogWarning("[Patch] no main-thread synchronization context was captured; promise resolution may stall.");
|
||||
|
||||
if (!Uri.TryCreate(ServerUrl.Value, UriKind.Absolute, out var server) || server.Scheme != "https")
|
||||
throw new InvalidOperationException("The RadicadePatch client requires an HTTPS server URL.");
|
||||
|
||||
LogFixedConnection();
|
||||
WarnIfMetadataPatchedForAnotherServer(server);
|
||||
|
||||
var metadataRoot = Path.Combine(UnityEngine.Application.dataPath, "il2cpp_data", "Metadata");
|
||||
var gameKeyId = ReadMetadataKey(Path.Combine(metadataRoot, "global-metadata.dat"));
|
||||
var interopKeyId = gameKeyId == null ? ReadMetadataKey(Path.Combine(metadataRoot, "global-metadata.interop.dat")) : null;
|
||||
ImageKeyId = gameKeyId ?? interopKeyId;
|
||||
if (ImageKeyId == null)
|
||||
Log.LogWarning("[ImageSig] no image key id was found in the game metadata; rewritten URLs will not state one.");
|
||||
|
||||
var harmony = new Harmony("com.radicade.patch");
|
||||
|
||||
var steps = new (string Label, Action Apply)[]
|
||||
{
|
||||
("module hiding", ApplyModuleHiding),
|
||||
("hile gate", ArmHileGate),
|
||||
("connection gate", () => ConnectionGate.Install(harmony)),
|
||||
("august challenge", () => harmony.PatchAll(typeof(AugustChallengePatches))),
|
||||
("august launch diagnostics", () => harmony.PatchAll(typeof(AugustLaunchDiagnostics))),
|
||||
("august launch validator", () => harmony.PatchAll(typeof(AugustLaunchValidatorPatch))),
|
||||
("rsa verify neutraliser", () => RsaVerifyNeutralizerPatches.Apply(harmony)),
|
||||
("file check", () => harmony.PatchAll(typeof(FileCheckEntryPatch))),
|
||||
("eac native client stub", () => harmony.PatchAll(typeof(EacNativeClientStubPatch))),
|
||||
("eac health query", () => harmony.PatchAll(typeof(EacHealthQueryLiePatch))),
|
||||
("cheat scan suppression", () => harmony.PatchAll(typeof(CheatScanSuppressPatch))),
|
||||
("cheat detection suppression", () => harmony.PatchAll(typeof(CheatDetectionSuppressPatch))),
|
||||
("cheat dorm drop guard", () => harmony.PatchAll(typeof(CheatDormDropGuardPatch))),
|
||||
("cheat manager trace", () => harmony.PatchAll(typeof(CheatManagerTracePatch))),
|
||||
("exit initiator tracer", () => harmony.PatchAll(typeof(ExitInitiatorTracerPatch))),
|
||||
("exit sequence trace", () => harmony.PatchAll(typeof(ExitSequenceTracePatch))),
|
||||
("unity quit veto", () => harmony.PatchAll(typeof(UnityQuitVetoPatch))),
|
||||
("exit state veto", () => harmony.PatchAll(typeof(ExitStateVetoPatch))),
|
||||
("crash address map", CrashAddressMap.Start),
|
||||
("tool collision reset guard", () => harmony.PatchAll(typeof(ToolCollisionResetGuardPatch))),
|
||||
("collider cast guard", () => ColliderCastGuardPatch.Apply(harmony)),
|
||||
("unbound prop freeze", () => harmony.PatchAll(typeof(UnboundPropFreezePatch))),
|
||||
("keepsake tool", () => KeepsakeToolPatch.Apply(harmony)),
|
||||
("dynamic ui prefab repair", () => DynamicUiPrefabRepair.Apply(harmony)),
|
||||
("client text rewrites", () => ClientTextRewrites.Apply(harmony)),
|
||||
("voice chat restriction capture", () => VoiceChatRestrictionPatches.ArmInstanceCapture(harmony)),
|
||||
("voice chat ban flags", () => VoiceChatBanFlagPatches.Apply(harmony)),
|
||||
("invite online friends", () => InviteOnlineFriendsPatch.Apply(harmony)),
|
||||
("log flood gate", () => ApplyLogFloodGate(harmony)),
|
||||
("realtime exit watch", () => RealtimeDiagnostics.ApplyExitWatch(harmony)),
|
||||
("realtime diagnostics", () => RealtimeDiagnostics.Apply(harmony, RealtimeLog?.Value ?? true)),
|
||||
("embedded tls", () => EmbeddedTlsPatches.Apply(harmony)),
|
||||
("http response tolerance", () => HttpTolerancePatches.Apply(harmony)),
|
||||
("log noise filter", () => LogNoiseFilter.Apply(harmony)),
|
||||
("nameserver connection", () => harmony.PatchAll(typeof(NameserverConnectionPatches))),
|
||||
("websocket redirect", () => WebSocketRedirectPatches.Apply(harmony)),
|
||||
};
|
||||
|
||||
var failed = 0;
|
||||
for (var i = 0; i < steps.Length; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
steps[i].Apply();
|
||||
Log.LogInfo($"[Patch] {i + 1}/{steps.Length} {steps[i].Label}");
|
||||
}
|
||||
catch (Exception stepEx)
|
||||
{
|
||||
failed++;
|
||||
Log.LogError($"[Patch] {i + 1}/{steps.Length} {steps[i].Label} failed: {stepEx}");
|
||||
}
|
||||
}
|
||||
|
||||
Log.LogInfo("PATCH LIST END");
|
||||
if (failed > 0)
|
||||
Log.LogWarning($"[Patch] {failed} of {steps.Length} steps failed, so the client may not behave as configured.");
|
||||
|
||||
ConnectionGate.Open();
|
||||
}
|
||||
|
||||
void ApplyModuleHiding()
|
||||
{
|
||||
if (!ModuleHidingPolicy.Enabled)
|
||||
return;
|
||||
|
||||
var hidden = PebModuleHider.HideModule("winhttp.dll");
|
||||
if (!hidden)
|
||||
Log.LogWarning("[ModuleHide] winhttp.dll was not found in the PEB module lists; a module scan will still see it.");
|
||||
|
||||
var wiped = PeHeaderWiper.WipeSignatures("winhttp.dll");
|
||||
if (!wiped)
|
||||
Log.LogWarning("[ModuleHide] the winhttp.dll PE signature wipe did not apply (signatures missing or VirtualProtect failed).");
|
||||
|
||||
if (hidden && wiped)
|
||||
Log.LogInfo("[ModuleHide] winhttp.dll is unlinked from the PEB module lists and its PE signatures are wiped.");
|
||||
}
|
||||
|
||||
void ArmHileGate()
|
||||
{
|
||||
HileGate.Enabled = true;
|
||||
Log.LogInfo(HileGate.DropEnabled
|
||||
? "[HileGate] armed: anti-cheat report POSTs are dropped at the HTTP layer."
|
||||
: "[HileGate] armed: anti-cheat report POSTs are captured in full and then allowed through (RADICADE_HILE_GATE=drop drops them instead).");
|
||||
}
|
||||
|
||||
void ApplyLogFloodGate(Harmony harmony)
|
||||
{
|
||||
LogFloodGate.Apply(harmony);
|
||||
LogFloodGate.RequestSelfTestIfAsked();
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch]
|
||||
internal static class AugustLaunchDiagnostics
|
||||
{
|
||||
[HarmonyPrefix, HarmonyPatch(typeof(MILEHJBNIKI), "LMMMGKJCPOF")]
|
||||
[HarmonyPatch(typeof(MILEHJBNIKI), "MBNHJLNEIKL")]
|
||||
private static void Trace(FDJFJOBADEI __0)
|
||||
{
|
||||
if (__0 == FDJFJOBADEI.LAUNCH_VALIDATION_FAILED)
|
||||
RadicadeEacBypassPlugin.LogSource.LogWarning("[August launch diagnostic] " + Il2CppSystem.Environment.StackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch]
|
||||
internal static class AugustChallengePatches
|
||||
{
|
||||
[HarmonyPrefix, HarmonyPatch(typeof(EACManager), nameof(EACManager.GenerateChallengeResponse))]
|
||||
private static bool Challenge(ref string __result)
|
||||
{
|
||||
__result = "radicade-eac-bypass";
|
||||
return false;
|
||||
}
|
||||
|
||||
[HarmonyPrefix, HarmonyPatch(typeof(EACManager), "JGFKOKIDHIN")]
|
||||
private static bool Ready(ref bool __result)
|
||||
{
|
||||
__result = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(IDLGLOLKLEB.MKGADIPJDLK), "IDDOELEGIFB")]
|
||||
internal static class AugustLaunchValidatorPatch
|
||||
{
|
||||
static readonly System.Threading.CancellationTokenSource Pump = new();
|
||||
|
||||
[HarmonyPrefix]
|
||||
internal static bool Bypass(IDLGLOLKLEB.MKGADIPJDLK __instance)
|
||||
{
|
||||
var promise = __instance.promise;
|
||||
if (promise == null)
|
||||
return true;
|
||||
|
||||
var context = RadicadeEacBypassPlugin.MainThreadContext;
|
||||
try
|
||||
{
|
||||
var before = Il2CppSystem.Threading.SynchronizationContext.Current;
|
||||
if (context != null)
|
||||
Il2CppSystem.Threading.SynchronizationContext.SetSynchronizationContext(context);
|
||||
try
|
||||
{
|
||||
promise.BEJNCICKNOM();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
RadicadeEacBypassPlugin.LogSource.LogError($"[August] Resolve failed: {e}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Il2CppSystem.Threading.SynchronizationContext.SetSynchronizationContext(before);
|
||||
}
|
||||
}
|
||||
catch (Exception outer)
|
||||
{
|
||||
RadicadeEacBypassPlugin.LogSource.LogError($"[August] Context install failed: {outer}");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal static class RsaVerifyNeutralizerPatches
|
||||
{
|
||||
static readonly byte[] P1KeyModulus = System.Convert.FromBase64String(
|
||||
"X07yXkxaaLcZ1wVXfkWjgFkkqdoLhDFm0GPODsF+Q47pSUlbLvtXGqStnyEJEIrQmgDiicAvCdGRq4lovr2l5sIPMaoyizsbVHBdwLUrCsji0RvSBnmvN+8KqQ8STnB4DP4pAsPilfD35def4WuX/xMCXB5+hQUVhv27HPV8Dj9XzHuJAijIM9UwDZmvUcECyiO4wv+TaZi2+ELBtaLCQR8Gm1ZPeDEwP62Ch6MJy0jx5pkvvD0KdF9Wye+3/Wx31Zn/Trdo9HL4sGFWPDM9H9kQhZd5wkTHuxpwGIIhlzIwvY2/pBGdZKP6fi1D2jROEmVkBDyhmYY9nO+s3/bndQ==");
|
||||
|
||||
static int p1Calls, totalCalls;
|
||||
|
||||
internal static void Apply(Harmony harmony)
|
||||
{
|
||||
PatchVerify(harmony, typeof(Il2CppSystem.Security.Cryptography.RSACryptoServiceProvider), "VerifyHash",
|
||||
new[]
|
||||
{
|
||||
typeof(Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppStructArray<byte>),
|
||||
typeof(Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppStructArray<byte>),
|
||||
typeof(Il2CppSystem.Security.Cryptography.HashAlgorithmName),
|
||||
typeof(Il2CppSystem.Security.Cryptography.RSASignaturePadding)
|
||||
});
|
||||
|
||||
PatchVerify(harmony, typeof(Il2CppSystem.Security.Cryptography.RSACryptoServiceProvider), "VerifyHash",
|
||||
new[]
|
||||
{
|
||||
typeof(Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppStructArray<byte>),
|
||||
typeof(int),
|
||||
typeof(Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppStructArray<byte>)
|
||||
});
|
||||
|
||||
PatchVerify(harmony, typeof(Il2CppSystem.Security.Cryptography.RSACryptoServiceProvider), "VerifyData",
|
||||
new[]
|
||||
{
|
||||
typeof(Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppStructArray<byte>),
|
||||
typeof(Il2CppSystem.Object),
|
||||
typeof(Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppStructArray<byte>)
|
||||
});
|
||||
}
|
||||
|
||||
static void PatchVerify(Harmony harmony, Type target, string methodName, Type[] args)
|
||||
{
|
||||
try
|
||||
{
|
||||
var m = AccessTools.Method(target, methodName, args);
|
||||
if (m == null)
|
||||
{
|
||||
RadicadeEacBypassPlugin.LogSource.LogWarning(
|
||||
$"[RsaVerify] {target.Name}.{methodName}({args.Length} args) not found - skipped");
|
||||
return;
|
||||
}
|
||||
harmony.Patch(m, prefix: new HarmonyMethod(typeof(RsaVerifyNeutralizerPatches).GetMethod(nameof(VerifyPrefix))));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
RadicadeEacBypassPlugin.LogSource.LogWarning(
|
||||
$"[RsaVerify] failed to patch {target.Name}.{methodName}: {e.GetType().Name}: {e.Message}");
|
||||
}
|
||||
} public static bool VerifyPrefix(
|
||||
Il2CppSystem.Security.Cryptography.RSACryptoServiceProvider __instance, ref bool __result)
|
||||
{
|
||||
totalCalls++;
|
||||
var isP1 = false;
|
||||
try
|
||||
{
|
||||
var modulus = __instance.ExportParameters(false).Modulus;
|
||||
isP1 = modulus != null && modulus.Length == P1KeyModulus.Length && modulus.SequenceEqual(P1KeyModulus);
|
||||
if (isP1)
|
||||
p1Calls++;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
__result = isP1;
|
||||
if (totalCalls <= 8)
|
||||
RadicadeEacBypassPlugin.LogSource.LogWarning(
|
||||
$"[ImageSig] RSA verify #{totalCalls} ({(isP1 ? "p1 key" : "game key")}) forced {(isP1 ? "true" : "false")}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch]
|
||||
internal static class FileCheckEntryPatch
|
||||
{
|
||||
private static System.Collections.Generic.IEnumerable<System.Reflection.MethodBase> TargetMethods()
|
||||
{
|
||||
yield return AccessTools.Method(typeof(GBOAMDFCNJA), "PEEADMOKNLI");
|
||||
yield return AccessTools.Method(typeof(GBOAMDFCNJA), "IBPPCNEAGDP");
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
internal static bool Bypass(ref MFBKDKPGMNP<GBOAMDFCNJA.CMIGBOACMAG> __result)
|
||||
{
|
||||
var result = GBOAMDFCNJA.CMIGBOACMAG.NDGAGMGBOMD("radicade: file check bypassed");
|
||||
__result = NOKBHGDKGNF.BGFCCOEKHOE(result);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
internal static unsafe class PebModuleHider
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct PROCESS_BASIC_INFORMATION
|
||||
{
|
||||
public IntPtr Reserved1;
|
||||
public IntPtr PebBaseAddress;
|
||||
public IntPtr Reserved2_0;
|
||||
public IntPtr Reserved2_1;
|
||||
public IntPtr UniqueProcessId;
|
||||
public IntPtr Reserved3;
|
||||
}
|
||||
|
||||
[DllImport("ntdll.dll")]
|
||||
private static extern int NtQueryInformationProcess(
|
||||
IntPtr hProcess, int processInfoClass, ref PROCESS_BASIC_INFORMATION pbi,
|
||||
int cb, out int returnLength);
|
||||
|
||||
private struct LIST_ENTRY { public IntPtr Flink, Blink; }
|
||||
|
||||
private const int OffInMemoryOrder = 0x10;
|
||||
private const int OffInInitOrder = 0x20;
|
||||
private const int OffBaseDllName = 0x58;
|
||||
|
||||
public static bool HideModule(string dllName)
|
||||
{
|
||||
var pbi = default(PROCESS_BASIC_INFORMATION);
|
||||
int status = NtQueryInformationProcess(new IntPtr(-1), 0, ref pbi,
|
||||
Marshal.SizeOf<PROCESS_BASIC_INFORMATION>(), out _);
|
||||
if (status != 0 || pbi.PebBaseAddress == IntPtr.Zero)
|
||||
return false;
|
||||
|
||||
byte* peb = (byte*)pbi.PebBaseAddress;
|
||||
byte* ldr = *(byte**)(peb + 0x18);
|
||||
if (ldr == null)
|
||||
return false;
|
||||
|
||||
LIST_ENTRY* head = (LIST_ENTRY*)(ldr + 0x10);
|
||||
byte* target = null;
|
||||
for (LIST_ENTRY* n = (LIST_ENTRY*)head->Flink; n != head && target == null; n = (LIST_ENTRY*)n->Flink)
|
||||
{
|
||||
byte* entry = (byte*)n;
|
||||
ushort* us = (ushort*)(entry + OffBaseDllName);
|
||||
ushort lenBytes = *us;
|
||||
char* buf = *(char**)(entry + OffBaseDllName + 8);
|
||||
if (buf == null || lenBytes == 0)
|
||||
continue;
|
||||
string name = new string(buf, 0, lenBytes / 2);
|
||||
if (name.Equals(dllName, StringComparison.OrdinalIgnoreCase))
|
||||
target = entry;
|
||||
}
|
||||
|
||||
if (target == null)
|
||||
return false;
|
||||
|
||||
Unlink((LIST_ENTRY*)target);
|
||||
Unlink((LIST_ENTRY*)(target + OffInMemoryOrder));
|
||||
Unlink((LIST_ENTRY*)(target + OffInInitOrder));
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void Unlink(LIST_ENTRY* e)
|
||||
{
|
||||
var flink = (LIST_ENTRY*)e->Flink;
|
||||
var blink = (LIST_ENTRY*)e->Blink;
|
||||
if (flink == e || blink == e)
|
||||
return;
|
||||
blink->Flink = e->Flink;
|
||||
flink->Blink = e->Blink;
|
||||
e->Flink = (IntPtr)e;
|
||||
e->Blink = (IntPtr)e;
|
||||
}
|
||||
}
|
||||
|
||||
internal static unsafe class PeHeaderWiper
|
||||
{
|
||||
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
private static extern IntPtr GetModuleHandleW(string moduleName);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern bool VirtualProtect(IntPtr address, UIntPtr size, uint newProtect, out uint oldProtect);
|
||||
|
||||
public static bool WipeSignatures(string dllName)
|
||||
{
|
||||
IntPtr baseAddr = GetModuleHandleW(dllName);
|
||||
if (baseAddr == IntPtr.Zero)
|
||||
return false;
|
||||
|
||||
byte* p = (byte*)baseAddr;
|
||||
int eLfanew = *(int*)(p + 0x3C);
|
||||
if (eLfanew <= 0 || eLfanew > 0x1000)
|
||||
return false;
|
||||
|
||||
if (!VirtualProtect(baseAddr, (UIntPtr)0x1000, 0x04 , out uint old))
|
||||
return false;
|
||||
|
||||
bool wasMz = p[0] == (byte)'M' && p[1] == (byte)'Z';
|
||||
bool wasPe = *(uint*)(p + eLfanew) == 0x00004550;
|
||||
|
||||
*(ushort*)p = 0;
|
||||
*(uint*)(p + eLfanew) = 0;
|
||||
|
||||
VirtualProtect(baseAddr, (UIntPtr)0x1000, old, out _);
|
||||
return wasMz && wasPe;
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch]
|
||||
internal static class ExitInitiatorTracerPatch
|
||||
{
|
||||
private static System.Collections.Generic.IEnumerable<System.Reflection.MethodBase> TargetMethods()
|
||||
{
|
||||
yield return AccessTools.Method(typeof(KJAMNFCDBIO), "GMAKAKECEDK");
|
||||
yield return AccessTools.Method(typeof(KJAMNFCDBIO), "FLMPEJAIKFO");
|
||||
yield return AccessTools.Method(typeof(KJAMNFCDBIO), "LHNNEFHHPBG");
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
internal static bool TraceAndBlock()
|
||||
{
|
||||
var armed = HileGate.DroppedCount > 0;
|
||||
if (armed)
|
||||
{
|
||||
var stack = "<unavailable>";
|
||||
try { stack = Environment.StackTrace; } catch { }
|
||||
RadicadeEacBypassPlugin.LogSource.LogWarning("[ExitInit] blocked a managed exit initiator - caller trace:\n" + stack);
|
||||
}
|
||||
return !armed;
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch]
|
||||
[HarmonyPatch(typeof(BestHTTP.HTTPManager), "SendRequest", new[] { typeof(BestHTTP.HTTPRequest) })]
|
||||
internal static class ExitSequenceTracePatch
|
||||
{
|
||||
private static int _dumped;
|
||||
|
||||
[HarmonyPriority(Priority.First)]
|
||||
private static void Prefix(BestHTTP.HTTPRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var path = request?.Uri?.AbsolutePath;
|
||||
if (path == null) return;
|
||||
if (path.IndexOf("/matchmaking/player/logout", StringComparison.OrdinalIgnoreCase) < 0 &&
|
||||
path.IndexOf("/playersettings/playersettings", StringComparison.OrdinalIgnoreCase) < 0)
|
||||
return;
|
||||
if (++_dumped > 4) return;
|
||||
|
||||
RadicadeEacBypassPlugin.LogSource.LogWarning(
|
||||
"[ExitTrace] shutdown request " + path + " - managed caller stack:" + ExitTraceHelper.SafeStack());
|
||||
}
|
||||
catch (Exception traceEx)
|
||||
{
|
||||
RadicadeEacBypassPlugin.LogSource.LogWarning("[ExitTrace] dump failed: " + traceEx.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch]
|
||||
internal static class UnityQuitVetoPatch
|
||||
{
|
||||
internal static bool Enabled => _enabled;
|
||||
|
||||
private static readonly bool _enabled = IsEnabled();
|
||||
|
||||
private static int _blocks;
|
||||
|
||||
private static bool IsEnabled()
|
||||
{
|
||||
try
|
||||
{
|
||||
var raw = Environment.GetEnvironmentVariable("RADICADE_QUIT_VETO");
|
||||
if (string.IsNullOrWhiteSpace(raw)) return false;
|
||||
return raw.Trim().ToLowerInvariant() is "1" or "on" or "true" or "yes";
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
private static System.Collections.Generic.IEnumerable<System.Reflection.MethodBase> TargetMethods()
|
||||
{
|
||||
var noArg = AccessTools.Method(typeof(KJAMNFCDBIO), "OFBGCPAHADA", Type.EmptyTypes);
|
||||
if (noArg != null) yield return noArg;
|
||||
|
||||
var flag = AccessTools.Method(typeof(KJAMNFCDBIO), "OFBGCPAHADA", new[] { typeof(bool) });
|
||||
if (flag != null) yield return flag;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
private static bool Veto(ref bool __result)
|
||||
{
|
||||
if (!Enabled) return true;
|
||||
|
||||
__result = false;
|
||||
|
||||
var count = ++_blocks;
|
||||
if (count <= 3 || count % 25 == 0)
|
||||
RadicadeEacBypassPlugin.LogSource.LogWarning(
|
||||
"[QuitVeto] wantsToQuit answered false (block #" + count + ") - Unity's quit is cancelled. " +
|
||||
"This is the anti-cheat shutdown, not a user quit; the veto is armed by RADICADE_QUIT_VETO=1, set it to 0 to disable." +
|
||||
ExitTraceHelper.SafeStack());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
internal static class ModuleHidingPolicy
|
||||
{
|
||||
internal static bool Enabled { get; } = ReadFlag("RADICADE_MODULE_HIDE");
|
||||
|
||||
private static bool ReadFlag(string name)
|
||||
{
|
||||
try
|
||||
{
|
||||
var raw = Environment.GetEnvironmentVariable(name);
|
||||
if (string.IsNullOrWhiteSpace(raw)) return false;
|
||||
return raw.Trim().ToLowerInvariant() is "1" or "on" or "true" or "yes";
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
[HarmonyPatch]
|
||||
internal static class CheatScanSuppressPatch
|
||||
{
|
||||
internal static bool Enabled => !_disabled;
|
||||
|
||||
private static readonly bool _disabled = IsDisabled();
|
||||
|
||||
private static int _skips;
|
||||
|
||||
private static bool IsDisabled()
|
||||
{
|
||||
try
|
||||
{
|
||||
var raw = Environment.GetEnvironmentVariable("RADICADE_CHEAT_SCAN");
|
||||
if (string.IsNullOrWhiteSpace(raw)) return false;
|
||||
return raw.Trim().ToLowerInvariant() is "0" or "off" or "false" or "no";
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
private static System.Reflection.MethodBase TargetMethod()
|
||||
{
|
||||
var type = AccessTools.TypeByName("CheatManager");
|
||||
if (type == null) throw new System.MissingMemberException("CheatManager could not be resolved.");
|
||||
|
||||
var method = AccessTools.Method(type, "EKGOMPDGNEK", System.Type.EmptyTypes);
|
||||
if (method == null) throw new System.MissingMethodException("CheatManager.EKGOMPDGNEK() is gone - the scan moved or was renamed.");
|
||||
|
||||
return method;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
private static bool Skip()
|
||||
{
|
||||
if (!Enabled) return true;
|
||||
|
||||
var count = ++_skips;
|
||||
if (count <= 3 || count % 25 == 0)
|
||||
RadicadeEacBypassPlugin.LogSource.LogWarning("[CheatScan] skipped CheatManager.EKGOMPDGNEK() (#" + count
|
||||
+ ") - this is the pass behind the Obscured/winhttp.dll verdict, so no report and no shutdown follow from it.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch]
|
||||
internal static class CheatDetectionSuppressPatch
|
||||
{
|
||||
internal static bool Enabled => !_disabled;
|
||||
|
||||
private static readonly bool _disabled = IsDisabled();
|
||||
|
||||
private static int _skips;
|
||||
|
||||
private static bool IsDisabled()
|
||||
{
|
||||
try
|
||||
{
|
||||
var raw = Environment.GetEnvironmentVariable("RADICADE_CHEAT_TYPE");
|
||||
if (string.IsNullOrWhiteSpace(raw)) return false;
|
||||
return raw.Trim().ToLowerInvariant() is "0" or "off" or "false" or "no";
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
private static System.Collections.Generic.IEnumerable<System.Reflection.MethodBase> TargetMethods()
|
||||
{
|
||||
var type = AccessTools.TypeByName("CheatManager");
|
||||
if (type == null) throw new System.MissingMemberException("CheatManager could not be resolved.");
|
||||
|
||||
var found = new System.Collections.Generic.List<string>();
|
||||
var missing = new System.Collections.Generic.List<string>();
|
||||
foreach (var name in new[] { "OnObscuredTypeCheatDetected", "OnTimeCheatDetected", "OnAdvancedMovementCheatDetected" })
|
||||
{
|
||||
var method = AccessTools.Method(type, name, System.Type.EmptyTypes);
|
||||
if (method == null)
|
||||
{
|
||||
missing.Add(name);
|
||||
continue;
|
||||
}
|
||||
|
||||
found.Add(name);
|
||||
yield return method;
|
||||
}
|
||||
|
||||
_guarded = found.Count;
|
||||
_unguarded = missing.Count == 0 ? null : string.Join(", ", missing);
|
||||
}
|
||||
|
||||
internal static int Guarded => _guarded;
|
||||
|
||||
internal static string Unguarded => _unguarded;
|
||||
|
||||
private static int _guarded;
|
||||
|
||||
private static string _unguarded;
|
||||
|
||||
[HarmonyPrefix]
|
||||
private static bool Skip(System.Reflection.MethodBase __originalMethod)
|
||||
{
|
||||
if (!Enabled) return true;
|
||||
|
||||
var count = ++_skips;
|
||||
if (count <= 5 || count % 25 == 0)
|
||||
RadicadeEacBypassPlugin.LogSource.LogWarning("[CheatType] skipped CheatManager." + __originalMethod.Name + "() (#" + count
|
||||
+ ") - this is a verdict handler that ends in the dorm drop with FOKOIBODNAD and the shutdown behind it, so the room survives the detection.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch]
|
||||
internal static class CheatDormDropGuardPatch
|
||||
{
|
||||
private const string AntiCheatReason = "385120";
|
||||
|
||||
internal static bool Enabled => !_disabled;
|
||||
|
||||
private static readonly bool _disabled = IsDisabled();
|
||||
|
||||
private static int _skips;
|
||||
|
||||
private static bool IsDisabled()
|
||||
{
|
||||
try
|
||||
{
|
||||
var raw = Environment.GetEnvironmentVariable("RADICADE_CHEAT_DROP");
|
||||
if (string.IsNullOrWhiteSpace(raw)) return false;
|
||||
return raw.Trim().ToLowerInvariant() is "0" or "off" or "false" or "no";
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
private static System.Reflection.MethodBase TargetMethod()
|
||||
{
|
||||
var type = AccessTools.TypeByName("CheatManager");
|
||||
if (type == null) throw new System.MissingMemberException("CheatManager could not be resolved.");
|
||||
|
||||
var method = AccessTools.Method(type, "FOKOIBODNAD", new[] { typeof(string) });
|
||||
if (method == null)
|
||||
throw new System.MissingMethodException("CheatManager.FOKOIBODNAD(string) is gone - the dorm drop moved or was renamed, so the guard is NOT armed.");
|
||||
|
||||
return method;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
private static bool Skip(string __0)
|
||||
{
|
||||
if (!Enabled) return true;
|
||||
|
||||
if (string.IsNullOrEmpty(__0) || __0.IndexOf(AntiCheatReason, StringComparison.Ordinal) < 0)
|
||||
return true;
|
||||
|
||||
var count = ++_skips;
|
||||
if (count <= 5 || count % 25 == 0)
|
||||
RadicadeEacBypassPlugin.LogSource.LogWarning("[CheatDrop] skipped CheatManager.FOKOIBODNAD('" + __0 + "') (#" + count
|
||||
+ ") - the anti-cheat's own drop, so the player stays where they are instead of being sent to the dormroom mid-session.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch]
|
||||
internal static class CheatManagerTracePatch
|
||||
{
|
||||
private const int PerMethodLimit = 4;
|
||||
private const int LineLimit = 90;
|
||||
|
||||
private static readonly System.Diagnostics.Stopwatch Clock = System.Diagnostics.Stopwatch.StartNew();
|
||||
private static readonly System.Collections.Generic.Dictionary<string, int> Seen = new System.Collections.Generic.Dictionary<string, int>();
|
||||
private static int _lines;
|
||||
|
||||
private static System.Collections.Generic.IEnumerable<System.Reflection.MethodBase> TargetMethods()
|
||||
{
|
||||
var outer = AccessTools.TypeByName("CheatManager");
|
||||
if (outer == null)
|
||||
{
|
||||
RadicadeEacBypassPlugin.LogSource.LogWarning("[CheatTrace] CheatManager not resolvable - nothing traced.");
|
||||
yield break;
|
||||
}
|
||||
|
||||
var types = new System.Collections.Generic.List<System.Type> { outer };
|
||||
foreach (var nested in outer.GetNestedTypes(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic))
|
||||
types.Add(nested);
|
||||
|
||||
foreach (var type in types)
|
||||
{
|
||||
foreach (var method in type.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.DeclaredOnly))
|
||||
{
|
||||
if (method.IsAbstract || method.ContainsGenericParameters) continue;
|
||||
yield return method;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly bool Enabled = IsEnabled();
|
||||
|
||||
private static bool IsEnabled()
|
||||
{
|
||||
try
|
||||
{
|
||||
var raw = Environment.GetEnvironmentVariable("RADICADE_CHEAT_TRACE");
|
||||
if (string.IsNullOrWhiteSpace(raw)) return false;
|
||||
return raw.Trim().ToLowerInvariant() is "1" or "on" or "true" or "yes";
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
private static void Trace(System.Reflection.MethodBase __originalMethod)
|
||||
{
|
||||
if (!Enabled) return;
|
||||
try
|
||||
{
|
||||
if (_lines >= LineLimit) return;
|
||||
|
||||
var key = __originalMethod.DeclaringType.Name + "." + __originalMethod.Name;
|
||||
|
||||
Seen.TryGetValue(key, out var count);
|
||||
if (count >= PerMethodLimit) return;
|
||||
Seen[key] = count + 1;
|
||||
_lines++;
|
||||
|
||||
RadicadeEacBypassPlugin.LogSource.LogWarning("[CheatTrace] +" + Clock.ElapsedMilliseconds + "ms " + key);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
internal static class ExitTraceHelper
|
||||
{
|
||||
internal static string SafeStack()
|
||||
{
|
||||
try { return "\n" + Environment.StackTrace; } catch { return "\n<stack unavailable>"; }
|
||||
}
|
||||
}
|
||||
|
||||
internal static class EacGate
|
||||
{
|
||||
internal static bool Enabled => !_disabled;
|
||||
|
||||
private static readonly bool _disabled = IsDisabled();
|
||||
|
||||
private static int _logCount;
|
||||
|
||||
private static bool IsDisabled()
|
||||
{
|
||||
try
|
||||
{
|
||||
var raw = Environment.GetEnvironmentVariable("RADICADE_EAC_NEUTRALIZE");
|
||||
if (string.IsNullOrWhiteSpace(raw)) return false;
|
||||
return raw.Trim().ToLowerInvariant() is "0" or "off" or "false" or "no";
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
internal static System.Type Find(string fullName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var byName = System.Type.GetType(fullName + ", EasyAntiCheat.Client");
|
||||
if (byName != null) return byName;
|
||||
}
|
||||
catch { }
|
||||
|
||||
try
|
||||
{
|
||||
var forced = typeof(EasyAntiCheat.Client.Hydra.Runtime).Assembly;
|
||||
var direct = forced.GetType(fullName);
|
||||
if (direct != null) return direct;
|
||||
}
|
||||
catch { }
|
||||
|
||||
try { return AccessTools.TypeByName(fullName); } catch { return null; }
|
||||
}
|
||||
|
||||
private const int LogLimit = 8;
|
||||
|
||||
internal static void Log(string message)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (++_logCount > LogLimit) return;
|
||||
RadicadeEacBypassPlugin.LogSource.LogWarning(message);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch]
|
||||
internal static class EacNativeClientStubPatch
|
||||
{
|
||||
private static System.Collections.Generic.IEnumerable<System.Reflection.MethodBase> TargetMethods()
|
||||
{
|
||||
var type = EacGate.Find("EasyAntiCheat.Client.Hydra.NativeClient");
|
||||
if (type == null)
|
||||
{
|
||||
EacGate.Log("[EacStub] Hydra.NativeClient not resolvable - EAC runtime left untouched.");
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (var method in type.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static))
|
||||
if (method.Name == "Initialize")
|
||||
yield return method;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
private static bool Skip(System.Reflection.MethodBase __originalMethod)
|
||||
{
|
||||
if (!EacGate.Enabled) return true;
|
||||
|
||||
EacGate.Log("[EacStub] skipped " + __originalMethod.DeclaringType + "." + __originalMethod.Name
|
||||
+ " - EasyAntiCheat's native client never starts, so its module scanner never runs.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch]
|
||||
internal static class EacHealthQueryLiePatch
|
||||
{
|
||||
private static System.Collections.Generic.IEnumerable<System.Reflection.MethodBase> TargetMethods()
|
||||
{
|
||||
var native = EacGate.Find("EasyAntiCheat.Client.Hydra.NativeClient");
|
||||
if (native != null)
|
||||
foreach (var method in native.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static))
|
||||
if (method.Name == "IsStartedWithEasyAntiCheat" && method.ReturnType == typeof(bool))
|
||||
yield return method;
|
||||
|
||||
var runtime = EacGate.Find("EasyAntiCheat.Client.Hydra.Runtime");
|
||||
if (runtime != null)
|
||||
foreach (var method in runtime.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static))
|
||||
if ((method.Name == "IsActive" || method.Name == "get_Initialized") && method.ReturnType == typeof(bool))
|
||||
yield return method;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
private static bool Lie(ref bool __result, System.Reflection.MethodBase __originalMethod)
|
||||
{
|
||||
if (!EacGate.Enabled) return true;
|
||||
|
||||
__result = true;
|
||||
EacGate.Log("[EacStub] " + __originalMethod.DeclaringType + "." + __originalMethod.Name + " answered true.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch]
|
||||
[HarmonyPatch]
|
||||
internal static class ToolCollisionResetGuardPatch
|
||||
{
|
||||
internal static bool Enabled => !_disabled;
|
||||
|
||||
private static readonly bool _disabled = IsDisabled();
|
||||
|
||||
private static bool IsDisabled()
|
||||
{
|
||||
try
|
||||
{
|
||||
var raw = Environment.GetEnvironmentVariable("RADICADE_TOOL_RESET");
|
||||
if (string.IsNullOrWhiteSpace(raw)) return false;
|
||||
return raw.Trim().ToLowerInvariant() is "0" or "off" or "false" or "no";
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
private static System.Reflection.MethodBase TargetMethod()
|
||||
{
|
||||
var method = AccessTools.Method(typeof(Tool), "ResetToDefaultCollisionLayer", Type.EmptyTypes);
|
||||
if (method == null)
|
||||
throw new MissingMethodException(
|
||||
"Tool.ResetToDefaultCollisionLayer() not found - the room crash guard is NOT armed.");
|
||||
return method;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
private static bool Skip(Tool __instance)
|
||||
{
|
||||
if (!Enabled) return true;
|
||||
ToolCollisionLayerRestore.Restore(__instance);
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string Describe(Tool tool)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (tool == null) return "<null tool>";
|
||||
var owner = tool.gameObject;
|
||||
return (owner == null) ? "<no gameObject>" : "'" + owner.name + "'";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return "<describe failed: " + ex.GetType().Name + ">";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static class ExitStateVetoPatch
|
||||
{
|
||||
internal static bool Enabled => _enabled;
|
||||
|
||||
private static readonly bool _enabled = IsEnabled();
|
||||
|
||||
private static int _blocks;
|
||||
|
||||
private static bool IsEnabled()
|
||||
{
|
||||
try
|
||||
{
|
||||
var raw = Environment.GetEnvironmentVariable("RADICADE_EXIT_VETO");
|
||||
if (string.IsNullOrWhiteSpace(raw)) return false;
|
||||
raw = raw.Trim().ToLowerInvariant();
|
||||
return raw is "1" or "on" or "true" or "yes";
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
private static System.Collections.Generic.IEnumerable<System.Reflection.MethodBase> TargetMethods()
|
||||
{
|
||||
var property = AccessTools.PropertySetter(typeof(KJAMNFCDBIO), "JGKJEDKNFHN");
|
||||
if (property != null) yield return property;
|
||||
|
||||
var raw = AccessTools.Method(typeof(KJAMNFCDBIO), "EHPDJDJLPKM",
|
||||
new[] { typeof(KJAMNFCDBIO.CNNHHNMPLJE) });
|
||||
if (raw != null) yield return raw;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
private static bool Block(KJAMNFCDBIO.CNNHHNMPLJE __0)
|
||||
{
|
||||
if (!Enabled) return true;
|
||||
if (__0 == KJAMNFCDBIO.CNNHHNMPLJE.Running) return true;
|
||||
|
||||
var count = ++_blocks;
|
||||
var caller = count == 1 ? ExitTraceHelper.SafeStack() : string.Empty;
|
||||
if (count <= 3 || count % 25 == 0)
|
||||
RadicadeEacBypassPlugin.LogSource.LogWarning(
|
||||
$"[ExitVeto] blocked AppExitState -> {__0} (block #{count}); the session keeps running. " +
|
||||
"This is the anti-cheat-driven exit, not a user quit - the veto is armed by RADICADE_EXIT_VETO=1, set it to 0 to disable." + caller);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<AssemblyName>RadicadePatch</AssemblyName>
|
||||
<RootNamespace>RadicadePatch</RootNamespace>
|
||||
|
||||
<BepInExRoot Condition="'$(BepInExRoot)' == ''">C:\RadicadeBeta\BepInEx</BepInExRoot>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="$(BepInExRoot)\core\BepInEx.Core.dll;$(BepInExRoot)\core\BepInEx.Unity.IL2CPP.dll;$(BepInExRoot)\core\0Harmony.dll;$(BepInExRoot)\core\Il2CppInterop.Runtime.dll" Private="false" />
|
||||
<Reference Include="$(BepInExRoot)\interop\*.dll" Private="false" />
|
||||
<Compile Include="..\RadicadeEacBypass\EmbeddedTlsPatches.cs" Link="Shared\EmbeddedTlsPatches.cs" />
|
||||
<Compile Include="..\RadicadeEacBypass\NameserverConnectionPatches.cs" Link="Shared\NameserverConnectionPatches.cs" />
|
||||
<Compile Include="..\RadicadeEacBypass\NameserverRedirect.cs" Link="Shared\NameserverRedirect.cs" />
|
||||
<Compile Include="..\RadicadeEacBypass\ImageSignatureRequest.cs" Link="Shared\ImageSignatureRequest.cs" />
|
||||
<Compile Include="..\RadicadeEacBypass\HubSocketAuth.cs" Link="Shared\HubSocketAuth.cs" />
|
||||
<Compile Include="..\RadicadeEacBypass\WebSocketRedirectPatches.cs" Link="Shared\WebSocketRedirectPatches.cs" />
|
||||
|
||||
<Compile Include="..\RadicadeEacBypass\Diag.cs" Link="Shared\Diag.cs" />
|
||||
<Compile Include="..\RadicadeEacBypass\ResponseRepair.cs" Link="Shared\ResponseRepair.cs" />
|
||||
<Compile Include="..\RadicadeEacBypass\HttpTolerancePatches.cs" Link="Shared\HttpTolerancePatches.cs" />
|
||||
<Compile Include="..\RadicadeEacBypass\RealtimeDiagnostics.cs" Link="Shared\RealtimeDiagnostics.cs" />
|
||||
|
||||
<Compile Include="..\RadicadeEacBypass\VoiceChatRestrictionPatches.cs" Link="Shared\VoiceChatRestrictionPatches.cs" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,40 @@
|
||||
using UnityEngine;
|
||||
|
||||
internal static class ToolCollisionLayerRestore
|
||||
{
|
||||
private static int _failures;
|
||||
|
||||
internal static void Restore(Tool tool)
|
||||
{
|
||||
if (tool == null) return;
|
||||
try
|
||||
{
|
||||
if (tool.MEDCLKFANNC) return;
|
||||
int layer = (int)tool.EHNBBIAIJIK;
|
||||
if (layer < 0 || layer > 31)
|
||||
throw new InvalidOperationException("Invalid default collision layer " + layer);
|
||||
|
||||
foreach (var collider in tool.GetComponentsInChildren<Collider>(true))
|
||||
{
|
||||
if (collider == null || collider.isTrigger) continue;
|
||||
if (collider.GetComponentInParent<Tool>() != tool) continue;
|
||||
var owner = collider.gameObject;
|
||||
if (!ShouldRestore(owner.layer, (int)tool.HeldCollisionLayer,
|
||||
(int)tool.EquippedCollisionLayer, (int)tool.AttachedCollisionLayer)) continue;
|
||||
if (owner.layer == layer) continue;
|
||||
owner.layer = layer;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (++_failures <= 5)
|
||||
RadicadeEacBypassPlugin.LogSource.LogWarning("[ToolGuard] collision-layer restore failed: " + ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ShouldRestore(int layer, int held, int equipped, int attached)
|
||||
{
|
||||
return layer == held || layer == equipped || layer == attached
|
||||
|| layer is 0 or 10 or 11 or 13 or 17 or 18 or 23 or 24 or 27;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
using System;
|
||||
using HarmonyLib;
|
||||
using Il2CppInterop.Runtime;
|
||||
using RecRoom;
|
||||
using RecRoom.ObjectModel;
|
||||
using UnityEngine;
|
||||
|
||||
[HarmonyPatch]
|
||||
internal static class UnboundPropFreezePatch
|
||||
{
|
||||
private static readonly bool _disabled = IsDisabled();
|
||||
|
||||
|
||||
private static int _alreadyInert;
|
||||
|
||||
private static bool _loggedNoRigidbody;
|
||||
|
||||
private static bool _loggedFailure;
|
||||
|
||||
private static readonly Type[] Companions = new Type[3]
|
||||
{
|
||||
typeof(EmbodiedObject),
|
||||
typeof(RbexEntity),
|
||||
typeof(TransformEntity)
|
||||
};
|
||||
|
||||
internal static bool Enabled => !_disabled;
|
||||
|
||||
private static bool IsDisabled()
|
||||
{
|
||||
try
|
||||
{
|
||||
string environmentVariable = Environment.GetEnvironmentVariable("RADICADE_UNBOUND_PROP_FREEZE");
|
||||
if (string.IsNullOrWhiteSpace(environmentVariable))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
bool result;
|
||||
switch (environmentVariable.Trim().ToLowerInvariant())
|
||||
{
|
||||
case "0":
|
||||
case "off":
|
||||
case "false":
|
||||
case "no":
|
||||
result = true;
|
||||
break;
|
||||
default:
|
||||
result = false;
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(RigidbodyEx), "Awake")]
|
||||
private static void Postfix(RigidbodyEx __instance)
|
||||
{
|
||||
if (!Enabled || __instance == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
if (!IsBound(__instance))
|
||||
{
|
||||
Freeze(__instance);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (!_loggedFailure)
|
||||
{
|
||||
_loggedFailure = true;
|
||||
RadicadeEacBypassPlugin.LogSource.LogWarning((object)("[PropGuard] could not freeze an unbound prop (" + ex.GetType().Name + ": " + ex.Message + "); the prop is left exactly as the game built it."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsBound(RigidbodyEx rigidbodyEx)
|
||||
{
|
||||
Type[] companions = Companions;
|
||||
foreach (Type type in companions)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (rigidbodyEx.GetComponent(Il2CppType.From(type)) != null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void Freeze(RigidbodyEx rigidbodyEx)
|
||||
{
|
||||
Rigidbody unityRigidbody = rigidbodyEx.unityRigidbody;
|
||||
if (unityRigidbody == null)
|
||||
{
|
||||
if (!_loggedNoRigidbody)
|
||||
{
|
||||
_loggedNoRigidbody = true;
|
||||
}
|
||||
try
|
||||
{
|
||||
rigidbodyEx.ClearUnityRigidbodyVelocity();
|
||||
return;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (unityRigidbody.isKinematic)
|
||||
{
|
||||
_alreadyInert++;
|
||||
return;
|
||||
}
|
||||
unityRigidbody.isKinematic = true;
|
||||
unityRigidbody.velocity = Vector3.zero;
|
||||
unityRigidbody.angularVelocity = Vector3.zero;
|
||||
|
||||
}
|
||||
|
||||
private static string Describe(RigidbodyEx rigidbodyEx)
|
||||
{
|
||||
try
|
||||
{
|
||||
GameObject gameObject = ((Component)rigidbodyEx).gameObject;
|
||||
return (gameObject == null) ? "<no game object>" : ("'" + gameObject.name + "'");
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "<unnamed>";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using HarmonyLib;
|
||||
|
||||
internal static class VoiceChatBanFlagPatches
|
||||
{
|
||||
private static bool _armed;
|
||||
|
||||
internal static void Apply(Harmony harmony)
|
||||
{
|
||||
if (_armed) return;
|
||||
_armed = true;
|
||||
|
||||
Patch(harmony, "IsVoiceChatBanned", nameof(BannedPostfix));
|
||||
Patch(harmony, "AllowedToVoiceChat", nameof(AllowedPostfix));
|
||||
}
|
||||
|
||||
private static void Patch(Harmony harmony, string propertyName, string handler)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
|
||||
var getter = AccessTools.Method(typeof(SettingsModel), "get_" + propertyName)
|
||||
?? AccessTools.Property(typeof(SettingsModel), propertyName)?.GetGetMethod(true);
|
||||
if (getter == null)
|
||||
{
|
||||
RadicadeEacBypassPlugin.LogSource.LogWarning("[VoiceBanFlag] SettingsModel."
|
||||
+ propertyName + " has no getter in this build, so the Audio page keeps its own value.");
|
||||
return;
|
||||
}
|
||||
|
||||
harmony.Patch(getter, postfix: new HarmonyMethod(typeof(VoiceChatBanFlagPatches)
|
||||
.GetMethod(handler, BindingFlags.Static | BindingFlags.NonPublic)));
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
RadicadeEacBypassPlugin.LogSource.LogWarning("[VoiceBanFlag] could not patch SettingsModel."
|
||||
+ propertyName + ": " + ex.GetType().Name + ": " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private static void BannedPostfix(ref bool __result)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!VoiceChatRestrictionPatches.RestrictionInForce) return;
|
||||
|
||||
__result = true;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private static void AllowedPostfix(ref bool __result)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!VoiceChatRestrictionPatches.RestrictionInForce) return;
|
||||
|
||||
__result = false;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0"?>
|
||||
|
||||
<Obfuscator>
|
||||
<Var name="InPath" value="bin/Release/net6.0" />
|
||||
<Var name="OutPath" value="bin/obfuscated" />
|
||||
|
||||
|
||||
<Var name="BepInExRoot" value="C:\RadicadeBeta\BepInEx" />
|
||||
<Var name="ExtraFrameworkFolders" value="$(BepInExRoot)\core;$(BepInExRoot)\interop" />
|
||||
|
||||
<Var name="KeepPublicApi" value="false" />
|
||||
<Var name="HidePrivateApi" value="true" />
|
||||
<Var name="HideStrings" value="true" />
|
||||
<Var name="UseUnicodeNames" value="false" />
|
||||
<Var name="ReuseNames" value="true" />
|
||||
<Var name="RenameProperties" value="true" />
|
||||
<Var name="RenameEvents" value="true" />
|
||||
<Var name="RenameFields" value="true" />
|
||||
<Var name="RegenerateDebugInfo" value="false" />
|
||||
<Var name="SuppressIldasm" value="true" />
|
||||
|
||||
|
||||
<Module file="$(InPath)/RadicadePatch.dll">
|
||||
|
||||
<SkipMethod name="Prepare" />
|
||||
<SkipMethod name="Cleanup" />
|
||||
<SkipMethod name="TargetMethod" />
|
||||
<SkipMethod name="TargetMethods" />
|
||||
<SkipMethod name="Prefix" />
|
||||
<SkipMethod name="Postfix" />
|
||||
<SkipMethod name="Transpiler" />
|
||||
<SkipMethod name="Finalizer" />
|
||||
<SkipMethod name="ReversePatch" />
|
||||
<SkipMethod name="ILManipulator" />
|
||||
|
||||
|
||||
<SkipMethod rx=".*(Prefix|Postfix|Transpiler|Finalizer)$" />
|
||||
<SkipMethod name="EnableDialog" />
|
||||
<SkipMethod name="GraphicEvent" />
|
||||
<SkipMethod name="IncomingDialog" />
|
||||
<SkipMethod name="NoteCollider" />
|
||||
<SkipMethod name="Repair" />
|
||||
<SkipMethod name="Swallow" />
|
||||
|
||||
|
||||
<SkipMethod name="Block" />
|
||||
<SkipMethod name="Bypass" />
|
||||
<SkipMethod name="Challenge" />
|
||||
<SkipMethod name="Lie" />
|
||||
<SkipMethod name="Ready" />
|
||||
<SkipMethod name="Skip" />
|
||||
<SkipMethod name="Trace" />
|
||||
<SkipMethod name="TraceAndBlock" />
|
||||
<SkipMethod name="Veto" />
|
||||
</Module>
|
||||
</Obfuscator>
|
||||
Reference in New Issue
Block a user