Radicade Public Code

This commit is contained in:
JogmansonReal
2026-10-16 23:33:13 -07:00
commit 55175acdf6
832 changed files with 363261 additions and 0 deletions
@@ -0,0 +1,225 @@
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;
var started = Environment.TickCount;
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;
}
var elapsed = Environment.TickCount - started;
log.LogInfo($"[AddrMap] mapped {mapped} methods from {images} images in {elapsed} ms -> {path} ({unmappable} without a mappable entry point)");
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));
}
}
+1430
View File
@@ -0,0 +1,1430 @@
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using BepInEx.Unity.IL2CPP;
using HarmonyLib;
using RecRoom.AntiCheat;
using System.Linq;
using System.Runtime.InteropServices;
[BepInPlugin("com.radicade.eacbypass", "Radicade August Client", "2.0.0")]
public sealed class RadicadeEacBypassPlugin : BasePlugin
{
internal static ManualLogSource LogSource;
internal static ConfigEntry<string> ServerUrl, TlsAllowedHosts;
internal static Il2CppSystem.Threading.SynchronizationContext MainThreadContext;
internal static string ImageKeyId;
public override void Load()
{
LogSource = Log;
if (UnityEngine.Application.version != "20230825")
throw new NotSupportedException("This plugin requires Rec Room 20230825.");
MainThreadContext = Il2CppSystem.Threading.SynchronizationContext.Current;
Log.LogInfo($"[August] main-thread sync context: {(MainThreadContext != null ? MainThreadContext.GetType().Name : "NULL")}");
ServerUrl = Config.Bind("Connection", "ServerUrl", "https://localhost:3030", "Private server URL.");
if (!Uri.TryCreate(ServerUrl.Value, UriKind.Absolute, out var server) || server.Scheme != "https")
throw new InvalidOperationException("The August client requires an HTTPS server URL.");
TlsAllowedHosts = Config.Bind("TLS", "AllowedHosts", server.Host, "Exact private server hosts allowed by the embedded TLS integration.");
if (!EmbeddedTlsPatches.IsHostAllowed(server.Host))
throw new InvalidOperationException("TLS AllowedHosts must include the configured private server host.");
var metadata = Path.Combine(UnityEngine.Application.dataPath, "il2cpp_data", "Metadata", "global-metadata.interop.dat");
ImageKeyId = ImageSignatureRequest.DetectKeyId(File.ReadAllBytes(metadata));
if (ModuleHidingPolicy.Enabled)
{
var hidden = PebModuleHider.HideModule("winhttp.dll");
Log.LogInfo(hidden
? "[ModuleHide] winhttp.dll unlinked from PEB module lists (scanner-invisible)"
: "[ModuleHide] WARNING: winhttp.dll not found in PEB lists - scanner will still see it");
var wiped = PeHeaderWiper.WipeSignatures("winhttp.dll");
Log.LogInfo(wiped
? "[ModuleHide] winhttp.dll PE signatures wiped (MZ+PE) - region scans cannot classify it"
: "[ModuleHide] WARNING: PE signature wipe did not apply (signatures missing or VirtualProtect failed)");
}
else
{
Log.LogInfo("[ModuleHide] disabled by default (RADICADE_MODULE_HIDE=1 re-arms runs 14/15): winhttp.dll keeps its loader-list entry and its PE headers, so a module scan has nothing to call obscured.");
}
Log.LogInfo(ModuleHidingPolicy.DescribeModuleList());
HileGate.Enabled = true;
Log.LogInfo("[HileGate] armed - anti-cheat report POSTs are " + (HileGate.DropEnabled
? "dropped at the HTTP layer (RADICADE_HILE_GATE=drop)"
: "captured in full and then allowed through, so the report body is on the record (RADICADE_HILE_GATE=drop restores the drop)") + ".");
var harmony = new Harmony("com.radicade.eacbypass");
EmbeddedTlsPatches.Apply(harmony);
harmony.PatchAll(typeof(NameserverConnectionPatches));
WebSocketRedirectPatches.Apply(harmony);
harmony.PatchAll(typeof(AugustChallengePatches));
harmony.PatchAll(typeof(AugustLaunchDiagnostics));
harmony.PatchAll(typeof(AugustLaunchValidatorPatch));
RsaVerifyNeutralizerPatches.Apply(harmony);
harmony.PatchAll(typeof(FileCheckEntryPatch));
harmony.PatchAll(typeof(ExitInitiatorTracerPatch));
Log.LogInfo("[ExitInit] armed - managed exit initiators are blocked once a hile report has been dropped, and traced.");
try
{
harmony.PatchAll(typeof(ExitStateVetoPatch));
Log.LogInfo("[ExitVeto] patches applied; blockExitState enabled=" + ExitStateVetoPatch.Enabled
+ " (opt-in fallback only; a normal quit and server logouts work either way. RADICADE_EXIT_VETO=1 arms it)");
}
catch (Exception vetoEx)
{
Log.LogError("[ExitVeto] failed to apply - continuing without it: " + vetoEx);
}
try
{
harmony.PatchAll(typeof(ExitSequenceTracePatch));
harmony.PatchAll(typeof(UnityQuitVetoPatch));
harmony.PatchAll(typeof(EacNativeClientStubPatch));
harmony.PatchAll(typeof(EacHealthQueryLiePatch));
harmony.PatchAll(typeof(CheatManagerTracePatch));
harmony.PatchAll(typeof(CheatScanSuppressPatch));
Log.LogInfo("[CheatScan] verdict suppression enabled=" + CheatScanSuppressPatch.Enabled);
Log.LogInfo("[CheatType] cheat-detection suppression enabled=" + CheatDetectionSuppressPatch.Enabled
+ " (RADICADE_CHEAT_TYPE=0 restores the detection and the dorm drop that follows it)");
Log.LogInfo("[CheatDrop] dorm-drop guard enabled=" + CheatDormDropGuardPatch.Enabled
+ " (RADICADE_CHEAT_DROP=0 lets every drop run)");
try
{
harmony.PatchAll(typeof(CheatDetectionSuppressPatch));
Log.LogInfo("[CheatType] verdict handlers guarded=" + CheatDetectionSuppressPatch.Guarded
+ (CheatDetectionSuppressPatch.Unguarded == null
? " (all three)"
: " - UNGUARDED, not found on CheatManager: " + CheatDetectionSuppressPatch.Unguarded));
}
catch (Exception cheatTypeEx)
{
Log.LogError("[CheatType] verdict suppression NOT armed: " + cheatTypeEx.Message);
}
try
{
harmony.PatchAll(typeof(CheatDormDropGuardPatch));
Log.LogInfo("[CheatDrop] dorm drop (EFLCHFDDPJP) held back for the anti-cheat reason; "
+ "every other reason still drops the player.");
}
catch (Exception cheatDropEx)
{
Log.LogError("[CheatDrop] dorm-drop guard NOT armed: " + cheatDropEx.Message);
}
Log.LogInfo("[EacStub] EasyAntiCheat Hydra runtime neutralisation enabled=" + EacGate.Enabled
+ " (RADICADE_EAC_NEUTRALIZE=0 before launch leaves EAC alone)");
Log.LogInfo("[ExitTrace] shutdown-request stack dump armed; [QuitVeto] wantsToQuit veto enabled="
+ UnityQuitVetoPatch.Enabled);
}
catch (Exception quitEx)
{
Log.LogError("[ExitTrace] failed to apply - continuing without them: " + quitEx);
}
try
{
CrashAddressMap.Start();
Log.LogInfo("[AddrMap] enabled=" + CrashAddressMap.Enabled
+ " target=0x" + CrashAddressMap.TargetRva.ToString("x")
+ " (RADICADE_ADDR_MAP=0 disables; RADICADE_ADDR_TARGET=0x... re-aims it)");
}
catch (Exception mapEx)
{
Log.LogError("[AddrMap] failed to start: " + mapEx);
}
try
{
harmony.PatchAll(typeof(ToolCollisionResetGuardPatch));
Log.LogInfo("[ToolGuard] Tool.ResetToDefaultCollisionLayer skipped=" + ToolCollisionResetGuardPatch.Enabled
+ " (RADICADE_TOOL_RESET=0 before launch restores the original reset)");
}
catch (Exception toolEx)
{
Log.LogError("[ToolGuard] failed to apply - the room crash is NOT guarded: " + toolEx);
}
Log.LogInfo($"[August] Connection patches loaded for {ServerUrl.Value}; image key {ImageKeyId ?? "unknown"}.");
}
}
[HarmonyPatch]
internal static class AugustLaunchDiagnostics
{
[HarmonyPrefix, HarmonyPatch(typeof(NDIIPBKGGIG), "FIBOEONALKB")]
[HarmonyPatch(typeof(NDIIPBKGGIG), "IKHPLCCBENN")]
private static void Trace(JKBPKDAGEHN __0)
{
if (__0 == JKBPKDAGEHN.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), "MGEAHKJIIMO")]
private static bool Ready(ref bool __result)
{
__result = true;
return false;
}
}
[HarmonyPatch(typeof(ADJOODNBCKG.NFPDGJNDFOF), "GBAMNAKGEGD")]
internal static class AugustLaunchValidatorPatch
{
static readonly System.Threading.CancellationTokenSource Pump = new();
[HarmonyPrefix]
internal static bool Bypass(ADJOODNBCKG.NFPDGJNDFOF __instance)
{
var promise = __instance.promise;
if (promise == null)
return true;
RadicadeEacBypassPlugin.LogSource.LogInfo("[August] Bypassing native launch validator (hash check cannot pass in a patched install).");
var context = RadicadeEacBypassPlugin.MainThreadContext;
try
{
var before = Il2CppSystem.Threading.SynchronizationContext.Current;
RadicadeEacBypassPlugin.LogSource.LogInfo(
$"[August] validator thread context: {(before != null ? before.GetType().Name : "null")}; installing main-thread context");
if (context != null)
Il2CppSystem.Threading.SynchronizationContext.SetSynchronizationContext(context);
try
{
RadicadeEacBypassPlugin.LogSource.LogInfo(
$"[August] resolving: done={SafeState(promise, "EPCPPDDJCMO")} handlers={SafeHandlerCount(promise)}");
promise.OLKGFACPIPC();
RadicadeEacBypassPlugin.LogSource.LogInfo("[August] Launch-validation promise resolved; boot continues.");
}
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;
}
static int SafeHandlerCount(PFDOLDLEIBA promise)
{
try { return promise.HFPMLNDOGLF?.Count ?? 0; }
catch { return -1; }
}
static bool SafeState(PFDOLDLEIBA promise, string prop)
{
try
{
return prop switch
{
"EPCPPDDJCMO" => promise.EPCPPDDJCMO,
"MPJBGFFEEOP" => promise.MPJBGFFEEOP,
"NEIINCCHEPF" => promise.NEIINCCHEPF,
_ => false,
};
}
catch { 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))));
RadicadeEacBypassPlugin.LogSource.LogInfo(
$"[RsaVerify] patched {target.Name}.{methodName}({string.Join(", ", Array.ConvertAll(args, t => t.Name))})");
}
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(KKHDHILFOGO), "GGIFLDPHCIP");
yield return AccessTools.Method(typeof(KKHDHILFOGO), "GOCCLPJDJIC");
}
[HarmonyPrefix]
internal static bool Bypass(ref MCFFIFIPNKE<KKHDHILFOGO.BJGODLEIDGE> __result)
{
var result = KKHDHILFOGO.BJGODLEIDGE.NOMPJGOIAEB("radicade: file check bypassed");
__result = PFDOLDLEIBA.PLJINLOCHGM(result);
RadicadeEacBypassPlugin.LogSource.LogInfo("[FileCheck] entry bypassed - resolved success promise returned");
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(CCKNJCHHBLL), "BEFDPLAELIH");
yield return AccessTools.Method(typeof(CCKNJCHHBLL), "GIEEBIOOFML");
yield return AccessTools.Method(typeof(CCKNJCHHBLL), "FNOEHNDLHHG");
}
[HarmonyPrefix]
internal static bool TraceAndBlock()
{
var stack = "<unavailable>";
try { stack = Environment.StackTrace; } catch { }
var armed = HileGate.DroppedCount > 0;
RadicadeEacBypassPlugin.LogSource.LogWarning(armed
? "[ExitInit] blocked managed exit initiator - caller trace:\n" + stack
: "[ExitInit] allowed managed exit initiator (no report filed yet) - 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(CCKNJCHHBLL), "GBGMBMGDGGD", Type.EmptyTypes);
if (noArg != null) yield return noArg;
var flag = AccessTools.Method(typeof(CCKNJCHHBLL), "GBGMBMGDGGD", 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; }
}
internal static string DescribeModuleList()
{
try
{
using (var self = System.Diagnostics.Process.GetCurrentProcess())
{
var seen = false;
var path = string.Empty;
foreach (System.Diagnostics.ProcessModule module in self.Modules)
{
if (!string.Equals(module.ModuleName, "winhttp.dll", StringComparison.OrdinalIgnoreCase)) continue;
seen = true;
path = module.FileName ?? string.Empty;
break;
}
return seen
? "[ModuleList] winhttp.dll is enumerated by a Toolhelp module walk: " + path
: "[ModuleList] winhttp.dll is NOT enumerated by a Toolhelp module walk - a loaded image the loader lists do not know about is the most literal definition of obscured.";
}
}
catch (Exception ex)
{
return "[ModuleList] enumeration failed: " + ex.Message;
}
}
}
[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, "MJLAGDJLOBC", System.Type.EmptyTypes);
if (method == null) throw new System.MissingMethodException("CheatManager.MJLAGDJLOBC() 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.MJLAGDJLOBC() (#" + 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 EFLCHFDDPJP 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, "EFLCHFDDPJP", new[] { typeof(string) });
if (method == null)
throw new System.MissingMethodException("CheatManager.EFLCHFDDPJP(string) is gone - the dorm drop moved or was renamed, so the guard is NOT armed.");
return method;
}
[HarmonyPrefix]
private static bool Skip(string JELLDPBCBLG)
{
if (!Enabled) return true;
if (string.IsNullOrEmpty(JELLDPBCBLG) || JELLDPBCBLG.IndexOf(AntiCheatReason, StringComparison.Ordinal) < 0)
return true;
var count = ++_skips;
if (count <= 5 || count % 25 == 0)
RadicadeEacBypassPlugin.LogSource.LogWarning("[CheatDrop] skipped CheatManager.EFLCHFDDPJP('" + JELLDPBCBLG + "') (#" + 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;
}
}
}
[HarmonyPrefix]
private static void Trace(System.Reflection.MethodBase __originalMethod)
{
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 int _skips;
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;
var count = ++_skips;
if (count <= 5 || count % 50 == 0)
RadicadeEacBypassPlugin.LogSource.LogWarning(
"[ToolGuard] skipped Tool.ResetToDefaultCollisionLayer on " + Describe(__instance)
+ " (#" + count + ") - this method is the frame directly above the access violation "
+ "at GameAssembly.dll+0x470728d, which walks the tool's ECS collision-layer components. "
+ "Tool resets still apply; only the collision layer is left as it was.");
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(CCKNJCHHBLL), "GLNIKJNLCIB");
if (property != null) yield return property;
var raw = AccessTools.Method(typeof(CCKNJCHHBLL), "KALEFEDAHCC",
new[] { typeof(CCKNJCHHBLL.CCFIKLBABNB) });
if (raw != null) yield return raw;
}
[HarmonyPrefix]
private static bool Block(CCKNJCHHBLL.CCFIKLBABNB __0)
{
if (!Enabled) return true;
if (__0 == CCKNJCHHBLL.CCFIKLBABNB.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;
}
}
+34
View File
@@ -0,0 +1,34 @@
# Radicade August Client
Startup integration for Rec Room **20230825**, compiled against that build's
generated BepInEx interop assemblies. Requires the metadata-reader repair in
`tools/AugustInterop` first. It does not use the incompatible March UI patches.
Build from the repository root:
```powershell
dotnet build 2023Patch/RadicadeAugustClient/RadicadeAugustClient.csproj -c Release
```
Override `-p:BepInExRoot=<game>/BepInEx` for a different installation. No automatic
deployment occurs. Install `bin/Release/net6.0/RadicadeAugustClient.dll` into
`BepInEx/plugins`, with the old RadicadeEacBypass.dll disabled/backed up. The two
plugins share a config identity and must not be installed together.
Local config (`BepInEx/config/com.radicade.eacbypass.cfg`):
```ini
[Connection]
ServerUrl = https://localhost:3030
[TLS]
AllowedHosts = localhost
```
TLS exceptions are limited to the configured host allowlist. The plugin checks
the game version and requires HTTPS. It reads the normalized metadata copy only
to identify the image signature key. Native game files are not modified.
Verified: build succeeds, plugin loads, TLS succeeds, and server configuration
downloads. **Not yet playable:** the next client error is launch validation;
login, Photon multiplayer, watch UI and room loading are not verified or fully
ported. See `docs/2023-AUGUST-BUILD.md` for the current status and backups.
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<LangVersion>latest</LangVersion>
<BepInExRoot Condition="'$(BepInExRoot)' == ''">C:\RecRoom\2023-08-29T06-53-33Z\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" />
</ItemGroup>
</Project>
+67
View File
@@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
/// <summary>
/// Single switch for the per-request chatter the patch used to print on every
/// login (the [Net], [Connection], [Auth], [HttpStatus] and per-connection [TLS]
/// lines). It is OFF by default: the log now carries patch status and real
/// problems only. Set RADICADE_DIAG=1 to get the full trace back when something
/// needs diagnosing.
/// </summary>
internal static class Diag
{
internal static readonly bool Verbose = ReadFlag("RADICADE_DIAG");
private static readonly object Gate = new object();
private static readonly HashSet<string> Seen = new HashSet<string>(StringComparer.Ordinal);
internal static bool ReadFlag(string name)
{
try
{
var raw = Environment.GetEnvironmentVariable(name);
if (string.IsNullOrWhiteSpace(raw)) return false;
var value = raw.Trim().ToLowerInvariant();
return !(value == "0" || value == "off" || value == "false" || value == "no");
}
catch
{
return false;
}
}
/// <summary>Per-request tracing: printed only with RADICADE_DIAG=1.</summary>
internal static void Trace(string message)
{
if (!Verbose) return;
Info(message);
}
internal static void Info(string message)
{
try { RadicadeEacBypassPlugin.LogSource.LogInfo(message); } catch { }
}
internal static void Warn(string message)
{
try { RadicadeEacBypassPlugin.LogSource.LogWarning(message); } catch { }
}
internal static void Error(string message)
{
try { RadicadeEacBypassPlugin.LogSource.LogError(message); } catch { }
}
/// <summary>True the first time this key is seen in the session.</summary>
internal static bool FirstTime(string key)
{
try
{
lock (Gate) return Seen.Add(key);
}
catch
{
return false;
}
}
}
@@ -0,0 +1,137 @@
using System;
using System.Linq;
using HarmonyLib;
using TLS = Org.BouncyCastle.Crypto.Tls.LegacyTlsAuthentication;
using TimeValid = Org.BouncyCastle.Crypto.Tls.TimeValidVerifyer;
[HarmonyPatch]
internal static class EmbeddedTlsPatches
{
internal static void Apply(Harmony harmony)
{
try
{
harmony.Patch(AccessTools.Method(typeof(TLS), "NotifyServerCertificate"),
prefix: new HarmonyMethod(typeof(EmbeddedTlsPatches), nameof(NotifyServerCertificatePrefix)));
RadicadeEacBypassPlugin.LogSource.LogInfo("[TLS] Patched LegacyTlsAuthentication.NotifyServerCertificate (host-scoped accept)");
}
catch (Exception ex)
{
RadicadeEacBypassPlugin.LogSource.LogWarning($"[TLS] NotifyServerCertificate patch failed: {ex.GetType().Name}: {ex.Message}");
}
try
{
harmony.Patch(AccessTools.Method(typeof(TimeValid), "IsValid"),
prefix: new HarmonyMethod(typeof(EmbeddedTlsPatches), nameof(IsValidPrefix)));
RadicadeEacBypassPlugin.LogSource.LogInfo("[TLS] Patched TimeValidVerifyer.IsValid (host-scoped accept)");
}
catch (Exception ex)
{
RadicadeEacBypassPlugin.LogSource.LogWarning($"[TLS] TimeValidVerifyer patch failed: {ex.GetType().Name}: {ex.Message}");
}
}
static bool NotifyServerCertificatePrefix(TLS __instance)
{
try
{
var uri = __instance.TargetUri;
var host = uri?.Host;
var verifyerType = __instance.verifyer != null ? __instance.verifyer.GetType().FullName : "<null>";
Diag.Trace(
$"[TLS] NotifyServerCertificate: host={host ?? "<null>"}, verifyer={verifyerType}");
if (host != null && IsHostAllowed(host))
{
Diag.Trace(
$"[TLS] Accepting server certificate for allowed Radicade host '{host}' (embedded-stack bypass)");
return false;
}
}
catch (Exception ex)
{
RadicadeEacBypassPlugin.LogSource.LogWarning($"[TLS] NotifyServerCertificate prefix error: {ex.GetType().Name}: {ex.Message}");
}
return true;
}
static bool IsValidPrefix(Il2CppSystem.Uri targetUri, ref bool __result)
{
try
{
var host = targetUri?.Host;
if (host != null && IsHostAllowed(host))
{
Diag.Trace(
$"[TLS] TimeValidVerifyer.IsValid forced TRUE for allowed Radicade host '{host}'");
__result = true;
return false;
}
}
catch (Exception ex)
{
RadicadeEacBypassPlugin.LogSource.LogWarning($"[TLS] IsValid prefix error: {ex.GetType().Name}: {ex.Message}");
}
return true;
}
public static bool IsHostAllowed(string host)
{
if (string.IsNullOrWhiteSpace(host)) return false;
var raw = RadicadeEacBypassPlugin.TlsAllowedHosts?.Value;
if (string.IsNullOrWhiteSpace(raw)) return false;
return raw.Split(',')
.Select(h => h.Trim().TrimEnd('.'))
.Where(h => h.Length > 0)
.Any(h => host.TrimEnd('.').Equals(h, StringComparison.OrdinalIgnoreCase));
}
}
@@ -0,0 +1,107 @@
using System;
using System.IO;
using System.Text;
using System.Reflection;
using BestHTTP;
using HarmonyLib;
/// <summary>
/// Fabric-level repairs for two things the game cannot survive as-is:
/// a chunked response the server truncated, and two responses whose shape the
/// client's reader rejects.
/// </summary>
internal static class HttpTolerancePatches
{
private static long _healed;
private static int _lastHealReport;
internal static void Apply(Harmony harmony)
{
try
{
var target = TargetMethod();
if (target == null)
{
Diag.Warn("[Http] BestHTTP.HTTPResponse.ReadChunkLength(Stream) was not found: " +
"a truncated chunked body will keep failing its request.");
return;
}
harmony.Patch(target,
finalizer: new HarmonyMethod(AccessTools.Method(typeof(HttpTolerancePatches), nameof(Finalizer))));
Diag.Info("[Http] truncated chunked responses are completed instead of failing their request.");
}
catch (Exception ex)
{
Diag.Warn("[Http] could not arm the tolerant chunked reader (" + ex.GetType().Name + ": " + ex.Message + ").");
}
}
internal static MethodBase TargetMethod() =>
AccessTools.Method(typeof(HTTPResponse), "ReadChunkLength", new[] { typeof(Stream) });
/// <summary>
/// "Can't parse '' as a hex number" is BestHTTP reading a chunk-size line from a
/// connection the server had already closed - the pooled keep-alive connection
/// it picked was dead. Ending the body there (chunk length 0) delivers the
/// response that did arrive instead of failing the request, which is what the
/// heartbeats need: their ack body is empty anyway.
/// </summary>
private static Exception Finalizer(Exception __exception, ref int __result)
{
try
{
if (__exception == null) return null;
if (!ResponseRepair.IsTruncatedChunkFailure(__exception.Message)) return __exception;
ReportHealed();
__result = 0;
__exception = null;
return null;
}
catch
{
return __exception;
}
}
private static void ReportHealed()
{
var count = System.Threading.Interlocked.Increment(ref _healed);
var now = Environment.TickCount;
if (count != 1 && unchecked(now - _lastHealReport) < 60000) return;
_lastHealReport = now;
Diag.Warn("[Http] a chunked response ended early (dead pooled connection); the request was completed "
+ "with the bytes that arrived instead of failing. Occurrences so far: " + count + ".");
}
/// <summary>
/// Applies <see cref="ResponseRepair"/> to a live response before the client's own
/// callback reads it. Called from the request funnel, always on the transport
/// thread that owns the response.
/// </summary>
internal static void RepairResponse(HTTPResponse response, string path, string query)
{
try
{
if (response == null || string.IsNullOrEmpty(path)) return;
if (response.StatusCode != 200) return;
string body;
try { body = response.DataAsText; }
catch { return; }
var repaired = ResponseRepair.RepairBody(path, query, body, out var note);
if (repaired == null) return;
response.Data = Encoding.UTF8.GetBytes(repaired);
if (Diag.FirstTime("repair:" + path))
Diag.Warn("[Repair] " + path + ": " + note + ".");
}
catch (Exception ex)
{
Diag.Warn("[Repair] could not repair a " + path + " response: " + ex.GetType().Name + ": " + ex.Message);
}
}
}
@@ -0,0 +1,124 @@
using System;
using System.Text.Json;
using BestHTTP;
using Il2CppInterop.Runtime;
internal static class HubSocketAuth
{
internal static string Token { get; private set; }
internal static string LastLoginRefusalCode { get; private set; }
private static readonly string[] AuthorizationHeaders = { "Authorization", "authorization", "AUTHORIZATION" };
private static readonly System.Collections.Generic.HashSet<string> Stated = new();
internal static bool IsServer(string url)
{
if (!Uri.TryCreate(url, UriKind.Absolute, out var request) ||
!Uri.TryCreate(RadicadeEacBypassPlugin.ServerUrl.Value, UriKind.Absolute, out var server)) return false;
return request.Host.Equals(server.Host, StringComparison.OrdinalIgnoreCase) && request.Port == server.Port &&
(request.Scheme == server.Scheme || request.Scheme == (server.Scheme == "https" ? "wss" : "ws"));
}
internal static void Capture(HTTPRequest request)
{
if (request?.Uri == null || !IsServer(request.Uri.ToString())) return;
foreach (var name in AuthorizationHeaders)
{
var values = request.GetHeaderValues(name);
if (values == null || values.Count == 0) continue;
var value = values[0];
if (value != null && value.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
Token = value.Substring(7).Trim();
}
}
internal static void CaptureFromLoginResponse(HTTPRequest request, string url)
{
if (!IsServer(url) || !new Uri(url).AbsolutePath.EndsWith("/connect/token", StringComparison.OrdinalIgnoreCase)) return;
Token = null;
var original = request.Callback;
request.Callback = DelegateSupport.ConvertDelegate<OnRequestFinishedDelegate>(
new Action<HTTPRequest, HTTPResponse>((req, response) =>
{
try
{
if (response != null && response.StatusCode == 200)
{
using var document = JsonDocument.Parse(response.DataAsText);
if (document.RootElement.TryGetProperty("access_token", out var token) &&
token.ValueKind == JsonValueKind.String &&
!string.IsNullOrWhiteSpace(token.GetString()))
{
Token = token.GetString();
LastLoginRefusalCode = null;
Diag.Trace("[Realtime] Login token captured (value omitted)");
}
else
{
Token = null;
var error = document.RootElement.TryGetProperty("error", out var errorElement) ? errorElement.GetString() : null;
var description = document.RootElement.TryGetProperty("error_description", out var descriptionElement) ? descriptionElement.GetString() : null;
LastLoginRefusalCode = error;
RadicadeEacBypassPlugin.LogSource.LogWarning(
"[Realtime] connect/token answered HTTP 200 without an access_token - that is a login REFUSAL, not a login. " +
"error='" + (error ?? "-") + "', error_description='" + (description ?? "-") + "'.");
}
}
}
catch (Exception ex) { RadicadeEacBypassPlugin.LogSource.LogWarning("[Realtime] Token parse failed: " + ex.GetType().Name); }
finally { original?.Invoke(req, response); }
}));
}
internal static void AuthenticateHubRequest(HTTPRequest request, string url)
{
if (!IsServer(url) || !NameserverRedirect.IsHubUrl(url)) return;
if (!string.IsNullOrWhiteSpace(Token))
{
request.Uri = new Il2CppSystem.Uri(NameserverRedirect.EnsureHubToken(url, Token));
request.SetHeader("Authorization", "Bearer " + Token);
}
Diag.Trace("[Realtime] Hub request " + new Uri(url).AbsolutePath +
"; token=" + (NameserverRedirect.HasHubToken(request.Uri.ToString()) ? "present" : "missing"));
}
internal static void AuthenticateServerRequest(HTTPRequest request, string url)
{
if (request == null || !IsServer(url) || string.IsNullOrWhiteSpace(Token)) return;
try
{
if (IsAnonymousPath(new Uri(url).AbsolutePath)) return;
foreach (var name in AuthorizationHeaders)
{
var values = request.GetHeaderValues(name);
if (values != null && values.Count > 0) return;
}
request.SetHeader("Authorization", "Bearer " + Token);
var path = new Uri(url).AbsolutePath;
if (Stated.Add(path))
Diag.Trace("[Auth] stated the session token on " + path + " (the client sent none)");
}
catch (Exception ex)
{
Diag.Warn("[Auth] could not state the token on " + url + ": " + ex.GetType().Name);
}
}
private static bool IsAnonymousPath(string path) =>
path.StartsWith("/auth/connect/token", StringComparison.OrdinalIgnoreCase) ||
path.StartsWith("/auth/cachedlogin", StringComparison.OrdinalIgnoreCase) ||
path.StartsWith("/auth/eac/", StringComparison.OrdinalIgnoreCase) ||
path.StartsWith("/images/", StringComparison.OrdinalIgnoreCase) ||
path.StartsWith("/cdn/", StringComparison.OrdinalIgnoreCase);
}
@@ -0,0 +1,187 @@
#nullable enable
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Linq;
using System.Text;
public static class ImageSignatureRequest
{
const int HeaderScanLimit = 0x100;
const int MinimumRecords = 32;
const int PrefixProbe = 8;
const int PackingProbe = 1024;
const int MaxLiteralLength = 1 << 20;
static readonly UTF8Encoding StrictUtf8 = new UTF8Encoding(false, true);
public static string? DetectKeyId(byte[]? metadata)
{
if (metadata == null || metadata.Length < 24 ||
BinaryPrimitives.ReadUInt32LittleEndian(metadata) != 0xFAB11BAF ||
BinaryPrimitives.ReadInt32LittleEndian(metadata.AsSpan(4)) != 27)
return null;
int table = BinaryPrimitives.ReadInt32LittleEndian(metadata.AsSpan(8));
int size = BinaryPrimitives.ReadInt32LittleEndian(metadata.AsSpan(12));
int data = BinaryPrimitives.ReadInt32LittleEndian(metadata.AsSpan(16));
int dataSize = BinaryPrimitives.ReadInt32LittleEndian(metadata.AsSpan(20));
if (table < 24 || size < 0 || size % 8 != 0 || (long)table + size > metadata.Length ||
data < 24 || dataSize < 0 || (long)data + dataSize > metadata.Length)
return null;
return ScanKeyTails(metadata, table, size / 8, data, dataSize);
}
public static string? DetectKeyIdFromGameMetadata(byte[]? metadata)
{
if (metadata == null || metadata.Length < 24) return null;
if (BinaryPrimitives.ReadUInt32LittleEndian(metadata) == 0xFAB11BAF &&
BinaryPrimitives.ReadInt32LittleEndian(metadata.AsSpan(4)) == 27)
return DetectKeyId(metadata);
return DetectKeyIdByHeaderScan(metadata);
}
static string? ScanKeyTails(byte[] metadata, int tableOffset, int recordCount, int dataOffset, int dataSize)
{
string? key = null;
for (int record = 0; record < recordCount; record++)
{
var (length, index) = Record(metadata, tableOffset, record);
if (length < 0 || index < 0 || (long)index + length > dataSize) return null;
if (length != 8 && length != 18) continue;
var literal = Encoding.UTF8.GetString(metadata, dataOffset + index, length);
if (literal != ".rec.net" && literal != ".radicade.internal") continue;
var candidate = "KEY:RSA:p1" + literal;
if (key != null && key != candidate) return null;
key = candidate;
}
return key;
}
static (int Length, int Index) Record(byte[] metadata, int tableOffset, int record)
{
int offset = tableOffset + 8 * record;
return (BinaryPrimitives.ReadInt32LittleEndian(metadata.AsSpan(offset)),
BinaryPrimitives.ReadInt32LittleEndian(metadata.AsSpan(offset + 4)));
}
public static string? DetectKeyIdByHeaderScan(byte[]? metadata)
{
if (metadata == null || metadata.Length < HeaderScanLimit + 8) return null;
var fields = new List<int>();
for (int offset = 0; offset + 4 <= HeaderScanLimit; offset += 4)
{
int value = BinaryPrimitives.ReadInt32LittleEndian(metadata.AsSpan(offset));
if (value > 0 && value <= metadata.Length && !fields.Contains(value)) fields.Add(value);
}
var tables = new List<(int Offset, int Count)>();
foreach (var tableOffset in fields)
foreach (var tableSize in fields)
{
if (tableSize % 8 != 0 || tableSize / 8 < MinimumRecords) continue;
if (tableOffset < HeaderScanLimit || (long)tableOffset + tableSize > metadata.Length) continue;
tables.Add((tableOffset, tableSize / 8));
}
var slices = new List<(int Offset, int Size)>();
foreach (var dataOffset in fields)
foreach (var dataSize in fields)
{
if (dataSize <= 0 || dataOffset < HeaderScanLimit) continue;
if ((long)dataOffset + dataSize > metadata.Length) continue;
slices.Add((dataOffset, dataSize));
}
tables.Sort((a, b) => b.Count.CompareTo(a.Count));
foreach (var (tableOffset, count) in tables)
foreach (var (dataOffset, dataSize) in slices)
{
if (!LooksLikeLiteralTable(metadata, tableOffset, count, dataOffset, dataSize, PrefixProbe)) continue;
if (!SampleDecodesAsText(metadata, tableOffset, count, dataOffset, dataSize)) continue;
if (!LooksLikeLiteralTable(metadata, tableOffset, count, dataOffset, dataSize, PackingProbe)) continue;
var key = ScanKeyTails(metadata, tableOffset, count, dataOffset, dataSize);
if (key != null) return key;
}
return null;
}
static bool LooksLikeLiteralTable(byte[] metadata, int tableOffset, int count, int dataOffset, int dataSize, int probe)
{
int examined = Math.Min(count, probe);
int contiguous = 0;
long running = 0;
for (int record = 0; record < examined; record++)
{
var (length, index) = Record(metadata, tableOffset, record);
if (length < 0 || length > MaxLiteralLength || index < 0 || (long)index + length > dataSize) return false;
if (index == running) contiguous++;
running = index + length;
}
return contiguous >= examined - Math.Max(1, examined / 100);
}
static bool SampleDecodesAsText(byte[] metadata, int tableOffset, int count, int dataOffset, int dataSize)
{
int step = Math.Max(1, count / 48);
for (int record = 0; record < count; record += step)
{
var (length, index) = Record(metadata, tableOffset, record);
if (length < 0 || index < 0 || (long)index + length > dataSize) return false;
if (length == 0) continue;
try
{
StrictUtf8.GetCharCount(metadata, dataOffset + index, length);
}
catch (ArgumentException)
{
return false;
}
}
return true;
}
public static string AddKeyId(string url, string serverUrl, string? keyId)
{
if (string.IsNullOrEmpty(keyId) ||
!Uri.TryCreate(url, UriKind.Absolute, out var request) ||
!Uri.TryCreate(serverUrl, UriKind.Absolute, out var server) ||
(server.Scheme != "http" && server.Scheme != "https") ||
request.Scheme != server.Scheme || request.Authority != server.Authority ||
!request.AbsolutePath.StartsWith(server.AbsolutePath.TrimEnd('/') + "/images/", StringComparison.OrdinalIgnoreCase))
return url;
var parameters = request.Query.TrimStart('?').Split('&', StringSplitOptions.RemoveEmptyEntries)
.Where(p => !Uri.UnescapeDataString(p.Split('=')[0]).Equals("rcsig", StringComparison.OrdinalIgnoreCase))
.Append("rcsig=" + Uri.EscapeDataString(keyId));
return request.GetLeftPart(UriPartial.Path) + "?" + string.Join("&", parameters) + request.Fragment;
}
}
@@ -0,0 +1,510 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using BestHTTP;
using HarmonyLib;
using Il2CppInterop.Runtime;
internal static class HileGate
{
internal static bool Enabled;
internal static int DroppedCount;
public static bool DropEnabled { get; } = IsDropEnabled();
private static bool IsDropEnabled()
{
try
{
var raw = System.Environment.GetEnvironmentVariable("RADICADE_HILE_GATE");
if (string.IsNullOrWhiteSpace(raw)) return false;
return raw.Trim().ToLowerInvariant() == "drop";
}
catch { return false; }
}
private static bool _dumped;
public static bool ShouldDrop(HTTPRequest request)
{
if (!Enabled || request?.Uri == null) return false;
var path = request.Uri.AbsolutePath;
if (path == null ||
!path.EndsWith("/api/PlayerReporting/v1/hile", System.StringComparison.OrdinalIgnoreCase))
return false;
Dump(request);
return DropEnabled;
}
public static void Dump(HTTPRequest request)
{
if (_dumped) return;
_dumped = true;
try
{
var body = "<unreadable>";
try
{
var raw = request.RawData;
if (raw != null && raw.Length > 0)
{
body = System.Text.Encoding.UTF8.GetString(raw);
if (body.Length > 2000) body = body.Substring(0, 2000) + "...[truncated]";
}
else
{
var entity = request.GetEntityBody();
if (entity != null && entity.Length > 0)
{
var text = System.Text.Encoding.UTF8.GetString(entity);
body = text + " (" + entity.Length + " bytes in the entity body)";
}
else
{
body = "<empty body>";
}
}
}
catch (System.Exception bodyEx)
{
body = "<body read failed: " + bodyEx.Message + ">";
}
var caller = "<unavailable>";
try { caller = System.Environment.StackTrace; } catch { }
RadicadeEacBypassPlugin.LogSource.LogWarning(
"[HileGate] anti-cheat report captured (full wire form):\n " + request.MethodType
+ " " + request.Uri + "\n body: " + body
+ "\n managed caller stack:" + caller);
}
catch (System.Exception ex)
{
RadicadeEacBypassPlugin.LogSource.LogWarning("[HileGate] capture failed: " + ex.Message);
}
}
}
[HarmonyPatch(typeof(HTTPManager), "SendRequest", new[] { typeof(HTTPRequest) })]
internal static class NameserverConnectionPatches
{
[HarmonyPriority(Priority.Last)]
private static bool Prefix(HTTPRequest request, ref HTTPRequest __result)
{
if (RequestFunnel.Handle(request)) return true;
__result = request;
return false;
}
}
internal static class RequestFunnel
{
private static readonly HashSet<long> Seen = new();
private static bool IsSocketScheme(string url) =>
url.StartsWith("ws:", StringComparison.OrdinalIgnoreCase) ||
url.StartsWith("wss:", StringComparison.OrdinalIgnoreCase);
internal static bool Handle(HTTPRequest request)
{
if (HileGate.ShouldDrop(request))
{
HileGate.DroppedCount++;
RadicadeEacBypassPlugin.LogSource.LogWarning("[HileGate] dropped anti-cheat report POST (no response will arrive).");
return false;
}
try
{
var uri = request?.Uri?.ToString();
if (!string.IsNullOrEmpty(uri))
Diag.Trace("[Net] " + request.MethodType + " " + uri);
}
catch
{
}
if (request?.Uri == null) return true;
if (!Seen.Add(request.Pointer.ToInt64())) return true;
var original = request.Uri.ToString();
var rewritten = IsSocketScheme(original)
? NameserverRedirect.RewriteWebSocket(original, RadicadeEacBypassPlugin.ServerUrl.Value)
: NameserverRedirect.Rewrite(original, RadicadeEacBypassPlugin.ServerUrl.Value);
var withKeyId = ImageSignatureRequest.AddKeyId(rewritten, RadicadeEacBypassPlugin.ServerUrl.Value,
RadicadeEacBypassPlugin.ImageKeyId);
if (withKeyId != rewritten)
{
Diag.Trace("[ImageSig] stated key id on " + withKeyId);
}
rewritten = withKeyId;
if (rewritten != original)
{
request.Uri = new Il2CppSystem.Uri(rewritten);
AlignHostHeader(request, rewritten);
Diag.Trace("[Connection] Redirected nameserver request to the configured Radicade server.");
}
if (RealtimeDiagnostics.Verbose && IsSocketScheme(original))
{
try
{
var outgoing = new System.Text.StringBuilder("[Realtime] socket request: " + original);
var headers = request.Headers;
if (headers != null)
{
foreach (var header in headers)
{
outgoing.Append(" | ").Append(header.Key).Append("=");
var values = header.Value;
if (values != null)
foreach (var value in values) outgoing.Append(value).Append(",");
}
}
RadicadeEacBypassPlugin.LogSource.LogWarning(outgoing.ToString());
}
catch (Exception ex)
{
RadicadeEacBypassPlugin.LogSource.LogWarning("[Realtime] socket request dump failed: "
+ ex.GetType().Name + ": " + ex.Message);
}
}
HubSocketAuth.Capture(request);
HubSocketAuth.CaptureFromLoginResponse(request, request.Uri.ToString());
HubSocketAuth.AuthenticateHubRequest(request, request.Uri.ToString());
HubSocketAuth.AuthenticateServerRequest(request, request.Uri.ToString());
if (Diag.Verbose) AuthGrantTrace.Report(request);
HttpOutcomeTrace.Wrap(request);
return true;
}
private static void AlignHostHeader(HTTPRequest request, string url)
{
if (request == null || string.IsNullOrEmpty(url)) return;
Uri target;
if (!Uri.TryCreate(url, UriKind.Absolute, out target) || string.IsNullOrEmpty(target.Host)) return;
var authority = target.IsDefaultPort ? target.Host : target.Host + ":" + target.Port;
try
{
var stated = request.GetHeaderValues("Host");
if (stated != null && stated.Count > 0)
{
if (stated.Count == 1 && stated[0] == authority) return;
request.RemoveHeader("Host");
}
request.SetHeader("Host", authority);
}
catch (Exception ex)
{
RadicadeEacBypassPlugin.LogSource.LogWarning("[Connection] could not align the Host header on " + url +
": " + ex.GetType().Name + ": " + ex.Message);
}
}
}
internal static class AuthGrantTrace
{
private static readonly HashSet<long> Reported = new();
private static readonly string[] Fields =
{
"account_id", "username", "password", "birthday", "avatar", "avatarJson",
"email", "platform_auth", "device_id", "device_class", "client_id", "ver"
};
internal static void Report(HTTPRequest request)
{
try
{
if (request?.Uri == null) return;
if (!request.Uri.ToString().EndsWith("/auth/connect/token", StringComparison.OrdinalIgnoreCase)) return;
if (!Reported.Add(request.Pointer.ToInt64())) return;
var body = ReadBody(request);
if (string.IsNullOrEmpty(body))
{
RadicadeEacBypassPlugin.LogSource.LogWarning("[AuthGrant] connect/token body was unreadable; the grant is unknown for this request.");
return;
}
string Value(string name)
{
foreach (var pair in body.Split('&'))
{
var separator = pair.IndexOf('=');
if (separator <= 0) continue;
if (!string.Equals(pair.Substring(0, separator), name, StringComparison.OrdinalIgnoreCase)) continue;
return pair.Substring(separator + 1);
}
return null;
}
bool Present(string name)
{
var value = Value(name);
return !string.IsNullOrWhiteSpace(value);
}
var grant = Value("grant_type") ?? "<absent>";
var present = new List<string>();
foreach (var field in Fields)
if (Present(field)) present.Add(field);
RadicadeEacBypassPlugin.LogSource.LogInfo("[AuthGrant] connect/token grant_type=" + grant
+ "; fields present: " + (present.Count == 0 ? "<none>" : string.Join(", ", present))
+ " (values omitted)");
}
catch (Exception ex)
{
RadicadeEacBypassPlugin.LogSource.LogWarning("[AuthGrant] could not read the grant: " + ex.GetType().Name + ": " + ex.Message);
}
}
private static string ReadBody(HTTPRequest request)
{
try
{
var raw = request.RawData;
if (raw != null && raw.Length > 0) return System.Text.Encoding.UTF8.GetString(raw);
}
catch { }
try
{
var entity = request.GetEntityBody();
if (entity != null && entity.Length > 0) return System.Text.Encoding.UTF8.GetString(entity);
}
catch { }
return null;
}
}
[HarmonyPatch]
internal static class SendRequestImplFunnelPatch
{
private static IEnumerable<MethodBase> TargetMethods()
{
var impl = AccessTools.Method(typeof(HTTPManager), "SendRequestImpl", new[] { typeof(HTTPRequest) });
return impl == null ? Enumerable.Empty<MethodBase>() : new MethodBase[] { impl };
}
private static void Prefix(object[] __args)
{
try
{
if (__args == null || __args.Length == 0) return;
RequestFunnel.Handle(__args[0] as HTTPRequest);
}
catch (Exception ex)
{
Diag.Warn("[Net] funnel trace failed: " + ex.GetType().Name + ": " + ex.Message);
}
}
}
internal static class HttpOutcomeTrace
{
private static readonly HashSet<long> Wrapped = new();
private static int _completed;
internal static void Wrap(HTTPRequest request)
{
if (request == null) return;
string url;
string method;
string path;
string query;
try
{
if (request.Uri == null) return;
url = Shorten(request.Uri.ToString());
method = request.MethodType.ToString();
path = request.Uri.AbsolutePath;
query = request.Uri.Query;
}
catch
{
return;
}
try
{
if (!Wrapped.Add(request.Pointer.ToInt64())) return;
var original = request.Callback;
request.Callback = DelegateSupport.ConvertDelegate<OnRequestFinishedDelegate>(
new Action<HTTPRequest, HTTPResponse>((req, response) =>
{
try
{
HttpTolerancePatches.RepairResponse(response, path, query);
var completed = System.Threading.Interlocked.Increment(ref _completed);
var status = -1;
var bytes = 0;
var reason = string.Empty;
if (response != null)
{
status = response.StatusCode;
try { var data = response.Data; if (data != null) bytes = data.Length; } catch { }
try { if (!response.IsSuccess && !string.IsNullOrEmpty(response.Message)) reason = " reason=" + response.Message; } catch { }
}
Diag.Trace("[HttpStatus] #" + completed + " " + method + " " + url + " -> "
+ (response == null ? "NO RESPONSE" : status.ToString())
+ " bytes=" + bytes + reason);
if (RealtimeDiagnostics.Verbose && url.IndexOf("/notify/hub/v1", StringComparison.OrdinalIgnoreCase) >= 0)
{
try
{
var answer = new System.Text.StringBuilder();
answer.Append("[Realtime] socket answer: status=").Append(response == null ? -1 : status)
.Append(" message=").Append(response == null ? "<none>" : response.Message);
var answerHeaders = response?.Headers;
if (answerHeaders != null)
{
foreach (var header in answerHeaders)
{
answer.Append(" | ").Append(header.Key).Append("=");
var values = header.Value;
if (values != null)
foreach (var value in values) answer.Append(value).Append(",");
}
}
string body = null;
try { body = response?.DataAsText; } catch { }
if (!string.IsNullOrEmpty(body))
answer.Append(" | body=").Append(body.Length > 200 ? body.Substring(0, 200) : body);
RadicadeEacBypassPlugin.LogSource.LogWarning(answer.ToString());
}
catch (Exception ex)
{
RadicadeEacBypassPlugin.LogSource.LogWarning("[Realtime] socket answer dump failed: "
+ ex.GetType().Name + ": " + ex.Message);
}
}
}
catch
{
}
finally
{
try { original?.Invoke(req, response); } catch { }
}
}));
}
catch (Exception ex)
{
Diag.Warn("[HttpStatus] could not instrument " + method + " " + url + ": "
+ ex.GetType().Name + ": " + ex.Message);
}
}
private static string Shorten(string url)
{
try
{
var uri = new Uri(url);
var path = uri.AbsolutePath;
if (path.StartsWith("/auth/cachedlogin/forplatformid/", StringComparison.OrdinalIgnoreCase))
return "/auth/cachedlogin/forplatformid/{redacted}";
return path + (string.IsNullOrEmpty(uri.Query) ? string.Empty : "?" + uri.Query.Length + " chars");
}
catch
{
return "<unparsable>";
}
}
}
@@ -0,0 +1,195 @@
using System;
using System.Collections.Generic;
public static class NameserverRedirect
{
private static readonly Dictionary<string, string> ServicePrefixes = new(StringComparer.OrdinalIgnoreCase)
{
["api.rec.net"] = "",
["cdn.rec.net"] = "",
["images.rec.net"] = "/images",
["img.rec.net"] = "/images",
["data.rec.net"] = "/data",
["storage.rec.net"] = "/storage",
["notify.rec.net"] = "/notify",
["notifications.rec.net"] = "/notify",
["notify.radicade.internal"] = "/notify",
["notifications.radicade.internal"] = "/notify",
["auth.rec.net"] = "/auth",
["accounts.rec.net"] = "/accounts",
["rooms.rec.net"] = "/rooms",
["matchmaking.rec.net"] = "/matchmaking",
["chat.rec.net"] = "/chat",
["api.radicade.app"] = "",
["cdn.radicade.app"] = "/cdn",
["ai.radicade.app"] = "/ai",
["auth.radicade.app"] = "/auth",
["www.radicade.app"] = "/www",
["cards.radicade.app"] = "/cards",
["notifications.radicade.app"] = "/notify",
["images.radicade.app"] = "/images",
["commerce.radicade.app"] = "/commerce",
["data.radicade.app"] = "/data",
["datacollection.radicade.app"] = "/datacollection",
["discovery.radicade.app"] = "/discovery",
["matchmaking.radicade.app"] = "/matchmaking",
["storage.radicade.app"] = "/storage",
["chat.radicade.app"] = "/chat",
["geo.radicade.app"] = "/geo",
["leaderboard.radicade.app"] = "/leaderboard",
["accounts.radicade.app"] = "/accounts",
["link.radicade.app"] = "/link",
["locker.radicade.app"] = "/locker",
["moderation.radicade.app"] = "/moderation",
["roomcomments.radicade.app"] = "/roomcomments",
["roomieintegrations.radicade.app"] = "/roomieintegrations",
["clubs.radicade.app"] = "/clubs",
["rooms.radicade.app"] = "/rooms",
["econ.radicade.app"] = "/econ",
["bugreporting.radicade.app"] = "/bugreporting",
["cms.radicade.app"] = "/cms",
["gamelogs.radicade.app"] = "/gamelogs",
["platformnotifications.radicade.app"] = "/platformnotifications",
["stringscdn.radicade.app"] = "/stringscdn",
["strings.radicade.app"] = "/strings",
["lists.radicade.app"] = "/lists",
["playersettings.radicade.app"] = "/playersettings",
["studio.radicade.app"] = "/studio",
["studiocdn.radicade.app"] = "/studiocdn",
["thorn.radicade.app"] = "/thorn",
["videos.radicade.app"] = "/videos",
};
public static string Rewrite(string requestUrl, string serverUrl)
{
if (!Uri.TryCreate(requestUrl, UriKind.Absolute, out var request) ||
(request.Scheme != "http" && request.Scheme != "https"))
return requestUrl;
bool nameserver = request.Host.Equals("ns.rec.net", StringComparison.OrdinalIgnoreCase) ||
request.Host.Equals("ns.radicade.internal", StringComparison.OrdinalIgnoreCase);
bool patchedServer = request.Host.Equals("ns.radicade.app", StringComparison.OrdinalIgnoreCase);
ServicePrefixes.TryGetValue(request.Host, out var servicePrefix);
if (!nameserver && !patchedServer && servicePrefix == null)
return requestUrl;
if (!Uri.TryCreate(serverUrl, UriKind.Absolute, out var server) ||
(server.Scheme != "http" && server.Scheme != "https") ||
!string.IsNullOrEmpty(server.UserInfo) || !string.IsNullOrEmpty(server.Query) ||
!string.IsNullOrEmpty(server.Fragment))
throw new ArgumentException("Connection.ServerUrl must be an HTTP(S) base URL without credentials, query or fragment.", nameof(serverUrl));
var path = request.AbsolutePath;
if (nameserver)
{
if (path == "/") path = "/ns";
}
else if (servicePrefix is { Length: > 0 })
{
if (servicePrefix == "/notify" && path.StartsWith("/notify/", StringComparison.OrdinalIgnoreCase))
path = path.Substring("/notify".Length);
else if (path.StartsWith("/images/", StringComparison.OrdinalIgnoreCase))
path = path.Substring("/images".Length);
else if (path.StartsWith("/img/", StringComparison.OrdinalIgnoreCase))
path = path.Substring("/img".Length);
path = servicePrefix + path;
}
return server.AbsoluteUri.TrimEnd('/') + path + request.Query;
}
public static string RewriteWebSocket(string requestUrl, string serverUrl)
{
if (!Uri.TryCreate(requestUrl, UriKind.Absolute, out var request))
return requestUrl;
var scheme = request.Scheme.ToLowerInvariant();
var secure = scheme == "wss" || scheme == "https";
if (scheme != "ws" && scheme != "wss" && scheme != "http" && scheme != "https")
return requestUrl;
var asHttp = new UriBuilder(request) { Scheme = secure ? "https" : "http" }.Uri.AbsoluteUri;
var rewritten = Rewrite(asHttp, serverUrl);
if (rewritten == asHttp) return requestUrl;
if (!Uri.TryCreate(rewritten, UriKind.Absolute, out var result)) return requestUrl;
return new UriBuilder(result) { Scheme = result.Scheme == "https" ? "wss" : "ws" }.Uri.AbsoluteUri;
}
public static bool IsHubUrl(string url)
{
if (string.IsNullOrWhiteSpace(url)) return false;
var lower = url.ToLowerInvariant();
var query = lower.IndexOf('?');
var path = query < 0 ? lower : lower.Substring(0, query);
return path.Contains("/hub/v1") || path.EndsWith("/hub")
|| path.Contains("/hub/")
|| path.Contains("notifyhub")
|| path.Contains("/notify/hub");
}
public static bool HasHubToken(string url)
{
if (string.IsNullOrWhiteSpace(url)) return false;
var query = url.IndexOf('?');
if (query < 0) return false;
foreach (var pair in url.Substring(query + 1).Split('&'))
{
var equals = pair.IndexOf('=');
if (equals <= 0 || equals + 1 >= pair.Length) continue;
if (pair.Substring(0, equals).Equals("access_token", StringComparison.OrdinalIgnoreCase)) return true;
}
return false;
}
public static string EnsureHubToken(string url, string token)
{
if (!IsHubUrl(url)) return url;
if (HasHubToken(url)) return url;
if (string.IsNullOrWhiteSpace(token)) return url;
var raw = token.Trim();
if (raw.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) raw = raw.Substring(7).Trim();
else if (raw.Equals("Bearer", StringComparison.OrdinalIgnoreCase)) raw = string.Empty;
if (raw.Length == 0) return url;
var query = url.IndexOf('?');
var basePart = query < 0 ? url : url.Substring(0, query);
var rest = query < 0 ? string.Empty : url.Substring(query + 1).TrimStart('&');
var accessToken = "access_token=" + Uri.EscapeDataString(raw);
return basePart + "?" + accessToken + (rest.Length == 0 ? string.Empty : "&" + rest);
}
}
@@ -0,0 +1,903 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Threading;
using HarmonyLib;
internal static class RealtimeDiagnostics
{
private static readonly object Gate = new object();
private static readonly Dictionary<string, int> Targets = new Dictionary<string, int>();
private static readonly Dictionary<string, int> NotificationIds = new Dictionary<string, int>();
private static readonly HashSet<string> KnownNotifications = new HashSet<string>();
private static readonly HashSet<string> HighFrequencyIds = new HashSet<string>(StringComparer.Ordinal)
{
"PresenceUpdate",
"AccountUpdate",
"SelfAccountUpdate",
"SubscriptionUpdatePresence",
"SubscriptionUpdateProfile",
"SubscriptionUpdateGameSession",
"SubscriptionUpdateRoom",
"SubscriptionUpdateRoomPlaylist"
};
private static int Frames;
private static int Failures;
private const int VerboseFrames = 40;
private const int PreviewChars = 400;
internal static void StartHandlerKeyProbe(Harmony harmony)
{
if (Interlocked.Exchange(ref HandlerKeyProbeStarted, 1) != 0) return;
var worker = new Thread(() =>
{
Type dispatcher = null;
bool hooksInstalled = false;
bool tablesDumped = false;
for (int attempt = 1; attempt <= HandlerKeyProbeAttempts; attempt++)
{
try
{
if (dispatcher == null) dispatcher = FindNotificationDispatcher();
if (!hooksInstalled && dispatcher != null)
hooksInstalled = InstallRegistrationHooks(harmony, dispatcher);
if (!tablesDumped && attempt <= TableDumpAttempts)
tablesDumped = DumpDispatcherKeys(attempt, dispatcher);
if (hooksInstalled && (tablesDumped || attempt >= TableDumpAttempts)) return;
}
catch (Exception ex)
{
if (attempt == 1)
RadicadeEacBypassPlugin.LogSource.LogWarning(
$"[Realtime] handler-key probe failed ({ex.GetType().Name}: {ex.Message}); retrying.");
}
Thread.Sleep(HandlerKeyProbeIntervalMs);
}
if (!hooksInstalled)
RadicadeEacBypassPlugin.LogSource.LogWarning(
$"[Realtime] no handler-registration method was located after {HandlerKeyProbeAttempts} attempts " +
"- notification ids cannot be checked against the client's own registrations in this build.");
})
{ IsBackground = true, Name = "RadicadeNotifyKeyProbe" };
worker.Start();
}
private const int HandlerKeyProbeAttempts = 60;
private const int HandlerKeyProbeIntervalMs = 2000;
private const int TableDumpAttempts = 3;
private static int HandlerKeyProbeStarted;
private static readonly HashSet<string> RegisteredHandlerKeys = new HashSet<string>(StringComparer.Ordinal);
private static bool ChatKeyReported;
private static bool InstallRegistrationHooks(Harmony harmony, Type dispatcher)
{
var hooked = 0;
var callbacks = 0;
var generics = 0;
foreach (var method in AccessTools.GetDeclaredMethods(dispatcher))
{
try
{
if (method.ReturnType != typeof(void)) continue;
var parameters = method.GetParameters();
if (parameters.Length != 1 && parameters.Length != 2) continue;
var keyType = parameters[0].ParameterType;
if (keyType != typeof(string) && !keyType.IsEnum) continue;
if (parameters.Length == 2)
{
if (!IsCallback(parameters[1].ParameterType)) continue;
callbacks++;
if (method.IsGenericMethodDefinition) { generics++; continue; }
}
harmony.Patch(method,
prefix: new HarmonyMethod(AccessTools.Method(typeof(RealtimeDiagnostics), nameof(RegistrationPrefix))));
hooked++;
}
catch (Exception ex)
{
RadicadeEacBypassPlugin.LogSource.LogWarning(
$"[Realtime] could not hook {dispatcher.Name}.{method.Name} for key logging ({ex.GetType().Name}: {ex.Message}).");
}
}
if (hooked == 0) return false;
RadicadeEacBypassPlugin.LogSource.LogInfo(
$"[Realtime] watching {hooked} dispatcher method(s) on {dispatcher.FullName} " +
$"({callbacks} registration, {generics} generic overload(s) skipped but funnel through the core, rest channels): " +
"every notification id this client accepts will be printed as it registers.");
return true;
}
private static bool IsCallback(Type type)
{
try
{
if (typeof(Delegate).IsAssignableFrom(type)) return true;
for (var current = type; current != null; current = current.BaseType)
{
var name = current.Name;
if (name == "Delegate" || name == "MulticastDelegate") return true;
}
}
catch { }
return false;
}
private static void RegistrationPrefix(object[] __args, MethodBase __originalMethod)
{
try
{
if (__args == null || __args.Length == 0) return;
var key = DescribeKey(__args[0]);
if (key == null) return;
if (__args.Length == 1)
{
RadicadeEacBypassPlugin.LogSource.LogInfo(
$"[Realtime] the client called {__originalMethod?.Name}({key}) - " +
"a realtime channel/lookup name it expects the server to publish on.");
return;
}
bool first;
lock (Gate) first = RegisteredHandlerKeys.Add(key);
if (!first) return;
RadicadeEacBypassPlugin.LogSource.LogInfo(
$"[Realtime] the client registered a notification handler under {key} " +
$"({__originalMethod?.DeclaringType?.Name}.{__originalMethod?.Name}) - " +
$"a notification whose Id EQUALS {key} is handled, anything else is dropped.");
if (key.IndexOf("Progression", StringComparison.Ordinal) >= 0
|| key.IndexOf("AppVersion", StringComparison.Ordinal) >= 0
|| key.IndexOf("Announcement", StringComparison.Ordinal) >= 0)
RadicadeEacBypassPlugin.LogSource.LogInfo(
$"[Realtime] {key} binds a payload of type {DescribeCallback(__args.Length > 1 ? __args[1] : null)}");
ReportChatKeyMatch();
}
catch { }
}
private static string DescribeCallback(object? callback)
{
if (callback == null) return "(no callback)";
try
{
var type = callback.GetType();
var candidates = new List<Type>();
if (type.IsGenericType) candidates.AddRange(type.GetGenericArguments());
for (var current = type; current != null; current = current.BaseType)
{
MethodInfo? invoke = null;
try { invoke = current.GetMethod("Invoke", BindingFlags.Public | BindingFlags.Instance); }
catch { }
if (invoke == null) continue;
foreach (var parameter in invoke.GetParameters()) candidates.Add(parameter.ParameterType);
}
var payloads = candidates
.Where(t => t != null && t != typeof(void))
.Distinct()
.ToList();
if (payloads.Count == 0) return $"{type.FullName} (no payload readable)";
return string.Join(" | ", payloads.Select(DescribePayloadType));
}
catch (Exception ex)
{
return $"(unreadable: {ex.GetType().Name})";
}
}
private static string DescribePayloadType(Type type)
{
try
{
if (type.IsPrimitive || type == typeof(string)) return type.Name;
var members = new List<string>();
foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance))
members.Add(field.Name);
foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
members.Add(property.Name);
members = members.Distinct().OrderBy(m => m, StringComparer.Ordinal).ToList();
if (members.Count == 0) return type.FullName ?? type.Name;
return $"{type.FullName} {{{string.Join(", ", members)}}}";
}
catch
{
return type.FullName ?? type.Name;
}
}
private static string DescribeKey(object value)
{
if (value is string text) return Quote(text);
if (value == null) return null;
try
{
var type = value.GetType();
return type.IsEnum ? Quote($"{type.Name}.{value}") : null;
}
catch
{
return null;
}
}
private static void ReportChatKeyMatch()
{
lock (Gate)
{
if (ChatKeyReported) return;
var numeric = RegisteredHandlerKeys.Contains("\"90\"");
var named = RegisteredHandlerKeys.FirstOrDefault(
key => key.IndexOf("ChatMessage", StringComparison.Ordinal) >= 0);
if (!numeric && named == null) return;
ChatKeyReported = true;
RadicadeEacBypassPlugin.LogSource.LogInfo(
$"[Realtime] chat push key check: this client registered " +
$"{(numeric ? "a handler for the chat enum's number (\"90\")" : "NO handler for the chat enum's number (\"90\")")}" +
$"{(named == null ? string.Empty : $", and {named} by name")}. " +
"Only an exact match is delivered, so the server's notification Id has to be the " +
"spelling that was registered - for chat that is the name.");
}
}
private const int MaxTables = 25;
private const int MaxKeysPerTable = 60;
private static bool DumpDispatcherKeys(int attempt, Type dispatcher)
{
var tables = new List<string>();
if (dispatcher != null) CollectStringKeyedTables(dispatcher, tables);
if (tables.Count == 0)
{
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
var name = assembly.GetName().Name ?? string.Empty;
if (!name.StartsWith("RecNet", StringComparison.Ordinal)) continue;
Type[] types;
try { types = assembly.GetTypes(); }
catch (ReflectionTypeLoadException ex) { types = ex.Types.Where(t => t != null).ToArray(); }
catch { continue; }
foreach (var type in types)
{
try { CollectStringKeyedTables(type, tables); } catch { }
if (tables.Count >= MaxTables) break;
}
if (tables.Count >= MaxTables) break;
}
}
if (tables.Count == 0) return false;
RadicadeEacBypassPlugin.LogSource.LogInfo(
dispatcher != null
? $"[Realtime] the client's notification dispatcher ({dispatcher.FullName}) holds these string keys (attempt {attempt}). " +
"A notification Id must EQUAL one of them to be handled - this is the contract:"
: $"[Realtime] could not pin the notification dispatcher, so every string-keyed static table in RecNet.* is listed " +
$"instead (attempt {attempt}). The handler keys are among them; a notification Id must EQUAL one to be handled:");
foreach (var table in tables)
RadicadeEacBypassPlugin.LogSource.LogInfo($"[Realtime] {table}");
var flat = " " + string.Join(" ", tables) + " ";
RadicadeEacBypassPlugin.LogSource.LogInfo(
$"[Realtime] chat push key check: the client registered {Registered(flat, "\"90\"")} for \"90\" and " +
$"{Registered(flat, "ChatMessageReceived")} for \"ChatMessageReceived\". " +
"Whichever is registered is the string the server's chat notification Id must equal.");
return true;
}
private static string Registered(string flatKeys, string key)
=> flatKeys.Contains(" " + key, StringComparison.Ordinal) ? "a handler" : "NO handler";
private static void CollectStringKeyedTables(Type type, List<string> tables)
{
foreach (var field in type.GetFields(
BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly))
{
if (tables.Count >= MaxTables) return;
Type fieldType;
try { fieldType = field.FieldType; } catch { continue; }
if (!IsStringKeyed(fieldType)) continue;
object value;
try { value = field.GetValue(null); }
catch { continue; }
AddTable(tables, $"{type.Name}.{field.Name}", value);
}
}
private static bool IsStringKeyed(Type type)
{
try
{
return type.IsGenericType && type.GetGenericArguments()[0] == typeof(string);
}
catch
{
return false;
}
}
private static void AddTable(List<string> tables, string label, object value)
{
if (value == null) return;
var keys = new List<string>();
try
{
if (value is System.Collections.IDictionary map)
{
foreach (var key in map.Keys) keys.Add(Quote(key?.ToString()));
}
else if (value is System.Collections.IEnumerable items)
{
foreach (var item in items) keys.Add(KeyOf(item));
}
else return;
}
catch
{
return;
}
if (keys.Count == 0) return;
keys.Sort(StringComparer.Ordinal);
var shown = keys.Count <= MaxKeysPerTable
? string.Join(", ", keys)
: string.Join(", ", keys.Take(MaxKeysPerTable)) + $", … (+{keys.Count - MaxKeysPerTable} more)";
tables.Add($"{label}: {shown}");
}
private static string KeyOf(object item)
{
if (item == null) return "(null)";
try
{
var type = item.GetType();
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(KeyValuePair<,>))
{
var key = type.GetProperty("Key");
if (key != null) return Quote(key.GetValue(item)?.ToString());
}
}
catch { }
return Quote(item.ToString());
}
private static string Quote(string text) => text == null ? "(null)" : $"\"{text}\"";
private static Type FindNotificationDispatcher()
{
var byName = FindType("LMBGGGGNBOB");
if (byName != null && DeclaresNotificationEnum(byName)) return byName;
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
Type[] types;
try { types = assembly.GetTypes(); }
catch (ReflectionTypeLoadException ex) { types = ex.Types.Where(t => t != null).ToArray(); }
catch { continue; }
foreach (var type in types)
{
try
{
if (!type.IsEnum || !IsNotificationEnum(type)) continue;
RadicadeEacBypassPlugin.LogSource.LogInfo(
$"[Realtime] notification enum located: {type.FullName} " +
$"(message = {Convert.ToInt64(Enum.Parse(type, "MessageReceived"), System.Globalization.CultureInfo.InvariantCulture)}, " +
$"kick = {Convert.ToInt64(Enum.Parse(type, "ModerationKick"), System.Globalization.CultureInfo.InvariantCulture)})");
if (type.DeclaringType != null) return type.DeclaringType;
continue;
}
catch { }
}
}
return null;
}
private static bool DeclaresNotificationEnum(Type type)
{
try
{
foreach (var nested in type.GetNestedTypes(BindingFlags.Public | BindingFlags.NonPublic))
if (nested.IsEnum && IsNotificationEnum(nested)) return true;
}
catch { }
return false;
}
private static bool IsNotificationEnum(Type type)
{
var members = Enum.GetNames(type);
return Array.IndexOf(members, "MessageReceived") >= 0
&& Array.IndexOf(members, "ModerationKick") >= 0
&& Array.IndexOf(members, "PresenceHeartbeatResponse") >= 0;
}
internal static System.DateTime? ServerRequestedExitUtc;
private const int ExitRequestWindowSeconds = 120;
internal static bool ServerRequestedExitRecently()
=> ServerRequestedExitUtc.HasValue
&& (System.DateTime.UtcNow - ServerRequestedExitUtc.Value).TotalSeconds < ExitRequestWindowSeconds;
private static void NoteServerRequestedExit(string id)
{
if (id != "20" && id != "6") return;
if (ServerRequestedExitRecently()) return;
ServerRequestedExitUtc = System.DateTime.UtcNow;
RadicadeEacBypassPlugin.LogSource.LogInfo(
$"[Realtime] Id={id} arrived - the server asked this client to leave, so the AppExit guard will let the exit through.");
}
internal static void ApplyExitWatch(Harmony harmony)
{
try
{
var transport = FindType("BestHTTP.SignalRCore.Transports.WebSocketTransport");
var target = transport == null ? null : AccessTools.DeclaredMethod(transport, "OnMessage");
if (target == null)
{
RadicadeEacBypassPlugin.LogSource.LogWarning(
"[Realtime] could not watch for the server's quit/logout frames - a moderator's Quit Game may be suppressed as a room-load exit.");
return;
}
harmony.Patch(target, prefix: new HarmonyMethod(typeof(RealtimeDiagnostics), nameof(ExitWatchPrefix)));
}
catch (Exception ex)
{
RadicadeEacBypassPlugin.LogSource.LogWarning($"[Realtime] exit watch failed to install: {ex.GetType().Name}: {ex.Message}");
}
}
private static void ExitWatchPrefix(string __1)
{
var text = __1;
if (string.IsNullOrEmpty(text)) return;
if (text.IndexOf("\"Notification\"", StringComparison.Ordinal) < 0) return;
if (text.IndexOf("\"Id\":\"20\"", StringComparison.Ordinal) >= 0 || text.IndexOf("\"Id\":20", StringComparison.Ordinal) >= 0)
{
NoteServerRequestedExit("20");
return;
}
if (text.IndexOf("\"Id\":\"6\"", StringComparison.Ordinal) >= 0 || text.IndexOf("\"Id\":6", StringComparison.Ordinal) >= 0)
NoteServerRequestedExit("6");
}
internal static bool Verbose = true;
internal static void Apply(Harmony harmony, bool verbose)
{
Verbose = verbose;
var transport = FindType("BestHTTP.SignalRCore.Transports.WebSocketTransport");
if (transport == null)
{
RadicadeEacBypassPlugin.LogSource.LogWarning("[Realtime] BestHTTP.SignalRCore.Transports.WebSocketTransport not found - notify frame logging stays inactive.");
}
else
{
Hook(harmony, transport, "OnOpen", nameof(OnOpenPrefix), "socket open");
Hook(harmony, transport, "HandleHandshakeResponse", nameof(HandshakePrefix), "SignalR handshake answer");
Hook(harmony, transport, "OnMessage", nameof(OnMessagePrefix), "inbound frames");
Hook(harmony, transport, "OnError", nameof(OnErrorPrefix), "transport errors");
Hook(harmony, transport, "OnClosed", nameof(OnClosedPrefix), "socket close with code/reason");
}
var hub = FindType("BestHTTP.SignalRCore.HubConnection");
if (hub == null)
{
RadicadeEacBypassPlugin.LogSource.LogWarning("[Realtime] BestHTTP.SignalRCore.HubConnection not found - hub-pump guard stays inactive.");
}
else
{
HookFinalizer(harmony, hub, "OnMessages", nameof(OnMessagesFinalizer));
}
RadicadeEacBypassPlugin.LogSource.LogInfo(Verbose
? "[Realtime] notify-hub tap armed: every inbound frame is read and logged."
: "[Realtime] notify-hub tap armed for moderation only - a voice chat restriction is applied from the frame it arrives in; frames are not logged (Diagnostics=false).");
if (Verbose) StartHandlerKeyProbe(harmony);
}
private static Type FindType(string name)
{
var direct = AccessTools.TypeByName(name);
if (direct != null) return direct;
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
try
{
var found = assembly.GetType(name, false);
if (found != null) return found;
}
catch { }
}
return null;
}
private static void Hook(Harmony harmony, Type type, string method, string prefix, string what)
{
try
{
var target = AccessTools.DeclaredMethod(type, method);
if (target == null)
{
RadicadeEacBypassPlugin.LogSource.LogWarning($"[Realtime] {type.Name}.{method} not found - {what} will not be logged.");
return;
}
harmony.Patch(target, prefix: new HarmonyMethod(typeof(RealtimeDiagnostics), prefix));
if (Verbose)
RadicadeEacBypassPlugin.LogSource.LogInfo($"[Realtime] hooked {type.Name}.{method} ({what})");
}
catch (Exception ex)
{
RadicadeEacBypassPlugin.LogSource.LogWarning($"[Realtime] could not hook {type.Name}.{method}: {ex.GetType().Name}: {ex.Message}");
}
}
private static void HookFinalizer(Harmony harmony, Type type, string method, string finalizer)
{
try
{
var target = AccessTools.DeclaredMethod(type, method);
if (target == null)
{
RadicadeEacBypassPlugin.LogSource.LogWarning($"[Realtime] {type.Name}.{method} not found - the hub-pump guard stays inactive.");
return;
}
harmony.Patch(target, finalizer: new HarmonyMethod(typeof(RealtimeDiagnostics), finalizer));
if (Verbose)
RadicadeEacBypassPlugin.LogSource.LogInfo($"[Realtime] guarded {type.Name}.{method} (a frame the client cannot parse is logged, not thrown)");
}
catch (Exception ex)
{
RadicadeEacBypassPlugin.LogSource.LogWarning($"[Realtime] could not guard {type.Name}.{method}: {ex.GetType().Name}: {ex.Message}");
}
}
private static void OnOpenPrefix()
{
if (Verbose)
RadicadeEacBypassPlugin.LogSource.LogInfo("[Realtime] socket open - the notify hub WebSocket upgraded and is ready.");
}
private static void HandshakePrefix(string __0)
{
var text = __0 ?? string.Empty;
if (text.IndexOf("error", StringComparison.OrdinalIgnoreCase) >= 0)
RadicadeEacBypassPlugin.LogSource.LogWarning($"[Realtime] hub handshake answered with an error: {Trim(text, 300)}");
else if (Verbose)
RadicadeEacBypassPlugin.LogSource.LogInfo($"[Realtime] hub handshake accepted: {Trim(text, 120)}");
}
private static void OnMessagePrefix(string __1) => ObserveFrame(__1);
private static void OnErrorPrefix(string __1) =>
RadicadeEacBypassPlugin.LogSource.LogWarning($"[Realtime] transport error: {Trim(__1, 300)}");
private static void OnClosedPrefix(ushort __1, string __2) =>
RadicadeEacBypassPlugin.LogSource.LogWarning($"[Realtime] socket closed code={__1} reason={Trim(__2, 200)}");
private static Exception OnMessagesFinalizer(Exception __exception)
{
if (__exception == null) return null;
lock (Gate) Failures++;
RadicadeEacBypassPlugin.LogSource.LogWarning(
$"[Realtime] the client failed to handle a hub frame ({__exception.GetType().Name}: {__exception.Message}) - " +
$"frame swallowed so the notify socket and the rest of the batch survive (failures={Failures}). " +
"A parse error here means a notification payload the client's own parser rejects; the IdToken/summary lines " +
"above name which notification it was.");
return null;
}
private static void ObserveFrame(string data)
{
if (string.IsNullOrEmpty(data))
{
if (Verbose)
RadicadeEacBypassPlugin.LogSource.LogInfo("[Realtime] empty frame received (keep-alive).");
return;
}
foreach (var record in data.Split('\u001e'))
{
if (record.Length == 0) continue;
if (!Verbose && record.IndexOf(VoiceChatRestrictionPatches.NotificationId, StringComparison.Ordinal) < 0)
continue;
int index;
lock (Gate) index = ++Frames;
DescribeRecord(index, record);
}
}
private static void DescribeRecord(int index, string record)
{
try
{
using var document = JsonDocument.Parse(record);
var root = document.RootElement;
var type = root.TryGetProperty("type", out var typeElement) ? typeElement.GetRawText() : "?";
var target = root.TryGetProperty("target", out var targetElement) ? targetElement.GetString() : null;
if (string.Equals(target, "Notification", StringComparison.Ordinal))
{
DescribeNotification(index, root, record);
return;
}
Count(Targets, target ?? ("type " + type));
if (!Verbose) return;
if (index <= VerboseFrames)
RadicadeEacBypassPlugin.LogSource.LogInfo($"[Realtime] frame #{index} target={target ?? "-"} type={type} len={record.Length}: {Trim(record, PreviewChars)}");
else
Summarize(index);
}
catch (Exception ex)
{
RadicadeEacBypassPlugin.LogSource.LogWarning($"[Realtime] frame #{index} is not readable as a hub record ({ex.GetType().Name}: {ex.Message}): {Trim(record, PreviewChars)}");
}
}
private static void DescribeNotification(int index, JsonElement root, string record)
{
string envelope = null;
if (root.TryGetProperty("arguments", out var arguments)
&& arguments.ValueKind == JsonValueKind.Array
&& arguments.GetArrayLength() > 0)
{
var first = arguments[0];
envelope = first.ValueKind == JsonValueKind.String ? first.GetString() : first.GetRawText();
}
string id = null;
string idToken = "(no Id)";
string msg = "(no Msg)";
if (!string.IsNullOrEmpty(envelope))
{
try
{
using var inner = JsonDocument.Parse(envelope);
var payload = inner.RootElement;
if (TryGetProperty(payload, "Id", out var idElement))
{
idToken = idElement.ValueKind.ToString();
id = idElement.ValueKind == JsonValueKind.String ? idElement.GetString() : idElement.GetRawText();
}
if (TryGetProperty(payload, "Msg", out var msgElement))
msg = Trim(msgElement.GetRawText(), 200);
}
catch (Exception ex)
{
RadicadeEacBypassPlugin.LogSource.LogWarning($"[Realtime] Notification #{index} envelope did not parse at all ({ex.GetType().Name}: {ex.Message}): {Trim(envelope, PreviewChars)}");
envelope = null;
}
}
if (!string.IsNullOrEmpty(id)) NoteServerRequestedExit(id);
if (!string.IsNullOrEmpty(id) && !string.IsNullOrEmpty(envelope))
VoiceChatRestrictionPatches.Observe(id, envelope);
Count(Targets, "Notification");
Count(NotificationIds, id ?? "(no Id)");
bool firstOfItsKind = false;
if (!string.IsNullOrEmpty(id))
lock (Gate) firstOfItsKind = KnownNotifications.Add(id);
if (!Verbose) return;
bool worthFullDetail = string.IsNullOrEmpty(id) || !HighFrequencyIds.Contains(id);
if (worthFullDetail || firstOfItsKind || index <= VerboseFrames)
RadicadeEacBypassPlugin.LogSource.LogInfo($"[Realtime] Notification #{index} Id={id ?? "(none)"} IdToken={idToken} Msg={msg}");
else
Summarize(index);
if (worthFullDetail || firstOfItsKind)
RadicadeEacBypassPlugin.LogSource.LogInfo($"[Realtime] {id ?? "envelope"} envelope: {Trim(envelope ?? record, PreviewChars)}");
}
private static void Summarize(int index)
{
if (index % 25 != 0) return;
string targets;
string ids;
int failures;
lock (Gate)
{
targets = Join(Targets);
ids = Join(NotificationIds);
failures = Failures;
} RadicadeEacBypassPlugin.LogSource.LogInfo($"[Realtime] {index} frames so far - targets: {targets}; notifications: {ids}; handler failures: {failures}");
}
private static bool TryGetProperty(JsonElement element, string name, out JsonElement value)
{
if (element.ValueKind == JsonValueKind.Object)
{
if (element.TryGetProperty(name, out value)) return true;
var lower = char.ToLowerInvariant(name[0]) + name.Substring(1);
if (element.TryGetProperty(lower, out value)) return true;
}
value = default;
return false;
}
private static void Count(Dictionary<string, int> counts, string key)
{
lock (Gate)
{
counts.TryGetValue(key, out var current);
counts[key] = current + 1;
}
}
private static string Join(Dictionary<string, int> counts)
{
var parts = new List<string>();
foreach (var pair in counts) parts.Add($"{pair.Key} x{pair.Value}");
return parts.Count == 0 ? "none" : string.Join(", ", parts);
}
private static string Trim(string text, int limit)
{
if (string.IsNullOrEmpty(text)) return string.Empty;
var flat = text.Replace("\r", " ").Replace("\n", " ");
return flat.Length <= limit ? flat : flat.Substring(0, limit) + "...";
}
}
@@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using BestHTTP;
using HarmonyLib;
internal static class RequestRouteLog
{
private static readonly object Gate = new();
private static readonly HashSet<string> Seen = new();
private const int Limit = 300;
internal static void Apply(Harmony harmony)
{
try
{
var response = AccessTools.TypeByName("BestHTTP.HTTPResponse");
if (response == null)
{
RadicadeEacBypassPlugin.LogSource.LogWarning("[HttpLog] BestHTTP.HTTPResponse not found - REST routes stay unlogged.");
return;
}
System.Reflection.ConstructorInfo constructor = null;
foreach (var candidate in AccessTools.GetDeclaredConstructors(response))
{
var parameters = candidate.GetParameters();
if (parameters.Length == 4
&& parameters[0].ParameterType.Name.Contains("HTTPRequest")
&& parameters[2].ParameterType.Name == "Boolean"
&& parameters[3].ParameterType.Name == "Boolean")
{
constructor = candidate;
break;
}
}
if (constructor == null)
{
RadicadeEacBypassPlugin.LogSource.LogWarning("[HttpLog] HTTPResponse ctor not found - REST routes stay unlogged.");
return;
}
harmony.Patch(constructor, postfix: new HarmonyMethod(typeof(RequestRouteLog), nameof(ResponsePostfix)));
RadicadeEacBypassPlugin.LogSource.LogInfo("[HttpLog] hooked HTTPResponse(HTTPRequest, Stream) - every REST route and status is logged once");
}
catch (Exception ex)
{
RadicadeEacBypassPlugin.LogSource.LogWarning($"[HttpLog] could not hook HTTPResponse: {ex.GetType().Name}: {ex.Message}");
}
}
private static void ResponsePostfix(HTTPResponse __instance, HTTPRequest __0)
{
try
{
var uri = __0?.Uri?.ToString();
if (string.IsNullOrEmpty(uri)) return;
if (!HubSocketAuth.IsServer(uri)) return;
var path = new Uri(uri).AbsolutePath;
if (NameserverRedirect.IsHubUrl(uri)) return;
if (path.Contains("/images/", StringComparison.OrdinalIgnoreCase)) return;
if (path.Contains("/cdn/", StringComparison.OrdinalIgnoreCase)) return;
int status;
try { status = __instance.StatusCode; }
catch { return; }
var method = "REQ";
try { method = __0.MethodType.ToString().ToUpperInvariant(); }
catch { }
var line = $"{status} {method} {path}";
lock (Gate)
{
if (Seen.Count >= Limit || !Seen.Add(line)) return;
}
RadicadeEacBypassPlugin.LogSource.LogInfo($"[HttpLog] {line}");
}
catch
{
}
}
}
@@ -0,0 +1,164 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
/// <summary>
/// Pure string surgery for two responses the client cannot cope with as the
/// server sends them. No engine or BestHTTP types live here so the rules can be
/// exercised outside the game.
/// </summary>
internal static class ResponseRepair
{
/// <summary>Loopback probe target: any transport fails (or answers) instantly, so a probe phase can never stall.</summary>
internal const string LoopbackProbeAddress = "127.0.0.1:50000";
private const string QosPathSuffix = "/matchmaking/player/qos";
private const string UserSegmentPathSuffix = "/data/commonquery/usersegment";
private const string AddressKey = "\"address\"";
/// <summary>
/// BestHTTP's chunked reader throws exactly this when the chunk-size line is
/// empty, which is what a keep-alive connection the server had already closed
/// looks like. It is not a bad request, so it must not fail one.
/// </summary>
internal static bool IsTruncatedChunkFailure(string message)
{
if (string.IsNullOrEmpty(message)) return false;
return message.IndexOf("as a hex number", StringComparison.OrdinalIgnoreCase) >= 0;
}
/// <summary>
/// Rewrites a response body when the client would otherwise mis-handle it.
/// Returns null when the body is left alone; <paramref name="note"/> explains
/// what changed when it did.
/// </summary>
internal static string RepairBody(string path, string query, string body, out string note)
{
note = null;
if (string.IsNullOrEmpty(path) || string.IsNullOrEmpty(body)) return null;
if (EndsWith(path, QosPathSuffix)) return RepairProbeList(body, ref note);
if (EndsWith(path, UserSegmentPathSuffix)) return RepairUserSegment(query, body, ref note);
return null;
}
/// <summary>
/// The probe list points at retired GCP addresses: five of its six entries are
/// unreachable, so the client's region-probe phase waits out a ping timeout on
/// each boot and login sits there. Pinning every entry at loopback keeps the
/// list's shape but makes the probes answer (or refuse) immediately.
/// </summary>
private static string RepairProbeList(string body, ref string note)
{
var repaired = new StringBuilder(body.Length);
var index = 0;
var replaced = 0;
while (true)
{
var key = body.IndexOf(AddressKey, index, StringComparison.Ordinal);
if (key < 0)
{
repaired.Append(body, index, body.Length - index);
break;
}
repaired.Append(body, index, key - index + AddressKey.Length);
var cursor = key + AddressKey.Length;
while (cursor < body.Length && char.IsWhiteSpace(body[cursor])) cursor++;
if (cursor >= body.Length || body[cursor] != ':')
{
repaired.Append(body, key + AddressKey.Length, cursor - key - AddressKey.Length);
index = cursor;
continue;
}
repaired.Append(body, key + AddressKey.Length, cursor - key - AddressKey.Length);
repaired.Append(':');
cursor++;
while (cursor < body.Length && char.IsWhiteSpace(body[cursor])) cursor++;
if (cursor >= body.Length || body[cursor] != '"')
{
index = cursor;
continue;
}
var close = body.IndexOf('"', cursor + 1);
if (close < 0)
{
repaired.Append(body, cursor, body.Length - cursor);
break;
}
repaired.Append('"').Append(LoopbackProbeAddress).Append('"');
replaced++;
index = close + 1;
}
if (replaced == 0) return null;
note = replaced.ToString(CultureInfo.InvariantCulture) + " region probe address(es) pinned to "
+ LoopbackProbeAddress + " (the server's list points at retired hosts)";
return repaired.ToString();
}
/// <summary>
/// The client's model for this route is a name -> row map
/// (Dictionary&lt;string, ...&gt;), so the flat member object the server answers
/// with makes its reader demand '{' where a string sits and throw
/// "expected:'{', actual:'\"none\"'" (a crash report per login). Wrapping the
/// flat object under the row key(s) the request named gives it the map it wants.
/// </summary>
private static string RepairUserSegment(string query, string body, ref string note)
{
var trimmed = body.TrimStart();
if (trimmed.Length == 0 || trimmed[0] != '{') return null;
var keys = new List<string>();
foreach (var value in QueryValues(query))
{
AddKey(keys, value);
AddKey(keys, "user_segment:" + value);
}
if (keys.Count == 0) AddKey(keys, "user_segment");
var repaired = new StringBuilder(body.Length * (keys.Count + 1) + 16);
repaired.Append('{');
for (var i = 0; i < keys.Count; i++)
{
if (i > 0) repaired.Append(',');
repaired.Append('"').Append(keys[i]).Append("\":").Append(body);
}
repaired.Append('}');
note = "userSegment answered as a name -> row map (" + string.Join(", ", keys) + ")";
return repaired.ToString();
}
private static void AddKey(List<string> keys, string key)
{
if (string.IsNullOrEmpty(key)) return;
foreach (var existing in keys)
if (existing == key) return;
keys.Add(key);
}
internal static IEnumerable<string> QueryValues(string query)
{
if (string.IsNullOrEmpty(query)) yield break;
foreach (var pair in query.TrimStart('?').Split('&'))
{
if (pair.Length == 0) continue;
var separator = pair.IndexOf('=');
var value = separator < 0 ? pair : pair.Substring(separator + 1);
value = Uri.UnescapeDataString(value).Trim();
if (value.Length == 0) continue;
yield return value;
}
}
private static bool EndsWith(string value, string suffix) =>
value.EndsWith(suffix, StringComparison.OrdinalIgnoreCase);
}
@@ -0,0 +1,32 @@
using System;
using HarmonyLib;
internal static class WebSocketRedirectPatches
{
internal static void Apply(Harmony harmony)
{
var type = AccessTools.TypeByName("BestHTTP.WebSocket.WebSocket");
int count = 0;
if (type != null)
foreach (var constructor in AccessTools.GetDeclaredConstructors(type))
{
var parameters = constructor.GetParameters();
if (parameters.Length == 0 || parameters[0].ParameterType != typeof(Il2CppSystem.Uri)) continue;
harmony.Patch(constructor, prefix: new HarmonyMethod(typeof(WebSocketRedirectPatches), nameof(Prefix)));
count++;
}
RadicadeEacBypassPlugin.LogSource.LogInfo("[Realtime] WebSocket URI hooks installed: " + count);
}
private static void Prefix(ref Il2CppSystem.Uri __0)
{
if (__0 == null) return;
var url = NameserverRedirect.RewriteWebSocket(__0.ToString(), RadicadeEacBypassPlugin.ServerUrl.Value);
if (HubSocketAuth.IsServer(url) && NameserverRedirect.IsHubUrl(url))
{
url = NameserverRedirect.EnsureHubToken(url, HubSocketAuth.Token);
Diag.Trace("[Realtime] Hub socket constructed; token=" +
(NameserverRedirect.HasHubToken(url) ? "present" : "missing"));
}
__0 = new Il2CppSystem.Uri(url);
}
}
@@ -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;
}
}
+47
View File
@@ -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();
}
}
+191
View File
@@ -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;
}
}
}
+205
View File
@@ -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}");
}
}
}
+100
View File
@@ -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 + ").");
}
}
}
+42
View File
@@ -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;
}
}
+1038
View File
@@ -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 { }
}
}
+56
View File
@@ -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>