- Launcher (sacred_re.exe) starts sacred.exe with sacred_re.dll injected via Detours; the DLL verifies the Sacred Gold 2.28 build before hooking. - Hook framework: RE_HOOK for game addresses, RE_HOOK_API for Windows APIs, per-hook enable/disable in sacred_re.ini, sacred_re.log. - Game code: Log (0x66f1c0) hooked, CmdLineHasFlag (0x813140) rewritten. - Borderless mod: forces windowed mode, covers the monitor, scales the 1024x768 image aspect-correct, remaps mouse input, keeps running in the background with keyboard input hidden while unfocused. - CMake/Ninja/MSVC x86 build, VS Code setup, disasm tool, function map. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
60 lines
2.2 KiB
JavaScript
60 lines
2.2 KiB
JavaScript
// Disassemble sacred.exe at a virtual address, independent of Ghidra's
|
|
// function boundaries (useful for code only reached via function pointers).
|
|
//
|
|
// node tools/disasm.mjs <exe> <va> [count=60]
|
|
//
|
|
// Stops after `count` instructions or at the first RET/INT3 run, whichever
|
|
// comes later than 8 instructions.
|
|
|
|
import { readFileSync } from "node:fs";
|
|
import { Decoder, DecoderOptions, Formatter, FormatterSyntax, Mnemonic } from "iced-x86";
|
|
|
|
const [exePath, vaText, countText = "60"] = process.argv.slice(2);
|
|
if (!exePath || !vaText) {
|
|
console.error("usage: node disasm.mjs <exe> <va> [count]");
|
|
process.exit(1);
|
|
}
|
|
const va = Number.parseInt(vaText, 16);
|
|
const count = Number.parseInt(countText, 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;
|
|
|
|
function fileOffset(address) {
|
|
const rva = address - imageBase;
|
|
for (let i = 0; i < sectionCount; i++) {
|
|
const s = sectionTable + 40 * i;
|
|
const virtualSize = file.readUInt32LE(s + 8);
|
|
const virtualAddress = file.readUInt32LE(s + 12);
|
|
const rawOffset = file.readUInt32LE(s + 20);
|
|
if (rva >= virtualAddress && rva < virtualAddress + virtualSize) {
|
|
return rawOffset + rva - virtualAddress;
|
|
}
|
|
}
|
|
throw new Error(`address 0x${address.toString(16)} is not in a section`);
|
|
}
|
|
|
|
const start = fileOffset(va);
|
|
const bytes = file.subarray(start, start + count * 15);
|
|
const decoder = new Decoder(32, bytes, DecoderOptions.None);
|
|
decoder.ip = BigInt(va);
|
|
const formatter = new Formatter(FormatterSyntax.Intel);
|
|
formatter.firstOperandCharIndex = 8;
|
|
|
|
for (let n = 0; n < count && decoder.canDecode; n++) {
|
|
const instruction = decoder.decode();
|
|
const address = Number(instruction.ip).toString(16).padStart(8, "0");
|
|
const offset = Number(instruction.ip) - va;
|
|
const raw = [...bytes.subarray(offset, offset + instruction.length)]
|
|
.map((b) => b.toString(16).padStart(2, "0"))
|
|
.join(" ");
|
|
console.log(`${address} ${raw.padEnd(30)} ${formatter.format(instruction)}`);
|
|
instruction.free();
|
|
}
|
|
decoder.free();
|
|
formatter.free();
|