mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-26 23:49:02 -07:00
Radicade Public Code
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
/bin/
|
||||
/obj/
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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] : "");
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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.`);
|
||||
Reference in New Issue
Block a user