Getting started
node-mgba gives Node.js direct control over libmgba, the emulation core of mGBA, through native bindings. There's no window and no socket in between: your code steps frames, presses buttons, reads memory and captures the screen itself. It needs Node.js 20 or newer on x64 Linux or Windows.
Installation
pnpm add node-mgbaQuickstart
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 (32-bit bus & all 9 regions) | Partial (planned on roadmap) |
Note on GBA Memory Snapshots: GBA emulation, controls, audio/video streaming, savestates, direct 32-bit bus reads, and custom observation queries (
emu.observe({ reads, slices })) are supported across all memory spaces (EWRAM,IWRAM,VRAM,SRAM,OAM,IO,PALETTE,BIOS,ROM). Multi-region bus snapshotting (emu.memory.snapshot(),emu.observe({ memory: ... })) andGamePlugin.getState()currently target Game Boy (DMG/CGB/SGB) memory layouts; full-bus GBA snapshotting and a dedicated GBA game plugin base class are planned on the roadmap.
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)
});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) |
Next steps
- Real-time emulation & agent loops: run the emulator at 59.73 FPS in a worker for livestreams and 24/7 agents.
- Writing plugins: package game-specific decoders and helpers.
- Binary schema DSL: describe game structs and decode RAM into typed objects.
- Testing decoders: unit-test decoders against mock memory, no ROM needed.
- Benchmarks: how fast each operation is, and how it was measured.