beerpsi-x/patchers/b2mpatch.ts
2024-10-27 03:17:55 +07:00

162 lines
5.0 KiB
TypeScript

import { Buffer } from "node:buffer";
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import process from "node:process";
import path from "node:path";
import { CreateLogger } from "npm:mei-logger";
import PE from "npm:pe-parser";
import type { PEFile } from "npm:pe-parser/parser";
import sanitize from "npm:sanitize-filename";
import { bytesToHex, parsePatcherHtml } from "./_parser.ts";
const logger = CreateLogger("b2mpatch");
function fileOffsetToRVA(pe: PEFile, offset: number) {
for (const section of pe.sections) {
if (
offset >= section.PointerToRawData &&
offset < section.PointerToRawData + section.SizeOfRawData
) {
return offset - section.PointerToRawData + section.VirtualAddress;
}
}
}
async function main() {
if (process.argv.length < 4) {
console.log(
"Usage: deno run -A b2mpatch.ts <patcher URL | local patcher HTML> <executableDir> [outputDir]",
);
process.exit(1);
}
const location = process.argv[2];
const executableDir = process.argv[3];
const output = process.argv[4] ?? ".";
const locationIsUrl = location.startsWith("http://") ||
location.startsWith("https://");
let html = "";
if (locationIsUrl) {
html = await fetch(location).then((r) => r.text());
} else {
html = readFileSync(location, { encoding: "utf-8" });
}
const patchers = parsePatcherHtml(location, html);
for (const patcher of patchers) {
let exePath = path.join(
executableDir,
sanitize(patcher.description) + path.extname(patcher.fname),
);
let useFileOffsets = false;
if (!existsSync(exePath)) {
logger.warn(
`Target file ${exePath} does not exist, falling back to ${patcher.fname}.`,
);
exePath = path.join(executableDir, patcher.fname);
}
if (!existsSync(exePath)) {
logger.warn(`Target file ${exePath} does not exist, using file offsets instead (requires gh:aixxe/mempatcher).`);
useFileOffsets = true;
}
let data: Buffer | null = null;
let pe: PEFile | null = null;
if (!useFileOffsets) {
data = readFileSync(exePath);
pe = await PE.Parse(data);
}
let mempatch = `# ${patcher.fname} ${patcher.description}\n\n`;
for (const patch of patcher.patches) {
if (patch.type !== undefined && patch.type !== "union") {
logger.warn(
`Unsupported BemaniPatcher patch type ${patch.type}`,
patch,
);
continue;
}
if (patch.type === "union" && !data) {
logger.warn(
`Union patch requires file data, skipping.`, patch,
);
continue;
}
mempatch += `# ${patch.name}\n`;
if (patch.tooltip) {
mempatch += `# ${patch.tooltip}\n`;
}
if (patch.danger) {
mempatch += `# WARNING: ${patch.danger}\n`;
}
for (const p of patch.patches) {
let on: string;
let off: string;
let offset: string;
if (patch.type === "union") {
const up = p as { name: string; patch: number[] };
on = bytesToHex(up.patch);
off = data!
.subarray(
patch.offset,
patch.offset + up.patch.length,
)
.toString("hex")
.toUpperCase();
offset = fileOffsetToRVA(pe, patch.offset)
.toString(16)
.toUpperCase();
mempatch += `## ${up.name}\n`;
} else {
const sp = p as {
offset: number;
on: number[];
off: number[];
};
on = bytesToHex(sp.on);
off = bytesToHex(sp.off);
if (useFileOffsets) {
offset = "F+" + sp.offset.toString(16).toUpperCase();
} else {
offset = fileOffsetToRVA(pe, sp.offset).toString(16)
.toUpperCase();
}
}
if (patch.type === "union" && on === off) {
mempatch += `${patcher.fname} ${offset} ${on} ${off}\n`;
} else {
mempatch += `# ${patcher.fname} ${offset} ${on} ${off}\n`;
}
}
mempatch += "\n";
}
writeFileSync(
path.join(
output,
sanitize(`${patcher.fname}-${patcher.description}.mph`),
),
mempatch,
);
}
}
main();