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>
This commit is contained in:
commit
8a7bdfd99f
44 files changed
+2505
No files matched your search
@@ -0,0 +1,84 @@
|
||||
// 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)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user