- docs/widescreen.md: render-thread frame, cWorldView0 layout and methods, view size = zoom * 1024.0/768.0 float constants and all their users, ortho projection extents, open questions (activity radius, buffers). - tools: find-imm (immediate/regex instruction search), rtti (vtables via MSVC RTTI), decompile.ps1 + Ghidra scripts for a persistent project; ApplyRtti recovers 410 vtables / ~1200 functions Ghidra misses. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
74 lines
2.6 KiB
JavaScript
74 lines
2.6 KiB
JavaScript
// List every instruction in sacred.exe's .text that uses one of the given
|
|
// immediate values (decimal or 0x-hex), e.g. to find hardcoded resolutions.
|
|
//
|
|
// node tools/find-imm.mjs <exe> 1024 768 [...]
|
|
// node tools/find-imm.mjs <exe> /96AB0h\]/ (regex on instruction text)
|
|
//
|
|
// Output: one line per hit, "address instruction".
|
|
|
|
import { readFileSync } from "node:fs";
|
|
import { Decoder, DecoderOptions, Formatter, FormatterSyntax, OpKind } from "iced-x86";
|
|
|
|
const [exePath, ...valueTexts] = process.argv.slice(2);
|
|
if (!exePath || valueTexts.length === 0) {
|
|
console.error("usage: node find-imm.mjs <exe> <value> [value...]");
|
|
process.exit(1);
|
|
}
|
|
const textPattern = valueTexts.length === 1 && /^\/.*\/$/.test(valueTexts[0])
|
|
? new RegExp(valueTexts[0].slice(1, -1), "i")
|
|
: null;
|
|
const wanted = textPattern ? new Set() : new Set(valueTexts.map((t) => BigInt(Number(t))));
|
|
|
|
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;
|
|
|
|
let text = null;
|
|
for (let i = 0; i < sectionCount; i++) {
|
|
const s = sectionTable + 40 * i;
|
|
const name = file.toString("latin1", s, s + 8).replace(/\0+$/, "");
|
|
if (name === ".text") {
|
|
text = {
|
|
virtualAddress: file.readUInt32LE(s + 12),
|
|
rawSize: file.readUInt32LE(s + 16),
|
|
rawOffset: file.readUInt32LE(s + 20),
|
|
};
|
|
}
|
|
}
|
|
if (!text) throw new Error("no .text section");
|
|
|
|
const bytes = file.subarray(text.rawOffset, text.rawOffset + text.rawSize);
|
|
const decoder = new Decoder(32, bytes, DecoderOptions.None);
|
|
decoder.ip = BigInt(imageBase + text.virtualAddress);
|
|
const formatter = new Formatter(FormatterSyntax.Intel);
|
|
|
|
const immediateKinds = new Set([
|
|
OpKind.Immediate8, OpKind.Immediate16, OpKind.Immediate32,
|
|
OpKind.Immediate8to16, OpKind.Immediate8to32,
|
|
]);
|
|
|
|
// Linear sweep: data embedded in .text can produce a few bogus hits.
|
|
while (decoder.canDecode) {
|
|
const instruction = decoder.decode();
|
|
if (textPattern) {
|
|
const text = formatter.format(instruction);
|
|
if (textPattern.test(text)) {
|
|
console.log(`${Number(instruction.ip).toString(16).padStart(8, "0")} ${text}`);
|
|
}
|
|
instruction.free();
|
|
continue;
|
|
}
|
|
for (let op = 0; op < instruction.opCount; op++) {
|
|
if (immediateKinds.has(instruction.opKind(op)) && wanted.has(instruction.immediate(op) & 0xffffffffn)) {
|
|
console.log(`${Number(instruction.ip).toString(16).padStart(8, "0")} ${formatter.format(instruction)}`);
|
|
break;
|
|
}
|
|
}
|
|
instruction.free();
|
|
}
|
|
decoder.free();
|
|
formatter.free();
|