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 the exports of a game module with their VAs (at the module's
|
||||
// preferred base, as in Ghidra). The game DLLs export most of their C++
|
||||
// classes with decorated names, so this is a quick index of what is where.
|
||||
//
|
||||
// node tools/exports.mjs <module> [regex] exports whose name matches
|
||||
// node tools/exports.mjs <module> 0x10018c60 the export at (or before) a VA
|
||||
//
|
||||
// Output: "va name", sorted by name (by address for a VA lookup).
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
const [modulePath, query] = process.argv.slice(2);
|
||||
if (!modulePath) {
|
||||
console.error("usage: node exports.mjs <module> [regex | 0xVA]");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const file = readFileSync(modulePath);
|
||||
const peOffset = file.readUInt32LE(0x3c);
|
||||
const sectionCount = file.readUInt16LE(peOffset + 6);
|
||||
const optionalSize = file.readUInt16LE(peOffset + 20);
|
||||
const optional = peOffset + 24;
|
||||
const imageBase = file.readUInt32LE(optional + 28);
|
||||
const sectionTable = optional + optionalSize;
|
||||
|
||||
function fileOffset(rva) {
|
||||
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 + Math.max(virtualSize, file.readUInt32LE(s + 16))) {
|
||||
return rawOffset + rva - virtualAddress;
|
||||
}
|
||||
}
|
||||
throw new Error(`rva 0x${rva.toString(16)} is not in a section`);
|
||||
}
|
||||
|
||||
const exportRva = file.readUInt32LE(optional + 96);
|
||||
if (!exportRva) {
|
||||
console.error("no export table");
|
||||
process.exit(1);
|
||||
}
|
||||
const dir = fileOffset(exportRva);
|
||||
const functionCount = file.readUInt32LE(dir + 20);
|
||||
const nameCount = file.readUInt32LE(dir + 24);
|
||||
const functions = fileOffset(file.readUInt32LE(dir + 28));
|
||||
const names = fileOffset(file.readUInt32LE(dir + 32));
|
||||
const ordinals = fileOffset(file.readUInt32LE(dir + 36));
|
||||
|
||||
const exports = [];
|
||||
for (let i = 0; i < nameCount && functionCount; i++) {
|
||||
const nameOffset = fileOffset(file.readUInt32LE(names + 4 * i));
|
||||
const end = file.indexOf(0, nameOffset);
|
||||
const name = file.toString("latin1", nameOffset, end);
|
||||
const ordinal = file.readUInt16LE(ordinals + 2 * i);
|
||||
const va = imageBase + file.readUInt32LE(functions + 4 * ordinal);
|
||||
exports.push({ va, name });
|
||||
}
|
||||
|
||||
const hex = (va) => va.toString(16).padStart(8, "0");
|
||||
let shown;
|
||||
if (query && /^0x[0-9a-f]+$/i.test(query)) {
|
||||
const va = Number.parseInt(query, 16);
|
||||
const before = exports.filter((e) => e.va <= va).sort((a, b) => b.va - a.va);
|
||||
shown = before.length ? exports.filter((e) => e.va === before[0].va) : [];
|
||||
} else {
|
||||
const pattern = query ? new RegExp(query.replace(/^\/|\/$/g, ""), "i") : null;
|
||||
shown = exports.filter((e) => !pattern || pattern.test(e.name)).sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
for (const e of shown) {
|
||||
console.log(`${hex(e.va)} ${e.name}`);
|
||||
}
|
||||
Reference in new issue
Block a user