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:
khallaandClaude Opus 5.5 committed 2026-10-31 01:39:44 +01:00
commit 8a7bdfd99f
44 files changed
+2505

No files matched your search

+73
View File
@@ -0,0 +1,73 @@
// List every instruction in a game module'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();