Files
Tiago Valente 814e5bdfc5 CheatSpecter 0.4.0
- Find: text search, grouped search, undo, went up/down by, where to search, before/after compare
- Debug tab: breakpoints, stepping, registers, stack, threads, what code touches, structure compare, pointer re-check
- Engine tab: Unreal, Unity, Godot, GameMaker and C++ class names; hidden settings and switches
- Code cheats with labels, variables, references and shared code; rule builder; script commands
- Cheats kept on by the PS5 without the browser
- Learn section with twelve lessons and an in-page practice game
- Section (master code) community cheats; SHN and MC4 export
- Groups, dropdowns, hotkeys, first-cheat guide, re-find after update, restore everything
- MCP server: 25 tools
- Quick start, wiki, contributor and security guides, changelog
2026-10-10 12:06:50 -07:00

703 lines
32 KiB
JavaScript

// A fake Unity IL2CPP game, laid out in plain buffers, for testing the Unity
// engine helper without a console.
//
// It builds what the real runtime keeps in memory: a copy of
// global-metadata.dat, class descriptions with their field and method
// records, type descriptors, the table of classes, and some objects. The
// member offsets come from a layout description, so the same game can be
// built the way different Unity versions would lay it out.
//
// The layouts follow Unity's libil2cpp headers as documented by Il2CppDumper
// and Il2CppInspector. "future" is not a real version: it moves things around
// (an extra pointer at the front of a class, a different field record) to
// prove that nothing in the helper depends on one known layout.
import { createGameContext } from '../src/lib/locations.ts';
const ABSTRACT = 0x80;
const SEALED = 0x100;
const KIND = { void: 0x01, bool: 0x02, char: 0x03, sbyte: 0x04, byte: 0x05, short: 0x06, ushort: 0x07, int: 0x08, uint: 0x09, long: 0x0a, ulong: 0x0b, float: 0x0c, double: 0x0d, string: 0x0e, IntPtr: 0x18, object: 0x1c };
/** Which metadata type a built-in keyword is. */
const KEYWORD_CLASS = { int: 'System.Int32', string: 'System.String', object: 'System.Object' };
function classLayout({ lead = 0, userData = true, stackSlot = false }) {
let o = 0;
const L = {};
const p = (name, size = 8) => {
const a = Math.min(size, 8);
o = Math.ceil(o / a) * a;
L[name] = o;
o += size;
};
p('image');
p('gcDesc');
for (let i = 0; i < lead; i++) p('lead' + i);
for (const n of ['name', 'ns']) p(n);
p('byval', 16);
p('thisArg', 16);
for (const n of ['elem', 'cast', 'decl', 'parent', 'generic', 'typeDef', 'interop', 'self', 'fields', 'events', 'props', 'methods', 'nested', 'ifaces', 'ifoffs', 'statics', 'rgctx', 'hier']) p(n);
if (userData) p('userData');
for (const n of ['gch', 'cctorStarted', 'cctorFinished']) p(n, 4);
p('cctorThread');
p('genericContainer');
p('instanceSize', 4);
if (stackSlot) p('stackSlot', 4);
for (const n of ['actualSize', 'elementSize', 'nativeSize', 'staticSize', 'tstaticSize', 'tstaticOff', 'flags', 'token']) p(n, 4);
for (const n of ['methodCount', 'propCount', 'fieldCount', 'eventCount', 'nestedCount', 'vtableCount', 'ifaceCount', 'ifoffCount']) p(n, 2);
p('hierDepth', 1);
L.size = Math.ceil((o + 8) / 8) * 8 + 0x20;
return L;
}
const METHOD_OLD = { ptr: 0, invoker: 0x08, name: 0x10, klass: 0x18, ret: 0x20, params: 0x28, token: 0x40, flags: 0x44, slot: 0x48, pcount: 0x4a, size: 0x50 };
const METHOD_NEW = { ptr: 0, virt: 0x08, invoker: 0x10, name: 0x18, klass: 0x20, ret: 0x28, params: 0x30, meta: 0x38, token: 0x48, flags: 0x4c, slot: 0x50, pcount: 0x52, size: 0x58 };
export const LAYOUTS = {
// Unity 2019-era: types are numbered, parameters are records with names.
v24: { version: 24, typeDefSize: 92, methodDefSize: 32, fieldDefSize: 12, handles: false, newTypeBits: false, cls: classLayout({ userData: false }), method: METHOD_OLD, params: 'info', field: { name: 0, type: 8, parent: 0x10, offset: 0x18, stride: 0x20 } },
// Unity 2021-era: types point into the metadata, parameters are bare types.
v29: { version: 29, typeDefSize: 88, methodDefSize: 32, fieldDefSize: 12, handles: true, newTypeBits: true, cls: classLayout({}), method: METHOD_NEW, params: 'types', field: { name: 0, type: 8, parent: 0x10, offset: 0x18, stride: 0x20 } },
// Not a real version; see the note at the top.
future: { version: 31, typeDefSize: 88, methodDefSize: 36, fieldDefSize: 12, handles: true, newTypeBits: true, cls: classLayout({ lead: 1, stackSlot: true }), method: METHOD_NEW, params: 'types', field: { parent: 0, name: 8, type: 0x10, offset: 0x18, stride: 0x28 } },
};
// ---- global-metadata.dat ----------------------------------------------------------
/**
* Builds a global-metadata.dat. `types` is [{ ns, name, flags, fields: [name], methods: [{ name, flags, params }] }].
* Returns the bytes plus where things ended up.
*/
export function buildMetadata({ version, typeDefSize, methodDefSize, fieldDefSize, types, extraStrings = [] }) {
const strings = [0];
const stringAt = new Map([['', 0]]);
const str = (s) => {
if (!stringAt.has(s)) {
stringAt.set(s, strings.length);
strings.push(...Buffer.from(s, 'utf8'), 0);
}
return stringAt.get(s);
};
extraStrings.forEach(str);
const fieldCount = types.reduce((n, t) => n + (t.fields?.length ?? 0), 0);
const methodCount = types.reduce((n, t) => n + (t.methods?.length ?? 0), 0);
const typeTable = Buffer.alloc(types.length * typeDefSize);
const methodTable = Buffer.alloc(methodCount * methodDefSize);
const fieldTable = Buffer.alloc(fieldCount * fieldDefSize);
let fi = 0;
let mi = 0;
types.forEach((t, i) => {
const at = i * typeDefSize;
typeTable.fill(0xff, at + 8, at + typeDefSize - 60); // the indexes in between are -1 when unused
typeTable.writeInt32LE(str(t.name), at);
typeTable.writeInt32LE(str(t.ns), at + 4);
typeTable.writeUInt32LE(t.flags ?? 0, at + typeDefSize - 60);
typeTable.writeInt32LE(t.fields?.length ? fi : -1, at + typeDefSize - 56);
typeTable.writeInt32LE(t.methods?.length ? mi : -1, at + typeDefSize - 52);
typeTable.writeUInt16LE(t.methods?.length ?? 0, at + typeDefSize - 24);
typeTable.writeUInt16LE(t.fields?.length ?? 0, at + typeDefSize - 20);
typeTable.writeUInt32LE(0x02000001 + i, at + typeDefSize - 4);
for (const f of t.fields ?? []) {
fieldTable.writeInt32LE(str(f), fi * fieldDefSize);
fieldTable.writeUInt32LE(0x04000001 + fi, fi * fieldDefSize + fieldDefSize - 4);
fi++;
}
for (const m of t.methods ?? []) {
const o = mi * methodDefSize;
methodTable.writeInt32LE(str(m.name), o);
methodTable.writeInt32LE(i, o + 4);
methodTable.writeUInt32LE(0x06000001 + mi, o + methodDefSize - 12);
methodTable.writeUInt16LE(m.flags ?? 0, o + methodDefSize - 8);
methodTable.writeUInt16LE(m.params ?? 0, o + methodDefSize - 2);
mi++;
}
});
// The header is a list of (offset, size) pairs, and the tables follow in the same order.
const HEADER = 0x110;
const header = Buffer.alloc(HEADER);
header.writeUInt32LE(0xfab11baf, 0);
header.writeInt32LE(version, 4);
const tables = { 0x18: Buffer.from(strings), 0x30: methodTable, 0x60: fieldTable, 0xa0: typeTable };
const parts = [header];
let at = HEADER;
const offsets = {};
for (let h = 8; h < HEADER; h += 8) {
const body = tables[h] ?? Buffer.alloc(0);
const padded = Buffer.concat([body, Buffer.alloc((4 - (body.length % 4)) % 4)]);
header.writeInt32LE(at, h);
header.writeInt32LE(body.length, h + 4);
offsets[h] = at;
parts.push(padded);
at += padded.length;
}
return { bytes: new Uint8Array(Buffer.concat(parts)), stringsOffset: offsets[0x18], typeDefsOffset: offsets[0xa0], stringIndex: (s) => stringAt.get(s) };
}
// ---- memory --------------------------------------------------------------------------
class Region {
constructor(name, start, prot, size) {
this.name = name;
this.start = start;
this.prot = prot;
this.buf = Buffer.alloc(size);
this.used = 0;
}
get end() {
return this.start + BigInt(this.buf.length);
}
alloc(size, align = 8) {
this.used = Math.ceil(this.used / align) * align;
const at = this.start + BigInt(this.used);
this.used += size;
if (this.used > this.buf.length) throw new Error(`fixture region "${this.name}" is full`);
return at;
}
}
/** Stands in for a debugger connection: reads, the memory map, value scans and pattern searches over the buffers. */
class FakeDebugger {
constructor(regions) {
this.regions = regions;
this.authed = false;
this.counts = { read: 0, scan: 0, pattern: 0, bytesScanned: 0 };
}
async maps() {
return this.regions.map((r) => ({ name: r.name, start: r.start, end: r.end, offset: 0n, prot: r.prot }));
}
slice(address, length) {
// As on the console: memory that is not there reads as zeros.
const out = Buffer.alloc(length);
const end = address + BigInt(length);
for (const r of this.regions) {
if (r.alias) continue;
const from = address > r.start ? address : r.start;
const to = end < r.end ? end : r.end;
if (from < to) r.buf.copy(out, Number(from - address), Number(from - r.start), Number(to - r.start));
}
return out;
}
async read(_pid, address, length) {
this.counts.read++;
return new Uint8Array(this.slice(address, length));
}
async auth() {
this.authed = true;
}
async scanStart(opts, onBlock) {
if (!this.authed) throw new Error('scan before auth');
// Value types 4 and 6 are unsigned 32- and 64-bit; compare type 0 is "exactly".
const size = opts.valueType === 6 ? 8 : opts.valueType === 4 ? 4 : 0;
if (!size || opts.compareType !== 0 || opts.value.length !== size) throw new Error('the fixture only does exact 4- and 8-byte scans');
this.counts.scan++;
this.counts.bytesScanned += opts.length;
const data = this.slice(opts.address, opts.length);
const want = Buffer.from(opts.value);
const step = opts.alignment ?? size;
let records = [];
for (let o = 0; o + size <= data.length; o += step) {
if (data.compare(want, 0, size, o, o + size) !== 0) continue;
const rec = Buffer.alloc(4 + size);
rec.writeUInt32LE(o, 0);
want.copy(rec, 4);
records.push(rec);
if (records.length === 500) {
onBlock({ records: new Uint8Array(Buffer.concat(records)), count: records.length });
records = [];
}
}
if (records.length) onBlock({ records: new Uint8Array(Buffer.concat(records)), count: records.length });
}
async aobScan({ address, length, pattern, mask, nth = 1 }) {
this.counts.pattern++;
const data = this.slice(address, length);
let seen = 0;
for (let o = 0; o + pattern.length <= data.length; o++) {
let ok = true;
for (let i = 0; i < pattern.length && ok; i++) if (mask[i] === 1 && data[o + i] !== pattern[i]) ok = false;
if (ok && ++seen === nth) return address + BigInt(o);
}
return 0n;
}
}
// ---- the game --------------------------------------------------------------------------
const field = (name, type, offset, extra = {}) => ({ name, type, offset, ...extra });
const method = (name, returns, params = [], extra = {}) => ({ name, returns, params, ...extra });
/**
* The classes of the fake game. `init: false` is a class the game has not
* used: it is in the metadata but has no description in memory. `lazy` has a
* description that has not been filled in yet.
*/
function gameTypes() {
return [
{ ns: '', name: '<Module>', init: false },
{ ns: 'System', name: 'Object', size: 0x10, kind: KIND.object, methods: [method('.ctor', 'void'), method('ToString', 'string'), method('Equals', 'bool', [['object', 'obj']]), method('Finalize', 'void', [], { abstract: true })] },
{ ns: 'System', name: 'ValueType', parent: 'System.Object', flags: ABSTRACT, size: 0x10, methods: [method('ToString', 'string')] },
{ ns: 'System', name: 'Enum', parent: 'System.ValueType', flags: ABSTRACT, size: 0x10, valueType: true },
{ ns: 'System', name: 'Int32', parent: 'System.ValueType', kind: KIND.int, valueType: true, size: 0x14, fields: [field('m_value', 'int', 0x10)] },
{
ns: 'System',
name: 'String',
parent: 'System.Object',
kind: KIND.string,
size: 0x18,
staticSize: 8,
fields: [field('m_stringLength', 'int', 0x10), field('m_firstChar', 'char', 0x14), field('Empty', 'string', 0, { static: true })],
methods: [method('get_Length', 'int')],
},
{ ns: 'System', name: 'Nullable`1', parent: 'System.ValueType', valueType: true, size: 0x18, fields: [field('value', 'T', 0x10), field('hasValue', 'bool', 0x14)] },
{ ns: 'System.Collections.Generic', name: 'List`1', parent: 'System.Object', size: 0x20, fields: [field('_items', 'T[]', 0x10), field('_size', 'int', 0x18)] },
{ ns: 'UnityEngine', name: 'Object', parent: 'System.Object', size: 0x18, fields: [field('m_CachedPtr', 'IntPtr', 0x10)], methods: [method('get_name', 'string')] },
{ ns: 'UnityEngine', name: 'MonoBehavior', parent: 'UnityEngine.Object', size: 0x18, methods: [method('.ctor', 'void')] },
{ ns: 'Game', name: 'Vector3', parent: 'System.ValueType', valueType: true, flags: SEALED, size: 0x1c, fields: [field('x', 'float', 0x10), field('y', 'float', 0x14), field('z', 'float', 0x18)] },
{ ns: 'Game', name: 'Mode', parent: 'System.Enum', valueType: true, flags: SEALED, size: 0x14, fields: [field('value__', 'int', 0x10), field('Easy', 'Game.Mode', 0, { static: true, literal: true }), field('Hard', 'Game.Mode', 0, { static: true, literal: true })] },
{ ns: 'Game', name: 'Item', parent: 'System.Object', size: 0x18, fields: [field('id', 'int', 0x10), field('price', 'float', 0x14)], methods: [method('.ctor', 'void', [['int', 'id']])] },
{
ns: 'Game',
name: 'SaveState',
parent: 'UnityEngine.MonoBehavior',
size: 0xc8,
staticSize: 0x10,
fields: [
field('owner', 'string', 0x18),
field('items', 'System.Collections.Generic.List`1<Game.Item>', 0x20),
field('position', 'Game.Vector3', 0x28),
field('mode', 'Game.Mode', 0x34),
field('dirty', 'bool', 0x38),
field('slots', 'Game.Item[]', 0x40),
field('money', 'System.Nullable`1<int>', 0x48),
field('grid', 'int[,]', 0x50),
field('level', 'int', 0x70),
field('tickets', 'int', 0xc0),
field('Instance', 'Game.SaveState', 0, { static: true }),
field('s_loads', 'int', 8, { static: true }),
field('MaxLevel', 'int', 0, { static: true, literal: true }),
field('t_scratch', 'int', -1, { static: true }),
],
methods: [
method('.ctor', 'void'),
method('get_Money', 'System.Nullable`1<int>'),
method('set_Money', 'void', [['System.Nullable`1<int>', 'value']]),
method('SpendTickets', 'bool', [
['int', 'amount'],
['ref int', 'left'],
]),
method('Load', 'Game.SaveState', [['string', 'path']], { static: true }),
method('Describe', 'string', [], { abstract: true }),
],
},
{ ns: 'Game', name: 'DerivedState', parent: 'Game.SaveState', size: 0xd0, fields: [field('bonus', 'int', 0xc8)], methods: [method('.ctor', 'void'), method('set_Money', 'void', [['System.Nullable`1<int>', 'value']])] },
{ ns: 'Game', name: 'Utils', parent: 'System.Object', flags: ABSTRACT | SEALED, size: 0x10, staticSize: 4, fields: [field('Seed', 'int', 0, { static: true })], methods: [method('Clamp', 'int', [['int', 'v'], ['int', 'lo'], ['int', 'hi']], { static: true })] },
{ ns: 'Game', name: 'Lazy', parent: 'System.Object', lazy: true, size: 0, fields: [field('a', 'int', 0x10)], methods: [method('Run', 'void')] },
{ ns: 'Game', name: 'Unused', parent: 'System.Object', init: false, fields: [field('x', 'int', 0x10)], methods: [method('Never', 'void')] },
{ ns: 'Other', name: 'Utils', parent: 'System.Object', size: 0x10, methods: [method('Help', 'void')] },
];
}
/**
* Builds the fake game for one layout and returns a GameContext over it plus
* the addresses a test needs. `options.moduleName` renames the code module;
* `options.noHarvest` leaves the module's data without class pointers;
* `options.hideMetadata` scrambles the metadata header as an encrypted game would.
*/
export function buildUnityGame(layoutName, options = {}) {
const lay = LAYOUTS[layoutName];
const C = lay.cls;
const moduleName = options.moduleName ?? 'Il2CppUserAssemblies.prx';
const types = gameTypes();
const meta = buildMetadata({
version: lay.version,
typeDefSize: lay.typeDefSize,
methodDefSize: lay.methodDefSize,
fieldDefSize: lay.fieldDefSize,
extraStrings: ['T'],
types: types.map((t) => ({ ns: t.ns, name: t.name, flags: t.flags, fields: (t.fields ?? []).map((f) => f.name), methods: (t.methods ?? []).map((m) => ({ name: m.name, flags: m.static ? 0x10 : 0, params: m.params.length })) })),
});
const R = {
exe: new Region('executable', 0x400000n, 5, 0x4000),
exeData: new Region('executable', 0x404000n, 3, 0x4000),
code: new Region(moduleName, 0x80000000n, 5, 0x10000),
rodata: new Region(moduleName, 0x80010000n, 1, 0x10000),
data: new Region(moduleName, 0x80020000n, 3, 0x4000),
// The metadata does not start its region here, as when a game reads the file into allocated memory.
meta: new Region('', 0x200000000n, 1, 0x3000 + Math.ceil(meta.bytes.length / 0x1000) * 0x1000),
heap: new Region('', 0x300000000n, 3, 0x40000),
gc: new Region('', 0x400000000n, 3, 0x10000),
junk: new Region('', 0x500000000n, 3, 0x4000),
stack: new Region('main stack', 0x7ee000000n, 3, 0x2000),
};
const regions = Object.values(R);
// ps5debug-NG also lists page-table runs that overlap the ordinary entries.
regions.push(Object.assign(Object.create(Region.prototype), { name: '(aux)', start: R.gc.start, prot: 3, buf: R.gc.buf, used: 0, alias: true }));
const regionOf = (a) => {
const r = regions.find((x) => !x.alias && a >= x.start && a < x.end);
if (!r) throw new Error(`fixture write outside memory at 0x${a.toString(16)}`);
return [r.buf, Number(a - r.start)];
};
const w64 = (a, v) => {
const [b, o] = regionOf(a);
b.writeBigUInt64LE(BigInt.asUintN(64, BigInt(v)), o);
};
const w32 = (a, v) => {
const [b, o] = regionOf(a);
b.writeUInt32LE(v >>> 0, o);
};
const w16 = (a, v) => {
const [b, o] = regionOf(a);
b.writeUInt16LE(v, o);
};
const w8 = (a, v) => {
const [b, o] = regionOf(a);
b.writeUInt8(v, o);
};
const wf32 = (a, v) => {
const [b, o] = regionOf(a);
b.writeFloatLE(v, o);
};
const heapString = (s) => {
const at = R.heap.alloc(s.length + 1, 1);
const [b, o] = regionOf(at);
b.write(s, o, 'utf8');
return at;
};
// Metadata, 0x2000 into its region, with unrelated bytes before it.
const metaBase = R.meta.start + 0x2000n;
R.meta.buf.fill(0x5a, 0, 0x2000);
Buffer.from(meta.bytes).copy(R.meta.buf, 0x2000);
if (options.hideMetadata) R.meta.buf.fill(0, 0x2000, 0x2008);
const stringsBase = metaBase + BigInt(meta.stringsOffset);
const typeDefsBase = metaBase + BigInt(meta.typeDefsOffset);
const metaString = (s) => stringsBase + BigInt(meta.stringIndex(s));
const typeDefAt = (i) => typeDefsBase + BigInt(i * lay.typeDefSize);
const byName = new Map();
types.forEach((t, index) => {
t.index = index;
t.fullName = t.ns ? `${t.ns}.${t.name}` : t.name;
byName.set(t.fullName, t);
if (t.init !== false) t.address = R.heap.alloc(C.size);
});
// ---- type descriptors ------------------------------------------------------
const genericClasses = new Map();
// Built-in types (int, string, object) keep a plain number as their data even where other types hold a
// metadata pointer: nothing uses it, so the runtime never converts it.
const typeData = (t) => (lay.handles && t.kind === undefined ? typeDefAt(t.index) : BigInt(t.index));
const bitsFor = (kind, attrs, byref, valueType) => (attrs | (kind << 16) | (byref ? (lay.newTypeBits ? 1 << 29 : 1 << 30) : 0) | (valueType && lay.newTypeBits ? 0x80000000 : 0)) >>> 0;
/** Writes a type descriptor for a C#-ish type expression and returns its address. */
function makeType(expr, attrs = 0) {
const at = R.rodata.alloc(16);
let byref = false;
if (expr.startsWith('ref ')) {
byref = true;
expr = expr.slice(4);
}
let data = 0n;
let kind;
let valueType = false;
const generic = /^(.+?)<(.+)>$/.exec(expr);
if (expr.endsWith('[,]')) {
// Il2CppArrayType: element type, dimensions.
const arr = R.rodata.alloc(16);
w64(arr, makeType(expr.slice(0, -3)));
w8(arr + 8n, 2);
data = arr;
kind = 0x14;
} else if (expr.endsWith('[]')) {
data = makeType(expr.slice(0, -2));
kind = 0x1d;
} else if (expr.endsWith('*')) {
data = makeType(expr.slice(0, -1));
kind = 0x0f;
} else if (generic) {
data = genericClass(generic[1], generic[2].split(',').map((s) => s.trim())).address;
kind = 0x15;
valueType = !!byName.get(generic[1]).valueType;
} else if (expr === 'T') {
// A generic parameter record in the metadata: owner, then name.
const param = R.rodata.alloc(16);
w32(param + 4n, meta.stringIndex('T'));
data = lay.handles ? param : 0n;
kind = 0x13;
} else if (KIND[expr] !== undefined) {
kind = KIND[expr];
data = KEYWORD_CLASS[expr] ? typeData(byName.get(KEYWORD_CLASS[expr])) : 0n;
valueType = kind !== KIND.string && kind !== KIND.object && kind !== KIND.void;
} else {
const t = byName.get(expr);
if (!t) throw new Error(`fixture: unknown type "${expr}"`);
data = typeData(t);
valueType = !!t.valueType;
kind = valueType ? 0x11 : 0x12;
}
w64(at, data);
w32(at + 8n, bitsFor(kind, attrs, byref, valueType));
return at;
}
/** The record that ties a generic class to its arguments (Il2CppGenericClass). */
function genericClass(defName, args) {
const key = `${defName}<${args.join(',')}>`;
if (genericClasses.has(key)) return genericClasses.get(key);
const def = byName.get(defName);
const g = { address: R.heap.alloc(0x20), def, args, key };
genericClasses.set(key, g);
const argv = R.rodata.alloc(8 * args.length);
args.forEach((a, i) => w64(argv + BigInt(i * 8), makeType(a)));
const inst = R.rodata.alloc(16);
w32(inst, args.length);
w64(inst + 8n, argv);
if (lay.handles) w64(g.address, makeType(defName));
else w32(g.address, def.index);
w64(g.address + 8n, inst);
return g;
}
// ---- classes -------------------------------------------------------------------
const image = R.heap.alloc(0x20);
let codeAt = 0x1000;
const methodRecords = [];
function hierarchyOf(t) {
const chain = [];
for (let cur = t; cur; cur = cur.parent ? byName.get(cur.parent) : null) chain.unshift(cur.classAddress ?? cur.address);
return chain;
}
/** Fills in a class description. `g` is set for an instance of a generic class. */
function writeClass(t, address, g) {
const c = address;
const def = g ? g.def : t;
const isValue = !!t.valueType;
w64(c + BigInt(C.image), image);
for (let i = 0; C['lead' + i] !== undefined; i++) w64(c + BigInt(C['lead' + i]), image);
w64(c + BigInt(C.name), metaString(def.name));
w64(c + BigInt(C.ns), metaString(def.ns));
const kind = g ? 0x15 : (t.kind ?? (isValue ? 0x11 : 0x12));
const data = g ? g.address : typeData(t);
w64(c + BigInt(C.byval), data);
w32(c + BigInt(C.byval) + 8n, bitsFor(kind, 0, false, isValue));
w64(c + BigInt(C.thisArg), data);
w32(c + BigInt(C.thisArg) + 8n, bitsFor(kind, 0, true, isValue));
// An enum's element class is its underlying number type; everything else points at itself.
const element = t.parent === 'System.Enum' ? byName.get('System.Int32').address : c;
w64(c + BigInt(C.elem), element);
w64(c + BigInt(C.cast), element);
w64(c + BigInt(C.parent), t.parent ? byName.get(t.parent).address : 0n);
w64(c + BigInt(C.generic), g ? g.address : 0n);
w64(c + BigInt(C.typeDef), typeDefAt(def.index));
w64(c + BigInt(C.self), c);
w32(c + BigInt(C.flags), t.flags ?? 0);
w32(c + BigInt(C.token), 0x02000001 + def.index);
w16(c + BigInt(C.methodCount), (t.methods ?? []).length);
w16(c + BigInt(C.fieldCount), (t.fields ?? []).length);
w32(c + BigInt(C.staticSize), t.staticSize ?? 0);
// Uninitialized memory after the counts, to make sure nothing reads past them as meaning something.
w16(c + BigInt(C.vtableCount), 4);
if (t.lazy) return;
w32(c + BigInt(C.instanceSize), t.size);
if (C.stackSlot !== undefined) w32(c + BigInt(C.stackSlot), isValue ? t.size - 0x10 : 8);
w32(c + BigInt(C.actualSize), t.size);
w32(c + BigInt(C.cctorFinished), 1);
const hier = hierarchyOf({ ...t, classAddress: c });
const hierAt = R.heap.alloc(hier.length * 8);
hier.forEach((h, i) => w64(hierAt + BigInt(i * 8), h));
w64(c + BigInt(C.hier), hierAt);
w8(c + BigInt(C.hierDepth), hier.length);
if (t.fields?.length) {
const F = lay.field;
const list = R.heap.alloc(t.fields.length * F.stride);
t.fields.forEach((f, i) => {
const at = list + BigInt(i * F.stride);
w64(at + BigInt(F.name), metaString(f.name));
let type = f.type;
if (g && type === 'T') type = g.args[0];
if (g && type === 'T[]') type = `${g.args[0]}[]`;
w64(at + BigInt(F.type), makeType(type, (f.static ? 0x10 : 0) | (f.literal ? 0x40 : 0) | 0x01));
w64(at + BigInt(F.parent), c);
w32(at + BigInt(F.offset), f.offset);
});
w64(c + BigInt(C.fields), list);
// Whatever is allocated next must not look like one more field.
w64(R.heap.alloc(8), 0x1111n);
}
if (t.staticSize) w64(c + BigInt(C.statics), (t.staticsAt = R.heap.alloc(t.staticSize)));
if (t.methods?.length) {
const M = lay.method;
const list = R.heap.alloc(t.methods.length * 8);
t.methods.forEach((m, i) => {
const at = R.heap.alloc(M.size);
w64(list + BigInt(i * 8), at);
const code = m.abstract ? 0n : R.code.start + BigInt(codeAt);
if (!m.abstract) codeAt += 0x40;
w64(at + BigInt(M.ptr), code);
if (M.virt !== undefined) w64(at + BigInt(M.virt), code);
w64(at + BigInt(M.invoker), R.code.start + 0x800n);
w64(at + BigInt(M.name), metaString(m.name));
w64(at + BigInt(M.klass), c);
w64(at + BigInt(M.ret), makeType(m.returns));
if (m.params.length) {
if (lay.params === 'info') {
// ParameterInfo: name, position, token, type.
const params = R.heap.alloc(m.params.length * 0x18);
m.params.forEach(([type, name], p) => {
const pa = params + BigInt(p * 0x18);
w64(pa, heapString(name));
w32(pa + 8n, p);
w32(pa + 12n, 0x08000001 + p);
w64(pa + 16n, makeType(type));
});
w64(at + BigInt(M.params), params);
} else {
const params = R.heap.alloc(m.params.length * 8);
m.params.forEach(([type], p) => w64(params + BigInt(p * 8), makeType(type)));
w64(at + BigInt(M.params), params);
}
}
// A pointer whose low half could pass for a method token, where the real token is not.
if (M.meta !== undefined) w64(at + BigInt(M.meta), 0x206000010n);
w32(at + BigInt(M.token), 0x06000001 + methodRecords.length);
w16(at + BigInt(M.flags), 0x0006 | (m.static ? 0x10 : 0));
w16(at + BigInt(M.slot), i);
w8(at + BigInt(M.pcount), m.params.length);
methodRecords.push({ type: t, method: m, record: at, code });
m.code = m.code ?? code;
});
w64(c + BigInt(C.methods), list);
}
}
for (const t of types) if (t.address !== undefined) writeClass(t, t.address, null);
// Instances of generic classes that the game has used.
const nullableInt = genericClass('System.Nullable`1', ['int']);
const listOfItem = genericClass('System.Collections.Generic.List`1', ['Game.Item']);
for (const g of [nullableInt, listOfItem]) {
const made = { ...g.def, fields: g.def.fields.map((f) => ({ ...f })), methods: [], size: g.def.size };
g.classAddress = R.heap.alloc(C.size);
writeClass(made, g.classAddress, g);
w64(g.address + 0x18n, g.classAddress);
}
// An array class has a name of its own, outside the metadata.
const itemArray = R.heap.alloc(C.size);
writeClass({ ...byName.get('System.Object'), name: 'Item', ns: 'Game', fields: [], methods: [], parent: 'System.Object', size: 0x20, kind: 0x1d, index: byName.get('Game.Item').index }, itemArray, null);
w64(itemArray + BigInt(C.name), heapString('Item[]'));
w64(itemArray + BigInt(C.elem), byName.get('Game.Item').address);
w64(itemArray + BigInt(C.cast), byName.get('Game.Item').address);
// The table of classes, one slot per metadata type.
const table = R.heap.alloc(types.length * 8);
for (const t of types) if (t.address !== undefined) w64(table + BigInt(t.index * 8), t.address);
w64(R.exeData.start + 0x100n, table);
// ---- objects --------------------------------------------------------------------
const cls = (n) => byName.get(n).address;
const newObject = (klass, size) => {
const at = R.gc.alloc(size, 16);
w64(at, klass);
return at;
};
const newString = (s) => {
const at = newObject(cls('System.String'), 0x18 + s.length * 2);
w32(at + 0x10n, s.length);
[...s].forEach((ch, i) => w16(at + 0x14n + BigInt(i * 2), ch.charCodeAt(0)));
return at;
};
const items = [1, 2, 3].map((id) => {
const at = newObject(cls('Game.Item'), 0x18);
w32(at + 0x10n, id);
wf32(at + 0x14n, id * 1.5);
return at;
});
const list = newObject(listOfItem.classAddress, 0x20);
w32(list + 0x18n, 3);
const newSave = (klass, size, money, level, tickets) => {
const at = newObject(klass, size);
w64(at + 0x18n, newString('Tiago'));
w64(at + 0x20n, list);
wf32(at + 0x28n, 1.5);
wf32(at + 0x2cn, -2);
wf32(at + 0x30n, 0.25);
w32(at + 0x34n, 1);
w8(at + 0x38n, 1);
w32(at + 0x48n, money);
w8(at + 0x4cn, 1);
w32(at + 0x70n, level);
w32(at + 0xc0n, tickets);
return at;
};
const save = newSave(cls('Game.SaveState'), 0xc8, 9999, 20, 99);
const save2 = newSave(cls('Game.SaveState'), 0xc8, 5, 1, 0);
const derived = newSave(cls('Game.DerivedState'), 0xd0, 123, 4, 7);
w32(derived + 0xc8n, 77);
const statics = byName.get('Game.SaveState').staticsAt;
w64(statics, save);
w32(statics + 8n, 3);
// Things that hold the SaveState class pointer without being objects of it.
const pair = R.gc.alloc(16, 16); // (class, small number), as in an interface table
w64(pair, cls('Game.SaveState'));
w64(pair + 8n, 5n);
const sloppy = R.gc.alloc(0xc8, 16); // right header, but a number where the "owner" reference would be
w64(sloppy, cls('Game.SaveState'));
w64(sloppy + 0x18n, 0x1234n);
w64(R.stack.start + 0x100n, save);
// ---- the code module's data: one pointer per class, method or string the code uses -----
const dataSlots = [];
if (!options.noHarvest) {
// Unresolved entries are odd numbers; the rest are a mix of everything.
dataSlots.push(0x20000123n, 0x40000457n, R.rodata.start + 0x40n, methodRecords[0].record, newString('hello'), save, 0n);
// Type descriptors live in this data too, and theirs point into the metadata. Then more methods and objects.
for (let i = 0; i < 12; i++) dataSlots.push(typeDefAt(i));
for (const m of methodRecords.slice(1, 12)) dataSlots.push(m.record);
dataSlots.push(...items, list, R.junk.start + 0x2800n, 0x123456789000n);
for (const n of ['Game.Utils', 'Game.SaveState', 'System.String', 'Game.Item', 'UnityEngine.MonoBehavior', 'Game.Vector3', 'Game.Mode', 'Other.Utils']) dataSlots.push(cls(n));
} else {
dataSlots.push(0x20000123n, R.rodata.start + 0x40n);
}
dataSlots.forEach((v, i) => w64(R.data.start + 0x200n + BigInt(i * 8), v));
// Noise that resembles the real thing: the text "SaveState" elsewhere, and a stray metadata magic number.
R.junk.buf.write('\0SaveState\0', 0x100, 'latin1');
R.junk.buf.writeUInt32LE(0xfab11baf, 0x800);
R.junk.buf.writeUInt32LE(29, 0x804);
w64(R.junk.start + 0x1000n, R.junk.start + 0x101n);
const dbg = new FakeDebugger(regions);
const ctx = createGameContext({ pid: 77, processName: 'eboot.bin', dbg: () => dbg, x86: null });
const methodAddress = (typeName, name) => byName.get(typeName).methods.find((m) => m.name === name).code;
return {
layout: lay,
ctx,
dbg,
regions: R,
metadata: meta,
metaBase,
types,
cls,
methodAddress,
staticsOf: (n) => byName.get(n).staticsAt,
objects: { save, save2, derived, items, list, pair, sloppy },
generic: { nullableInt: nullableInt.classAddress, listOfItem: listOfItem.classAddress, itemArray },
write: { w64, w32, w16, w8 },
/** Fills in the class the game had not set up yet, as its first use would. */
initialiseLazy() {
const t = byName.get('Game.Lazy');
writeClass({ ...t, lazy: false, size: 0x18 }, t.address, null);
},
};
}