mirror of
https://gitea.tendokyu.moe/beerpsi/x.git
synced 2024-11-23 23:00:56 +01:00
162 lines
5.0 KiB
TypeScript
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();
|