Files
2026-10-16 23:33:13 -07:00

226 lines
8.3 KiB
C#

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