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
+2
View File
@@ -0,0 +1,2 @@
/bin/
/obj/
+14
View File
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<BepInExCore Condition="'$(BepInExCore)' == ''">C:\RecRoom\2023-08-29T06-53-33Z\BepInEx\core</BepInExCore>
</PropertyGroup>
<ItemGroup>
<Reference Include="Iced"><HintPath>$(BepInExCore)\Iced.dll</HintPath></Reference>
<Reference Include="Mono.Cecil">
<HintPath>$(BepInExCore)\Mono.Cecil.dll</HintPath>
</Reference>
</ItemGroup>
</Project>
+134
View File
@@ -0,0 +1,134 @@
using System.Reflection;
using System.Reflection.PortableExecutable;
using Iced.Intel;
// Find the il2cpp methods that reference a given string literal.
//
// Neither the bytes nor the index of a literal appear directly in the code: on
// metadata v27+ the code loads a pointer from a metadata-usage slot, and statically
// that slot holds the encoded usage token `(type << 29) | (index << 1) | 1` with
// type 5 for string literals. So the search is:
// 1. resolve the literal's index from its text,
// 2. scan the image for the 8-byte token (this finds the slots),
// 3. scan the executable sections for instructions whose rip-relative operand
// lands on one of those slots. The displacement is the tail of such an
// instruction, so a cheap arithmetic pre-check narrows candidates and Iced
// then confirms by decoding from the candidate's start.
internal static class LiteralFinder
{
const ulong TokenBase = 5UL << 29;
static object Prop(object o, string n) => o.GetType().GetProperty(n)!.GetValue(o)!;
sealed class LineOutput : FormatterOutput
{
public string Value = "";
public override void Write(string text, FormatterTextKind kind) => Value += text;
}
public static void Run(object main, object metadata, string binaryPath, string needle)
{
var defs = (System.Collections.IEnumerable)metadata.GetType().GetField("methodDefs")!.GetValue(metadata)!;
var methods = new List<(object Method, ulong Address)>();
foreach (var m in defs.Cast<object>())
{
var address = (ulong)Prop(m, "MethodPointer");
if (address != 0) methods.Add((m, address));
}
methods.Sort((a, b) => a.Address.CompareTo(b.Address));
var table = metadata.GetType().GetField("stringLiterals",
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)!.GetValue(metadata) as Array;
var getLiteralText = metadata.GetType().GetMethod("GetStringLiteralFromIndex")!;
var tokens = new List<ulong>();
for (int i = 0; i < (table?.Length ?? 0); i++)
{
var text = (string)getLiteralText.Invoke(metadata, new object[] { (uint)i })!;
if (!text.Contains(needle, StringComparison.Ordinal)) continue;
// Both low-bit spellings resolve to the same index downstream.
tokens.Add(TokenBase | ((ulong)i << 1) | 1);
tokens.Add(TokenBase | ((ulong)i << 1));
Console.WriteLine($" literal #{i} = \"{text}\" token 0x{TokenBase | ((ulong)i << 1):X}");
}
if (tokens.Count == 0)
{
Console.WriteLine($"no literal contains \"{needle}\"");
return;
}
var bytes = File.ReadAllBytes(binaryPath);
using var pe = new PEReader(new MemoryStream(bytes));
var imageBase = pe.PEHeaders.PEHeader!.ImageBase;
// 1. slots: 8-byte tokens, so only probe 8-byte aligned positions.
var slots = new List<(string Section, ulong Address, ulong Value)>();
foreach (var section in pe.PEHeaders.SectionHeaders)
{
var size = Math.Min(section.SizeOfRawData, Math.Max(0, bytes.Length - section.PointerToRawData));
for (int i = section.PointerToRawData; i < section.PointerToRawData + size - 8; i += 8)
{
var value = BitConverter.ToUInt64(bytes, i);
if (!tokens.Contains(value)) continue;
var slot = imageBase + (ulong)section.VirtualAddress + (ulong)(i - section.PointerToRawData);
slots.Add((section.Name, slot, value));
Console.WriteLine($" slot 0x{slot:X} ({section.Name}, raw 0x{i:X}) = 0x{value:X}");
}
}
if (slots.Count == 0)
{
Console.WriteLine("no metadata-usage slot found");
return;
}
var slotSet = slots.Select(s => s.Address).ToHashSet();
// 2. references: pre-check the displacement, confirm by decoding from the start.
var formatter = new IntelFormatter();
int hits = 0, candidates = 0;
foreach (var section in pe.PEHeaders.SectionHeaders.Where(s => (s.SectionCharacteristics & SectionCharacteristics.MemExecute) != 0))
{
var rawToVa = (long)imageBase + section.VirtualAddress - section.PointerToRawData;
var size = Math.Min(section.SizeOfRawData, Math.Max(0, bytes.Length - section.PointerToRawData));
for (int i = section.PointerToRawData; i < section.PointerToRawData + size - 4; i++)
{
var disp = BitConverter.ToInt32(bytes, i);
var target = (ulong)((long)i + 4 + rawToVa + disp);
if (!slotSet.Contains(target)) continue;
candidates++;
for (int back = 0; back <= 7; back++)
{
var candidateRaw = i - back;
if (candidateRaw < section.PointerToRawData) break;
var reader = new ByteArrayCodeReader(bytes, candidateRaw, Math.Min(16, bytes.Length - candidateRaw));
var decoder = Decoder.Create(64, reader);
decoder.IP = (ulong)((long)candidateRaw + rawToVa);
decoder.Decode(out var instruction);
if (instruction.IsInvalid || !instruction.IsIPRelativeMemoryOperand) continue;
if (instruction.IPRelativeMemoryAddress != target) continue;
hits++;
var method = Nearest(methods, instruction.IP);
var name = method == null
? "<none>"
: Prop(Prop(method, "DeclaringType"), "Name") + "::" + Prop(method, "Name");
var offset = method == null ? 0 : instruction.IP - (ulong)Prop(method, "MethodPointer");
var output = new LineOutput();
formatter.Format(instruction, output);
Console.WriteLine($" REF 0x{instruction.IP:X} -> slot 0x{target:X} in {name} +0x{offset:X} [{output.Value}]");
break;
}
}
}
Console.WriteLine($"slots {slots.Count}; displacement candidates {candidates}; confirmed references {hits}");
}
static object? Nearest(List<(object Method, ulong Address)> methods, ulong address)
{
object? best = null;
foreach (var (method, addr) in methods)
{
if (addr > address) break;
best = method;
}
return best;
}
}
+69
View File
@@ -0,0 +1,69 @@
using System.Reflection;
using System.Collections;
using System.Reflection.PortableExecutable;
using Iced.Intel;
internal static class NativeInspect
{
static object Prop(object o, string n) => o.GetType().GetProperty(n)!.GetValue(o)!;
static string Label(object m) => m == null ? "<native>" : Prop(Prop(m,"DeclaringType"),"Name") + "::" + Prop(m,"Name");
public static void Run(Type main, object metadata, string binary, string typeName, string methodName)
{
var methods = ((IEnumerable)metadata.GetType().GetField("methodDefs")!.GetValue(metadata)!).Cast<object>()
.Select(m => (Method:m, Address:(ulong)Prop(m,"MethodPointer"))).Where(m=>m.Address!=0).OrderBy(m=>m.Address).ToArray();
// An address target (decimal or 0x hex) disassembles that instruction window directly.
var raw = typeName.Replace("0x", "").Trim();
if (raw.Length > 4 &&
(ulong.TryParse(raw, out var fixedAddress) ||
ulong.TryParse(raw, System.Globalization.NumberStyles.HexNumber, null, out fixedAddress)))
{
DisassembleOne(main, metadata, binary, fixedAddress, methods);
return;
}
var bytes=File.ReadAllBytes(binary);
using var pe=new PEReader(new MemoryStream(bytes));
ulong imageBase=pe.PEHeaders.PEHeader!.ImageBase;
int Raw(ulong address) { var rva=address-imageBase; var s=pe.PEHeaders.SectionHeaders.First(s=>rva>=(ulong)s.VirtualAddress && rva<(ulong)(s.VirtualAddress+s.SizeOfRawData)); return (int)(rva-(ulong)s.VirtualAddress)+s.PointerToRawData; }
object Nearest(ulong address) => methods.LastOrDefault(m=>m.Address<=address).Method;
foreach(var item in methods.Where(m=>Prop(Prop(m.Method,"DeclaringType"),"Name").ToString()==typeName && (methodName=="" || Prop(m.Method,"Name").ToString()==methodName)))
{
Console.WriteLine($"\nMETHOD {Label(item.Method)} 0x{item.Address:X}");
Emit(main, pe, imageBase, bytes, Raw, Nearest, methods, item.Address);
}
}
static void DisassembleOne(Type main, object metadata, string binary, ulong address, (object Method, ulong Address)[] methods)
{
var bytes=File.ReadAllBytes(binary);
using var pe=new PEReader(new MemoryStream(bytes));
ulong imageBase=pe.PEHeaders.PEHeader!.ImageBase;
int Raw(ulong address) { var rva=address-imageBase; var s=pe.PEHeaders.SectionHeaders.First(s=>rva>=(ulong)s.VirtualAddress && rva<(ulong)(s.VirtualAddress+s.SizeOfRawData)); return (int)(rva-(ulong)s.VirtualAddress)+s.PointerToRawData; }
object Nearest(ulong address) => methods.LastOrDefault(m=>m.Address<=address).Method;
Console.WriteLine($"\nMETHOD {Label(Nearest(address))}+0x{address - (Nearest(address)!=null ? (ulong)Prop(Nearest(address),"MethodPointer") : 0):X} 0x{address:X}");
Emit(main, pe, imageBase, bytes, Raw, Nearest, methods, address);
}
static void Emit(Type main, PEReader pe, ulong imageBase, byte[] bytes, Func<ulong,int> Raw, Func<ulong,object> Nearest,
(object Method, ulong Address)[] methods, ulong start)
{
var item = (Method: Nearest(start), Address: start);
foreach(var section in pe.PEHeaders.SectionHeaders.Where(s=>(s.SectionCharacteristics & SectionCharacteristics.MemExecute)!=0))
for(int i=section.PointerToRawData;i<section.PointerToRawData+section.SizeOfRawData-5;i++)
if(bytes[i]==0xE8 && (ulong)((long)imageBase+section.VirtualAddress+i-section.PointerToRawData+5+BitConverter.ToInt32(bytes,i+1))==item.Address)
{ ulong addr=imageBase+(ulong)(section.VirtualAddress+i-section.PointerToRawData); Console.WriteLine($"CALLER 0x{addr:X} {Label(Nearest(addr))}"); }
var decoder=Decoder.Create(64,new ByteArrayCodeReader(bytes,Raw(item.Address),Math.Min(10000,bytes.Length-Raw(item.Address)))); decoder.IP=item.Address;
var formatter=new IntelFormatter(); var output=new LineOutput();
var next = methods.FirstOrDefault(m => m.Address > item.Address).Address;
for(int i=0;i<1500 && (next == 0 || decoder.IP < next);i++)
{
decoder.Decode(out var instruction); output.Value=""; formatter.Format(instruction,output);
string note="";
if(instruction.FlowControl==FlowControl.Call && instruction.Op0Kind==OpKind.NearBranch64)
{ var called=Nearest(instruction.NearBranchTarget); if(called!=null && (ulong)Prop(called,"MethodPointer")==instruction.NearBranchTarget) note=" ; "+Label(called); }
if(instruction.IsIPRelativeMemoryOperand)
{ try { var literal=main.GetMethod("GetLiteralByAddress")!.Invoke(null,new object[]{instruction.IPRelativeMemoryAddress}); if(literal!=null) note+=" ; literal: "+literal; } catch {} }
Console.WriteLine($"{instruction.IP:X}: {output.Value}{note}");
}
}
sealed class LineOutput:FormatterOutput { public string Value=""; public override void Write(string text,FormatterTextKind kind)=>Value+=text; }
}
+64
View File
@@ -0,0 +1,64 @@
using System.Reflection;
using System.Runtime.Loader;
using Mono.Cecil;
using Mono.Cecil.Cil;
if (args.Length == 3 && args[0] == "--patch-loader")
{
if (Path.GetFullPath(args[1]).Equals(Path.GetFullPath(args[2]), StringComparison.OrdinalIgnoreCase))
throw new ArgumentException("Write a separate loader copy; do not overwrite the source.");
using var assembly = AssemblyDefinition.ReadAssembly(args[1]);
int changes = 0;
foreach (var type in assembly.MainModule.Types)
foreach (var method in type.Methods.Where(m => m.HasBody))
foreach (var instruction in method.Body.Instructions)
if (type.FullName == "BepInEx.Unity.IL2CPP.Il2CppInteropManager" &&
instruction.OpCode == OpCodes.Ldstr && (string)instruction.Operand == "global-metadata.dat")
{
instruction.Operand = "global-metadata.interop.dat";
Console.WriteLine($"Redirected metadata input in {method.Name}");
changes++;
}
if (changes != 1) throw new InvalidOperationException($"Expected one loader metadata reference, found {changes}.");
assembly.Write(args[2]);
Console.WriteLine($"Created loader copy with {changes} metadata path reference(s) changed.");
return;
}
if (args.Length is < 2 or > 5)
{
Console.Error.WriteLine("Usage: AugustInterop <BepInEx-core> <normalized-metadata> [GameAssembly.dll]");
Console.Error.WriteLine(" or: AugustInterop --patch-loader <original-loader.dll> <output-loader.dll>");
Environment.ExitCode = 1;
return;
}
var core = args[0];
AssemblyLoadContext.Default.Resolving += (_, name) =>
{
var path = Path.Combine(core, name.Name + ".dll");
return File.Exists(path) ? AssemblyLoadContext.Default.LoadFromAssemblyPath(path) : null;
};
var library = AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.Combine(core, "LibCpp2IL.dll"));
if (args.Length > 1)
{
var primitives = AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.Combine(core, "AssetRipper.Primitives.dll"));
var versionType = primitives.GetType("AssetRipper.Primitives.UnityVersion")!;
var version = versionType.GetMethod("Parse", new[] { typeof(string) })!.Invoke(null, new object[] { "2020.3.33f1" });
var type = library.GetType("LibCpp2IL.Metadata.Il2CppMetadata")!;
var metadata = type.GetMethod("ReadFrom")!.Invoke(null, new object[] { File.ReadAllBytes(args[1]), version });
Console.WriteLine("Metadata parsed successfully.");
foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.Public))
if (field.GetValue(metadata) is Array array) Console.WriteLine($"{field.Name}: {array.Length}");
if (args.Length > 2)
{
var registry = library.GetTypes().Single(t => t.Name == "LibCpp2IlBinaryRegistry");
registry.GetMethod("RegisterBuiltInBinarySupport")!.Invoke(null, null);
var main = library.GetType("LibCpp2IL.LibCpp2IlMain")!;
var result = main.GetMethod("LoadFromFile")!.Invoke(null, new[] { (object)args[2], Path.GetFullPath(args[1]), version });
Console.WriteLine($"Binary + metadata initialization: {result}");
if (args.Length > 3 && args[3] == "--literal-find" && args.Length > 4)
LiteralFinder.Run(main, metadata, args[2], args[4]);
else if (args.Length > 3) NativeInspect.Run(main, metadata, args[2], args[3], args.Length > 4 ? args[4] : "");
}
}
+47
View File
@@ -0,0 +1,47 @@
# August 2023 interop repair
For the exact Rec Room 20230825 metadata header fingerprint and the installed
BepInEx 6.0.0-be.697 loader. This fixes interop generation, not login or gameplay.
It does not change GameAssembly.dll or the metadata used by the native game.
Requirements: Node.js, .NET 9 SDK, and the August build's BepInEx core libraries.
Run from the repository root, with the game closed during installation.
```powershell
$gameRoot = 'C:\RecRoom\2023-08-29T06-53-33Z'
$core = Join-Path $gameRoot 'BepInEx\core'
$metadataDir = Join-Path $gameRoot 'RecRoom_Data\il2cpp_data\Metadata'
node tools/AugustInterop/normalize.cjs "$metadataDir\global-metadata.dat" artifacts/august-normalized-metadata.dat
dotnet run --project tools/AugustInterop -- "$core" artifacts/august-normalized-metadata.dat "$gameRoot\GameAssembly.dll"
```
The normalizer checks the exact shuffled header, table bounds, image/type counts,
and every repaired module row. All other records remain unchanged. The C# tool
uses the installed LibCpp2IL to verify metadata and native registration parsing.
To reproduce the loader modification, use the original loader backup as input:
```powershell
dotnet run --project tools/AugustInterop -- --patch-loader "$core\BepInEx.Unity.IL2CPP.dll.before-august-interop" artifacts/BepInEx.Unity.IL2CPP.dll
```
For a different installation path, pass `-p:BepInExCore=<path-to-core>` when
building. The patch tool requires a separate output file and exactly one matching
metadata filename instruction. Preserve the original loader before installation.
Installation performed locally:
- Normalized copy: `RecRoom_Data/il2cpp_data/Metadata/global-metadata.interop.dat`.
- Modified loader: `BepInEx/core/BepInEx.Unity.IL2CPP.dll`.
- Original loader: same path plus `.before-august-interop`.
- Native `global-metadata.dat`: unchanged by this interop repair.
Launch `RecRoom.exe +forcemode:screen`. Successful generation ends with
`Il2CppInteropGen ... Done!` and `Chainloader startup complete`, and writes
`BepInEx/interop/assembly-hash.txt`. Keep `UpdateInteropAssemblies = true`.
Do not copy March-generated interop assemblies into this build.
Rollback: close the game and restore the original loader from its backup. The
separate normalized file can remain on disk; the original loader never reads it.
The old loader will again reject this release's custom metadata if regeneration
is needed. If the game build changes, this exact mapping must be revalidated.
+63
View File
@@ -0,0 +1,63 @@
// Diagnostic copy only. Never overwrite the metadata consumed by the game.
const fs = require('node:fs');
const crypto = require('node:crypto');
const [source, destination] = process.argv.slice(2);
if (!source || !destination || require('node:path').resolve(source) === require('node:path').resolve(destination))
throw new Error('Specify separate source and diagnostic output files.');
const input = fs.readFileSync(source);
const header = Buffer.from(input.subarray(0, 256));
header.fill(0, 0xc0, 0xc4);
header.fill(0, 0xc8, 0xcc);
if (crypto.createHash('sha256').update(header).digest('hex') !== '2af65a04fe43613b66fafbe269264a6ec2b4c834304db272f0df68e7503b59e2')
throw new Error('Not the verified 20230825 header.');
// Standard v27 section order. Each pair is the offset/size FIELD POSITION
// in the shuffled header, not a hardcoded section address.
const fields = [
[0x2c,0xbc], [0xc8,0xc0], [0xe8,0xc4], [0x20,0xa8],
[0x24,0x68], [0x44,0xb0], [0x64,0xec], [0xf0,0x04],
[0x38,0x3c], [0x28,0x80], [0xa0,0x50], [0xf8,0xd4],
[0xe0,0x5c], [0x08,0x00], [0x18,0x58], [0x4c,0xb4],
[0x30,0x9c], [0x34,0x98], [0x6c,0x8c], [0x14,0x48],
[0xd8,0xcc], [0x0c,0xb8], [0x88,0x70], [0x7c,0xd0],
[0xa4,0x90], [0x78,0x94], [0xe4,0xf4], [0x54,0x40],
[0xfc,0x10], [0x74,0x84], [0xac,0xdc]
];
const output = Buffer.from(input);
output.writeUInt32LE(0xfab11baf, 0);
output.writeUInt32LE(27, 4);
for (let i=0; i<fields.length; i++) {
const [a,b] = fields[i];
const offset=input.readUInt32LE(a), size=input.readUInt32LE(b);
if (offset < 256 || offset + size > input.length) throw new Error(`Section ${i} out of bounds`);
output.writeUInt32LE(offset, 8+i*8);
output.writeUInt32LE(size, 12+i*8);
}
if (!input.subarray(256).equals(output.subarray(256))) throw new Error('Payload changed');
// This release also places invalid generic-container indices in each image's
// synthetic <Module> type. Those types cannot have generic parameters. Repair
// only verified module rows in the tooling copy; game metadata is untouched.
const types = output.readUInt32LE(8 + 19*8);
const typeCount = output.readUInt32LE(12 + 19*8) / 88;
const images = output.readUInt32LE(8 + 20*8);
const imageCount = output.readUInt32LE(12 + 20*8) / 40;
const strings = output.readUInt32LE(24);
if (imageCount !== 305 || typeCount !== 42537 || output.subarray(strings+42, strings+51).toString() !== '<Module>\0')
throw new Error('Unexpected August type/image structure');
const modules = new Set();
for (let i=0; i<imageCount; i++) {
const index = output.readInt32LE(images + i*40 + 8);
const p = types + index*88;
if (index < 0 || index >= typeCount || modules.has(index) ||
output.readUInt32LE(p+84) !== 0x02000001 || output.readInt32LE(p) !== 0 ||
output.readInt32LE(p+24) < 6673)
throw new Error(`Unexpected module row for image ${i}`);
modules.add(index);
output.writeInt32LE(42, p);
output.writeInt32LE(-1, p+24);
}
for (let i=0; i<typeCount; i++) {
const generic = output.readInt32LE(types+i*88+24);
if (generic < -1 || generic >= 6673) throw new Error(`Invalid generic container on type ${i}`);
}
fs.writeFileSync(destination, output);
console.log(`Wrote tooling copy: ${destination}; normalized header and ${modules.size} synthetic module rows.`);