Skip to content

Repository files navigation

node-mgba

CI npm version License: MIT Platform: Linux x64

Scriptable, headless mGBA emulator for Node.js — like PyBoy, but powered by libmgba for Game Boy, GBC, and GBA.

node-mgba provides native Node.js bindings to the C libmgba core. It is built for scripting, agent automation, and machine learning research, giving you direct programmatic control over the emulator:

  • Headless & fast: Runs without a GUI window at 1,000+ to 3,400+ FPS.
  • Controls & state: Step frames, inject button inputs, and save/load state in memory or to disk.
  • Direct memory access: Read and write directly to memory buses (WRAM, VRAM, HRAM) without socket overhead.
  • Screen & audio capture: Grab raw pixel buffers, encode to PNG/WebP, or stream audio/video frames.
  • Plugin decoders: Optional high-level memory decoders (includes Pokémon Red/Blue state parsing).

Originally built to power the 24/7 autonomous agent on Gemini Plays Pokémon (web viewer).

Installation

pnpm add node-mgba

Quickstart

import { Mgba } from 'node-mgba';

// Load ROM (supports .gb, .gbc, .gba)
const emu = await Mgba.load('./game.gb');

// Advance frames and send input
await emu.controls.tick(60);
await emu.controls.press('A');

// Read memory
const playerX = await emu.memory.read8(0xD362);

// Capture screenshot buffer
const pngBuffer = await emu.screen.toPng();

// Close when done
await emu.close();

Console Support & Limitations

Platform / Model Emulation & Controls Direct Memory (read8, readBatch, slice) Memory Snapshots (observe, GamePlugin.getState())
Game Boy (DMG / SGB) Supported (160×144) Supported Supported
Game Boy Color (CGB) Supported (160×144) Supported Supported
Game Boy Advance (AGB) Supported (240×160) Supported (EWRAM, IWRAM, ROM) Planned on Roadmap

Note on GBA Memory Snapshots: GBA emulation, controls, audio/video streaming, savestates, and direct memory reads are supported. Multi-region snapshots (emu.observe({ memory: ... }) and GamePlugin.getState()) are currently limited to Game Boy (DMG/CGB/SGB) models and planned for GBA.


Common Tasks

Input & Frame Stepping

// Press returns a TurnResult with keyframes
const turnResult = await emu.controls.press('A', 8);
console.log(`Captured ${turnResult.keyframes.length} keyframes`);

// Hold a button across multiple ticks
await emu.controls.hold('B');
await emu.controls.tick(30);
await emu.controls.release('B');

// Run an input sequence
const sequenceResult = await emu.controls.sequence([
    { type: 'press', button: 'UP', holdFrames: 6, releaseFrames: 4 },
    { type: 'wait', frames: 10 },
    { type: 'press', button: 'A', holdFrames: 6, releaseFrames: 4 },
]);

Screen Capture & Cropping

// Capture full screen as PNG or WebP
const pngBuffer = await emu.screen.toPng();
const webpBuffer = await emu.screen.toWebp({ quality: 85 });

// Crop a sub-region with optional integer scaling (e.g. 2x, 4x)
const croppedPng = await emu.screen.crop({
    x: 16,
    y: 16,
    width: 32,
    height: 32,
    scale: 2, // 64x64 output
});

Reading & Writing Memory

// Read unsigned integers
const byte = await emu.memory.read8(0xC000);
const u16 = await emu.memory.read16LE(0xC001);
const u32 = await emu.memory.read32LE(0xC003);

// Read GBA regions directly
const ewram = await emu.memory.readRegion('EWRAM', 0, 64);
const iwram = await emu.memory.readRegion('IWRAM', 0, 64);

// Write to memory
await emu.memory.write8(0xC500, 0x42);

// Read multiple addresses in one call
const [x, y, mapId] = await emu.memory.readBatch([0xD362, 0xD361, 0xD35E]);

Savestates

// Save and load state files
await emu.states.saveToFile('./save.state');
await emu.states.loadFromFile('./save.state');

// In-memory state handles
const handle = await emu.states.save();
await emu.states.restore(handle);

Waiting for In-Game Conditions

// Wait until memory matches a condition (or timeout is reached)
await emu.waitFor({
    timeoutFrames: 300,
    condition: async (instance) => {
        const battleStatus = await instance.memory.read8(0xD057);
        return battleStatus !== 0;
    },
});

Video & Audio Streaming

import { Mgba, WebSocketMediaSink, FfmpegRecordingSink, maskToButtonNames } from 'node-mgba';

// Stream video and audio chunks over WebSocket clients
const wsSink = new WebSocketMediaSink({
    name: 'live-stream',
    clients: () => wss.clients,
});

// Or record gameplay directly to an MP4 file
const recorder = new FfmpegRecordingSink({
    outputPath: './gameplay.mp4',
    fps: 60,
});

// Custom MediaSink with per-frame button input tracking
const customSink = {
    name: 'input-tracking-sink',
    onVideoFrame: (packet) => {
        // packet.keys contains the 32-bit button bitmask active on this exact frame
        const buttons = maskToButtonNames(packet.keys);
        console.log(`Frame #${packet.frameIndex} rendered with active buttons:`, buttons);
    },
    onAudioChunk: (chunk) => {
        // stereo PCM audio
    },
};

// Pass sinks when loading the emulator
const emu = await Mgba.load('./game.gb', {
    mediaSinks: [wsSink, recorder, customSink],
});

Game Plugins & State Decoding

import { PokemonRedBluePlugin } from 'node-mgba/plugins';

// Attach game plugin
const pokemon = await emu.use(PokemonRedBluePlugin);

// Retrieve structured game state
const state = await pokemon.getState();
console.log(state.player.position, state.party);

Real-Time Emulation & Autonomous Agent Loops

import { EmulatorController, GB_FPS } from 'node-mgba';

// Launch autonomous 59.73 FPS real-time execution in a worker actor
const controller = new EmulatorController({
    romPath: './game.gb',
    realtime: true,
    fps: GB_FPS,
});

await controller.initialize();

// Send interactive key inputs or enqueue AI button presses
await controller.pressButtons(['START']);

// Stream 60 FPS video frames
controller.on('frame', (frame) => {
    // Handle VideoPacket (RGBA pixel buffer)
});

Performance

Measured on AMD Ryzen 9 5950X with Pokémon Blue (pnpm run bench):

Metric Throughput Avg Latency Notes
Headless Stepping ~1,110 FPS 0.90 ms / frame Non-blocking worker_threads RPC
Direct Core Stepping ~3,420 FPS 0.29 ms / frame In-process native core
Single Memory Read/Write ~10,000 ops/s ~100 µs / op Direct WRAM bus access
Batch Memory Read (50 addrs) ~6,390 batches/s 0.16 ms / batch Single IPC round-trip
Schema DSL Struct Decode ~960,690 ops/s 1.0 µs / struct In-memory binary parser
Full Game State Decode ~22,270 decodes/s 44.9 µs / decode Populated party, bag, map, box
Savestate Restore ~2,280 restores/s 0.44 ms / restore Cycle-accurate state handle

For full benchmarks and methodology, see the Performance & Benchmarks Guide.


Development & Testing

Running Tests

Tests run against bundled homebrew fixtures by default:

# Run test suite
pnpm test

# Run Pokémon Red/Blue plugin tests with a ROM dump
export POKEMON_ROM_PATH="/path/to/pokemon_blue.gb"
pnpm test

# Override test ROM or savestate path
export ROM_PATH="/path/to/game.gb"
export SAVESTATE_PATH="/path/to/game.ss0"
pnpm test

Environment Variables

Variable Default Description
MGBA_LOG_LEVEL warn Native mGBA log level (silent, fatal, error, warn, info, debug).
POKEMON_ROM_PATH Path to Pokémon Red/Blue ROM for pokemon_red_blue.test.ts.
ROM_PATH Bundled homebrew fixtures Test ROM path (.gb, .gbc, or .gba).
SAVESTATE_PATH Bundled homebrew fixture Savestate fixture path (.ss0) for savestate tests.

Interactive Web GUI Studio

node-mgba includes a built-in Vue 3 web interface for debugging emulation, testing input sequences, and inspecting real-time RAM decoding:

# Build the native shim, TypeScript, and GUI bundle
pnpm run build

# Launch the GUI server with a Game Boy ROM (opens at http://localhost:3456)
ROM_PATH="/path/to/game.gb" pnpm run gui

# Or run the GUI frontend in Vite development mode with hot-reloading
pnpm run gui:dev

Note: The GUI's state inspector and telemetry HUD panels are currently tailored specifically for Pokémon Red & Blue (displaying real-time party stats, inventory, badges, and map coordinates).


Subpath Exports

Import Description
node-mgba Core emulator facade, controls, memory, and media sinks
node-mgba/plugins Plugin base class (GamePlugin) and built-in plugins (PokemonRedBluePlugin)
node-mgba/schema Declarative binary schema DSL for defining RAM decoders
node-mgba/testing In-memory MockMemoryReader for unit testing decoders
node-mgba/browser Browser media playback helpers (WebAudioPlayer, CanvasRenderer)

Documentation

About

Scriptable, headless mGBA emulator for Node.js — like PyBoy, but powered by `libmgba` for Game Boy, GBC, and GBA.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages