Files
sacred2_k/tools/rtti.mjs
T
khallaandClaude Opus 5.5 8a7bdfd99f Framework for Sacred 2 Gold, game log mirror, borderless fullscreen
Ported from sacredK with module-relative addresses, hooks by exported
name and a SHA-256 check of the six game modules (Community Patch
2.65.1). Borderless windowed at monitor size replaces exclusive
fullscreen, whose focus loss during loading made the game quit and
crash on exit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 01:39:44 +01:00

85 lines
3.3 KiB
JavaScript

// Find the vtables of an MSVC C++ class in a game module via RTTI and list their
// entries.
//
// node tools/rtti.mjs <exe> <ClassName> [entries=40]
//
// MSVC 32-bit RTTI: TypeDescriptor { pVFTable, spare, char name[] } with the
// name ".?AV<Class>@@"; CompleteObjectLocator { signature=0, offset, cdOffset,
// pTypeDescriptor, pClassHierarchyDescriptor }; vtable[-1] = &COL.
// A class has one vtable per (non-virtual) base subobject; `offset` tells which.
import { readFileSync } from "node:fs";
const [exePath, className, entriesText = "40"] = process.argv.slice(2);
if (!exePath || !className) {
console.error("usage: node rtti.mjs <exe> <ClassName> [entries]");
process.exit(1);
}
const maxEntries = Number.parseInt(entriesText, 10);
const file = readFileSync(exePath);
const peOffset = file.readUInt32LE(0x3c);
const sectionCount = file.readUInt16LE(peOffset + 6);
const optionalSize = file.readUInt16LE(peOffset + 20);
const imageBase = file.readUInt32LE(peOffset + 24 + 28);
const sectionTable = peOffset + 24 + optionalSize;
const sections = [];
for (let i = 0; i < sectionCount; i++) {
const s = sectionTable + 40 * i;
sections.push({
name: file.toString("latin1", s, s + 8).replace(/\0+$/, ""),
va: imageBase + file.readUInt32LE(s + 12),
virtualSize: file.readUInt32LE(s + 8),
rawOffset: file.readUInt32LE(s + 20),
rawSize: file.readUInt32LE(s + 16),
});
}
const section = (name) => sections.find((s) => s.name === name);
const toOffset = (va) => {
for (const s of sections) {
if (va >= s.va && va < s.va + Math.min(s.virtualSize, s.rawSize)) return s.rawOffset + va - s.va;
}
return -1;
};
const inSection = (va, s) => va >= s.va && va < s.va + s.virtualSize;
const hex = (v) => "0x" + v.toString(16).padStart(8, "0");
// 1. TypeDescriptor: name string lives 8 bytes into it (.data).
const mangled = Buffer.from(`.?AV${className}@@\0`, "latin1");
const typeDescriptors = [];
for (const s of sections) {
const raw = file.subarray(s.rawOffset, s.rawOffset + s.rawSize);
for (let i = raw.indexOf(mangled); i !== -1; i = raw.indexOf(mangled, i + 1)) {
typeDescriptors.push(s.va + i - 8);
}
}
if (typeDescriptors.length === 0) {
console.error(`no RTTI type descriptor for ${className}`);
process.exit(2);
}
// 2. CompleteObjectLocators referencing it, 3. vtables whose [-1] is a COL.
const rdata = section(".rdata");
const text = section(".text");
const rdataRaw = file.subarray(rdata.rawOffset, rdata.rawOffset + rdata.rawSize);
for (const td of typeDescriptors) {
console.log(`${className}: TypeDescriptor ${hex(td)}`);
for (let i = 0; i + 20 <= rdataRaw.length; i += 4) {
if (rdataRaw.readUInt32LE(i) !== 0 || rdataRaw.readUInt32LE(i + 12) !== td) continue;
const col = rdata.va + i;
const subobjectOffset = rdataRaw.readUInt32LE(i + 4);
for (let j = 0; j + 4 <= rdataRaw.length; j += 4) {
if (rdataRaw.readUInt32LE(j) !== col) continue;
const vtable = rdata.va + j + 4;
console.log(` vtable ${hex(vtable)} (subobject offset 0x${subobjectOffset.toString(16)}, COL ${hex(col)})`);
for (let k = 0; k < maxEntries; k++) {
const o = toOffset(vtable + 4 * k);
const target = file.readUInt32LE(o);
if (!inSection(target, text)) break;
console.log(` [+0x${(4 * k).toString(16).padStart(2, "0")}] ${hex(target)}`);
}
}
}
}