Close

From one game to a library

A project log for The Arcade Cabinet That Runs Nothing

An arcade cabinet built around an M5Stack AtomS3R that plays 27 DOS games and DOOM, and runs a demo reel of 41 titles including full-motion

juha-liljaJuha Lilja 2 hours ago0 Comments

The client didn't change. It still just connects and draws. First it was DOOM. Then it was a menu of 20-odd DOS titles, and picking one spins up a real DOSBox on the server, scaled down and streamed over, with input going back the other way. DOSBox itself could never fit on an ESP32-S3. A 6DOF flight sim and a DOS platformer are the same amount of "running on the ESP32," which is zero.

The menu isn't a firmware feature either. It's just another frame source that draws a list at 128x128. When you pick a game the source swaps underneath, the picture changes shape from 128x128 to 320x200, and the server rebuilds its scaler and swaps in that game's control profile. The device gets rectangles the whole time.

The DOS side is js-dos in Node, no browser. Each game is a separate Node process that grabs frames from the emulator and pushes them to the Python service. If the socket is backed up it drops the frame, because only the newest one matters:

ci.events().onFrame((rgb, rgba) => {  sendFrame(rgb || rgba, width, height, rgb ? 3 : 4);
});

function sendFrame(buf, width, height, channels) {  if (!socketWritable) {    framesDropped++;    return;  }  // 16-byte header, then the raw RGB  socketWritable = sock.write(header) &&                   sock.write(Buffer.from(buf.buffer, buf.byteOffset, buf.length));
}

One trap there: ci.sendKeyEvent() wants GLFW key codes, not DOSBox's own key numbering. Left arrow is 263, Enter 257, Esc 256, left Ctrl 341. My first table used DOSBox's ordinals and nothing responded.

Bundles get built straight from eXoDOS. from-exodos.py opens the game's zip, reads that game's own dosbox.conf, pulls the launch command and the cd into a subfolder out of [autoexec], and writes a .jsdos bundle with cycles=max. The zip writer is hand-rolled in under 90 lines of Node so the project doesn't need a zip dependency. That turned into a bug: I didn't write directory entries, and the emulator creates a file's parent folder but not its grandparent. So Wolfenstein 3D, which eXoDOS keeps two levels down in WOLF3D/WOLF3D/, died on load with WOLF3D/HELP: No such file or directory. The fix is to emit every parent directory before the first file:

for (const f of files) {  const parts = f.rel.split("/");  for (let i = 1; i < parts.length; i++) {    const dir = parts.slice(0, i).join("/") + "/";    if (!seen.has(dir)) { seen.add(dir); dirs.push({ rel: dir, data: Buffer.alloc(0), dir: true }); }  }
}

Discussions