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("# rvaimageNamespace.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)); } }