Files
Ludarium-Public/fixtures/synthetic/browserplay/generate-fixtures.mjs
T
Ludarium release export df869819ce
Public source validation / source (push) Successful in 2m16s
Publish Ludarium source
2026-09-03 02:08:58 +02:00

171 lines
10 KiB
JavaScript

import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { gunzipSync } from "node:zlib";
import { createHash } from "node:crypto";
// libdragon open-source IPL3 compatibility bootcode, pinned to commit
// 35f85a0797324a5ed0c723203e33ab3c1da94fdd (Unlicense), SHA-256
// f522db2e31a701f82597f399e76d55c9487760d015aff9d90176b463ae39a068.
const libdragonIpl3 = gunzipSync(Buffer.from("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", "base64"));
const output = process.argv[2];
if (!output) throw new Error("output directory is required");
mkdirSync(output, { recursive: true });
const expected = new Map([
["Ludarium SNES Fixture.sfc", "c5458603faf6e04e550d73c4724bb948fb75c5b54bf24280a006501c2ace52ed"],
["Ludarium GB Fixture.gb", "7ed74ff088ce50d18ecd37847e3c0b81774e9b54e254149fc6502ed8ec084870"],
["Ludarium GBC Fixture.gbc", "59111888ffff8c92c3f5bbd6bbe0f9182a7a53c634ec6b00d2ce8d9bd49f9f5a"],
["Ludarium GBA Fixture.gba", "5a393a7b3a5b9beeae7f9a5e0495f1669e1855dcf6379a73bd97e7a974787813"],
["Ludarium N64 Fixture.z64", "4f6e7fcb1962f02e59b9f896591254e9f1b68273e2179fc7a0c26e681500c326"],
["Ludarium NDS Fixture.nds", "b797bfaa0e5e20cf2091f8268975bfa3cb820dd88c2e06134f0c9ce2fb1180cf"],
["Ludarium PSP Fixture.pbp", "2ff952ec933c67ee6911049d77201da572aa7241e3f2ee7ff935ed61646964f4"],
["Ludarium PSX Fixture.chd", "58e9754dd2c868d091cbd2401182b5026e436a7b199437fb7dfab039fccb89e7"],
["Ludarium Genesis Fixture.md", "b07f1e19de6d517399ee398295e08d4b94f3cad8f5b0470b46ce37bdae0d508e"],
["Ludarium Master System Fixture.sms", "0302cdf0f3e67488cf2ad11b655ea73d0c66e60b9e35153e0fe2b626cf358a0e"],
["Ludarium Game Gear Fixture.gg", "f5fb90060122ec533a2200103b4d78c254f6bce72deb04ca11c065cb1d61af17"]
]);
const write = (name, bytes) => {
const digest = createHash("sha256").update(bytes).digest("hex");
if (digest !== expected.get(name)) throw new Error(`${name} checksum changed: ${digest}`);
writeFileSync(join(output, name), bytes, { mode: 0o444 });
};
const ascii = (buffer, offset, value, length) => buffer.write(value.slice(0, length), offset, "ascii");
const u32le = (buffer, offset, value) => buffer.writeUInt32LE(value >>> 0, offset);
const u32be = (buffer, offset, value) => buffer.writeUInt32BE(value >>> 0, offset);
const crc16 = bytes => {
let crc = 0xffff;
for (const byte of bytes) {
crc ^= byte;
for (let bit = 0; bit < 8; bit++) crc = (crc & 1) ? (crc >>> 1) ^ 0xa001 : crc >>> 1;
}
return crc;
};
const pspFixture = gunzipSync(readFileSync(join(import.meta.dirname, "psp", "ludarium-psp-fixture.pbp.gz")));
const psxFixture = gunzipSync(readFileSync(join(import.meta.dirname, "psx", "ludarium-psx-fixture.chd.gz")));
function gameBoy(color) {
const rom = Buffer.alloc(32 * 1024, 0);
rom.set([0xc3, 0x50, 0x01], 0x100); // JP $0150; no boot ROM or native code is required.
rom.set([0xce, 0xed, 0x66, 0x66], 0x104); // Deterministic cartridge signature used by the analyzer.
ascii(rom, 0x134, color ? "LUDARIUM GBC" : "LUDARIUM GB", 15);
rom[0x143] = color ? 0x80 : 0x00;
rom[0x147] = 0x00; // ROM-only cartridge.
rom[0x148] = 0x00; // 32 KiB.
rom[0x149] = 0x00;
rom.set([0xf3, 0x31, 0xfe, 0xff, 0x18, 0xfe], 0x150); // DI; LD SP,$FFFE; JR forever.
let checksum = 0;
for (let index = 0x134; index <= 0x14c; index++) checksum = (checksum - rom[index] - 1) & 0xff;
rom[0x14d] = checksum;
return rom;
}
function snes() {
const rom = Buffer.alloc(32 * 1024, 0);
rom.set([0x78, 0x18, 0xfb, 0xc2, 0x30, 0x80, 0xfe], 0); // Native-mode loop at $008000.
ascii(rom, 0x7fc0, "LUDARIUM SNES FIXTURE", 21);
rom[0x7fd5] = 0x20; // LoROM, slow ROM.
rom[0x7fd6] = 0x00;
rom[0x7fd7] = 0x05;
rom[0x7fd9] = 0x01;
for (const vector of [0x7fea, 0x7fec, 0x7fee, 0x7ffa, 0x7ffc, 0x7ffe]) rom.writeUInt16LE(0x8000, vector);
let checksum = 0;
for (const byte of rom) checksum = (checksum + byte) & 0xffff;
rom.writeUInt16LE(checksum ^ 0xffff, 0x7fdc);
rom.writeUInt16LE(checksum, 0x7fde);
return rom;
}
function gba() {
const rom = Buffer.alloc(32 * 1024, 0);
u32le(rom, 0, 0xeafffffe); // ARM B .
rom.set([0x24, 0xff, 0xae, 0x51], 0x04); // Deterministic cartridge signature used by the analyzer.
ascii(rom, 0xa0, "LUDARIUMGBA", 12);
ascii(rom, 0xac, "LUDE", 4);
ascii(rom, 0xb0, "00", 2);
rom[0xb2] = 0x96;
let checksum = 0;
for (let index = 0xa0; index <= 0xbc; index++) checksum = (checksum - rom[index]) & 0xff;
rom[0xbd] = (checksum - 0x19) & 0xff;
return rom;
}
function n64() {
const rom = Buffer.alloc(1024 * 1024, 0);
libdragonIpl3.copy(rom, 0);
u32be(rom, 8, 0x80000400);
u32be(rom, 0x10, 8);
ascii(rom, 0x20, "LUDARIUM N64 FIXTURE", 20);
ascii(rom, 0x3b, "LU", 2);
rom[0x3e] = 0x45;
u32be(rom, 0x1000, 0x1000ffff); // MIPS BEQ $zero,$zero,-1.
u32be(rom, 0x1004, 0x00000000);
return rom;
}
function nds() {
const rom = Buffer.alloc(8 * 1024, 0);
ascii(rom, 0x00, "LUDARIUM NDS", 12);
ascii(rom, 0x0c, "LUDE", 4);
ascii(rom, 0x10, "00", 2);
u32le(rom, 0x20, 0x200); u32le(rom, 0x24, 0x02000000); u32le(rom, 0x28, 0x02000000); u32le(rom, 0x2c, 4);
u32le(rom, 0x30, 0x400); u32le(rom, 0x34, 0x02380000); u32le(rom, 0x38, 0x02380000); u32le(rom, 0x3c, 4);
u32le(rom, 0x80, rom.length); u32le(rom, 0x84, 0x200);
rom.set([0x24, 0xff, 0xae, 0x51], 0xc0); // Deterministic cartridge signature used by the analyzer.
u32le(rom, 0x200, 0xeafffffe); u32le(rom, 0x400, 0xeafffffe);
rom.writeUInt16LE(crc16(rom.subarray(0, 0x15e)), 0x15e);
return rom;
}
// Sega hardware. A Mega Drive cartridge is identified by its console name at $100 and boots from the
// 68000 vector table at $0; an 8-bit Sega ROM is identified by the "TMR SEGA" header near the end of
// its first bank, whose region nibble separates Master System from Game Gear.
function megaDrive() {
const rom = Buffer.alloc(64 * 1024, 0);
u32be(rom, 0x00, 0x00fffe00); // Initial stack pointer.
u32be(rom, 0x04, 0x00000200); // Reset vector -> $200.
for (let vector = 0x08; vector < 0x100; vector += 4) u32be(rom, vector, 0x00000200);
ascii(rom, 0x100, "SEGA MEGA DRIVE ", 16);
ascii(rom, 0x110, "(C)LUDARIUM 2026", 16);
ascii(rom, 0x120, "LUDARIUM GENESIS FIXTURE".padEnd(48), 48);
ascii(rom, 0x150, "LUDARIUM GENESIS FIXTURE".padEnd(48), 48);
ascii(rom, 0x180, "GM 00000000-00", 14);
u32be(rom, 0x1a0, 0x00000000);
u32be(rom, 0x1a4, rom.length - 1);
u32be(rom, 0x1a8, 0x00ff0000);
u32be(rom, 0x1ac, 0x00ffffff);
ascii(rom, 0x1f0, "JUE", 3);
rom.writeUInt16BE(0x60fe, 0x200); // BRA.S * — a deterministic idle loop.
let checksum = 0;
for (let index = 0x200; index < rom.length; index += 2) checksum = (checksum + rom.readUInt16BE(index)) & 0xffff;
rom.writeUInt16BE(checksum, 0x18e);
return rom;
}
function segaEightBit(gameGear) {
const rom = Buffer.alloc(32 * 1024, 0);
rom.set([0xf3, 0xed, 0x56, 0x18, 0xfe], 0x00); // DI; IM 1; JR * — a deterministic idle loop.
ascii(rom, 0x7ff0, "TMR SEGA", 8);
rom[0x7ffc] = 0x23; // Product code, low BCD pair.
rom[0x7ffd] = 0x01; // Product code, high BCD pair.
rom[0x7ffe] = 0x51; // Top product digit and revision 1.
rom[0x7fff] = (gameGear ? 0x60 : 0x40) | 0x0c; // Region nibble plus the 32 KiB size nibble.
let checksum = 0;
for (let index = 0; index < 0x7ff0; index++) checksum = (checksum + rom[index]) & 0xffff;
rom.writeUInt16LE(checksum, 0x7ffa);
return rom;
}
write("Ludarium SNES Fixture.sfc", snes());
write("Ludarium GB Fixture.gb", gameBoy(false));
write("Ludarium GBC Fixture.gbc", gameBoy(true));
write("Ludarium GBA Fixture.gba", gba());
write("Ludarium N64 Fixture.z64", n64());
write("Ludarium NDS Fixture.nds", nds());
write("Ludarium Genesis Fixture.md", megaDrive());
write("Ludarium Master System Fixture.sms", segaEightBit(false));
write("Ludarium Game Gear Fixture.gg", segaEightBit(true));
write("Ludarium PSP Fixture.pbp", pspFixture);
write("Ludarium PSX Fixture.chd", psxFixture);