mirror of
https://gitea.tendokyu.moe/beerpsi/x.git
synced 2024-11-27 17:00:51 +01:00
147 lines
4.3 KiB
TypeScript
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();
|