@nesjs/core - NES Emulator Core Library
A complete NES (Nintendo Entertainment System) emulator core library that provides full NES hardware emulation functionality, UI framework agnostic.
Features
- 🎮 Complete NES hardware emulation (CPU, PPU, APU)
- 🎯 Support for multiple Mappers
- 🎵 Audio output support
- 🕹️ Dual controller support
- 💾 Save/Load state functionality
- 🔧 Cheat code support
- 🎨 Customizable renderer and audio interfaces
- 📝 TypeScript support
ROM Compatibility
Supported Mappers: 0, 1, 2, 3, 4, 5, 7, 9, 10, 11, 15, 16, 18, 19, 21, 22, 23, 24, 25, 26, 30, 31, 33, 34, 36, 38, 39, 41, 47, 48, 58, 60, 61, 62, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 78, 79, 85, 86, 88, 87, 89, 92, 93, 94, 97, 107, 112, 113, 115, 118, 119, 129, 140, 152, 154, 169, 180, 182, 184, 185, 195, 200, 201, 203, 206, 212, 213, 214, 225, 226, 228, 229, 231, 240, 241, 242, 244, 245, 246, 248, 249, 255.
Installation
npm install @nesjs/core
yarn add @nesjs/core
pnpm add @nesjs/core
Quick Start
Basic Usage
import { NES } from '@nesjs/core'
// Create NES instance
const nes = new NES({
audioSampleRate: 44100,
audioBufferSize: 1024,
enableCheat: true
})
// Implement renderer interface
class MyRenderer {
renderFrame(imageData: Uint8Array) {
// Handle 256x240 RGBA image data
console.log('Frame data:', imageData.length) // 245760 bytes
}
}
// Implement audio interface
class MyAudioOutput {
outputSample(sample: number) {
// Handle audio sample data
}
flushFrame() {
// Flush audio frame
}
}
// Set interfaces
nes.setRenderer(new MyRenderer())
nes.setAudioInterface(new MyAudioOutput())
// Load ROM
const romData = new Uint8Array(/* ROM file data */)
await nes.loadROM(romData)
// Run emulator
function gameLoop() {
nes.runFrame()
requestAnimationFrame(gameLoop)
}
gameLoop()
Controller Input
import { NESControllerButton } from '@nesjs/core'
// Get player gamepads
const gamepad = nes.getGamepad(1) // Player 1
const gamepad2 = nes.getGamepad(2) // Player 2
// Set button states
gamepad.setButton(NESControllerButton.A, 1) // Press A button
gamepad.setButton(NESControllerButton.A, 0) // Release A button
// Set multiple buttons
gamepad.setButtons([1, 0, 0, 0, 0, 0, 0, 0]) // Only A button pressed
// Get button states
const buttonStates = gamepad.getButtonStates()
console.log('A button state:', buttonStates.A)
Save State System
// Create save state
const saveData = nes.createSaveState()
// Load save state
const success = nes.loadSaveState(saveData)
// Binary save state (smaller file size)
const binaryData = nes.createBinarySaveState()
const loadSuccess = nes.loadBinarySaveState(binaryData)
// SRAM save
const sramData = nes.saveSRAM()
if (sramData) {
// Save SRAM data to file
localStorage.setItem('sram', JSON.stringify(sramData))
}
// Load SRAM
const savedSram = localStorage.getItem('sram')
if (savedSram) {
nes.loadSRAM(JSON.parse(savedSram))
}
Cheat Code Support
// Enable cheat functionality
await nes.enableCheat()
const cheater = nes.getCheater()
if (cheater) {
// Add cheat code
cheater.addCheat('079F-01-01')
// Enable/disable cheat
cheater.setCheatEnabled('079F-01-01', true)
// Get cheat info
const cheat = cheater.getCheat('079F-01-01')
// Clear all cheats
cheater.clearCheats()
}
API Reference
NES Class
Constructor
new NES(config?: EmulatorConfig, events?: EmulatorEvents)
Configuration Options (EmulatorConfig):
audioBufferSize?: number
- Audio buffer size, default 1024audioSampleRate?: number
- Audio sample rate, default 44100autoSaveInterval?: number
- Auto save interval (frames), default 3600enableCheat?: boolean
- Enable cheat functionality, default true
Event Callbacks (EmulatorEvents):
onFrameComplete?: (frameCount: number) => void
- Frame completion callbackonROMLoaded?: (romInfo: ROMInfo) => void
- ROM loaded callbackonError?: (error: Error) => void
- Error callback
Core Methods
setRenderer(renderer: RendererInterface)
Set the renderer interface.
interface RendererInterface {
renderFrame(imageData: Uint8Array): void
}
setAudioInterface(audioInterface: AudioOutputInterface)
Set the audio interface.
interface AudioOutputInterface {
outputSample(sample: number): void
flushFrame(): void
}
loadROM(romData: Uint8Array): Promise<void>
Load ROM file.
runFrame(): void
Run one emulation frame.
reset(): void
Reset the emulator.
getGamepad(player: 1 | 2): GamepadInterface
Get the gamepad interface for specified player.
getDebugInfo(): DebugInfo
Get debug information.
interface DebugInfo {
frameCount: number
cpuCycles: number
ppuScanline: number
mapperInfo?: string
cpu?: {
PC: number, A: number, X: number, Y: number
SP: number, P: number, cycles: number
}
ppu?: {
scanline: number, cycles: number, frame: number
}
}
GamepadInterface
setButton(button: NESControllerButton, pressed: GamepadButtonState)
Set individual button state.
setButtons(buttons: GamepadButtonState[])
Set multiple button states.
getButtonStates()
Get current state of all buttons.
reset()
Reset all buttons.
Button Constants
enum NESControllerButton {
A = 0,
B = 1,
SELECT = 2,
START = 3,
UP = 4,
DOWN = 5,
LEFT = 6,
RIGHT = 7
}
Implementation Guide
Implementing a Renderer
class CanvasRenderer implements RendererInterface {
private canvas: HTMLCanvasElement
private ctx: CanvasRenderingContext2D
constructor(canvas: HTMLCanvasElement) {
this.canvas = canvas
this.ctx = canvas.getContext('2d')!
// NES resolution
canvas.width = 256
canvas.height = 240
}
renderFrame(imageData: Uint8Array): void {
const imgData = this.ctx.createImageData(256, 240)
imgData.data.set(imageData)
this.ctx.putImageData(imgData, 0, 0)
}
}
Implementing Audio Output
class WebAudioOutput implements AudioOutputInterface {
private audioContext: AudioContext
private buffer: number[] = []
constructor() {
this.audioCtx = new AudioContext();
this.sampleRate = this.audioCtx.sampleRate;
this.samples = [];
}
outputSample(sample: number): void {
// Add sample to buffer
this.samples.push(sample / 0x7FFF) // Normalize to [-1, 1]
}
flushFrame(): void {
// Send buffer data to audio context
// Implementation depends on your audio architecture
if (this.samples.length > 0) {
const buffer = this.audioCtx.createBuffer(1, this.samples.length, this.sampleRate);
buffer.getChannelData(0).set(this.samples);
const source = this.audioCtx.createBufferSource();
source.buffer = buffer;
source.connect(this.audioCtx.destination);
source.start();
this.samples.length = 0;
}
}
}