Skip to content

Instantly share code, notes, and snippets.

@WinterSnowfall
Created December 11, 2022 22:59
Show Gist options
  • Save WinterSnowfall/7d87ed156fb65896fdba6e925e54bb2a to your computer and use it in GitHub Desktop.
Save WinterSnowfall/7d87ed156fb65896fdba6e925e54bb2a to your computer and use it in GitHub Desktop.
An adaptation of https://gist.github.com/cxx/6d1d44ce4a6107ed80e0a6c8c5b887c4 for the Atari 50: The Anniversary Celebration
// atari502mame.js - convert some of the arcade game data in Atari 50: The Anniversary Celebration to older MAME ROM sets
//
// Usage:
// node atari502mame.js [ROM directory]
//
// 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)
// - Missile Command (MAME 2003, loads with warnings, but appears playable)
// - Tempest (MAME 2003)
//
// Only partially extracted:
// - Black Widow (MAME 2003, unplayable)
// - Gravitar (MAME 2003, unplayable)
// - Lunar Lander (MAME 2003, unplayable)
// - Space Duel (MAME 2003, unplayable)
//
// Not supported:
// - AkkaArrh
// - Centipede
// - Cloak and Dagger
// - Crystal Castles
// - Liberator
// - Major Havok
// - MazeInvaders
// - Millipede
// - Super Breakout
// - Warlords
//
// Changelog:
// - 2022-12-12: Adapt Atari Vault script for Atari 50: The Anniversary Celebration
// - 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.rom',
output: ['035145.02', '035144.02', '035143.02', '035127.02']
}
});
}
function astdelux_mame2003(srcdir)
{
convert_roms('astdelux', srcdir, {
cpu1: {
input: 'AsteroidsDeluxe.rom',
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: 'BlackWidow.rom',
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.rom',
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: 'LunarLander.rom',
output: ['034599.01', '034598.01', '034597.01', '034572.02',
'034571.02', '034570.01', '034569.02']
}
});
}
function missile_mame2003(srcdir)
{
convert_roms('missile', srcdir, {
cpu1: {
input: 'MissileCommand.rom',
output: ['035820.02', '035821.02', '035822.02',
'035823.02', '035824.02', '035825.02']
}
});
}
function spacduel_mame2003(srcdir)
{
convert_roms('spacduel', srcdir, {
cpu1: {
input: 'SpaceDuel.rom',
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 tempest3_mame2003(srcdir)
{
convert_roms('tempest3', srcdir, {
cpu1: {
input: 'Tempest.rom',
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.rom'))) {
asteroid_mame2003(srcdir);
astdelux_mame2003(srcdir);
bwidow_mame2003(srcdir);
gravitar_mame2003(srcdir);
llander_mame2003(srcdir);
missile_mame2003(srcdir);
spacduel_mame2003(srcdir);
tempest3_mame2003(srcdir);
}
else
console.log('Usage: node atari502mame.js [ROM directory]');
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment