mirror of
https://gitea.tendokyu.moe/beerpsi/x.git
synced 2025-02-17 11:08:35 +01:00
deno is kinda nice
This commit is contained in:
parent
421c2dc09b
commit
cadee776ea
@ -8,10 +8,14 @@ disk image.
|
|||||||
- Dependencies: `pip install construct PyCryptodome`
|
- Dependencies: `pip install construct PyCryptodome`
|
||||||
|
|
||||||
## Patchers
|
## Patchers
|
||||||
- `patchers/b2spatch.mjs`: Convert from BemaniPatcher to Spice2x JSON
|
- `patchers/b2spatch.ts`: Convert from BemaniPatcher to Spice2x JSON
|
||||||
- Requires `cheerio`: `npm i cheerio`
|
- Use Deno: `deno run -WNE b2spatch.ts ...`
|
||||||
- You will need to rename the resulting JSON manually so that they match the
|
- You will need to rename the resulting JSON manually so that they match the
|
||||||
`{gameCode}-{timestamp}_{entrypoint}` format expected by Spice2x.
|
`{gameCode}-{timestamp}_{entrypoint}` format expected by Spice2x.
|
||||||
|
- `patchers/b2mpatch.ts`: Convert from BemaniPatcher to [mempatch-hook](https://github.com/djhackersdev/bemanitools/blob/master/doc/tools/mempatch-hook.md)
|
||||||
|
- Use Deno: `deno run -A b2mpatch.ts ...`
|
||||||
|
- You need to setup a folder of executables or DLLs you want to patch. They
|
||||||
|
can be named the patcher's description, or the patcher's filename.
|
||||||
|
|
||||||
## Arcaea
|
## Arcaea
|
||||||
- `acaca/arcpack.py`: Script to extract Arcaea Nintendo Switch `arc.pack` files
|
- `acaca/arcpack.py`: Script to extract Arcaea Nintendo Switch `arc.pack` files
|
||||||
|
58
patchers/_parser.ts
Normal file
58
patchers/_parser.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import vm from "node:vm";
|
||||||
|
import * as cheerio from "npm:cheerio";
|
||||||
|
import { BemaniPatch, BemaniPatcherArgs } from "./_types.ts";
|
||||||
|
|
||||||
|
export function bytesToHex(bytes: number[]) {
|
||||||
|
return bytes.map((b) => b.toString(16).padStart(2, "0").toUpperCase()).join(
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parsePatcherHtml(filename: string, contents: string) {
|
||||||
|
const $ = cheerio.load(contents);
|
||||||
|
|
||||||
|
let script = "";
|
||||||
|
|
||||||
|
for (const element of $("script").get()) {
|
||||||
|
const code = $(element).html();
|
||||||
|
|
||||||
|
if (!code?.includes("new Patcher")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
script += code;
|
||||||
|
script += "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (script.length === 0) {
|
||||||
|
console.warn(
|
||||||
|
`Failed to find any BemaniPatcher patches in ${filename}.`,
|
||||||
|
);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const patchers: BemaniPatcherArgs[] = [];
|
||||||
|
const context = {
|
||||||
|
window: {
|
||||||
|
addEventListener: (_type: string, listener: () => unknown) =>
|
||||||
|
listener(),
|
||||||
|
},
|
||||||
|
Patcher: class {
|
||||||
|
constructor(
|
||||||
|
fname: string,
|
||||||
|
description: string,
|
||||||
|
patches: BemaniPatch[],
|
||||||
|
) {
|
||||||
|
patchers.push({ fname, description, patches });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
PatchContainer: class {
|
||||||
|
constructor(_patchers: unknown[]) {}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
vm.createContext(context);
|
||||||
|
vm.runInContext(script, context);
|
||||||
|
|
||||||
|
return patchers;
|
||||||
|
}
|
137
patchers/_types.ts
Normal file
137
patchers/_types.ts
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
type BemaniBasePatch = {
|
||||||
|
name: string;
|
||||||
|
tooltip?: string;
|
||||||
|
danger?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BemaniStandardPatch = BemaniBasePatch & {
|
||||||
|
type: undefined;
|
||||||
|
patches: {
|
||||||
|
offset: number;
|
||||||
|
on: number[];
|
||||||
|
off: number[];
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BemaniUnionPatch = BemaniBasePatch & {
|
||||||
|
type: "union";
|
||||||
|
offset: number;
|
||||||
|
patches: {
|
||||||
|
name: string;
|
||||||
|
patch: number[];
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BemaniNumberPatch = BemaniBasePatch & {
|
||||||
|
type: "number";
|
||||||
|
offset: number;
|
||||||
|
size: number;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BemaniDynamicStringPatch = BemaniBasePatch & {
|
||||||
|
type: "dynamic";
|
||||||
|
mode?: "all";
|
||||||
|
target: "string";
|
||||||
|
patches: {
|
||||||
|
off: string;
|
||||||
|
on: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BemaniDynamicPatch = BemaniBasePatch & {
|
||||||
|
type: "dynamic";
|
||||||
|
mode?: "all";
|
||||||
|
target: undefined;
|
||||||
|
patches: {
|
||||||
|
off: (number | "XX")[];
|
||||||
|
on: (number | "XX")[];
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BemaniHexPatch = BemaniBasePatch & {
|
||||||
|
type: "hex";
|
||||||
|
offset: number;
|
||||||
|
off: number[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BemaniPatch =
|
||||||
|
| BemaniStandardPatch
|
||||||
|
| BemaniUnionPatch
|
||||||
|
| BemaniNumberPatch
|
||||||
|
| BemaniDynamicPatch
|
||||||
|
| BemaniDynamicStringPatch
|
||||||
|
| BemaniHexPatch;
|
||||||
|
|
||||||
|
export type BemaniPatcherArgs = {
|
||||||
|
fname: string;
|
||||||
|
description: string;
|
||||||
|
patches: BemaniPatch[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SpiceBasePatch = {
|
||||||
|
type: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
gameCode: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SpiceMemoryPatch = SpiceBasePatch & {
|
||||||
|
type: "memory";
|
||||||
|
patches: {
|
||||||
|
offset: number;
|
||||||
|
dllName: string;
|
||||||
|
dataDisabled: string;
|
||||||
|
dataEnabled: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SpiceNumberPatch = SpiceBasePatch & {
|
||||||
|
type: "number";
|
||||||
|
patch: {
|
||||||
|
dllName: string;
|
||||||
|
offset: number;
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
size: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SpiceUnionPatch = SpiceBasePatch & {
|
||||||
|
type: "union";
|
||||||
|
patches: {
|
||||||
|
name: string;
|
||||||
|
patch: {
|
||||||
|
dllName: string;
|
||||||
|
data: string;
|
||||||
|
offset: number;
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SpiceSignaturePatch = SpiceBasePatch & {
|
||||||
|
type: "signature";
|
||||||
|
signature: string;
|
||||||
|
replacement: string;
|
||||||
|
dllName: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The offset to start searching for the signature.
|
||||||
|
*/
|
||||||
|
offset?: number | string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of matches to get before returning a result.
|
||||||
|
*/
|
||||||
|
usage?: number | string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SpiceMetaPatch = {
|
||||||
|
gameCode: string;
|
||||||
|
version: string;
|
||||||
|
lastUpdated: string;
|
||||||
|
source: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SpicePatch = SpiceMemoryPatch | SpiceNumberPatch | SpiceUnionPatch | SpiceSignaturePatch;
|
138
patchers/b2mpatch.ts
Normal file
138
patchers/b2mpatch.ts
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
||||||
|
import process from "node:process";
|
||||||
|
import path from "node:path";
|
||||||
|
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";
|
||||||
|
|
||||||
|
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),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!existsSync(exePath)) {
|
||||||
|
console.warn(
|
||||||
|
`Target file ${exePath} does not exist, falling back to ${patcher.fname}.`,
|
||||||
|
);
|
||||||
|
exePath = path.join(executableDir, patcher.fname);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existsSync(exePath)) {
|
||||||
|
console.warn(`Target file ${exePath} does not exist, skipping.`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = readFileSync(exePath);
|
||||||
|
const 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") {
|
||||||
|
console.warn(
|
||||||
|
`Unsupported BemaniPatcher patch type ${patch.type}`,
|
||||||
|
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);
|
||||||
|
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();
|
@ -1,165 +0,0 @@
|
|||||||
/**
|
|
||||||
* Script to convert a BemaniPatcher HTML to a spice2x patch JSON.
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* - node b2spatch.js <patcher URL | local patcher HTML> <gameCode>
|
|
||||||
*/
|
|
||||||
|
|
||||||
import vm from "vm";
|
|
||||||
import * as cheerio from "cheerio";
|
|
||||||
import { readFileSync, writeFileSync } from "fs";
|
|
||||||
import path from "path";
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {number[]} bytes
|
|
||||||
*/
|
|
||||||
function bytesToHex(bytes) {
|
|
||||||
return bytes.reduce((acc, c) => acc + c.toString(16).padStart(2, "0").toUpperCase(), "");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse a BemaniPatcher HTML for patches.
|
|
||||||
* @param {string} contents
|
|
||||||
*/
|
|
||||||
function parsePatcherHtml(filename, contents) {
|
|
||||||
const $ = cheerio.load(contents);
|
|
||||||
|
|
||||||
let script = "";
|
|
||||||
|
|
||||||
for (const element of $("script").get()) {
|
|
||||||
const code = $(element).html();
|
|
||||||
|
|
||||||
if (!code.includes("new Patcher")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
script += code;
|
|
||||||
script += "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (script.length === 0) {
|
|
||||||
console.warn(`Failed to find any BemaniPatcher patches in ${filename}.`);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const patchers = [];
|
|
||||||
const context = {
|
|
||||||
window: {
|
|
||||||
addEventListener: (type, cb) => cb(),
|
|
||||||
},
|
|
||||||
Patcher: class {
|
|
||||||
constructor(fname, description, patches) {
|
|
||||||
patchers.push({ fname, description, patches });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
PatchContainer: class {
|
|
||||||
constructor(patchers) {}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
vm.createContext(context);
|
|
||||||
vm.runInContext(script, context);
|
|
||||||
|
|
||||||
return patchers;
|
|
||||||
}
|
|
||||||
|
|
||||||
function convertToSpicePatch(bmPatch, gameCode, dllName) {
|
|
||||||
let description = "";
|
|
||||||
|
|
||||||
if (bmPatch.tooltip) {
|
|
||||||
description += bmPatch.tooltip + "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bmPatch.danger) {
|
|
||||||
description += `WARNING: ${bmPatch.danger}`
|
|
||||||
}
|
|
||||||
|
|
||||||
description = description.trim()
|
|
||||||
|
|
||||||
const patch = {
|
|
||||||
name: bmPatch.name,
|
|
||||||
description,
|
|
||||||
gameCode,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bmPatch.type) {
|
|
||||||
if (bmPatch.type === "union") {
|
|
||||||
patch.type = "union";
|
|
||||||
patch.patches = bmPatch.patches.map((p) => ({
|
|
||||||
name: p.name,
|
|
||||||
patch: {
|
|
||||||
dllName,
|
|
||||||
data: bytesToHex(p.patch),
|
|
||||||
offset: bmPatch.offset,
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
} else if (bmPatch.type === "number") {
|
|
||||||
patch.type = "number";
|
|
||||||
patch.patch = {
|
|
||||||
dllName,
|
|
||||||
offset: bmPatch.offset,
|
|
||||||
min: bmPatch.min,
|
|
||||||
max: bmPatch.max,
|
|
||||||
size: bmPatch.size,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
console.warn(`Unsupported BemaniPatcher patch type ${bmPatch.type}`, bmPatch);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
patch.type = "memory";
|
|
||||||
patch.patches = bmPatch.patches.map((p) => ({
|
|
||||||
offset: p.offset,
|
|
||||||
dllName,
|
|
||||||
dataDisabled: bytesToHex(p.off),
|
|
||||||
dataEnabled: bytesToHex(p.on),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
return patch;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
if (process.argv.length < 4) {
|
|
||||||
console.log("Usage: node b2spatch.js <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 = [
|
|
||||||
{
|
|
||||||
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();
|
|
||||||
|
|
143
patchers/b2spatch.ts
Normal file
143
patchers/b2spatch.ts
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
/**
|
||||||
|
* 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 { bytesToHex, parsePatcherHtml } from "./_parser.ts";
|
||||||
|
import type {
|
||||||
|
BemaniPatch,
|
||||||
|
SpiceBasePatch,
|
||||||
|
SpiceMemoryPatch,
|
||||||
|
SpiceMetaPatch,
|
||||||
|
SpiceNumberPatch,
|
||||||
|
SpicePatch,
|
||||||
|
SpiceUnionPatch,
|
||||||
|
} from "./_types.ts";
|
||||||
|
|
||||||
|
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.
|
||||||
|
console.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 -WNE 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();
|
Loading…
x
Reference in New Issue
Block a user