A Game Boy / Game Boy Color emulator packaged as a BBS door: callers connect over telnet/SSH and play retro games in the BBS, with CP437 block/ASCII rendering, ANSI-music or streamed-PCM sound, per-user saves, network link play, and a ROM browser built for slow, remote terminals. It runs on any BBS that launches a door over stdio or hands it a connection via a DOOR32.SYS dropfile — Synchronet, EleBBS, Mystic, and friends, on Linux, Windows, macOS, or FreeBSD.
The Game Boy emulation itself is the Apache-2.0 gameboy_core
crate, vendored and patched to expose its APU for the ANSI-music engine (see
NOTICE and vendor/gameboy_core/PATCH-NOTES.md).
What it takes to make an emulator behave as a multi-user door over a remote link. Full detail (with file/symbol pointers) is in PATCH-NOTES.md.
- Sound without a sound card — a door has no local audio device, so the
Game Boy's audio reaches the caller two ways, selectable in the menu:
ANSI (SyncTERM ANSI-music/MML — universal but monophonic) and APC
(real PCM streamed as SyncTERM audio APCs — full Game Boy audio, on a terminal
that supports them). Local sound-card playback (rodio) is an optional
localaudiobuild feature, off by default, so the distributed binary is pure-Rust with no native audio dependency. - Cross-BBS connection layer — the door talks to the caller over whatever the
BBS provides: an inherited socket named in a DOOR32.SYS dropfile (the
Windows-BBS norm, where the handle is a Winsock
SOCKET, not an fd), or stdio. Its own input decoder and raw output replace a console-only terminal library, so it works the same on a Unix pty, a Windows BBS socket, or a redirected pipe. - CP437 output layer — the menu/UI Unicode glyphs are emitted as CP437 bytes
(e.g. the
0xDFhalf-block), which is what BBS terminals expect from a door; raw UTF-8 would garble on both CP437 and UTF-8 clients. - Door-friendly launch — flags work without a positional ROM (the door is launched with no ROM and shows its browser), it reads the caller's identity from the dropfile, and drops back to the menu on quit instead of exiting.
- ROM browser rebuilt for large libraries — type-ahead jump, a Tab-cycled
All / Game Boy Color / Game Boy filter, and names with a GB/GBC type tag. The
responsive Game Boy-style menu uses 80x24 as its minimum and expands the game
list and square preview viewport on larger terminals. The ROM directory is a
sysop setting (
--roms/ config), not a caller-facing browser — no letting callers wander the server's filesystem. - Offline game-art previews — a bundled sysop tool fetches only the Libretro artwork matching installed ROMs into a local, gitignored cache. SIXEL is used only when CTerm/Primary DA advertises it; every other terminal gets an internal ANSI half-block rendering. Caller sessions never make web requests.
- Per-user state — render mode, sound, and filter preferences persist per BBS
user, and battery saves are isolated under
.saves/<user>/. - Link backpressure pacing — the transmit frame rate is capped (default
20 fps,
--fps N) and frames are skipped when a slow/congested link can't keep up, so the door degrades gracefully instead of flooding the connection. - Live resize — a door connection delivers no
SIGWINCH/resize events and a door pty's size is frozen at launch, so terminal size is tracked by probing with a cursor-position report.
Prebuilt, dependency-free binaries are attached to each
release — Linux (x86_64 / arm64 / armv7 / i686, static), Windows
(x86_64 / i686), macOS (arm64 / x86_64), and FreeBSD (x86_64). No libasound2 or
other runtime libs required.
- Unpack the archive for your platform into a directory under your BBS's external
programs (e.g.
xtrn/gb/), so you have…/gb/lameboy. - Drop your own legally-obtained
.gb/.gbcROMs into aroms/folder beside the binary. - Optional: populate local menu artwork for those ROMs:
python3 tools/fetch_game_art.py. Existing images are skipped, and artwork is not included in or required by the door distribution. - Add the door in your BBS's door/external-program config (SCFG on Synchronet, the door manager on EleBBS/Mystic/…) with the matching command line below, then recycle/restart the BBS.
Verify a download against SHA256SUMS.txt from the release.
The only argument that matters is how the caller is connected; everything else
has sane defaults (and a lameboy.ini, below). The two setups below cover most
BBSes — the flag reference is further down.
Add it in SCFG → External Programs → Online Programs. The executable here is
the released lameboy.exe (rename it to anything you like). The simplest
setup uses the Standard I/O method (stdio) and --user %4 — no drop file:
[gameboy]
1: Name ........................ Game Boy
2: Internal Code ............... GAMEBOY
3: Start-up Directory .......... c:\sbbs\xtrn\gb
4: Command Line ................ lameboy.exe --user %4
5: Clean-up Command Line .......
6: Execution Cost .............. None
7: Access Requirements .........
8: Execution Requirements ......
9: Multiple Concurrent Users ... Yes
10: Native Executable ........... Yes
11: I/O Method .................. Standard
12: Use Shell or New Context .... No
13: Modify User Data ............ No
14: Execute on Event ............ No
15: Pause After Execution ....... No
16: Disable Local Display ....... No
17: BBS Drop File Type .......... DOOR32.SYS
18: Place Drop File In .......... Node Directory
19: Time Options...
%4 is the zero-padded user number; it keys per-user saves and preferences.
Prefer a socket? Set line 11 I/O Method to Socket and line 4 to
lameboy.exe --dropfile %f. Leave the drop file in the Node
Directory (line 18); Synchronet expands %f to its full path, so the door
finds it regardless of the working directory. --user is then optional — the
identity is read from the drop file.
Configure a native door using the socket I/O method and a DOOR32.SYS
drop file, then point --dropfile at it:
Door type / executable ..... Native
I/O method ................. Socket
Drop file .................. DOOR32.SYS
Command line ............... lameboy --dropfile DOOR32.SYS
(or the full path to the node's DOOR32.SYS)
--dropfile must resolve to the DOOR32.SYS file — the door reads the
inherited socket handle from it. You may pass the file itself or the directory
that contains it (e.g. --dropfile .\; the bare-directory form needs v0.4.1+).
If the drop file can't be read, the door falls back to stdio — which a
socket-mode door isn't connected to, so its output never reaches the caller
(the tell-tale symptom: raw escape codes pile up on the server console while the
user sees nothing).
The sound mode (Off / ANSI / APC) is chosen by the caller in the menu and
persists per user (APC streams full PCM to terminals that support SyncTERM audio
APCs). The released build never opens a local sound device, so --mute is
unnecessary (it's still accepted).
Optional file beside the binary, so you don't repeat settings on the command
line. Copy lameboy.ini.example to lameboy.ini and edit.
Command-line flags override it:
roms = roms ; ROM directory (default: roms/ beside the binary)
fps = 20 ; transmit frame-rate cap, 5-60
ansi_music = true ; offer ANSI-music sound (false = silent/APC only)
link_server = futureland.today:7676 ; public link-play relayAll optional; each overrides lameboy.ini. Only --dropfile/--user (the
per-call connection) are normally passed by the BBS.
| Flag | Description |
|---|---|
--dropfile <path> |
DOOR32.SYS dropfile: use its inherited socket + user identity |
--user <id> |
Per-user key for saves + preferences (e.g. Synchronet %4) |
--roms <path> |
ROM directory (default: roms/ beside the binary) |
--fps <n> |
Transmit frame-rate cap, 5–60 (default 20) |
--ansi-music |
Force ANSI-music sound on (default on; disable via the ini) |
--mute |
Never open a local sound device (localaudio builds only) |
--link <host:port> |
Link-cable relay for network multiplayer |
--block / --ascii |
Force a render mode (otherwise the caller's saved choice) |
lameboy.ini.example ships with link_server = futureland.today:7676 so
callers from different BBSes can meet in the same multiplayer lobby. Leave that
enabled unless you specifically want a private relay; a larger shared relay means
more players available for link games like Pokemon trades/battles and Tetris.
To run your own relay instead, build and start the bundled link server, then
point link_server or --link at it:
cd link-server
cargo build --release
target/release/gb-link-server 7676The relay listens on 0.0.0.0, so the relay host must allow inbound TCP on that
port. For a public relay, also forward/open that port on the BBS firewall or
router. Door instances only need outbound access to the configured
host:port.
cargo build --release # door build: pure-Rust, no audio deps
cp target/release/lameboy ./lameboy # deploy next to roms/
cargo build --release --features localaudio # optional: real local sound-card audioThe default build omits rodio, so the binary is pure-Rust and links no native
audio library — that's what lets release CI cross-compile it for every target.
--features localaudio adds rodio for real playback when running locally (needs
ALSA headers on Linux: libasound2-dev). The vendored, patched
gameboy_core is built from vendor/ (see its
PATCH-NOTES.md for the APU change that powers ANSI music). target/, roms/,
and the built binary are gitignored.
Releases are built by CI on a v* tag.
Artwork is optional and strictly local at runtime. From the door directory:
python3 tools/fetch_game_art.py # box art for every ROM in roms/
python3 tools/fetch_game_art.py --kind snap # gameplay screenshots instead
python3 tools/fetch_game_art.py --match '*Kirby*' --forceThe tool maps .gb and .gbc files to the corresponding Libretro thumbnail
repositories, applies RetroArch's filename substitutions, downloads only exact
matches, skips cached files, and writes misses to art/missing.txt. Run
python3 tools/fetch_game_art.py --help for alternate ROM/cache directories,
parallelism, dry-run, and filtering options. Downloaded images under art/ are
gitignored; the repository ships only the fetcher and cache documentation.
The menu probes CTerm and xterm-family graphics capabilities. It renders a
square, bandwidth-capped SIXEL image when explicitly supported and otherwise
uses its built-in ANSI half-block renderer at the selected color depth. No
external gif2ans, img2sixel, ImageMagick, or runtime network access is needed.
| Key | Action |
|---|---|
| ↑ / ↓ | Move within the active game list or menu |
| ← / → / Tab | Switch between the Game and Menu panels |
| Space | Cycle the game filter (All → GBC → GB) in the game list |
| Type letters | Jump to a game (type-ahead) in the list |
| ← / → / Space | Change a value inside an options page |
| Z / Enter | Play the selected game / confirm |
| Esc | Back from options; otherwise quit |
| Q | Quit the door |
| Key | Action |
|---|---|
| Arrow keys | D-Pad |
| Z | A button |
| X | B button |
| Enter | Start |
| Space | Select |
| Esc / Q | Return to the menu |
Battery saves are written automatically on exit and loaded on launch. With a
--user key they are isolated under .saves/<user>/; without one they fall back
to a shared .saves/ next to the ROM.
lameboy is MIT — see LICENSE. It bundles third-party components under
their own licenses, listed in NOTICE (notably the Apache-2.0
gameboy_core emulator).
gameboy_core— Game Boy emulation core (Apache-2.0)crossterm— terminal handlingrodio— optional local sound-card playback (localaudiofeature)