Last active
April 6, 2021 13:27
-
-
Save cxx/6d1d44ce4a6107ed80e0a6c8c5b887c4 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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