Skip to content

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 ​

bash
pnpm add node-mgba

Quickstart ​

typescript
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 / ModelEmulation & ControlsDirect Memory (read8, readBatch, slice)Memory Snapshots (observe, GamePlugin.getState())
Game Boy (DMG / SGB)Supported (160×144)SupportedSupported
Game Boy Color (CGB)Supported (160×144)SupportedSupported
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: ... })) and GamePlugin.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 ​

typescript
// 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 ​

typescript
// 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 ​

typescript
// 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 ​

typescript
// 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 ​

typescript
// 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 ​

typescript
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 ​

typescript
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 ​

typescript
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 ​

ImportDescription
node-mgbaCore emulator facade, controls, memory, and media sinks
node-mgba/pluginsPlugin base class (GamePlugin) and built-in plugins (PokemonRedBluePlugin)
node-mgba/schemaDeclarative binary schema DSL for defining RAM decoders
node-mgba/testingIn-memory MockMemoryReader for unit testing decoders
node-mgba/browserBrowser media playback helpers (WebAudioPlayer, CanvasRenderer)

Next steps ​