Skip to content

Commit

Permalink
Add b2spice script
Browse files Browse the repository at this point in the history
  • Loading branch information
aixxe committed Apr 17, 2024
1 parent 71ae132 commit 59bb75e
Show file tree
Hide file tree
Showing 3 changed files with 163 additions and 3 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
FROM node:21.6.1-alpine as buildenv

COPY src/ /build/src/
COPY b2mpatch.js diff2patch.js package.json package-lock.json /build/
COPY b2mpatch.js diff2patch.js b2spice.js package.json package-lock.json /build/

WORKDIR /build

Expand Down
22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,32 @@ example.exe 447A0B 4584ED750E 4AD3FF80F8
# example.exe 447A0B 40488B00FF 4AD3FF80F8
```

---
### Extra scripts

Additionally, `diff2patch.js` can be used to generate a mempatch file from two binaries.
`diff2patch.js` can be used to generate a mempatch file from two binaries.

```bash
# generate omnimix patch using the original and modified binaries
docker run --rm -it -u $(id -u):$(id -g) -v $(pwd):/data aixxe/b2mpatch diff2patch.js \
--original=/data/bin/2018091900/bm2dx.dll \
--modified=/data/bin/2018091900/bm2dx_omni.dll \
--patch=/data/patches/2018-09-19-omnimix.mph
```

`b2spice.js` converts BemaniPatcher patches to [proposed](https://github.com/spice2x/spice2x.github.io/issues/161) remote patch format.

Assuming the following directory structure:

```
bin
└── 2023090500
├── 2023-09-05 (LDJ-003).dll
├── 2023-09-05 (LDJ-010).dll
└── 2023-09-05 (LDJ-012).dll
```

```bash
docker run --rm -it -u $(id -u):$(id -g) -v $(pwd):/data aixxe/b2mpatch b2spice.js \
--url=https://github.com/mon/BemaniPatcher/raw/master/resident.html \
--prefix=LDJ --dir=/data/bin/2023090500 --output=/data/resources/
```
142 changes: 142 additions & 0 deletions b2spice.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
#!/usr/bin/env node
'use strict';

const fs = require('node:fs');
const log = require('npmlog');
const axios = require('axios');
const PE = require('pe-parser');
const path = require('node:path');
const parse = require('./src/parser');
const Sandbox = require('./src/sandbox');
const { program } = require('commander');
const sanitize = require('sanitize-filename');
const { formatBytes } = require('./src/converter');

program.name('b2spice')
.description('Converts patches from BemaniPatcher to spice2x format')
.requiredOption('--prefix <prefix>', 'game code prefix (e.g. LDJ, KFC, etc.)')
.requiredOption('--dir <dir...>', 'directory containing executables to patch')
.requiredOption('--url <url...>', 'full URL to a BemaniPatcher page')
.option('-o, --output <dir>', 'directory to write output files to', '.')
.parse();

const convertPatch = (patch, prefix, dll) =>
{
if ('type' in patch && patch.type === 'number')
{
log.warn('convert', 'number patch not supported, skipping "%s"...', patch.name);
return;
}

const result = {
name: patch.name,
description: patch.tooltip || '',
gameCode: prefix
};

const is_union = ('type' in patch && patch.type === 'union');

if (is_union)
{
result.type = 'union';
result.patches = [];

for (const item of patch.patches)
{
result.patches.push({
name: item.name,
type: 'union',
patch: {
dllName: dll,
data: formatBytes(item.patch),
offset: patch.offset,
}
});
}

return result;
}
else
{
result.type = 'memory';
result.patches = [];

for (const item of patch.patches)
{
result.patches.push({
offset: item.offset,
dllName: dll,
dataDisabled: formatBytes(item.off),
dataEnabled: formatBytes(item.on),
});
}
}

return result;
};

(async () => {
const sandbox = new Sandbox();
const options = program.opts();

if (options.url.length !== options.dir.length)
return log.error('init', '--url and --dir must have the same number of arguments');

if (!fs.existsSync(options.output))
fs.mkdirSync(options.output, { recursive: true });

for (let i = 0; i < options.url.length; ++i)
{
const url = options.url[i];
const dir = options.dir[i];

log.info('query', 'querying patches from %s', url);

const response = await axios.get(url)
.catch(error => log.error('query', 'get url "%s" failed: %s', url, error.message));

if (!response)
continue;

const script = parse(response.data);

for (const patch of sandbox.run(script))
{
// Try to use the description of the Patcher as the initial filename.
let exe = path.resolve(dir, sanitize(patch.description) + path.extname(patch.fname));

if (!fs.existsSync(exe))
{
log.warn('convert', 'target file "%s" does not exist, falling back to "%s"...', exe, patch.fname);
exe = path.resolve(dir, patch.fname);
}

if (!fs.existsSync(exe))
return log.error('convert', 'target file "%s" does not exist', exe);

let data = fs.readFileSync(exe);
let pe = await PE.Parse(data);

// Build output filename from PE header values.
const timeDateStamp = pe.nt_headers.FileHeader.TimeDateStamp.toString();
const addressOfEntryPoint = pe.nt_headers.OptionalHeader.AddressOfEntryPoint.toString();

// Convert all patches to spice2x format.
let result = [];

for (const item of patch.args)
{
const converted = convertPatch(item, options.prefix, patch.fname);

if (converted)
result.push(converted);
}

const filename = sanitize(`${timeDateStamp}${addressOfEntryPoint}.json`);
const output = path.resolve(options.output, filename);

log.info('convert', 'writing patch file "%s"...', output);
fs.writeFileSync(output, JSON.stringify(result, null, 4));
}
}
})();

0 comments on commit 59bb75e

Please sign in to comment.