Files
Radicade/2023Patch/RadicadePatch/Plugin.cs
T
2026-10-16 23:33:13 -07:00

1039 lines
38 KiB
C#

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;
}
}