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,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();
|
||||
Reference in new issue
Block a user