Initial sacred_re scaffold with borderless fullscreen mod

- 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>
This commit is contained in:
khallaandClaude Opus 5.5 committed 2026-10-13 23:55:50 +02:00
commit d9f5c85430
31 files changed
+1184

No files matched your search

+59
View File
@@ -0,0 +1,59 @@
// 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();