Skip to content

Instantly share code, notes, and snippets.

@cxx
Last active April 6, 2021 13:27
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cxx/6d1d44ce4a6107ed80e0a6c8c5b887c4 to your computer and use it in GitHub Desktop.
Save cxx/6d1d44ce4a6107ed80e0a6c8c5b887c4 to your computer and use it in GitHub Desktop.
// avault2mame.js - convert arcade game data in Atari Vault to older MAME ROM sets
//
// Usage:
// node avault2mame.js [ROM directory]
// (ex. on Linux) node avault2mame.js ~/.steam/steam/steamapps/common/Atari\ Vault/AtariVault_Data/StreamingAssets/FOCAL_Emulator
//
// Requirements:
// - Node.js v6 or later
// - [Microsoft Windows] .NET Framework 4.5 or later (included in Windows 8/10)
// - [Linux] /usr/bin/zip
//
// Supported (libretro core):
// - Asteroids (MAME 2003)
// - Asteroids Deluxe (MAME 2003)
// - Black Widow (MAME 2003, wrong checksums)
// - Gravitar (MAME 2003)
// - Lunar Lander (MAME 2003, unplayable?)
// - Major Havoc (MAME 2003, wrong checksums)
// - Missile Command (MAME 2003)
// - Red Baron (MAME 2003)
// - Space Duel (MAME 2003)
// - Sprint 2 (MAME, "proms" ROMs are missing)
// - Tempest (MAME 2003)
//
// Not supported:
// - Centipede
// - Crystal Castles
// - Liberator
// - Millipede
// - Pong (TTL only, no ROMs)
// - Super Breakout
// - Warlords
//
// Changelog:
// - 2017-12-20: Place dummy files for missing ROMs.
// - 2017-04-24: Initial release.
const child_process = require('child_process');
const fs = require('fs');
const os = require('os');
const path = require('path');
function split(buf, size)
{
const ret = [];
for (let i = 0; i < buf.length; i += size)
ret.push(buf.slice(i, Math.min(i+size,buf.length)));
return ret;
}
function split_at(buf, ...pos)
{
const ret = [];
pos = [0, ...pos, buf.length];
for (let i = 1; i < pos.length; i++)
ret.push(buf.slice(pos[i-1], pos[i]));
return ret;
}
function reverse_bmp(buf)
{
const off_bits = buf.readUInt32LE(10);
const width = buf.readUInt32LE(14+4);
const height = buf.readUInt32LE(14+8);
const ret = Buffer.allocUnsafe(width * height);
for (let i = 0; i < height; i++) {
const src_start = off_bits + width * (height-1-i);
buf.copy(ret, width*i, src_start, src_start+width);
}
return ret;
}
function split_nibbles(buf)
{
const upper = Buffer.allocUnsafe(buf.length);
const lower = Buffer.allocUnsafe(buf.length);
for (let i = 0; i < buf.length; i++) {
upper[i] = buf[i] >> 4 & 0xf;
lower[i] = buf[i] >> 0 & 0xf;
}
return [upper, lower];
}
function encode_gfx(buf, layout)
{
const np = layout.planes;
const dest = Buffer.alloc(buf.length * np / 8);
if (Array.isArray(layout.total)) {
const [num, den] = layout.total;
layout = Object.assign({}, layout, {
total: dest.length * 8 / layout.charincrement * num / den,
planeoffset: layout.planeoffset.map(x => {
if (Array.isArray(x)) {
let [num, den, add] = x;
add = add || 0;
return dest.length * 8 * num / den + add;
}
else
return x;
})
});
}
let i = 0;
for (let c = 0; c < layout.total; c++) {
const charoffset = layout.charincrement * c;
for (let y = 0; y < layout.height; y++) {
const yoffset = charoffset + layout.yoffset[y];
for (let x = 0; x < layout.width; x++) {
const xoffset = yoffset + layout.xoffset[x];
for (let p = 0; p < np; p++) {
const offset = xoffset + layout.planeoffset[p];
dest[offset >> 3] |=
((buf[i] >> np-1-p) & 1) << (~offset & 7);
}
i++;
}
}
}
return dest;
}
function zip(name, dir)
{
let cmd;
if (os.type() === 'Windows_NT')
cmd = `powershell Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::CreateFromDirectory('${dir}', '${name}.zip')`;
else
cmd = `zip -j ${name}.zip ${dir}/*`;
child_process.execSync(cmd);
}
function convert_roms(name, srcdir, maps)
{
const bins = {};
for (const region in maps) {
const map = maps[region];
let bin;
if (map.input instanceof Buffer)
bin = map.input;
else {
let file;
let layout;
if (typeof map.input === 'string')
file = map.input;
else
({file, layout} = map.input);
bin = fs.readFileSync(path.join(srcdir, file));
if (layout)
bin = encode_gfx(bin, layout);
}
if (map.transform)
bin = map.transform(bin);
bins[region] = bin;
}
const dstdir = fs.mkdtempSync(path.join(os.tmpdir(), name));
for (const region in maps) {
let bin = bins[region];
let {output} = maps[region];
if (typeof output === 'string') {
output = [output];
bin = [bin];
}
if (!Array.isArray(bin))
bin = split(bin, bin.length/output.length);
for (let i = 0; i < output.length; i++)
fs.writeFileSync(path.join(dstdir, output[i]), bin[i]);
}
zip(name, dstdir);
for (const f of fs.readdirSync(dstdir))
fs.unlinkSync(path.join(dstdir, f));
fs.rmdirSync(dstdir);
console.log(`saved as ${name}.zip.`);
}
// from https://github.com/mamedev/mame/blob/b888b8c4edaeccea889b97e1c2df6f914ae6e303/src/mame/drivers/sprint2.cpp
// license:BSD-3-Clause
// copyright-holders:Mike Balfour
const SPRINT2_TILE_LAYOUT = {
width: 8,
height: 8,
total: 64,
planes: 1,
planeoffset: [0],
xoffset: [0, 1, 2, 3, 4, 5, 6, 7],
yoffset: [0x00, 0x08, 0x10, 0x18, 0x20, 0x28, 0x30, 0x38],
charincrement: 0x40
};
const SPRINT2_CAR_LAYOUT = {
width: 16,
height: 8,
total: 32,
planes: 1,
planeoffset: [0],
xoffset: [0x7, 0x6, 0x5, 0x4, 0x3, 0x2, 0x1, 0x0,
0xf, 0xe, 0xd, 0xc, 0xb, 0xa, 0x9, 0x8],
yoffset: [0x00, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70],
charincrement: 0x80
};
function asteroid_mame2003(srcdir)
{
convert_roms('asteroid', srcdir, {
cpu1: {
input: 'Asteroids.bin',
output: ['035145.02', '035144.02', '035143.02', '035127.02']
}
});
}
function astdelux_mame2003(srcdir)
{
convert_roms('astdelux', srcdir, {
cpu1: {
input: 'Asteroids Deluxe.bin',
output: ['036430.02', '036431.02', '036432.02',
'036433.03', '036800.02', '036799.01']
}
});
}
function bwidow_mame2003(srcdir)
{
convert_roms('bwidow', srcdir, {
cpu1: { // wrong checksums
input: 'Black Widow.bin',
output: ['136017.107', '136017.108', '136017.109', '136017.110',
'136017.101', '136017.102', '136017.103', '136017.104',
'136017.105', '136017.106'],
transform: bin => {
const a = split_at(bin, 0x0800);
return [a[0], ...split(a[1], 0x1000)];
}
}
});
}
function gravitar_mame2003(srcdir)
{
convert_roms('gravitar', srcdir, {
cpu1: {
input: 'Gravitar.bin',
output: ['136010.210', '136010.207', '136010.208', '136010.309',
'136010.301', '136010.302', '136010.303', '136010.304',
'136010.305', '136010.306'],
transform: bin => {
const a = split_at(bin, 0x0800);
return [a[0], ...split(a[1], 0x1000)];
}
}
});
}
function llander_mame2003(srcdir)
{
convert_roms('llander', srcdir, {
cpu1: {
input: 'Lunar Lander.bin',
output: ['034599.01', '034598.01', '034597.01', '034572.02',
'034571.02', '034570.01', '034569.02']
}
});
}
function mhavoc_mame2003(srcdir)
{
convert_roms('mhavoc', srcdir, {
program: { // wrong checksums
input: 'Major Havoc.bin',
output: ['136025.210', '136025.216', '136025.217'],
transform: bin => {
const a = split_at(bin, 0x1000);
return [Buffer.concat([a[0],a[0]]), ...split(a[1], 0x4000)];
}
},
alpha: {
input: 'Major Havoc alpha banks.bin',
output: ['136025.215', '136025.318']
},
vector: {
input: 'Major Havoc vector banks.bin',
output: ['136025.106', '136025.107']
},
gamma: {
input: 'Major Havoc gamma.bin',
output: ['136025.108']
}
});
}
function missile_mame2003(srcdir)
{
convert_roms('missile', srcdir, {
cpu1: {
input: 'Missile Command.bin',
output: ['035820.02', '035821.02', '035822.02',
'035823.02', '035824.02', '035825.02']
}
});
}
function redbaron_mame2003(srcdir)
{
convert_roms('redbaron', srcdir, {
cpu1: {
input: 'Red Baron.bin',
output: ['037587.01', '037000.01e', '036998.01e', '036997.01e',
'036996.01e', '036995.01e', '037006.01e', '037007.01e'],
transform: bin => {
const a = split(bin, 0x800);
return [Buffer.concat([a[0],a[2]]), a[1], ...a.slice(3)]
}
}
});
}
function spacduel_mame2003(srcdir)
{
convert_roms('spacduel', srcdir, {
cpu1: {
input: 'Space Duel.bin',
output: ['136006.106', '136006.107', '136006.201', '136006.102',
'136006.103', '136006.104', '136006.105'],
transform: bin => {
const a = split_at(bin, 0x0800);
return [a[0], ...split(a[1], 0x1000)];
}
}
});
}
function sprint2_mame2000(srcdir)
{
convert_roms('sprint2', srcdir, {
cpu1: {
input: 'Sprint2.bin',
output: ['6290-01.b1', '6291-01.c1', '6404sp2.d1', '6405sp2.e1']
},
gfx1: {
input: 'Sprint2Tiles.bmp',
output: ['6396-01.p4', '6397-01.r4'],
transform: bin => {
bin = encode_gfx(reverse_bmp(bin), SPRINT2_TILE_LAYOUT);
return split_nibbles(bin);
}
},
gfx2: {
input: 'Sprint2Sprites.bmp',
output: ['6399-01.j6', '6398-01.k6'],
transform: bin => {
bin = encode_gfx(reverse_bmp(bin), SPRINT2_CAR_LAYOUT);
return split_nibbles(bin);
}
}
});
}
function sprint2(srcdir)
{
convert_roms('sprint2', srcdir, {
maincpu: {
input: 'Sprint2.bin',
output: ['6290-01.b1', '6291-01.c1', '6404.d1', '6405.e1']
},
gfx1: {
input: 'Sprint2Tiles.bmp',
output: ['6396-01.p4', '6397-01.r4'],
transform: bin => {
bin = encode_gfx(reverse_bmp(bin), SPRINT2_TILE_LAYOUT);
return split_nibbles(bin);
}
},
gfx2: {
input: 'Sprint2Sprites.bmp',
output: ['6399-01.j6', '6398-01.k6'],
transform: bin => {
bin = encode_gfx(reverse_bmp(bin), SPRINT2_CAR_LAYOUT);
return split_nibbles(bin);
}
},
// missing
proms: {
input: Buffer.alloc(0x120),
output: ['6400-01.m2', '6401-01.e2'],
transform: bin => split_at(bin, 0x100)
}
});
}
function tempest3_mame2003(srcdir)
{
convert_roms('tempest3', srcdir, {
cpu1: {
input: 'Tempest.bin',
output: ['237.002', '136.002', '235.002',
'134.002', '133.002', '138.002']
}
});
}
const srcdir = process.argv[2] || '';
if (fs.existsSync(path.join(srcdir, 'Asteroids.bin'))) {
asteroid_mame2003(srcdir);
astdelux_mame2003(srcdir);
bwidow_mame2003(srcdir);
gravitar_mame2003(srcdir);
llander_mame2003(srcdir);
mhavoc_mame2003(srcdir);
missile_mame2003(srcdir);
redbaron_mame2003(srcdir);
spacduel_mame2003(srcdir);
sprint2(srcdir);
tempest3_mame2003(srcdir);
}
else
console.log('Usage: node avault2mame.js [ROM directory]');
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment