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

147 lines
4.3 KiB
TypeScript

/**
* Script to convert a BemaniPatcher HTML to a spice2x patch JSON.
*
* Usage:
* - deno run -WNE b2spatch.ts <patcher URL | local patcher HTML> <gameCode> [outputDir]
*/
import { readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
import process from "node:process";
import { CreateLogger } from "npm:mei-logger";
import { bytesToHex, parsePatcherHtml } from "./_parser.ts";
import type {
BemaniPatch,
SpiceBasePatch,
SpiceMemoryPatch,
SpiceMetaPatch,
SpiceNumberPatch,
SpicePatch,
SpiceUnionPatch,
} from "./_types.ts";
const logger = CreateLogger("b2spatch");
function convertToSpicePatch(
bmPatch: BemaniPatch,
gameCode: string,
dllName: string,
) {
let description = "";
if (bmPatch.tooltip) {
description += bmPatch.tooltip + "\n";
}
if (bmPatch.danger) {
description += `WARNING: ${bmPatch.danger}`;
}
description = description.trim();
const patch: SpiceBasePatch = {
type: "memory",
name: bmPatch.name,
description,
gameCode,
};
if (bmPatch.type) {
if (bmPatch.type === "union") {
patch.type = "union";
(patch as SpiceUnionPatch).patches = bmPatch.patches.map((p) => ({
name: p.name,
patch: {
dllName,
data: bytesToHex(p.patch),
offset: bmPatch.offset,
},
}));
} else if (bmPatch.type === "number") {
const numWidth = bmPatch.size * 8;
patch.type = "number";
(patch as SpiceNumberPatch).patch = {
dllName,
offset: bmPatch.offset,
min: bmPatch.min ?? -(2 ** (numWidth - 1)),
max: bmPatch.max ?? 2 ** (numWidth - 1) - 1,
size: bmPatch.size,
};
} else {
// Technically you can match BemaniPatcher's DynamicPatch type to
// Spice's signature patch, but one DynamicPatch can have multiple signatures,
// while Spice only supports one signature per patch.
logger.warn(
`Unsupported BemaniPatcher patch type ${bmPatch.type}`,
bmPatch,
);
}
} else {
patch.type = "memory";
(patch as SpiceMemoryPatch).patches = bmPatch.patches.map((p) => ({
offset: p.offset,
dllName,
dataDisabled: bytesToHex(p.off),
dataEnabled: bytesToHex(p.on),
}));
}
return patch as SpicePatch;
}
async function main() {
if (process.argv.length < 4) {
console.log(
"Usage: deno run -A b2spatch.ts <patcher URL | local patcher HTML> <gameCode> [output dir]",
);
process.exit(1);
}
const location = process.argv[2];
const gameCode = 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) {
const lastUpdated = new Date();
const spice: (SpiceMetaPatch | SpicePatch)[] = [
{
gameCode,
version: patcher.description,
lastUpdated: `${lastUpdated.getFullYear()}-${
(lastUpdated.getMonth() + 1).toString().padStart(2, "0")
}-${lastUpdated.getDate().toString().padStart(2, "0")} ${
lastUpdated.getHours().toString().padStart(2, "0")
}:${lastUpdated.getMinutes().toString().padStart(2, "0")}:${
lastUpdated.getSeconds().toString().padStart(2, "0")
}`,
source: locationIsUrl
? location
: "https://sp2x.two-torial.xyz/",
},
];
for (const patch of patcher.patches) {
spice.push(convertToSpicePatch(patch, gameCode, patcher.fname));
}
writeFileSync(
path.join(output, `${gameCode}-${patcher.description}.json`),
JSON.stringify(spice, null, 4),
);
}
}
main();