function loadboot(p) { var boot = [ 0x31, 0xFE, 0xFF, 0xAF, 0x21, 0xFF, 0x9F, 0x32, 0xCB, 0x7C, 0x20, 0xFB, 0x21, 0x26, 0xFF, 0x0E, 0x11, 0x3E, 0x80, 0x32, 0xE2, 0x0C, 0x3E, 0xF3, 0xE2, 0x32, 0x3E, 0x77, 0x77, 0x3E, 0xFC, 0xE0, 0x47, 0x11, 0x04, 0x01, 0x21, 0x10, 0x80, 0x1A, 0xCD, 0x95, 0x00, 0xCD, 0x96, 0x00, 0x13, 0x7B, 0xFE, 0x34, 0x20, 0xF3, 0x11, 0xD8, 0x00, 0x06, 0x08, 0x1A, 0x13, 0x22, 0x23, 0x05, 0x20, 0xF9, 0x3E, 0x19, 0xEA, 0x10, 0x99, 0x21, 0x2F, 0x99, 0x0E, 0x0C, 0x3D, 0x28, 0x08, 0x32, 0x0D, 0x20, 0xF9, 0x2E, 0x0F, 0x18, 0xF3, 0x67, 0x3E, 0x64, 0x57, 0xE0, 0x42, 0x3E, 0x91, 0xE0, 0x40, 0x04, 0x1E, 0x02, 0x0E, 0x0C, 0xF0, 0x44, 0xFE, 0x90, 0x20, 0xFA, 0x0D, 0x20, 0xF7, 0x1D, 0x20, 0xF2, 0x0E, 0x13, 0x24, 0x7C, 0x1E, 0x83, 0xFE, 0x62, 0x28, 0x06, 0x1E, 0xC1, 0xFE, 0x64, 0x20, 0x06, 0x7B, 0xE2, 0x0C, 0x3E, 0x87, 0xE2, 0xF0, 0x42, 0x90, 0xE0, 0x42, 0x15, 0x20, 0xD2, 0x05, 0x20, 0x4F, 0x16, 0x20, 0x18, 0xCB, 0x4F, 0x06, 0x04, 0xC5, 0xCB, 0x11, 0x17, 0xC1, 0xCB, 0x11, 0x17, 0x05, 0x20, 0xF5, 0x22, 0x23, 0x22, 0x23, 0xC9, 0xCE, 0xED, 0x66, 0x66, 0xCC, 0x0D, 0x00, 0x0B, 0x03, 0x73, 0x00, 0x83, 0x00, 0x0C, 0x00, 0x0D, 0x00, 0x08, 0x11, 0x1F, 0x88, 0x89, 0x00, 0x0E, 0xDC, 0xCC, 0x6E, 0xE6, 0xDD, 0xDD, 0xD9, 0x99, 0xBB, 0xBB, 0x67, 0x63, 0x6E, 0x0E, 0xEC, 0xCC, 0xDD, 0xDC, 0x99, 0x9F, 0xBB, 0xB9, 0x33, 0x3E, 0x3C, 0x42, 0xB9, 0xA5, 0xB9, 0xA5, 0x42, 0x3C, 0x21, 0x04, 0x01, 0x11, 0xA8, 0x00, 0x1A, 0x13, 0xBE, 0x00, 0x00, 0x23, 0x7D, 0xFE, 0x34, 0x20, 0xF5, 0x06, 0x19, 0x78, 0x86, 0x23, 0x05, 0x20, 0xFB, 0x86, 0x00, 0x00, 0x3E, 0x01, 0xE0, 0x50 ]; for (var i in boot) { p.memory[i] = boot[i]; } p.r.pc = 0; p.usingBootRom = true; } var GameboyJS; (function (GameboyJS) { "use strict"; // CPU class var CPU = function(gameboy) { this.gameboy = gameboy; this.r = {A:0, F: 0, B:0, C:0, D:0, E:0, H:0, L:0, pc:0, sp:0}; this.IME = true; this.clock = {c: 0, serial: 0}; this.isHalted = false; this.isPaused = false; this.usingBootRom = false; this.createDevices(); }; CPU.INTERRUPTS = { VBLANK: 0, LCDC: 1, TIMER: 2, SERIAL: 3, HILO: 4 }; CPU.interruptRoutines = { 0: function(p){GameboyJS.cpuOps.RSTn(p, 0x40);}, 1: function(p){GameboyJS.cpuOps.RSTn(p, 0x48);}, 2: function(p){GameboyJS.cpuOps.RSTn(p, 0x50);}, 3: function(p){GameboyJS.cpuOps.RSTn(p, 0x58);}, 4: function(p){GameboyJS.cpuOps.RSTn(p, 0x60);} }; CPU.prototype.createDevices = function() { this.memory = new GameboyJS.Memory(this); this.timer = new GameboyJS.Timer(this, this.memory); this.apu = new GameboyJS.APU(this.memory); this.SERIAL_INTERNAL_INSTR = 512; // instr to wait per bit if internal clock this.enableSerial = 0; this.serialHandler = GameboyJS.ConsoleSerial; }; CPU.prototype.reset = function() { this.memory.reset(); this.r.sp = 0xFFFE; }; CPU.prototype.loadRom = function(data) { this.memory.setRomData(data); }; CPU.prototype.getRamSize = function() { var size = 0; switch (this.memory.rb(0x149)) { case 1: size = 2048; break; case 2: size = 2048 * 4; break; case 3: size = 2048 * 16; break; } return size; }; CPU.prototype.getGameName = function() { var name = ''; for (var i = 0x134; i < 0x143; i++) { var char = this.memory.rb(i) || 32; name += String.fromCharCode(char); } return name; }; // Start the execution of the emulator CPU.prototype.run = function() { if (this.usingBootRom) { this.r.pc = 0x0000; } else { this.r.pc = 0x0100; } this.frame(); }; CPU.prototype.stop = function() { clearTimeout(this.nextFrameTimer); }; // Fetch-and-execute loop // Will execute instructions for the duration of a frame // // The screen unit will notify the vblank period which // is considered the end of a frame // // The function is called on a regular basis with a timeout CPU.prototype.frame = function() { if (!this.isPaused) { this.nextFrameTimer = setTimeout(this.frame.bind(this), 1000 / GameboyJS.Screen.physics.FREQUENCY); } try { var vblank = false; while (!vblank) { var oldInstrCount = this.clock.c; if (!this.isHalted) { var opcode = this.fetchOpcode(); GameboyJS.opcodeMap[opcode](this); this.r.F &= 0xF0; // tmp fix if (this.enableSerial) { var instr = this.clock.c - oldInstrCount; this.clock.serial += instr; if (this.clock.serial >= 8 * this.SERIAL_INTERNAL_INSTR) { this.endSerialTransfer(); } } } else { this.clock.c += 4; } var elapsed = this.clock.c - oldInstrCount; vblank = this.gpu.update(elapsed); this.timer.update(elapsed); this.input.update(); this.apu.update(elapsed); this.checkInterrupt(); } this.clock.c = 0; } catch (e) { this.gameboy.handleException(e); } }; CPU.prototype.fetchOpcode = function() { var opcode = this.memory.rb(this.r.pc++); if (opcode === undefined) {console.log(opcode + ' at ' + (this.r.pc-1).toString(16));this.stop();return;} if (!GameboyJS.opcodeMap[opcode]) { console.error('Unknown opcode '+opcode.toString(16)+' at address '+(this.r.pc-1).toString(16)+', stopping execution...'); this.stop(); return null; } return opcode; }; // read register CPU.prototype.rr = function(register) { return this.r[register]; }; // write register CPU.prototype.wr = function(register, value) { this.r[register] = value; }; CPU.prototype.halt = function() { this.isHalted = true; }; CPU.prototype.unhalt = function() { this.isHalted = false; }; CPU.prototype.pause = function() { this.isPaused = true; }; CPU.prototype.unpause = function() { if (this.isPaused) { this.isPaused = false; this.frame(); } }; // Look for interrupt flags CPU.prototype.checkInterrupt = function() { if (!this.IME) { return; } for (var i = 0; i < 5; i++) { var IFval = this.memory.rb(0xFF0F); if (GameboyJS.Util.readBit(IFval, i) && this.isInterruptEnable(i)) { IFval &= (0xFF - (1<> (7-pixel)) + ((b2 & mask) >> (7-pixel))*2; pixelData[line * 8 + pixel] = colorValue; } } var i = 0; while (pixelData.length) { console.log(i++ + ' ' + pixelData.splice(0, 8).join('')); } }; Debug.list_visible_sprites = function(gameboy) { var memory = gameboy.cpu.memory; var indexes = new Array(); for (var i = 0xFE00; i < 0xFE9F; i += 4) { var x = memory.oamram(i + 1); var y = memory.oamram(i); var tileIndex = memory.oamram(i + 2); if (x == 0 || x >= 168) { continue; } indexes.push({oamIndex:i, x:x, y:y, tileIndex:tileIndex}); } return indexes; }; GameboyJS.Debug = Debug; }(GameboyJS || (GameboyJS = {}))); var GameboyJS; (function (GameboyJS) { "use strict"; var Screen; var GPU = function(screen, cpu) { this.cpu = cpu; this.screen = screen; this.LCDC= 0xFF40; this.STAT= 0xFF41; this.SCY = 0xFF42; this.SCX = 0xFF43; this.LY = 0xFF44; this.LYC = 0xFF45; this.BGP = 0xFF47; this.OBP0= 0xFF48; this.OBP1= 0xFF49; this.WY = 0xFF4A; this.WX = 0xFF4B; this.vram = cpu.memory.vram.bind(cpu.memory); this.OAM_START = 0xFE00; this.OAM_END = 0xFE9F; this.deviceram = cpu.memory.deviceram.bind(cpu.memory); this.oamram = cpu.memory.oamram.bind(cpu.memory); this.VBLANK_TIME = 70224; this.clock = 0; this.mode = 2; this.line = 0; Screen = GameboyJS.Screen; this.buffer = new Array(Screen.physics.WIDTH * Screen.physics.HEIGHT); this.tileBuffer = new Array(8); this.bgTileCache = {}; }; GPU.tilemap = { HEIGHT: 32, WIDTH: 32, START_0: 0x9800, START_1: 0x9C00, LENGTH: 0x0400 // 1024 bytes = 32*32 }; GPU.prototype.update = function(clockElapsed) { this.clock += clockElapsed; var vblank = false; switch (this.mode) { case 0: // HBLANK if (this.clock >= 204) { this.clock -= 204; this.line++; this.updateLY(); if (this.line == 144) { this.setMode(1); vblank = true; this.cpu.requestInterrupt(GameboyJS.CPU.INTERRUPTS.VBLANK); this.drawFrame(); } else { this.setMode(2); } } break; case 1: // VBLANK if (this.clock >= 456) { this.clock -= 456; this.line++; if (this.line > 153) { this.line = 0; this.setMode(2); } this.updateLY(); } break; case 2: // SCANLINE OAM if (this.clock >= 80) { this.clock -= 80; this.setMode(3); } break; case 3: // SCANLINE VRAM if (this.clock >= 172) { this.clock -= 172; this.drawScanLine(this.line); this.setMode(0); } break; } return vblank; }; GPU.prototype.updateLY = function() { this.deviceram(this.LY, this.line); var STAT = this.deviceram(this.STAT); if (this.deviceram(this.LY) == this.deviceram(this.LYC)) { this.deviceram(this.STAT, STAT | (1 << 2)); if (STAT & (1 << 6)) { this.cpu.requestInterrupt(GameboyJS.CPU.INTERRUPTS.LCDC); } } else { this.deviceram(this.STAT, STAT & (0xFF - (1 << 2))); } }; GPU.prototype.setMode = function(mode) { this.mode = mode; var newSTAT = this.deviceram(this.STAT); newSTAT &= 0xFC; newSTAT |= mode; this.deviceram(this.STAT, newSTAT); if (mode < 3) { if (newSTAT & (1 << (3+mode))) { this.cpu.requestInterrupt(GameboyJS.CPU.INTERRUPTS.LCDC); } } }; // Push one scanline into the main buffer GPU.prototype.drawScanLine = function(line) { var LCDC = this.deviceram(this.LCDC); var enable = GameboyJS.Util.readBit(LCDC, 7); if (enable) { var lineBuffer = new Array(Screen.physics.WIDTH); this.drawBackground(LCDC, line, lineBuffer); this.drawSprites(LCDC, line, lineBuffer); // TODO draw a line for the window here too } }; GPU.prototype.drawFrame = function() { var LCDC = this.deviceram(this.LCDC); var enable = GameboyJS.Util.readBit(LCDC, 7); if (enable) { //this.drawSprites(LCDC); this.drawWindow(LCDC); } this.bgTileCache = {}; this.screen.render(this.buffer); }; GPU.prototype.drawBackground = function(LCDC, line, lineBuffer) { if (!GameboyJS.Util.readBit(LCDC, 0)) { return; } var mapStart = GameboyJS.Util.readBit(LCDC, 3) ? GPU.tilemap.START_1 : GPU.tilemap.START_0; var dataStart, signedIndex = false; if (GameboyJS.Util.readBit(LCDC, 4)) { dataStart = 0x8000; } else { dataStart = 0x8800; signedIndex = true; } var bgx = this.deviceram(this.SCX); var bgy = this.deviceram(this.SCY); var tileLine = ((line + bgy) & 7); // browse BG tilemap for the line to render var tileRow = ((((bgy + line) / 8) | 0) & 0x1F); var firstTile = ((bgx / 8) | 0) + 32 * tileRow; var lastTile = firstTile + Screen.physics.WIDTH / 8 + 1; if ((lastTile & 0x1F) < (firstTile & 0x1F)) { lastTile -= 32; } var x = (firstTile & 0x1F) * 8 - bgx; // x position of the first tile's leftmost pixel for (var i = firstTile; i != lastTile; i++, (i & 0x1F) == 0 ? i-=32 : null) { var tileIndex = this.vram(i + mapStart); if (signedIndex) { tileIndex = GameboyJS.Util.getSignedValue(tileIndex) + 128; } // try to retrieve the tile data from the cache, or use readTileData() to read from ram // TODO find a better cache system now that the BG is rendered line by line var tileData = this.bgTileCache[tileIndex] || (this.bgTileCache[tileIndex] = this.readTileData(tileIndex, dataStart)); this.drawTileLine(tileData, tileLine); this.copyBGTileLine(lineBuffer, this.tileBuffer, x); x += 8; } this.copyLineToBuffer(lineBuffer, line); }; // Copy a tile line from a tileBuffer to a line buffer, at a given x position GPU.prototype.copyBGTileLine = function(lineBuffer, tileBuffer, x) { // copy tile line to buffer for (var k = 0; k < 8; k++, x++) { if (x < 0 || x >= Screen.physics.WIDTH) continue; lineBuffer[x] = tileBuffer[k]; } }; // Copy a scanline into the main buffer GPU.prototype.copyLineToBuffer = function(lineBuffer, line) { var bgPalette = GPU.getPalette(this.deviceram(this.BGP)); for (var x = 0; x < Screen.physics.WIDTH; x++) { var color = lineBuffer[x]; this.drawPixel(x, line, bgPalette[color]); } }; // Write a line of a tile (8 pixels) into a buffer array GPU.prototype.drawTileLine = function(tileData, line, xflip, yflip) { xflip = xflip | 0; yflip = yflip | 0; var l = yflip ? 7 - line : line; var byteIndex = l * 2; var b1 = tileData[byteIndex++]; var b2 = tileData[byteIndex++]; var offset = 8; for (var pixel = 0; pixel < 8; pixel++) { offset--; var mask = (1 << offset); var colorValue = ((b1 & mask) >> offset) + ((b2 & mask) >> offset)*2; var p = xflip ? offset : pixel; this.tileBuffer[p] = colorValue; } }; GPU.prototype.drawSprites = function(LCDC, line, lineBuffer) { if (!GameboyJS.Util.readBit(LCDC, 1)) { return; } var spriteHeight = GameboyJS.Util.readBit(LCDC, 2) ? 16 : 8; var sprites = new Array(); for (var i = this.OAM_START; i < this.OAM_END && sprites.length < 10; i += 4) { var y = this.oamram(i); var x = this.oamram(i+1); var index = this.oamram(i+2); var flags = this.oamram(i+3); if (y - 16 > line || y - 16 < line - spriteHeight) { continue; } sprites.push({x:x, y:y, index:index, flags:flags}) } if (sprites.length == 0) return; // cache object to store read tiles from this frame var cacheTile = {}; var spriteLineBuffer = new Array(Screen.physics.WIDTH); for (var i = 0; i < sprites.length; i++) { var sprite = sprites[i]; var tileLine = line - sprite.y + 16; var paletteNumber = GameboyJS.Util.readBit(flags, 4); var xflip = GameboyJS.Util.readBit(sprite.flags, 5); var yflip = GameboyJS.Util.readBit(sprite.flags, 6); var tileData = cacheTile[sprite.index] || (cacheTile[sprite.index] = this.readTileData(sprite.index, 0x8000, spriteHeight * 2)); this.drawTileLine(tileData, tileLine, xflip, yflip); this.copySpriteTileLine(spriteLineBuffer, this.tileBuffer, sprite.x - 8, paletteNumber); } this.copySpriteLineToBuffer(spriteLineBuffer, line); }; // Copy a tile line from a tileBuffer to a line buffer, at a given x position GPU.prototype.copySpriteTileLine = function(lineBuffer, tileBuffer, x, palette) { // copy tile line to buffer for (var k = 0; k < 8; k++, x++) { if (x < 0 || x >= Screen.physics.WIDTH || tileBuffer[k] == 0) continue; lineBuffer[x] = {color:tileBuffer[k], palette: palette}; } }; // Copy a sprite scanline into the main buffer GPU.prototype.copySpriteLineToBuffer = function(spriteLineBuffer, line) { var spritePalettes = {}; spritePalettes[0] = GPU.getPalette(this.deviceram(this.OBP0)); spritePalettes[1] = GPU.getPalette(this.deviceram(this.OBP1)); for (var x = 0; x < Screen.physics.WIDTH; x++) { if (!spriteLineBuffer[x]) continue; var color = spriteLineBuffer[x].color; if (color === 0) continue; var paletteNumber = spriteLineBuffer[x].palette; this.drawPixel(x, line, spritePalettes[paletteNumber][color]); } }; GPU.prototype.drawTile = function(tileData, x, y, buffer, bufferWidth, xflip, yflip, spriteMode) { xflip = xflip | 0; yflip = yflip | 0; spriteMode = spriteMode | 0; var byteIndex = 0; for (var line = 0; line < 8; line++) { var l = yflip ? 7 - line : line; var b1 = tileData[byteIndex++]; var b2 = tileData[byteIndex++]; for (var pixel = 0; pixel < 8; pixel++) { var mask = (1 << (7-pixel)); var colorValue = ((b1 & mask) >> (7-pixel)) + ((b2 & mask) >> (7-pixel))*2; if (spriteMode && colorValue == 0) continue; var p = xflip ? 7 - pixel : pixel; var bufferIndex = (x + p) + (y + l) * bufferWidth; buffer[bufferIndex] = colorValue; } } }; // get an array of tile bytes data (16 entries for 8*8px) GPU.prototype.readTileData = function(tileIndex, dataStart, tileSize) { tileSize = tileSize || 0x10; // 16 bytes / tile by default (8*8 px) var tileData = new Array(); var tileAddressStart = dataStart + (tileIndex * 0x10); for (var i = tileAddressStart; i < tileAddressStart + tileSize; i++) { tileData.push(this.vram(i)); } return tileData; }; GPU.prototype.drawWindow = function(LCDC) { if (!GameboyJS.Util.readBit(LCDC, 5)) { return; } var buffer = new Array(256*256); var mapStart = GameboyJS.Util.readBit(LCDC, 6) ? GPU.tilemap.START_1 : GPU.tilemap.START_0; var dataStart, signedIndex = false; if (GameboyJS.Util.readBit(LCDC, 4)) { dataStart = 0x8000; } else { dataStart = 0x8800; signedIndex = true; } // browse Window tilemap for (var i = 0; i < GPU.tilemap.LENGTH; i++) { var tileIndex = this.vram(i + mapStart); if (signedIndex) { tileIndex = GameboyJS.Util.getSignedValue(tileIndex) + 128; } var tileData = this.readTileData(tileIndex, dataStart); var x = i % GPU.tilemap.WIDTH; var y = (i / GPU.tilemap.WIDTH) | 0; this.drawTile(tileData, x * 8, y * 8, buffer, 256); } var wx = this.deviceram(this.WX) - 7; var wy = this.deviceram(this.WY); for (var x = Math.max(0, -wx); x < Math.min(Screen.physics.WIDTH, Screen.physics.WIDTH - wx); x++) { for (var y = Math.max(0, -wy); y < Math.min(Screen.physics.HEIGHT, Screen.physics.HEIGHT - wy); y++) { var color = buffer[(x & 255) + (y & 255) * 256]; this.drawPixel(x + wx, y + wy, color); } } }; GPU.prototype.drawPixel = function(x, y, color) { this.buffer[y * 160 + x] = color; }; GPU.prototype.getPixel = function(x, y) { return this.buffer[y * 160 + x]; }; // Get the palette mapping from a given palette byte as stored in memory // A palette will map a tile color to a final palette color index // used with Screen.colors to get a shade of grey GPU.getPalette = function(paletteByte) { var palette = []; for (var i = 0; i < 8; i += 2) { var shade = (paletteByte & (3 << i)) >> i; palette.push(shade); } return palette; }; GameboyJS.GPU = GPU; }(GameboyJS || (GameboyJS = {}))); var GameboyJS; (function (GameboyJS) { "use strict"; // Screen device var Screen = function(canvas, pixelSize) { this.context = canvas.getContext('2d'); this.canvas = canvas; this.pixelSize = pixelSize || 1; this.initImageData(); }; Screen.colors = [ 0xFF, 0xAA, 0x55, 0x00 ]; Screen.physics = { WIDTH : 160, HEIGHT : 144, FREQUENCY: 60 }; Screen.prototype.setPixelSize = function(pixelSize) { this.pixelSize = pixelSize; this.initImageData(); }; Screen.prototype.initImageData = function() { this.canvas.width = Screen.physics.WIDTH * this.pixelSize; this.canvas.height = Screen.physics.HEIGHT * this.pixelSize; this.imageData = this.context.createImageData(this.canvas.width, this.canvas.height); }; Screen.prototype.clearScreen = function() { this.context.fillStyle = '#FFF'; this.context.fillRect(0, 0, Screen.physics.WIDTH * this.pixelSize, Screen.physics.HEIGHT * this.pixelSize); }; Screen.prototype.fillImageData = function(buffer) { for (var y = 0; y < Screen.physics.HEIGHT; y++) { for (var py = 0; py < this.pixelSize; py++) { var _y = y * this.pixelSize + py; for (var x = 0; x < Screen.physics.WIDTH; x++) { for (var px = 0; px < this.pixelSize; px++) { var offset = _y * this.canvas.width + (x * this.pixelSize + px); var v = Screen.colors[buffer[y * Screen.physics.WIDTH + x]]; this.imageData.data[offset * 4] = v; this.imageData.data[offset * 4 + 1] = v; this.imageData.data[offset * 4 + 2] = v; this.imageData.data[offset * 4 + 3] = 255; } } } } }; Screen.prototype.render = function(buffer) { this.fillImageData(buffer); this.context.putImageData(this.imageData, 0, 0); }; GameboyJS.Screen = Screen; }(GameboyJS || (GameboyJS = {}))); var GameboyJS; (function (GameboyJS) { "use strict"; // This exception should be thrown whenever a critical feature that // has not been implemented is requested function UnimplementedException(message, fatal) { this.message = message; this.name = UnimplementedException; if (fatal === undefined) { fatal = true; } this.fatal = fatal; } GameboyJS.UnimplementedException = UnimplementedException; }(GameboyJS || (GameboyJS = {}))); var GameboyJS; (function (GameboyJS) { "use strict"; // Object for mapping the cartridge RAM var ExtRam = function() { this.extRam = null; this.ramSize = 0; this.ramBank = 0; }; ExtRam.prototype.loadRam = function(game, size) { this.gameName = game; this.ramSize = size; this.ramBanksize = this.ramSize >= 0x2000 ? 8192 : 2048; var key = this.getStorageKey(); var data = localStorage.getItem(key); if (data == null) { this.extRam = Array.apply(null, new Array(this.ramSize)).map(function(){return 0;}); } else { this.extRam = JSON.parse(data); if (this.extRam.length != size) { console.error('Found RAM data but not matching expected size.'); } } }; ExtRam.prototype.setRamBank = function(bank) { this.ramBank = bank; }; ExtRam.prototype.manageWrite = function(offset, value) { this.extRam[this.ramBank * 8192 + offset] = value; }; ExtRam.prototype.manageRead = function(offset) { return this.extRam[this.ramBank * 8192 + offset]; }; ExtRam.prototype.getStorageKey = function() { return this.gameName + '_EXTRAM';; }; // Actually save the RAM in the physical storage (localStorage) ExtRam.prototype.saveRamData = function() { localStorage.setItem(this.getStorageKey(), JSON.stringify(this.extRam)); }; GameboyJS.ExtRam = ExtRam; }(GameboyJS || (GameboyJS = {}))); var GameboyJS; (function (GameboyJS) { "use strict"; // This is the default buttons mapping for the Gamepad // It's optimized for the XBOX pad // // Any other mapping can be provided as a constructor argument of the Gamepad object // An alternative mapping should be an object with keys being the indexes // of the gamepad buttons and values the normalized gameboy button names var xboxMapping = { 0: 'UP', 1: 'DOWN', 2: 'LEFT', 3: 'RIGHT', 4: 'START', 5: 'SELECT', 11: 'A', 12: 'B' }; // Gamepad listener // Communication layer between the Gamepad API and the Input class // Any physical controller can be used but the mapping should be provided // in order to get an optimal layout of the buttons (see above) var Gamepad = function(mapping) { this.gamepad = null; this.state = {A:0,B:0,START:0,SELECT:0,LEFT:0,RIGHT:0,UP:0,DOWN:0}; this.pullInterval = null; this.buttonMapping = mapping || xboxMapping; }; // Initialize the keyboard listeners and set up the callbacks // for button press / release Gamepad.prototype.init = function(onPress, onRelease) { this.onPress = onPress; this.onRelease = onRelease; var self = this; window.addEventListener('gamepadconnected', function(e) { self.gamepad = e.gamepad; self.activatePull(); }); window.addEventListener('gamepaddisconnected', function(e) { self.gamepad = null; self.deactivatePull(); }); }; Gamepad.prototype.activatePull = function() { this.deactivatePull(); this.pullInterval = setInterval(this.pullState.bind(this), 100); }; Gamepad.prototype.deactivatePull = function() { clearInterval(this.pullInterval); }; // Check the state of the current gamepad in order to detect any press/release action Gamepad.prototype.pullState = function() { for (var index in this.buttonMapping) { var button = this.buttonMapping[index]; var oldState = this.state[button]; this.state[button] = this.gamepad.buttons[index].pressed; if (this.state[button] == 1 && oldState == 0) { this.managePress(button); } else if (this.state[button] == 0 && oldState == 1) { this.manageRelease(button); } } }; Gamepad.prototype.managePress = function(key) { this.onPress(key); }; Gamepad.prototype.manageRelease = function(key) { this.onRelease(key); }; GameboyJS.Gamepad = Gamepad; }(GameboyJS || (GameboyJS = {}))); var GameboyJS; (function (GameboyJS) { "use strict"; // The Input management system // // The pressKey() and releaseKey() functions should be called by a device class // like GameboyJS.Keyboard after a physical button trigger event // // They rely on the name of the original buttons as parameters (see Input.keys) var Input = function(cpu, pad) { this.cpu = cpu; this.memory = cpu.memory; this.P1 = 0xFF00; this.state = 0; pad.init(this.pressKey.bind(this), this.releaseKey.bind(this)); }; Input.keys = { START: 0x80, SELECT: 0x40, B: 0x20, A: 0x10, DOWN: 0x08, UP: 0x04, LEFT: 0x02, RIGHT: 0x01 }; Input.prototype.pressKey = function(key) { this.state |= Input.keys[key]; this.cpu.requestInterrupt(GameboyJS.CPU.INTERRUPTS.HILO); }; Input.prototype.releaseKey = function(key) { var mask = 0xFF - Input.keys[key]; this.state &= mask; }; Input.prototype.update = function() { var value = this.memory.rb(this.P1); value = ((~value) & 0x30); // invert the value so 1 means 'active' if (value & 0x10) { // direction keys listened value |= (this.state & 0x0F); } else if (value & 0x20) { // action keys listened value |= ((this.state & 0xF0) >> 4); } else if ((value & 0x30) == 0) { // no keys listened value &= 0xF0; } value = ((~value) & 0x3F); // invert back this.memory[this.P1] = value; }; GameboyJS.Input = Input; }(GameboyJS || (GameboyJS = {}))); var GameboyJS; (function (GameboyJS) { "use strict"; // Keyboard listener // Does the mapping between the keyboard and the Input class var Keyboard = function() {}; // Initialize the keyboard listeners and set up the callbacks // for button press / release Keyboard.prototype.init = function(onPress, onRelease) { this.onPress = onPress; this.onRelease = onRelease; var self = this; document.addEventListener('keydown', function(e) { self.managePress(e.keyCode); }); document.addEventListener('keyup', function(e) { self.manageRelease(e.keyCode); }); } Keyboard.prototype.managePress = function(keycode) { var key = this.translateKey(keycode); if (key) { this.onPress(key); } }; Keyboard.prototype.manageRelease = function(keycode) { var key = this.translateKey(keycode); if (key) { this.onRelease(key); } }; // Transform a keyboard keycode into a key of the Input.keys object Keyboard.prototype.translateKey = function(keycode) { var key = null; switch (keycode) { case 71: // G key = 'A'; break; case 66: // B key = 'B'; break; case 72: // H key = 'START'; break; case 78: // N key = 'SELECT'; break; case 37: // left key = 'LEFT'; break; case 38: // up key = 'UP'; break; case 39: // right key = 'RIGHT'; break; case 40: // down key = 'DOWN'; break; } return key; }; GameboyJS.Keyboard = Keyboard; }(GameboyJS || (GameboyJS = {}))); var GameboyJS; (function (GameboyJS) { "use strict"; // List of CPU operations // Most operations have been factorized here to limit code redundancy // // How to read operations: // Uppercase letters qualify the kind of operation (LD = LOAD, INC = INCREMENT, etc.) // Lowercase letters are used to hint parameters : // r = register, n = 1 memory byte, sp = sp register, // a = suffix for memory address, i = bit index // Example : LDrrar = LOAD operation with two-registers memory address // as first parameter and one register value as second // // Underscore-prefixed functions are here to delegate the logic between similar operations, // they should not be called from outside // // It's up to each operation to update the CPU clock var ops = { LDrrnn: function(p, r1, r2) {p.wr(r2, p.memory.rb(p.r.pc));p.wr(r1, p.memory.rb(p.r.pc+1)); p.r.pc+=2;p.clock.c += 12;}, LDrrar: function(p, r1, r2, r3) {ops._LDav(p, GameboyJS.Util.getRegAddr(p, r1, r2), p.r[r3]);p.clock.c += 8;}, LDrrra: function(p, r1, r2, r3) {p.wr(r1, p.memory.rb(GameboyJS.Util.getRegAddr(p, r2, r3)));p.clock.c += 8;}, LDrn: function(p, r1) {p.wr(r1, p.memory.rb(p.r.pc++));p.clock.c += 8;}, LDrr: function(p, r1, r2) {p.wr(r1, p.r[r2]);p.clock.c += 4;}, LDrar: function(p, r1, r2) {p.memory.wb(p.r[r1]+0xFF00, p.r[r2]);p.clock.c += 8;}, LDrra: function(p, r1, r2) {p.wr(r1, p.memory.rb(p.r[r2]+0xFF00));p.clock.c += 8;}, LDspnn: function(p) {p.wr('sp', (p.memory.rb(p.r.pc + 1) << 8) + p.memory.rb(p.r.pc));p.r.pc+=2;p.clock.c += 12;}, LDsprr: function(p, r1, r2) {p.wr('sp', GameboyJS.Util.getRegAddr(p, r1, r2));p.clock.c += 8;}, LDnnar: function(p, r1) {var addr=(p.memory.rb(p.r.pc + 1) << 8) + p.memory.rb(p.r.pc);p.memory.wb(addr,p.r[r1]);p.r.pc+=2; p.clock.c += 16;}, LDrnna: function(p, r1) {var addr=(p.memory.rb(p.r.pc + 1) << 8) + p.memory.rb(p.r.pc);p.wr(r1, p.memory.rb(addr));p.r.pc+=2; p.clock.c += 16;}, LDrrspn:function(p, r1, r2) {var rel = p.memory.rb(p.r.pc++);rel=GameboyJS.Util.getSignedValue(rel);var val=p.r.sp + rel; var c = (p.r.sp&0xFF) + (rel&0xFF) > 0xFF;var h = (p.r.sp & 0xF) + (rel & 0xF) > 0xF;val &= 0xFFFF; var f = 0; if(h)f|=0x20;if(c)f|=0x10;p.wr('F', f); p.wr(r1, val >> 8);p.wr(r2, val&0xFF); p.clock.c+=12;}, LDnnsp: function(p) {var addr = p.memory.rb(p.r.pc++) + (p.memory.rb(p.r.pc++)<<8); ops._LDav(p, addr, p.r.sp & 0xFF);ops._LDav(p, addr+1, p.r.sp >> 8);p.clock.c+=20;}, LDrran: function(p, r1, r2){var addr = GameboyJS.Util.getRegAddr(p, r1, r2);ops._LDav(p, addr, p.memory.rb(p.r.pc++));p.clock.c+=12;}, _LDav: function(p, addr, val){p.memory.wb(addr, val);}, LDHnar: function(p, r1){p.memory.wb(0xFF00 + p.memory.rb(p.r.pc++), p.r[r1]);p.clock.c+=12;}, LDHrna: function(p, r1){p.wr(r1, p.memory.rb(0xFF00 + p.memory.rb(p.r.pc++)));p.clock.c+=12;}, INCrr: function(p, r1, r2) {p.wr(r2, (p.r[r2]+1)&0xFF); if (p.r[r2] == 0) p.wr(r1, (p.r[r1]+1)&0xFF);p.clock.c += 8;}, INCrra: function(p, r1, r2) {var addr = GameboyJS.Util.getRegAddr(p, r1, r2);var val = (p.memory.rb(addr)+1)&0xFF;var z = val==0;var h=(p.memory.rb(addr)&0xF)+1 > 0xF; p.memory.wb(addr, val); p.r.F&=0x10;if(h)p.r.F|=0x20;if(z)p.r.F|=0x80; p.clock.c+=12;}, INCsp: function(p){p.wr('sp', p.r.sp+1); p.r.sp &= 0xFFFF; p.clock.c+=8;}, INCr: function(p, r1) {var h = ((p.r[r1]&0xF) + 1)&0x10;p.wr(r1, (p.r[r1] + 1)&0xFF);var z = p.r[r1]==0; p.r.F&=0x10;if(h)p.r.F|=0x20;if(z)p.r.F|=0x80; p.clock.c += 4;}, DECrr: function(p, r1, r2) {p.wr(r2, (p.r[r2] - 1) & 0xFF); if (p.r[r2] == 0xFF) p.wr(r1, (p.r[r1] - 1)&0xFF);p.clock.c += 8;}, DECsp: function(p){p.wr('sp', p.r.sp-1); p.r.sp &= 0xFFFF; p.clock.c+=8;}, DECr: function(p, r1) {var h = (p.r[r1]&0xF) < 1;p.wr(r1, (p.r[r1] - 1) & 0xFF);var z = p.r[r1]==0; p.r.F&=0x10;p.r.F|=0x40;if(h)p.r.F|=0x20;if(z)p.r.F|=0x80; p.clock.c += 4;}, DECrra: function(p, r1, r2){var addr = GameboyJS.Util.getRegAddr(p, r1, r2);var val = (p.memory.rb(addr)-1)&0xFF;var z = val==0;var h=(p.memory.rb(addr)&0xF) < 1; p.memory.wb(addr, val); p.r.F&=0x10;p.r.F|=0x40;if(h)p.r.F|=0x20;if(z)p.r.F|=0x80; p.clock.c+=12;}, ADDrr: function(p, r1, r2) {var n = p.r[r2];ops._ADDrn(p, r1, n); p.clock.c += 4;}, ADDrn: function(p, r1) {var n = p.memory.rb(p.r.pc++);ops._ADDrn(p, r1, n); p.clock.c+=8;}, _ADDrn: function(p, r1, n) {var h=((p.r[r1]&0xF)+(n&0xF))&0x10;p.wr(r1, p.r[r1]+n);var c=p.r[r1]&0x100;p.r[r1]&=0xFF; var f = 0;if (p.r[r1]==0)f|=0x80;if (h)f|=0x20;if (c)f|=0x10;p.wr('F', f);}, ADDrrrr:function(p, r1, r2, r3, r4) {ops._ADDrrn(p, r1, r2, (p.r[r3]<<8) + p.r[r4]); p.clock.c+=8;}, ADDrrsp:function(p, r1, r2) {ops._ADDrrn(p, r1, r2, p.r.sp); p.clock.c += 8;}, ADDspn: function(p) {var v = p.memory.rb(p.r.pc++);v = GameboyJS.Util.getSignedValue(v); var c = ((p.r.sp&0xFF) + (v&0xFF)) > 0xFF; var h = (p.r.sp & 0xF) + (v&0xF) > 0xF; var f = 0; if(h)f|=0x20;if(c)f|=0x10;p.wr('F', f); p.wr('sp', (p.r.sp + v) & 0xFFFF); p.clock.c+=16;}, _ADDrrn:function(p, r1, r2, n) {var v1 = (p.r[r1]<<8) + p.r[r2];var v2 = n; var res = v1 + v2;var c = res&0x10000;var h = ((v1&0xFFF) + (v2&0xFFF))&0x1000;var z = p.r.F&0x80; res&=0xFFFF;p.r[r2]=res&0xFF;res=res>>8;p.r[r1]=res&0xFF; var f=0;if(z)f|=0x80;if(h)f|=0x20;if(c)f|=0x10;p.r.F=f;}, ADCrr: function(p, r1, r2) {var n = p.r[r2]; ops._ADCrn(p, r1, n); p.clock.c += 4;}, ADCrn: function(p, r1) {var n = p.memory.rb(p.r.pc++); ops._ADCrn(p, r1, n); p.clock.c += 8;}, _ADCrn: function(p, r1, n) { var c = p.r.F&0x10?1:0;var h=((p.r[r1]&0xF)+(n&0xF)+c)&0x10; p.wr(r1, p.r[r1]+n+c);c=p.r[r1]&0x100;p.r[r1]&=0xFF; var f = 0;if (p.r[r1]==0)f|=0x80;if (h)f|=0x20;if (c)f|=0x10;p.r.F=f;}, ADCrrra:function(p, r1, r2, r3) {var n = p.memory.rb(GameboyJS.Util.getRegAddr(p, r2, r3)); ops._ADCrn(p, r1, n); p.clock.c += 8;}, ADDrrra:function(p, r1, r2, r3) {var v = p.memory.rb(GameboyJS.Util.getRegAddr(p, r2, r3));var h=((p.r[r1]&0xF)+(v&0xF))&0x10;p.wr(r1, p.r[r1]+v);var c=p.r[r1]&0x100;p.r[r1]&=0xFF; var f = 0;if (p.r[r1]==0)f|=0x80;if (h)f|=0x20;if (c)f|=0x10;p.wr('F', f); p.clock.c += 8;}, SUBr: function(p, r1) {var n = p.r[r1];ops._SUBn(p, n);p.clock.c += 4;}, SUBn: function(p) {var n = p.memory.rb(p.r.pc++);ops._SUBn(p, n);p.clock.c += 8;}, SUBrra: function(p, r1, r2) {var n = p.memory.rb(GameboyJS.Util.getRegAddr(p, r1, r2));ops._SUBn(p, n);p.clock.c+=8;}, _SUBn: function(p, n) {var c = p.r.A < n;var h = (p.r.A&0xF) < (n&0xF); p.wr('A', p.r.A - n);p.r.A&=0xFF; var z = p.r.A==0; var f = 0x40;if (z)f|=0x80;if (h)f|=0x20;if (c)f|=0x10;p.wr('F', f);}, SBCn: function(p) {var n = p.memory.rb(p.r.pc++); ops._SBCn(p, n); p.clock.c += 8;}, SBCr: function(p, r1) {var n = p.r[r1]; ops._SBCn(p, n); p.clock.c += 4;}, SBCrra: function(p, r1, r2) {var v = p.memory.rb((p.r[r1] << 8) + p.r[r2]); ops._SBCn(p, v); p.clock.c += 8;}, _SBCn: function(p, n) {var carry = p.r.F&0x10 ? 1 : 0; var c = p.r.A < n + carry;var h = (p.r.A&0xF) < (n&0xF) + carry; p.wr('A', p.r.A - n - carry); p.r.A&=0xFF; var z = p.r.A == 0; var f = 0x40;if (z)f|=0x80;if (h)f|=0x20;if (c)f|=0x10;p.r.F=f;}, ORr: function(p, r1) {p.r.A|=p.r[r1];p.r.F=(p.r.A==0)?0x80:0x00;p.clock.c += 4;}, ORn: function(p) {p.r.A|=p.memory.rb(p.r.pc++);p.r.F=(p.r.A==0)?0x80:0x00;p.clock.c += 8;}, ORrra: function(p, r1, r2) {p.r.A|=p.memory.rb((p.r[r1] << 8)+ p.r[r2]);p.r.F=(p.r.A==0)?0x80:0x00;p.clock.c += 8;}, ANDr: function(p, r1) {p.r.A&=p.r[r1];p.r.F=(p.r.A==0)?0xA0:0x20;p.clock.c += 4;}, ANDn: function(p) {p.r.A&=p.memory.rb(p.r.pc++);p.r.F=(p.r.A==0)?0xA0:0x20;p.clock.c += 8;}, ANDrra: function(p, r1, r2) {p.r.A&=p.memory.rb(GameboyJS.Util.getRegAddr(p, r1, r2));p.r.F=(p.r.A==0)?0xA0:0x20;p.clock.c += 8;}, XORr: function(p, r1) {p.r.A^=p.r[r1];p.r.F=(p.r.A==0)?0x80:0x00;p.clock.c += 4;}, XORn: function(p) {p.r.A^=p.memory.rb(p.r.pc++);p.r.F=(p.r.A==0)?0x80:0x00;p.clock.c += 8;}, XORrra: function(p, r1, r2) {p.r.A^=p.memory.rb((p.r[r1] << 8)+ p.r[r2]);p.r.F=(p.r.A==0)?0x80:0x00;p.clock.c += 8;}, CPr: function(p, r1) {var n = p.r[r1];ops._CPn(p, n); p.clock.c += 4;}, CPn: function(p) {var n =p.memory.rb(p.r.pc++);ops._CPn(p, n);p.clock.c+=8;}, CPrra: function(p, r1, r2) {var n = p.memory.rb(GameboyJS.Util.getRegAddr(p, r1, r2));ops._CPn(p, n);p.clock.c+=8;}, _CPn: function(p, n) { var c = p.r.A < n;var z = p.r.A == n;var h = (p.r.A&0xF) < (n&0xF); var f = 0x40;if(z)f+=0x80;if (h)f+=0x20;if (c)f+=0x10;p.r.F=f;}, RRCr: function(p, r1) {p.r.F=0;var out=p.r[r1] & 0x01;if(out)p.r.F|=0x10;p.r[r1]=(p.r[r1]>>1)|(out*0x80);if(p.r[r1]==0)p.r.F|=0x80;p.clock.c+=4;}, RRCrra: function(p, r1, r2) {var addr = GameboyJS.Util.getRegAddr(p, r1, r2);p.r.F=0;var out=p.memory.rb(addr)&0x01;if(out)p.r.F|=0x10;p.memory.wb(addr, (p.memory.rb(addr)>>1)|(out*0x80));if(p.memory.rb(addr)==0)p.r.F|=0x80;p.clock.c+=12;}, RLCr: function(p, r1) {p.r.F=0;var out=p.r[r1]&0x80?1:0;if(out)p.r.F|=0x10;p.r[r1]=((p.r[r1]<<1)+out)&0xFF;if(p.r[r1]==0)p.r.F|=0x80;p.clock.c+=4;}, RLCrra: function(p, r1, r2) {var addr = GameboyJS.Util.getRegAddr(p, r1, r2);p.r.F=0;var out=p.memory.rb(addr)&0x80?1:0;if(out)p.r.F|=0x10;p.memory.wb(addr, ((p.memory.rb(addr)<<1)+out)&0xFF);if(p.memory.rb(addr)==0)p.r.F|=0x80;p.clock.c+=12;}, RLr: function(p, r1) {var c=(p.r.F&0x10)?1:0;p.r.F=0;var out=p.r[r1]&0x80;out?p.r.F|=0x10:p.r.F&=0xEF;p.r[r1]=((p.r[r1]<<1)+c)&0xFF;if(p.r[r1]==0)p.r.F|=0x80;p.clock.c+=4;}, RLrra: function(p, r1, r2) {var addr = GameboyJS.Util.getRegAddr(p, r1, r2);var c=(p.r.F&0x10)?1:0;p.r.F=0;var out=p.memory.rb(addr)&0x80;out?p.r.F|=0x10:p.r.F&=0xEF;p.memory.wb(addr,((p.memory.rb(addr)<<1)+c)&0xFF);if(p.memory.rb(addr)==0)p.r.F|=0x80;p.clock.c+=12;}, RRr: function(p, r1) {var c=(p.r.F&0x10)?1:0;p.r.F=0;var out=p.r[r1]&0x01;out?p.r.F|=0x10:p.r.F&=0xEF;p.r[r1]=(p.r[r1]>>1)|(c*0x80);if(p.r[r1]==0)p.r.F|=0x80;p.clock.c+=4;}, RRrra: function(p, r1, r2) {var addr = GameboyJS.Util.getRegAddr(p, r1, r2);var c=(p.r.F&0x10)?1:0;p.r.F=0;var out=p.memory.rb(addr)&0x01;out?p.r.F|=0x10:p.r.F&=0xEF;p.memory.wb(addr,(p.memory.rb(addr)>>1)|(c*0x80));if(p.memory.rb(addr)==0)p.r.F|=0x80;p.clock.c+=12;}, SRAr: function(p, r1) {p.r.F = 0;if (p.r[r1]&0x01)p.r.F|=0x10;var msb=p.r[r1]&0x80;p.r[r1]=(p.r[r1]>>1)|msb;if (p.r[r1]==0)p.r.F|=0x80;p.clock.c+=4;}, SRArra: function(p, r1, r2) {var addr = GameboyJS.Util.getRegAddr(p, r1, r2);p.r.F = 0;if (p.memory.rb(addr)&0x01)p.r.F|=0x10;var msb=p.memory.rb(addr)&0x80;p.memory.wb(addr, (p.memory.rb(addr)>>1)|msb);if (p.memory.rb(addr)==0)p.r.F|=0x80;p.clock.c+=12;}, SLAr: function(p, r1) {p.r.F = 0;if (p.r[r1]&0x80)p.r.F|=0x10;p.r[r1]=(p.r[r1]<<1)&0xFF;if (p.r[r1]==0)p.r.F|=0x80;p.clock.c+=4;}, SLArra: function(p, r1, r2) {var addr = GameboyJS.Util.getRegAddr(p, r1, r2);p.r.F = 0;if (p.memory.rb(addr)&0x80)p.r.F|=0x10;p.memory.wb(addr, (p.memory.rb(addr)<<1)&0xFF);if (p.memory.rb(addr)==0)p.r.F|=0x80;p.clock.c+=12;}, SRLr: function(p, r1) {p.r.F = 0;if (p.r[r1]&0x01)p.r.F|=0x10;p.r[r1]=p.r[r1]>>1;if (p.r[r1]==0)p.r.F|=0x80;p.clock.c+=4;}, SRLrra: function(p, r1, r2) {var addr = GameboyJS.Util.getRegAddr(p, r1, r2);p.r.F = 0;if (p.memory.rb(addr)&0x01)p.r.F|=0x10;p.memory.wb(addr, p.memory.rb(addr)>>1);if (p.memory.rb(addr)==0)p.r.F|=0x80;p.clock.c+=12;}, BITir: function(p, i, r1) {var mask=1<> 4) | ((n&0x0F) << 4);}, JPnn: function(p) {p.wr('pc', (p.memory.rb(p.r.pc+1) << 8) + p.memory.rb(p.r.pc));p.clock.c += 16;}, JRccn: function(p, cc) {if (GameboyJS.Util.testFlag(p, cc)){var v=p.memory.rb(p.r.pc++);v=GameboyJS.Util.getSignedValue(v);p.r.pc += v;p.clock.c+=4;}else{p.r.pc++;}p.clock.c += 8;}, JPccnn: function(p, cc) {if (GameboyJS.Util.testFlag(p, cc)){p.wr('pc', (p.memory.rb(p.r.pc+1) << 8) + p.memory.rb(p.r.pc));p.clock.c+=4;}else{p.r.pc+=2;}p.clock.c += 12;}, JPrr: function(p, r1, r2) {p.r.pc = (p.r[r1] << 8) + p.r[r2];p.clock.c += 4;}, JRn: function(p) {var v=p.memory.rb(p.r.pc++);v=GameboyJS.Util.getSignedValue(v);p.r.pc += v;p.clock.c += 12;}, PUSHrr: function(p, r1, r2) {p.wr('sp', p.r.sp-1);p.memory.wb(p.r.sp, p.r[r1]);p.wr('sp', p.r.sp-1);p.memory.wb(p.r.sp, p.r[r2]);p.clock.c+=16;}, POPrr: function(p, r1, r2) {p.wr(r2, p.memory.rb(p.r.sp));p.wr('sp', p.r.sp+1);p.wr(r1, p.memory.rb(p.r.sp));p.wr('sp', p.r.sp+1);p.clock.c+=12;}, RSTn: function(p, n) {p.wr('sp', p.r.sp-1);p.memory.wb(p.r.sp,p.r.pc>>8);p.wr('sp', p.r.sp-1);p.memory.wb(p.r.sp,p.r.pc&0xFF);p.r.pc=n;p.clock.c+=16;}, RET: function(p) {p.r.pc = p.memory.rb(p.r.sp);p.wr('sp', p.r.sp+1);p.r.pc+=p.memory.rb(p.r.sp)<<8;p.wr('sp', p.r.sp+1);p.clock.c += 16;}, RETcc: function(p, cc) {if (GameboyJS.Util.testFlag(p, cc)){p.r.pc = p.memory.rb(p.r.sp);p.wr('sp', p.r.sp+1);p.r.pc+=p.memory.rb(p.r.sp)<<8;p.wr('sp', p.r.sp+1);p.clock.c+=12;}p.clock.c+=8;}, CALLnn: function(p) {ops._CALLnn(p); p.clock.c+=24;}, CALLccnn:function(p, cc) {if (GameboyJS.Util.testFlag(p, cc)){ops._CALLnn(p);p.clock.c+=12;}else{p.r.pc+=2;}p.clock.c+=12; }, _CALLnn:function(p){p.wr('sp', p.r.sp - 1); p.memory.wb(p.r.sp, ((p.r.pc+2)&0xFF00)>>8); p.wr('sp', p.r.sp - 1); p.memory.wb(p.r.sp, (p.r.pc+2)&0x00FF); var j=p.memory.rb(p.r.pc)+(p.memory.rb(p.r.pc+1)<<8);p.r.pc=j;}, CPL: function(p) {p.wr('A', (~p.r.A)&0xFF);p.r.F|=0x60,p.clock.c += 4;}, CCF: function(p) {p.r.F&=0x9F;p.r.F&0x10?p.r.F&=0xE0:p.r.F|=0x10;p.clock.c += 4;}, SCF: function(p) {p.r.F&=0x9F;p.r.F|=0x10;p.clock.c+=4;}, DAA: function(p) { var sub = (p.r.F&0x40) ? 1 : 0; var h = (p.r.F&0x20)?1:0;var c = (p.r.F&0x10)?1:0; if (sub) { if (h) { p.r.A = (p.r.A - 0x6) & 0xFF; } if (c) { p.r.A -= 0x60; } } else { if ((p.r.A&0xF) > 9 || h) { p.r.A += 0x6; } if (p.r.A > 0x9F || c) { p.r.A += 0x60; } } if (p.r.A&0x100) c = 1; p.r.A &= 0xFF; p.r.F &= 0x40;if (p.r.A == 0) p.r.F|=0x80;if (c) p.r.F|=0x10; p.clock.c += 4; }, HALT: function(p) {p.halt(); p.clock.c+=4;}, DI: function(p) {p.disableInterrupts();p.clock.c += 4;}, EI: function(p) {p.enableInterrupts();p.clock.c += 4;}, RETI: function(p) {p.enableInterrupts();ops.RET(p);}, CB: function(p) {var opcode = p.memory.rb(p.r.pc++); GameboyJS.opcodeCbmap[opcode](p); p.clock.c+=4;} }; GameboyJS.cpuOps = ops; }(GameboyJS || (GameboyJS = {}))); var GameboyJS; (function (GameboyJS) { "use strict"; var defaultOptions = { pad: {class: GameboyJS.Keyboard, mapping: null}, zoom: 1, romReaders: [], statusContainerId: 'status', gameNameContainerId: 'game-name', errorContainerId: 'error' }; // Gameboy class // // This object is the entry point of the application // Will delegate user actions to the emulated devices // and provide information where needed var Gameboy = function(canvas, options) { options = options || {}; this.options = GameboyJS.Util.extend({}, defaultOptions, options); var cpu = new GameboyJS.CPU(this); var screen = new GameboyJS.Screen(canvas, this.options.zoom); var gpu = new GameboyJS.GPU(screen, cpu); cpu.gpu = gpu; var pad = new this.options.pad.class(this.options.pad.mapping); var input = new GameboyJS.Input(cpu, pad); cpu.input = input; this.cpu = cpu; this.screen = screen; this.input = input; this.pad = pad; this.createRom(this.options.romReaders); this.statusContainer = document.getElementById(this.options.statusContainerId) || document.createElement('div'); this.gameNameContainer = document.getElementById(this.options.gameNameContainerId) || document.createElement('div'); this.errorContainer = document.getElementById(this.options.errorContainerId) || document.createElement('div'); }; // Create the ROM object and bind one or more readers Gameboy.prototype.createRom = function (readers) { var rom = new GameboyJS.Rom(this); if (readers.length == 0) { // add the default rom reader var romReader = new GameboyJS.RomFileReader(); rom.addReader(romReader); } else { for (var i in readers) { if (readers.hasOwnProperty(i)) { rom.addReader(readers[i]); } } } }; Gameboy.prototype.startRom = function(rom) { this.errorContainer.classList.add('hide'); this.cpu.reset(); try { this.cpu.loadRom(rom.data); this.setStatus('Game Running :'); this.setGameName(this.cpu.getGameName()); this.cpu.run(); } catch (e) { this.handleException(e); } }; Gameboy.prototype.pause = function(value) { if (value) { this.setStatus('Game Paused :'); this.cpu.pause(); } else { this.setStatus('Game Running :'); this.cpu.unpause(); } }; Gameboy.prototype.error = function(message) { this.setStatus('Error during execution'); this.setError('An error occurred during execution:' + message); this.cpu.stop(); }; Gameboy.prototype.setStatus = function(status) { this.statusContainer.innerHTML = status; }; // Display an error message Gameboy.prototype.setError = function(message) { this.errorContainer.classList.remove('hide'); this.errorContainer.innerHTML = message; }; // Display the name of the game running Gameboy.prototype.setGameName = function(name) { this.gameNameContainer.innerHTML = name; }; Gameboy.prototype.setSoundEnabled = function(value) { if (value) { this.cpu.apu.connect(); } else { this.cpu.apu.disconnect(); } }; Gameboy.prototype.setScreenZoom = function(value) { this.screen.setPixelSize(value); }; Gameboy.prototype.handleException = function(e) { if (e instanceof GameboyJS.UnimplementedException) { if (e.fatal) { this.error('This cartridge is not supported ('+ e.message +')'); } else { console.error(e.message); } } else { throw e; } }; GameboyJS.Gameboy = Gameboy; }(GameboyJS || (GameboyJS = {}))); var GameboyJS; (function (GameboyJS) { "use strict"; // Memory bank controllers var MBC = {}; // Create an MBC instance depending on the type specified in the cartridge MBC.getMbcInstance = function(memory, type) { var instance; switch (type) { case 0x00: instance = new MBC0(memory); break; case 0x01: case 0x02: case 0x03: instance = new MBC1(memory); break; case 0x0F: case 0x10: case 0x11: case 0x12: case 0x13: instance = new MBC3(memory); break; case 0x19: case 0x1A: case 0x1B: case 0x1C: case 0x1D: case 0x1E: instance = new MBC5(memory); break; default: throw new GameboyJS.UnimplementedException('MBC type not supported'); } return instance; }; var MBC1 = function(memory) { this.memory = memory; this.romBankNumber = 1; this.mode = 0; // mode 0 = ROM, mode 1 = RAM this.ramEnabled = true; this.extRam = new GameboyJS.ExtRam(); }; MBC1.prototype.loadRam = function(game, size) { this.extRam.loadRam(game, size); }; MBC1.prototype.manageWrite = function(addr, value) { switch (addr & 0xF000) { case 0x0000: case 0x1000: // enable RAM this.ramEnabled = (value & 0x0A) ? true : false; if (this.ramEnabled) { this.extRam.saveRamData(); } break; case 0x2000: case 0x3000: // ROM bank number lower 5 bits value &= 0x1F; if (value == 0) value = 1; var mask = this.mode ? 0 : 0xE0; this.romBankNumber = (this.romBankNumber & mask) +value; this.memory.loadRomBank(this.romBankNumber); break; case 0x4000: case 0x5000: // RAM bank or high bits ROM value &= 0x03; if (this.mode == 0) { // ROM upper bits this.romBankNumber = (this.romBankNumber&0x1F) | (value << 5); this.memory.loadRomBank(this.romBankNumber); } else { // RAM bank this.extRam.setRamBank(value); } break; case 0x6000: case 0x7000: // ROM / RAM mode this.mode = value & 1; break; case 0xA000: case 0xB000: this.extRam.manageWrite(addr - 0xA000, value); break; } }; MBC1.prototype.readRam = function(addr) { return this.extRam.manageRead(addr - 0xA000); }; var MBC3 = function(memory) { this.memory = memory; this.romBankNumber = 1; this.ramEnabled = true; this.extRam = new GameboyJS.ExtRam(); }; MBC3.prototype.loadRam = function(game, size) { this.extRam.loadRam(game, size); }; MBC3.prototype.manageWrite = function(addr, value) { switch (addr & 0xF000) { case 0x0000: case 0x1000: // enable RAM this.ramEnabled = (value & 0x0A) ? true : false; if (this.ramEnabled) { this.extRam.saveRamData(); } break; case 0x2000: case 0x3000: // ROM bank number value &= 0x7F; if (value == 0) value = 1; this.romBankNumber = value; this.memory.loadRomBank(this.romBankNumber); break; case 0x4000: case 0x5000: // RAM bank this.extRam.setRamBank(value); break; case 0x6000: case 0x7000: // Latch clock data throw new GameboyJS.UnimplementedException('cartridge clock not supported', false); break; case 0xA000: case 0xB000: this.extRam.manageWrite(addr - 0xA000, value); break; } }; MBC3.prototype.readRam = function(addr) { return this.extRam.manageRead(addr - 0xA000); }; // declare MBC5 for compatibility with most cartriges // does not support rumble feature var MBC5 = MBC3; // MBC0 exists for consistency and manages the no-MBC cartriges var MBC0 = function(memory) {this.memory = memory;}; MBC0.prototype.manageWrite = function(addr, value) { this.memory.loadRomBank(value); }; MBC0.prototype.readRam = function(addr) {return 0;}; MBC0.prototype.loadRam = function() {}; GameboyJS.MBC = MBC; }(GameboyJS || (GameboyJS = {}))); var GameboyJS; (function (GameboyJS) { "use strict"; // Memory unit var Memory = function(cpu) { this.MEM_SIZE = 65536; // 64KB this.MBCtype = 0; this.banksize = 0x4000; this.rom = null; this.mbc = null; this.cpu = cpu; }; Memory.addresses = { VRAM_START : 0x8000, VRAM_END : 0x9FFF, EXTRAM_START : 0xA000, EXTRAM_END : 0xBFFF, OAM_START : 0xFE00, OAM_END : 0xFE9F, DEVICE_START: 0xFF00, DEVICE_END: 0xFF7F }; // Memory can be accessed as an Array Memory.prototype = new Array(); Memory.prototype.reset = function() { this.length = this.MEM_SIZE; for (var i = Memory.addresses.VRAM_START; i <= Memory.addresses.VRAM_END; i++) { this[i] = 0; } for (var i = Memory.addresses.DEVICE_START; i <= Memory.addresses.DEVICE_END; i++) { this[i] = 0; } this[0xFFFF] = 0; }; Memory.prototype.setRomData = function(data) { this.rom = data; this.loadRomBank(0); this.mbc = GameboyJS.MBC.getMbcInstance(this, this[0x147]); this.loadRomBank(1); this.mbc.loadRam(this.cpu.getGameName(), this.cpu.getRamSize()); }; Memory.prototype.loadRomBank = function(index) { var start = index ? 0x4000 : 0x0; var romStart = index * 0x4000; for (var i = 0; i < this.banksize; i++) { this[i + start] = this.rom[romStart + i]; } }; // Video ram accessor Memory.prototype.vram = function(address) { if (address < Memory.addresses.VRAM_START || address > Memory.addresses.VRAM_END) { throw 'VRAM access in out of bounds address ' + address; } return this[address]; }; // OAM ram accessor Memory.prototype.oamram = function(address) { if (address < Memory.addresses.OAM_START || address > Memory.addresses.OAM_END) { throw 'OAMRAM access in out of bounds address ' + address; } return this[address]; }; // Device ram accessor Memory.prototype.deviceram = function(address, value) { if (address < Memory.addresses.DEVICERAM_START || address > Memory.addresses.DEVICERAM_END) { throw 'Device RAM access in out of bounds address ' + address; } if (typeof value === "undefined") { return this[address]; } else { this[address] = value; } }; // Memory read proxy function // Used to centralize memory read access Memory.prototype.rb = function (addr) { if (addr >= 0xFF10 && addr < 0xFF40) { var mask = apuMask[addr - 0xFF10]; return this[addr] | mask; } if ((addr >= 0xA000 && addr < 0xC000)) { return this.mbc.readRam(addr); } return this[addr]; }; // Bitmasks for audio addresses reads var apuMask = [ 0x80,0x3F,0x00,0xFF,0xBF, // NR10-NR15 0xFF,0x3F,0x00,0xFF,0xBF, // NR20-NR25 0x7F,0xFF,0x9F,0xFF,0xBF, // NR30-NR35 0xFF,0xFF,0x00,0x00,0xBF, // NR40-NR45 0x00,0x00,0x70, // NR50-NR52 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, // Wave RAM 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 ]; // Memory write proxy function // Used to centralize memory writes and delegate specific behaviour // to the correct units Memory.prototype.wb = function(addr, value) { if (addr < 0x8000 || (addr >= 0xA000 && addr < 0xC000)) { // MBC this.mbc.manageWrite(addr, value); } else if (addr >= 0xFF10 && addr <= 0xFF3F) { // sound registers this.cpu.apu.manageWrite(addr, value); } else if (addr == 0xFF00) { // input register this[addr] = ((this[addr] & 0x0F) | (value & 0x30)); } else { this[addr] = value; if ((addr & 0xFF00) == 0xFF00) { if (addr == 0xFF02) { if (value & 0x80) { this.cpu.enableSerialTransfer(); } } if (addr == 0xFF04) { this.cpu.resetDivTimer(); } if (addr == 0xFF46) { // OAM DMA transfer this.dmaTransfer(value); } } } } // Start a DMA transfer (OAM data from cartrige to RAM) Memory.prototype.dmaTransfer = function(startAddressPrefix) { var startAddress = (startAddressPrefix << 8); for (var i = 0; i < 0xA0; i++) { this[Memory.addresses.OAM_START + i] = this[startAddress + i]; } }; GameboyJS.Memory = Memory; }(GameboyJS || (GameboyJS = {}))); var GameboyJS; (function (GameboyJS) { "use strict"; var ops = GameboyJS.cpuOps; // Each opcode (0 to 0xFF) is associated to a CPU operation // CPU operations are implemented separately // The cbmap object holds operations for CB prefixed opcodes (0xCB00 to 0xCBFF) // Non existent opcodes are commented out and marked empty var map = { 0x00: function(p){p.clock.c += 4;}, 0x01: function(p){ops.LDrrnn(p, 'B', 'C');}, 0x02: function(p){ops.LDrrar(p, 'B', 'C', 'A');}, 0x03: function(p){ops.INCrr(p, 'B', 'C');}, 0x04: function(p){ops.INCr(p, 'B');}, 0x05: function(p){ops.DECr(p, 'B');}, 0x06: function(p){ops.LDrn(p, 'B');}, 0x07: function(p){var out=p.r.A & 0x80?1:0; out ? p.r.F=0x10:p.r.F=0; p.wr('A', ((p.r.A<<1)+out)&0xFF);p.clock.c+=4;}, 0x08: function(p){ops.LDnnsp(p);}, 0x09: function(p){ops.ADDrrrr(p, 'H', 'L', 'B', 'C');}, 0x0A: function(p){ops.LDrrra(p, 'A', 'B', 'C');}, 0x0B: function(p){ops.DECrr(p, 'B', 'C');}, 0x0C: function(p){ops.INCr(p, 'C');}, 0x0D: function(p){ops.DECr(p, 'C');}, 0x0E: function(p){ops.LDrn(p, 'C');}, 0x0F: function(p){var out=p.r.A & 0x01; out ? p.r.F=0x10:p.r.F=0; p.wr('A', (p.r.A>>1)|(out*0x80));p.clock.c+=4;}, 0x10: function(p){p.r.pc++;p.clock.c+=4;}, 0x11: function(p){ops.LDrrnn(p, 'D', 'E');}, 0x12: function(p){ops.LDrrar(p, 'D', 'E', 'A');}, 0x13: function(p){ops.INCrr(p, 'D', 'E');}, 0x14: function(p){ops.INCr(p, 'D');}, 0x15: function(p){ops.DECr(p, 'D');}, 0x16: function(p){ops.LDrn(p, 'D');}, 0x17: function(p){var c = (p.r.F&0x10)?1:0;var out=p.r.A & 0x80?1:0; out ? p.r.F=0x10:p.r.F=0; p.wr('A',((p.r.A<<1)+c)&0xFF);p.clock.c+=4;}, 0x18: function(p){ops.JRn(p);}, 0x19: function(p){ops.ADDrrrr(p, 'H', 'L', 'D', 'E');}, 0x1A: function(p){ops.LDrrra(p, 'A', 'D', 'E');}, 0x1B: function(p){ops.DECrr(p, 'D', 'E');}, 0x1C: function(p){ops.INCr(p, 'E');}, 0x1D: function(p){ops.DECr(p, 'E');}, 0x1E: function(p){ops.LDrn(p, 'E');}, 0x1F: function(p){var c = (p.r.F&0x10)?1:0;var out=p.r.A & 0x01; out ? p.r.F=0x10:p.r.F=0; p.wr('A', (p.r.A>>1)|(c*0x80));p.clock.c+=4;}, 0x20: function(p){ops.JRccn(p, 'NZ');}, 0x21: function(p){ops.LDrrnn(p, 'H', 'L');}, 0x22: function(p){ops.LDrrar(p, 'H', 'L', 'A');ops.INCrr(p, 'H', 'L');p.clock.c -= 8;}, 0x23: function(p){ops.INCrr(p, 'H', 'L');}, 0x24: function(p){ops.INCr(p, 'H');}, 0x25: function(p){ops.DECr(p, 'H');}, 0x26: function(p){ops.LDrn(p, 'H');}, 0x27: function(p){ops.DAA(p);}, 0x28: function(p){ops.JRccn(p, 'Z');}, 0x29: function(p){ops.ADDrrrr(p, 'H', 'L', 'H', 'L');}, 0x2A: function(p){ops.LDrrra(p, 'A', 'H', 'L');ops.INCrr(p, 'H', 'L');p.clock.c -= 8;}, 0x2B: function(p){ops.DECrr(p, 'H', 'L');}, 0x2C: function(p){ops.INCr(p, 'L');}, 0x2D: function(p){ops.DECr(p, 'L');}, 0x2E: function(p){ops.LDrn(p, 'L');}, 0x2F: function(p){ops.CPL(p);}, 0x30: function(p){ops.JRccn(p, 'NC');}, 0x31: function(p){ops.LDspnn(p);}, 0x32: function(p){ops.LDrrar(p, 'H', 'L', 'A');ops.DECrr(p, 'H', 'L');p.clock.c -= 8;}, 0x33: function(p){ops.INCsp(p);}, 0x34: function(p){ops.INCrra(p, 'H', 'L');}, 0x35: function(p){ops.DECrra(p, 'H', 'L');}, 0x36: function(p){ops.LDrran(p, 'H', 'L');}, 0x37: function(p){ops.SCF(p);}, 0x38: function(p){ops.JRccn(p, 'C');}, 0x39: function(p){ops.ADDrrsp(p, 'H', 'L');}, 0x3A: function(p){ops.LDrrra(p, 'A', 'H', 'L');ops.DECrr(p, 'H', 'L');p.clock.c -= 8;}, 0x3B: function(p){ops.DECsp(p);}, 0x3C: function(p){ops.INCr(p, 'A');}, 0x3D: function(p){ops.DECr(p, 'A');}, 0x3E: function(p){ops.LDrn(p, 'A');}, 0x3F: function(p){ops.CCF(p);}, 0x40: function(p){ops.LDrr(p, 'B', 'B');}, 0x41: function(p){ops.LDrr(p, 'B', 'C');}, 0x42: function(p){ops.LDrr(p, 'B', 'D');}, 0x43: function(p){ops.LDrr(p, 'B', 'E');}, 0x44: function(p){ops.LDrr(p, 'B', 'H');}, 0x45: function(p){ops.LDrr(p, 'B', 'L');}, 0x46: function(p){ops.LDrrra(p, 'B', 'H', 'L');}, 0x47: function(p){ops.LDrr(p, 'B', 'A');}, 0x48: function(p){ops.LDrr(p, 'C', 'B');}, 0x49: function(p){ops.LDrr(p, 'C', 'C');}, 0x4A: function(p){ops.LDrr(p, 'C', 'D');}, 0x4B: function(p){ops.LDrr(p, 'C', 'E');}, 0x4C: function(p){ops.LDrr(p, 'C', 'H');}, 0x4D: function(p){ops.LDrr(p, 'C', 'L');}, 0x4E: function(p){ops.LDrrra(p, 'C', 'H', 'L');}, 0x4F: function(p){ops.LDrr(p, 'C', 'A');}, 0x50: function(p){ops.LDrr(p, 'D', 'B');}, 0x51: function(p){ops.LDrr(p, 'D', 'C');}, 0x52: function(p){ops.LDrr(p, 'D', 'D');}, 0x53: function(p){ops.LDrr(p, 'D', 'E');}, 0x54: function(p){ops.LDrr(p, 'D', 'H');}, 0x55: function(p){ops.LDrr(p, 'D', 'L');}, 0x56: function(p){ops.LDrrra(p, 'D', 'H', 'L');}, 0x57: function(p){ops.LDrr(p, 'D', 'A');}, 0x58: function(p){ops.LDrr(p, 'E', 'B');}, 0x59: function(p){ops.LDrr(p, 'E', 'C');}, 0x5A: function(p){ops.LDrr(p, 'E', 'D');}, 0x5B: function(p){ops.LDrr(p, 'E', 'E');}, 0x5C: function(p){ops.LDrr(p, 'E', 'H');}, 0x5D: function(p){ops.LDrr(p, 'E', 'L');}, 0x5E: function(p){ops.LDrrra(p, 'E', 'H', 'L');}, 0x5F: function(p){ops.LDrr(p, 'E', 'A');}, 0x60: function(p){ops.LDrr(p, 'H', 'B');}, 0x61: function(p){ops.LDrr(p, 'H', 'C');}, 0x62: function(p){ops.LDrr(p, 'H', 'D');}, 0x63: function(p){ops.LDrr(p, 'H', 'E');}, 0x64: function(p){ops.LDrr(p, 'H', 'H');}, 0x65: function(p){ops.LDrr(p, 'H', 'L');}, 0x66: function(p){ops.LDrrra(p, 'H', 'H', 'L');}, 0x67: function(p){ops.LDrr(p, 'H', 'A');}, 0x68: function(p){ops.LDrr(p, 'L', 'B');}, 0x69: function(p){ops.LDrr(p, 'L', 'C');}, 0x6A: function(p){ops.LDrr(p, 'L', 'D');}, 0x6B: function(p){ops.LDrr(p, 'L', 'E');}, 0x6C: function(p){ops.LDrr(p, 'L', 'H');}, 0x6D: function(p){ops.LDrr(p, 'L', 'L');}, 0x6E: function(p){ops.LDrrra(p, 'L', 'H', 'L');}, 0x6F: function(p){ops.LDrr(p, 'L', 'A');}, 0x70: function(p){ops.LDrrar(p, 'H', 'L', 'B');}, 0x71: function(p){ops.LDrrar(p, 'H', 'L', 'C');}, 0x72: function(p){ops.LDrrar(p, 'H', 'L', 'D');}, 0x73: function(p){ops.LDrrar(p, 'H', 'L', 'E');}, 0x74: function(p){ops.LDrrar(p, 'H', 'L', 'H');}, 0x75: function(p){ops.LDrrar(p, 'H', 'L', 'L');}, 0x76: function(p){ops.HALT(p);}, 0x77: function(p){ops.LDrrar(p, 'H', 'L', 'A');}, 0x78: function(p){ops.LDrr(p, 'A', 'B');}, 0x79: function(p){ops.LDrr(p, 'A', 'C');}, 0x7A: function(p){ops.LDrr(p, 'A', 'D');}, 0x7B: function(p){ops.LDrr(p, 'A', 'E');}, 0x7C: function(p){ops.LDrr(p, 'A', 'H');}, 0x7D: function(p){ops.LDrr(p, 'A', 'L');}, 0x7E: function(p){ops.LDrrra(p, 'A', 'H', 'L');}, 0x7F: function(p){ops.LDrr(p, 'A', 'A');}, 0x80: function(p){ops.ADDrr(p, 'A', 'B');}, 0x81: function(p){ops.ADDrr(p, 'A', 'C');}, 0x82: function(p){ops.ADDrr(p, 'A', 'D');}, 0x83: function(p){ops.ADDrr(p, 'A', 'E');}, 0x84: function(p){ops.ADDrr(p, 'A', 'H');}, 0x85: function(p){ops.ADDrr(p, 'A', 'L');}, 0x86: function(p){ops.ADDrrra(p, 'A', 'H', 'L');}, 0x87: function(p){ops.ADDrr(p, 'A', 'A');}, 0x88: function(p){ops.ADCrr(p, 'A', 'B');}, 0x89: function(p){ops.ADCrr(p, 'A', 'C');}, 0x8A: function(p){ops.ADCrr(p, 'A', 'D');}, 0x8B: function(p){ops.ADCrr(p, 'A', 'E');}, 0x8C: function(p){ops.ADCrr(p, 'A', 'H');}, 0x8D: function(p){ops.ADCrr(p, 'A', 'L');}, 0x8E: function(p){ops.ADCrrra(p, 'A', 'H', 'L');}, 0x8F: function(p){ops.ADCrr(p, 'A', 'A');}, 0x90: function(p){ops.SUBr(p, 'B');}, 0x91: function(p){ops.SUBr(p, 'C');}, 0x92: function(p){ops.SUBr(p, 'D');}, 0x93: function(p){ops.SUBr(p, 'E');}, 0x94: function(p){ops.SUBr(p, 'H');}, 0x95: function(p){ops.SUBr(p, 'L');}, 0x96: function(p){ops.SUBrra(p, 'H', 'L');}, 0x97: function(p){ops.SUBr(p, 'A');}, 0x98: function(p){ops.SBCr(p, 'B');}, 0x99: function(p){ops.SBCr(p, 'C');}, 0x9A: function(p){ops.SBCr(p, 'D');}, 0x9B: function(p){ops.SBCr(p, 'E');}, 0x9C: function(p){ops.SBCr(p, 'H');}, 0x9D: function(p){ops.SBCr(p, 'L');}, 0x9E: function(p){ops.SBCrra(p, 'H', 'L');}, 0x9F: function(p){ops.SBCr(p, 'A');}, 0xA0: function(p){ops.ANDr(p, 'B');}, 0xA1: function(p){ops.ANDr(p, 'C');}, 0xA2: function(p){ops.ANDr(p, 'D');}, 0xA3: function(p){ops.ANDr(p, 'E');}, 0xA4: function(p){ops.ANDr(p, 'H');}, 0xA5: function(p){ops.ANDr(p, 'L');}, 0xA6: function(p){ops.ANDrra(p, 'H', 'L');}, 0xA7: function(p){ops.ANDr(p, 'A');}, 0xA8: function(p){ops.XORr(p, 'B');}, 0xA9: function(p){ops.XORr(p, 'C');}, 0xAA: function(p){ops.XORr(p, 'D');}, 0xAB: function(p){ops.XORr(p, 'E');}, 0xAC: function(p){ops.XORr(p, 'H');}, 0xAD: function(p){ops.XORr(p, 'L');}, 0xAE: function(p){ops.XORrra(p, 'H', 'L');}, 0xAF: function(p){ops.XORr(p, 'A');}, 0xB0: function(p){ops.ORr(p, 'B');}, 0xB1: function(p){ops.ORr(p, 'C');}, 0xB2: function(p){ops.ORr(p, 'D');}, 0xB3: function(p){ops.ORr(p, 'E');}, 0xB4: function(p){ops.ORr(p, 'H');}, 0xB5: function(p){ops.ORr(p, 'L');}, 0xB6: function(p){ops.ORrra(p, 'H', 'L');}, 0xB7: function(p){ops.ORr(p, 'A');}, 0xB8: function(p){ops.CPr(p, 'B');}, 0xB9: function(p){ops.CPr(p, 'C');}, 0xBA: function(p){ops.CPr(p, 'D');}, 0xBB: function(p){ops.CPr(p, 'E');}, 0xBC: function(p){ops.CPr(p, 'H');}, 0xBD: function(p){ops.CPr(p, 'L');}, 0xBE: function(p){ops.CPrra(p, 'H', 'L');}, 0xBF: function(p){ops.CPr(p, 'A');}, 0xC0: function(p){ops.RETcc(p, 'NZ');}, 0xC1: function(p){ops.POPrr(p, 'B', 'C');}, 0xC2: function(p){ops.JPccnn(p, 'NZ');}, 0xC3: function(p){ops.JPnn(p);}, 0xC4: function(p){ops.CALLccnn(p, 'NZ');}, 0xC5: function(p){ops.PUSHrr(p, 'B', 'C');}, 0xC6: function(p){ops.ADDrn(p, 'A');}, 0xC7: function(p){ops.RSTn(p, 0x00);}, 0xC8: function(p){ops.RETcc(p, 'Z');}, 0xC9: function(p){ops.RET(p);}, 0xCA: function(p){ops.JPccnn(p, 'Z');}, 0xCB: function(p){ops.CB(p);}, 0xCC: function(p){ops.CALLccnn(p, 'Z');}, 0xCD: function(p){ops.CALLnn(p);}, 0xCE: function(p){ops.ADCrn(p, 'A');}, 0xCF: function(p){ops.RSTn(p, 0x08);}, 0xD0: function(p){ops.RETcc(p, 'NC');}, 0xD1: function(p){ops.POPrr(p, 'D', 'E');}, 0xD2: function(p){ops.JPccnn(p, 'NC');}, //0xD3 empty 0xD4: function(p){ops.CALLccnn(p, 'NC');}, 0xD5: function(p){ops.PUSHrr(p, 'D', 'E');}, 0xD6: function(p){ops.SUBn(p);}, 0xD7: function(p){ops.RSTn(p, 0x10);}, 0xD8: function(p){ops.RETcc(p, 'C');}, 0xD9: function(p){ops.RETI(p);}, 0xDA: function(p){ops.JPccnn(p, 'C');}, //0xDB empty 0xDC: function(p){ops.CALLccnn(p, 'C');}, //0xDD empty 0xDE: function(p){ops.SBCn(p);}, 0xDF: function(p){ops.RSTn(p, 0x18);}, 0xE0: function(p){ops.LDHnar(p, 'A');}, 0xE1: function(p){ops.POPrr(p, 'H', 'L');}, 0xE2: function(p){ops.LDrar(p, 'C', 'A');}, //0xE3 empty //0xE4 empty 0xE5: function(p){ops.PUSHrr(p, 'H', 'L');}, 0xE6: function(p){ops.ANDn(p);}, 0xE7: function(p){ops.RSTn(p, 0x20);}, 0xE8: function(p){ops.ADDspn(p);}, 0xE9: function(p){ops.JPrr(p, 'H', 'L');}, 0xEA: function(p){ops.LDnnar(p, 'A');}, //0xEB empty //0xEC empty //0xED empty 0xEE: function(p){ops.XORn(p);}, 0xEF: function(p){ops.RSTn(p, 0x28);}, 0xF0: function(p){ops.LDHrna(p, 'A');}, 0xF1: function(p){ops.POPrr(p, 'A', 'F');}, 0xF2: function(p){ops.LDrra(p, 'A', 'C');}, 0xF3: function(p){ops.DI(p);}, //0xF4 empty 0xF5: function(p){ops.PUSHrr(p, 'A', 'F');}, 0xF6: function(p){ops.ORn(p);}, 0xF7: function(p){ops.RSTn(p, 0x30);}, 0xF8: function(p){ops.LDrrspn(p, 'H', 'L');}, 0xF9: function(p){ops.LDsprr(p, 'H', 'L');}, 0xFA: function(p){ops.LDrnna(p, 'A');}, 0xFB: function(p){ops.EI(p);}, //0xFC empty //0xFD empty 0xFE: function(p){ops.CPn(p);}, 0xFF: function(p){ops.RSTn(p, 0x38);} }; var cbmap = { 0x00: function(p){ops.RLCr(p, 'B');}, 0x01: function(p){ops.RLCr(p, 'C');}, 0x02: function(p){ops.RLCr(p, 'D');}, 0x03: function(p){ops.RLCr(p, 'E');}, 0x04: function(p){ops.RLCr(p, 'H');}, 0x05: function(p){ops.RLCr(p, 'L');}, 0x06: function(p){ops.RLCrra(p, 'H', 'L');}, 0x07: function(p){ops.RLCr(p, 'A');}, 0x08: function(p){ops.RRCr(p, 'B');}, 0x09: function(p){ops.RRCr(p, 'C');}, 0x0A: function(p){ops.RRCr(p, 'D');}, 0x0B: function(p){ops.RRCr(p, 'E');}, 0x0C: function(p){ops.RRCr(p, 'H');}, 0x0D: function(p){ops.RRCr(p, 'L');}, 0x0E: function(p){ops.RRCrra(p, 'H', 'L');}, 0x0F: function(p){ops.RRCr(p, 'A');}, 0x10: function(p){ops.RLr(p, 'B');}, 0x11: function(p){ops.RLr(p, 'C');}, 0x12: function(p){ops.RLr(p, 'D');}, 0x13: function(p){ops.RLr(p, 'E');}, 0x14: function(p){ops.RLr(p, 'H');}, 0x15: function(p){ops.RLr(p, 'L');}, 0x16: function(p){ops.RLrra(p, 'H', 'L');}, 0x17: function(p){ops.RLr(p, 'A');}, 0x18: function(p){ops.RRr(p, 'B');}, 0x19: function(p){ops.RRr(p, 'C');}, 0x1A: function(p){ops.RRr(p, 'D');}, 0x1B: function(p){ops.RRr(p, 'E');}, 0x1C: function(p){ops.RRr(p, 'H');}, 0x1D: function(p){ops.RRr(p, 'L');}, 0x1E: function(p){ops.RRrra(p, 'H', 'L');}, 0x1F: function(p){ops.RRr(p, 'A');}, 0x20: function(p){ops.SLAr(p, 'B');}, 0x21: function(p){ops.SLAr(p, 'C');}, 0x22: function(p){ops.SLAr(p, 'D');}, 0x23: function(p){ops.SLAr(p, 'E');}, 0x24: function(p){ops.SLAr(p, 'H');}, 0x25: function(p){ops.SLAr(p, 'L');}, 0x26: function(p){ops.SLArra(p, 'H', 'L');}, 0x27: function(p){ops.SLAr(p, 'A');}, 0x28: function(p){ops.SRAr(p, 'B');}, 0x29: function(p){ops.SRAr(p, 'C');}, 0x2A: function(p){ops.SRAr(p, 'D');}, 0x2B: function(p){ops.SRAr(p, 'E');}, 0x2C: function(p){ops.SRAr(p, 'H');}, 0x2D: function(p){ops.SRAr(p, 'L');}, 0x2E: function(p){ops.SRArra(p, 'H', 'L');}, 0x2F: function(p){ops.SRAr(p, 'A');}, 0x30: function(p){ops.SWAPr(p, 'B');}, 0x31: function(p){ops.SWAPr(p, 'C');}, 0x32: function(p){ops.SWAPr(p, 'D');}, 0x33: function(p){ops.SWAPr(p, 'E');}, 0x34: function(p){ops.SWAPr(p, 'H');}, 0x35: function(p){ops.SWAPr(p, 'L');}, 0x36: function(p){ops.SWAPrra(p, 'H', 'L');}, 0x37: function(p){ops.SWAPr(p, 'A');}, 0x38: function(p){ops.SRLr(p, 'B');}, 0x39: function(p){ops.SRLr(p, 'C');}, 0x3A: function(p){ops.SRLr(p, 'D');}, 0x3B: function(p){ops.SRLr(p, 'E');}, 0x3C: function(p){ops.SRLr(p, 'H');}, 0x3D: function(p){ops.SRLr(p, 'L');}, 0x3E: function(p){ops.SRLrra(p, 'H', 'L');}, 0x3F: function(p){ops.SRLr(p, 'A');}, 0x40: function(p){ops.BITir(p, 0, 'B');}, 0x41: function(p){ops.BITir(p, 0, 'C');}, 0x42: function(p){ops.BITir(p, 0, 'D');}, 0x43: function(p){ops.BITir(p, 0, 'E');}, 0x44: function(p){ops.BITir(p, 0, 'H');}, 0x45: function(p){ops.BITir(p, 0, 'L');}, 0x46: function(p){ops.BITirra(p, 0, 'H', 'L');}, 0x47: function(p){ops.BITir(p, 0, 'A');}, 0x48: function(p){ops.BITir(p, 1, 'B');}, 0x49: function(p){ops.BITir(p, 1, 'C');}, 0x4A: function(p){ops.BITir(p, 1, 'D');}, 0x4B: function(p){ops.BITir(p, 1, 'E');}, 0x4C: function(p){ops.BITir(p, 1, 'H');}, 0x4D: function(p){ops.BITir(p, 1, 'L');}, 0x4E: function(p){ops.BITirra(p, 1, 'H', 'L');}, 0x4F: function(p){ops.BITir(p, 1, 'A');}, 0x50: function(p){ops.BITir(p, 2, 'B');}, 0x51: function(p){ops.BITir(p, 2, 'C');}, 0x52: function(p){ops.BITir(p, 2, 'D');}, 0x53: function(p){ops.BITir(p, 2, 'E');}, 0x54: function(p){ops.BITir(p, 2, 'H');}, 0x55: function(p){ops.BITir(p, 2, 'L');}, 0x56: function(p){ops.BITirra(p, 2, 'H', 'L');}, 0x57: function(p){ops.BITir(p, 2, 'A');}, 0x58: function(p){ops.BITir(p, 3, 'B');}, 0x59: function(p){ops.BITir(p, 3, 'C');}, 0x5A: function(p){ops.BITir(p, 3, 'D');}, 0x5B: function(p){ops.BITir(p, 3, 'E');}, 0x5C: function(p){ops.BITir(p, 3, 'H');}, 0x5D: function(p){ops.BITir(p, 3, 'L');}, 0x5E: function(p){ops.BITirra(p, 3, 'H', 'L');}, 0x5F: function(p){ops.BITir(p, 3, 'A');}, 0x60: function(p){ops.BITir(p, 4, 'B');}, 0x61: function(p){ops.BITir(p, 4, 'C');}, 0x62: function(p){ops.BITir(p, 4, 'D');}, 0x63: function(p){ops.BITir(p, 4, 'E');}, 0x64: function(p){ops.BITir(p, 4, 'H');}, 0x65: function(p){ops.BITir(p, 4, 'L');}, 0x66: function(p){ops.BITirra(p, 4, 'H', 'L');}, 0x67: function(p){ops.BITir(p, 4, 'A');}, 0x68: function(p){ops.BITir(p, 5, 'B');}, 0x69: function(p){ops.BITir(p, 5, 'C');}, 0x6A: function(p){ops.BITir(p, 5, 'D');}, 0x6B: function(p){ops.BITir(p, 5, 'E');}, 0x6C: function(p){ops.BITir(p, 5, 'H');}, 0x6D: function(p){ops.BITir(p, 5, 'L');}, 0x6E: function(p){ops.BITirra(p, 5, 'H', 'L');}, 0x6F: function(p){ops.BITir(p, 5, 'A');}, 0x70: function(p){ops.BITir(p, 6, 'B');}, 0x71: function(p){ops.BITir(p, 6, 'C');}, 0x72: function(p){ops.BITir(p, 6, 'D');}, 0x73: function(p){ops.BITir(p, 6, 'E');}, 0x74: function(p){ops.BITir(p, 6, 'H');}, 0x75: function(p){ops.BITir(p, 6, 'L');}, 0x76: function(p){ops.BITirra(p, 6, 'H', 'L');}, 0x77: function(p){ops.BITir(p, 6, 'A');}, 0x78: function(p){ops.BITir(p, 7, 'B');}, 0x79: function(p){ops.BITir(p, 7, 'C');}, 0x7A: function(p){ops.BITir(p, 7, 'D');}, 0x7B: function(p){ops.BITir(p, 7, 'E');}, 0x7C: function(p){ops.BITir(p, 7, 'H');}, 0x7D: function(p){ops.BITir(p, 7, 'L');}, 0x7E: function(p){ops.BITirra(p, 7, 'H', 'L');}, 0x7F: function(p){ops.BITir(p, 7, 'A');}, 0x80: function(p){ops.RESir(p, 0, 'B');}, 0x81: function(p){ops.RESir(p, 0, 'C');}, 0x82: function(p){ops.RESir(p, 0, 'D');}, 0x83: function(p){ops.RESir(p, 0, 'E');}, 0x84: function(p){ops.RESir(p, 0, 'H');}, 0x85: function(p){ops.RESir(p, 0, 'L');}, 0x86: function(p){ops.RESirra(p, 0, 'H', 'L');}, 0x87: function(p){ops.RESir(p, 0, 'A');}, 0x88: function(p){ops.RESir(p, 1, 'B');}, 0x89: function(p){ops.RESir(p, 1, 'C');}, 0x8A: function(p){ops.RESir(p, 1, 'D');}, 0x8B: function(p){ops.RESir(p, 1, 'E');}, 0x8C: function(p){ops.RESir(p, 1, 'H');}, 0x8D: function(p){ops.RESir(p, 1, 'L');}, 0x8E: function(p){ops.RESirra(p, 1, 'H', 'L');}, 0x8F: function(p){ops.RESir(p, 1, 'A');}, 0x90: function(p){ops.RESir(p, 2, 'B');}, 0x91: function(p){ops.RESir(p, 2, 'C');}, 0x92: function(p){ops.RESir(p, 2, 'D');}, 0x93: function(p){ops.RESir(p, 2, 'E');}, 0x94: function(p){ops.RESir(p, 2, 'H');}, 0x95: function(p){ops.RESir(p, 2, 'L');}, 0x96: function(p){ops.RESirra(p, 2, 'H', 'L');}, 0x97: function(p){ops.RESir(p, 2, 'A');}, 0x98: function(p){ops.RESir(p, 3, 'B');}, 0x99: function(p){ops.RESir(p, 3, 'C');}, 0x9A: function(p){ops.RESir(p, 3, 'D');}, 0x9B: function(p){ops.RESir(p, 3, 'E');}, 0x9C: function(p){ops.RESir(p, 3, 'H');}, 0x9D: function(p){ops.RESir(p, 3, 'L');}, 0x9E: function(p){ops.RESirra(p, 3, 'H', 'L');}, 0x9F: function(p){ops.RESir(p, 3, 'A');}, 0xA0: function(p){ops.RESir(p, 4, 'B');}, 0xA1: function(p){ops.RESir(p, 4, 'C');}, 0xA2: function(p){ops.RESir(p, 4, 'D');}, 0xA3: function(p){ops.RESir(p, 4, 'E');}, 0xA4: function(p){ops.RESir(p, 4, 'H');}, 0xA5: function(p){ops.RESir(p, 4, 'L');}, 0xA6: function(p){ops.RESirra(p, 4, 'H', 'L');}, 0xA7: function(p){ops.RESir(p, 4, 'A');}, 0xA8: function(p){ops.RESir(p, 5, 'B');}, 0xA9: function(p){ops.RESir(p, 5, 'C');}, 0xAA: function(p){ops.RESir(p, 5, 'D');}, 0xAB: function(p){ops.RESir(p, 5, 'E');}, 0xAC: function(p){ops.RESir(p, 5, 'H');}, 0xAD: function(p){ops.RESir(p, 5, 'L');}, 0xAE: function(p){ops.RESirra(p, 5, 'H', 'L');}, 0xAF: function(p){ops.RESir(p, 5, 'A');}, 0xB0: function(p){ops.RESir(p, 6, 'B');}, 0xB1: function(p){ops.RESir(p, 6, 'C');}, 0xB2: function(p){ops.RESir(p, 6, 'D');}, 0xB3: function(p){ops.RESir(p, 6, 'E');}, 0xB4: function(p){ops.RESir(p, 6, 'H');}, 0xB5: function(p){ops.RESir(p, 6, 'L');}, 0xB6: function(p){ops.RESirra(p, 6, 'H', 'L');}, 0xB7: function(p){ops.RESir(p, 6, 'A');}, 0xB8: function(p){ops.RESir(p, 7, 'B');}, 0xB9: function(p){ops.RESir(p, 7, 'C');}, 0xBA: function(p){ops.RESir(p, 7, 'D');}, 0xBB: function(p){ops.RESir(p, 7, 'E');}, 0xBC: function(p){ops.RESir(p, 7, 'H');}, 0xBD: function(p){ops.RESir(p, 7, 'L');}, 0xBE: function(p){ops.RESirra(p, 7, 'H', 'L');}, 0xBF: function(p){ops.RESir(p, 7, 'A');}, 0xC0: function(p){ops.SETir(p, 0, 'B');}, 0xC1: function(p){ops.SETir(p, 0, 'C');}, 0xC2: function(p){ops.SETir(p, 0, 'D');}, 0xC3: function(p){ops.SETir(p, 0, 'E');}, 0xC4: function(p){ops.SETir(p, 0, 'H');}, 0xC5: function(p){ops.SETir(p, 0, 'L');}, 0xC6: function(p){ops.SETirra(p, 0, 'H', 'L');}, 0xC7: function(p){ops.SETir(p, 0, 'A');}, 0xC8: function(p){ops.SETir(p, 1, 'B');}, 0xC9: function(p){ops.SETir(p, 1, 'C');}, 0xCA: function(p){ops.SETir(p, 1, 'D');}, 0xCB: function(p){ops.SETir(p, 1, 'E');}, 0xCC: function(p){ops.SETir(p, 1, 'H');}, 0xCD: function(p){ops.SETir(p, 1, 'L');}, 0xCE: function(p){ops.SETirra(p, 1, 'H', 'L');}, 0xCF: function(p){ops.SETir(p, 1, 'A');}, 0xD0: function(p){ops.SETir(p, 2, 'B');}, 0xD1: function(p){ops.SETir(p, 2, 'C');}, 0xD2: function(p){ops.SETir(p, 2, 'D');}, 0xD3: function(p){ops.SETir(p, 2, 'E');}, 0xD4: function(p){ops.SETir(p, 2, 'H');}, 0xD5: function(p){ops.SETir(p, 2, 'L');}, 0xD6: function(p){ops.SETirra(p, 2, 'H', 'L');}, 0xD7: function(p){ops.SETir(p, 2, 'A');}, 0xD8: function(p){ops.SETir(p, 3, 'B');}, 0xD9: function(p){ops.SETir(p, 3, 'C');}, 0xDA: function(p){ops.SETir(p, 3, 'D');}, 0xDB: function(p){ops.SETir(p, 3, 'E');}, 0xDC: function(p){ops.SETir(p, 3, 'H');}, 0xDD: function(p){ops.SETir(p, 3, 'L');}, 0xDE: function(p){ops.SETirra(p, 3, 'H', 'L');}, 0xDF: function(p){ops.SETir(p, 3, 'A');}, 0xE0: function(p){ops.SETir(p, 4, 'B');}, 0xE1: function(p){ops.SETir(p, 4, 'C');}, 0xE2: function(p){ops.SETir(p, 4, 'D');}, 0xE3: function(p){ops.SETir(p, 4, 'E');}, 0xE4: function(p){ops.SETir(p, 4, 'H');}, 0xE5: function(p){ops.SETir(p, 4, 'L');}, 0xE6: function(p){ops.SETirra(p, 4, 'H', 'L');}, 0xE7: function(p){ops.SETir(p, 4, 'A');}, 0xE8: function(p){ops.SETir(p, 5, 'B');}, 0xE9: function(p){ops.SETir(p, 5, 'C');}, 0xEA: function(p){ops.SETir(p, 5, 'D');}, 0xEB: function(p){ops.SETir(p, 5, 'E');}, 0xEC: function(p){ops.SETir(p, 5, 'H');}, 0xED: function(p){ops.SETir(p, 5, 'L');}, 0xEE: function(p){ops.SETirra(p, 5, 'H', 'L');}, 0xEF: function(p){ops.SETir(p, 5, 'A');}, 0xF0: function(p){ops.SETir(p, 6, 'B');}, 0xF1: function(p){ops.SETir(p, 6, 'C');}, 0xF2: function(p){ops.SETir(p, 6, 'D');}, 0xF3: function(p){ops.SETir(p, 6, 'E');}, 0xF4: function(p){ops.SETir(p, 6, 'H');}, 0xF5: function(p){ops.SETir(p, 6, 'L');}, 0xF6: function(p){ops.SETirra(p, 6, 'H', 'L');}, 0xF7: function(p){ops.SETir(p, 6, 'A');}, 0xF8: function(p){ops.SETir(p, 7, 'B');}, 0xF9: function(p){ops.SETir(p, 7, 'C');}, 0xFA: function(p){ops.SETir(p, 7, 'D');}, 0xFB: function(p){ops.SETir(p, 7, 'E');}, 0xFC: function(p){ops.SETir(p, 7, 'H');}, 0xFD: function(p){ops.SETir(p, 7, 'L');}, 0xFE: function(p){ops.SETirra(p, 7, 'H', 'L');}, 0xFF: function(p){ops.SETir(p, 7, 'A');} }; GameboyJS.opcodeMap = map; GameboyJS.opcodeCbmap = cbmap; }(GameboyJS || (GameboyJS = {}))); var GameboyJS; (function (GameboyJS) { "use strict"; // A RomAjaxReader is able to load a file through an AJAX request var RomAjaxReader = function() { }; // The callback argument will be called when a file is successfully // read, with the data as argument (Uint8Array) RomAjaxReader.prototype.setCallback = function(onLoadCallback) { this.callback = onLoadCallback; }; // This function should be called by application code // and will trigger the AJAX call itself and push data to the ROM object RomAjaxReader.prototype.loadFromUrl = function(url) { if (!url) { throw 'No url has been set in order to load a ROM file.'; } var cb = this.callback; var xhr = new XMLHttpRequest(); xhr.open('GET', url, true); xhr.responseType = "arraybuffer"; xhr.onload = function() { var rom = new Uint8Array(xhr.response); cb && cb(rom); }; xhr.send(); }; GameboyJS.RomAjaxReader = RomAjaxReader; }(GameboyJS || (GameboyJS = {}))); var GameboyJS; (function (GameboyJS) { "use strict"; // A RomDropFileReader is able to load a drag and dropped file var RomDropFileReader = function(el) { this.dropElement = el; if (!this.dropElement) { throw 'The RomDropFileReader needs a drop zone.'; } var self = this; this.dropElement.addEventListener('dragenter', function(e) { e.preventDefault(); e.target.classList.add('drag-active'); }); this.dropElement.addEventListener('dragleave', function(e) { e.preventDefault(); e.target.classList.remove('drag-active'); }); this.dropElement.addEventListener('dragover', function(e) { e.preventDefault(); }); this.dropElement.addEventListener('drop', function (e) { e.target.classList.remove('drag-active'); if (e.dataTransfer.files.length == 0) { return; } e.preventDefault(); self.loadFromFile(e.dataTransfer.files[0]); }); }; // The callback argument will be called when a file is successfully // read, with the data as argument (Uint8Array) RomDropFileReader.prototype.setCallback = function(onLoadCallback) { this.callback = onLoadCallback; }; // The file loading logic is the same as the regular file reader RomDropFileReader.prototype.loadFromFile = function(file) { if (file === undefined) { return; } var fr = new FileReader(); var cb = this.callback; fr.onload = function() { cb && cb(new Uint8Array(fr.result)); }; fr.onerror = function(e) { console.log('Error reading the file', e.target.error.code) }; fr.readAsArrayBuffer(file); }; GameboyJS.RomDropFileReader = RomDropFileReader; }(GameboyJS || (GameboyJS = {}))); var GameboyJS; (function (GameboyJS) { "use strict"; // A RomFileReader is able to load a local file from an input element // // Expects to be provided a file input element, // or will try to find one with the "file" DOM ID var RomFileReader = function(el) { this.domElement = el || document.getElementById('file'); if (!this.domElement) { throw 'The RomFileReader needs a valid input element.'; } var self = this; this.domElement.addEventListener('change', function(e){ self.loadFromFile(e.target.files[0]); }); }; // The callback argument will be called when a file is successfully // read, with the data as argument (Uint8Array) RomFileReader.prototype.setCallback = function(onLoadCallback) { this.callback = onLoadCallback; }; // Automatically called when the DOM input is provided with a file RomFileReader.prototype.loadFromFile = function(file) { if (file === undefined) { return; } var fr = new FileReader(); var cb = this.callback; fr.onload = function() { cb && cb(new Uint8Array(fr.result)); }; fr.onerror = function(e) { console.log('Error reading the file', e.target.error.code) }; fr.readAsArrayBuffer(file); }; GameboyJS.RomFileReader = RomFileReader; }(GameboyJS || (GameboyJS = {}))); var GameboyJS; (function (GameboyJS) { "use strict"; var Rom = function(gameboy, romReader) { this.gameboy = gameboy; if (romReader) { this.addReader(romReader); } }; Rom.prototype.addReader = function(romReader) { var self = this; romReader.setCallback(function(data) { if (!validate(data)) { self.gameboy.error('The file is not a valid GameBoy ROM.'); return; } self.data = data; self.gameboy.startRom(self); }); }; // Validate the checksum of the cartridge header function validate(data) { var hash = 0; for (var i = 0x134; i <= 0x14C; i++) { hash = hash - data[i] - 1; } return (hash & 0xFF) == data[0x14D]; }; GameboyJS.Rom = Rom; }(GameboyJS || (GameboyJS = {}))); var GameboyJS; (function (GameboyJS) { "use strict"; // Handlers for the Serial port of the Gameboy // The ConsoleSerial is an output-only serial port // designed for debug purposes as some test roms output data on the serial port // // Will regularly output the received byte (converted to string) in the console logs // This handler always push the value 0xFF as an input var ConsoleSerial = { current: '', timeout: null, out: function(data) { ConsoleSerial.current += String.fromCharCode(data); if (data == 10) { ConsoleSerial.print(); } else { clearTimeout(ConsoleSerial.timeout); ConsoleSerial.timeout = setTimeout(ConsoleSerial.print, 500); } }, in: function() { return 0xFF; }, print: function() { clearTimeout(ConsoleSerial.timeout); console.log('serial: '+ConsoleSerial.current); ConsoleSerial.current = ''; } }; GameboyJS.ConsoleSerial = ConsoleSerial; // A DummySerial outputs nothing and always inputs 0xFF var DummySerial = { out: function() {}, in: function() { return 0xFF; } }; GameboyJS.DummySerial = DummySerial; }(GameboyJS || (GameboyJS = {}))); var GameboyJS; (function (GameboyJS) { "use strict"; // Audio Processing unit // Listens the write accesses to the audio-reserved memory addresses // and dispatches the data to the sound channels var APU = function(memory) { this.memory = memory; this.enabled = false; AudioContext = window.AudioContext || window.webkitAudioContext; var audioContext = new AudioContext(); this.channel1 = new GameboyJS.Channel1(this, 1, audioContext); this.channel2 = new GameboyJS.Channel1(this, 2, audioContext); this.channel3 = new GameboyJS.Channel3(this, 3, audioContext); this.channel4 = new GameboyJS.Channel4(this, 4, audioContext); }; APU.prototype.connect = function() { this.channel1.enable(); this.channel2.enable(); this.channel3.enable(); }; APU.prototype.disconnect = function() { this.channel1.disable(); this.channel2.disable(); this.channel3.disable(); }; // Updates the states of each channel given the elapsed time // (in instructions) since last update APU.prototype.update = function(clockElapsed) { if (this.enabled == false) return; this.channel1.update(clockElapsed); this.channel2.update(clockElapsed); this.channel3.update(clockElapsed); this.channel4.update(clockElapsed); }; APU.prototype.setSoundFlag = function(channel, value) { var mask = 0xFF - (1 << (channel - 1)); value = value << (channel - 1) var byteValue = this.memory.rb(APU.registers.NR52); byteValue &= mask; byteValue |= value; this.memory[APU.registers.NR52] = byteValue; }; // Manage writes to audio registers // Will update the channels depending on the address APU.prototype.manageWrite = function(addr, value) { if (this.enabled == false && addr < APU.registers.NR52) { return; } this.memory[addr] = value; switch (addr) { // Channel 1 addresses case 0xFF10: this.channel1.clockSweep = 0; this.channel1.sweepTime = ((value & 0x70) >> 4); this.channel1.sweepSign = (value & 0x08) ? -1 : 1; this.channel1.sweepShifts = (value & 0x07); this.channel1.sweepCount = this.channel1.sweepShifts; break; case 0xFF11: // todo : bits 6-7 this.channel1.setLength(value & 0x3F); break; case 0xFF12: this.channel1.envelopeSign = (value & 0x08) ? 1 : -1; var envelopeVolume = (value & 0xF0) >> 4; this.channel1.setEnvelopeVolume(envelopeVolume); this.channel1.envelopeStep = (value & 0x07); break; case 0xFF13: var frequency = this.channel1.getFrequency(); frequency &= 0xF00; frequency |= value; this.channel1.setFrequency(frequency); break; case 0xFF14: var frequency = this.channel1.getFrequency(); frequency &= 0xFF; frequency |= (value & 7) << 8; this.channel1.setFrequency(frequency); this.channel1.lengthCheck = (value & 0x40) ? true : false; if (value & 0x80) this.channel1.play(); break; // Channel 2 addresses case 0xFF16: // todo : bits 6-7 this.channel2.setLength(value & 0x3F); break; case 0xFF17: this.channel2.envelopeSign = (value & 0x08) ? 1 : -1; var envelopeVolume = (value & 0xF0) >> 4; this.channel2.setEnvelopeVolume(envelopeVolume); this.channel2.envelopeStep = (value & 0x07); break; case 0xFF18: var frequency = this.channel2.getFrequency(); frequency &= 0xF00; frequency |= value; this.channel2.setFrequency(frequency); break; case 0xFF19: var frequency = this.channel2.getFrequency(); frequency &= 0xFF; frequency |= (value & 7) << 8; this.channel2.setFrequency(frequency); this.channel2.lengthCheck = (value & 0x40) ? true : false; if (value & 0x80) { this.channel2.play(); } break; // Channel 3 addresses case 0xFF1A: // todo break; case 0xFF1B: this.channel3.setLength(value); break; case 0xFF1C: // todo break; case 0xFF1D: var frequency = this.channel3.getFrequency(); frequency &= 0xF00; frequency |= value; this.channel3.setFrequency(frequency); break; case 0xFF1E: var frequency = this.channel3.getFrequency(); frequency &= 0xFF; frequency |= (value & 7) << 8; this.channel3.setFrequency(frequency); this.channel3.lengthCheck = (value & 0x40) ? true : false; if (value & 0x80) { this.channel3.play(); } break; // Channel 4 addresses case 0xFF20: this.channel4.setLength(value & 0x3F); break; case 0xFF21: // todo break; case 0xFF22: // todo break; case 0xFF23: this.channel4.lengthCheck = (value & 0x40) ? true : false; if (value & 0x80) { this.channel4.play(); } break; // channel 3 wave bytes case 0xFF30:case 0xFF31:case 0xFF32:case 0xFF33:case 0xFF34:case 0xFF35:case 0xFF36:case 0xFF37: case 0xFF38:case 0xFF39:case 0xFF3A:case 0xFF3B:case 0xFF3C:case 0xFF3D:case 0xFF3E:case 0xFF3F: var index = addr - 0xFF30; this.channel3.setWaveBufferByte(index, value); break; // general audio switch case 0xFF26: value &= 0xF0; this.memory[addr] = value; this.enabled = (value & 0x80) == 0 ? false : true; if (!this.enabled) { for (var i = 0xFF10; i < 0xFF27; i++) this.memory[i] = 0; // todo stop sound } break; } }; APU.registers = { NR10: 0xFF10, NR11: 0xFF11, NR12: 0xFF12, NR13: 0xFF13, NR14: 0xFF14, NR21: 0xFF16, NR22: 0xFF17, NR23: 0xFF18, NR24: 0xFF19, NR30: 0xFF1A, NR31: 0xFF1B, NR32: 0xFF1C, NR33: 0xFF1D, NR34: 0xFF1E, NR41: 0xFF20, NR42: 0xFF21, NR43: 0xFF22, NR44: 0xFF23, NR50: 0xFF24, NR51: 0xFF25, NR52: 0xFF26 }; GameboyJS.APU = APU; }(GameboyJS || (GameboyJS = {}))); var GameboyJS; (function (GameboyJS) { "use strict"; var Channel1 = function(apu, channelNumber, audioContext) { this.apu = apu; this.channelNumber = channelNumber; this.playing = false; this.soundLengthUnit = 0x4000; // 1 / 256 second of instructions this.soundLength = 64; // defaults to 64 periods this.lengthCheck = false; this.sweepTime = 0; // from 0 to 7 this.sweepStepLength = 0x8000; // 1 / 128 seconds of instructions this.sweepCount = 0; this.sweepShifts = 0; this.sweepSign = 1; // +1 / -1 for increase / decrease freq this.frequency = 0; this.envelopeStep = 0; this.envelopeStepLength = 0x10000;// 1 / 64 seconds of instructions this.envelopeCheck = false; this.envelopeSign = 1; this.clockLength = 0; this.clockEnvelop = 0; this.clockSweep = 0; var gainNode = audioContext.createGain(); gainNode.gain.value = 0; var oscillator = audioContext.createOscillator(); oscillator.type = 'square'; oscillator.frequency.value = 1000; oscillator.connect(gainNode); oscillator.start(0); this.audioContext = audioContext; this.gainNode = gainNode; this.oscillator = oscillator; }; Channel1.prototype.play = function() { if (this.playing) return; this.playing = true; this.apu.setSoundFlag(this.channelNumber, 1); this.gainNode.connect(this.audioContext.destination); this.clockLength = 0; this.clockEnvelop = 0; this.clockSweep = 0; if (this.sweepShifts > 0) this.checkFreqSweep(); }; Channel1.prototype.stop = function() { this.playing = false; this.apu.setSoundFlag(this.channelNumber, 0); this.gainNode.disconnect(); }; Channel1.prototype.checkFreqSweep = function() { var oldFreq = this.getFrequency(); var newFreq = oldFreq + this.sweepSign * (oldFreq >> this.sweepShifts); if (newFreq > 0x7FF) { newFreq = 0; this.stop(); } return newFreq; }; Channel1.prototype.update = function(clockElapsed) { this.clockEnvelop += clockElapsed; this.clockSweep += clockElapsed; if ((this.sweepCount || this.sweepTime) && this.clockSweep > (this.sweepStepLength * this.sweepTime)) { this.clockSweep -= (this.sweepStepLength * this.sweepTime); this.sweepCount--; var newFreq = this.checkFreqSweep(); // process and check new freq this.apu.memory[0xFF13] = newFreq & 0xFF; this.apu.memory[0xFF14] &= 0xF8; this.apu.memory[0xFF14] |= (newFreq & 0x700) >> 8; this.setFrequency(newFreq); this.checkFreqSweep(); // check again with new value } if (this.envelopeCheck && this.clockEnvelop > this.envelopeStepLength) { this.clockEnvelop -= this.envelopeStepLength; this.envelopeStep--; this.setEnvelopeVolume(this.envelopeVolume + this.envelopeSign); if (this.envelopeStep <= 0) { this.envelopeCheck = false; } } if (this.lengthCheck) { this.clockLength += clockElapsed; if (this.clockLength > this.soundLengthUnit) { this.soundLength--; this.clockLength -= this.soundLengthUnit; if (this.soundLength == 0) { this.setLength(0); this.stop(); } } } }; Channel1.prototype.setFrequency = function(value) { this.frequency = value; this.oscillator.frequency.value = 131072 / (2048 - this.frequency); }; Channel1.prototype.getFrequency = function() { return this.frequency; }; Channel1.prototype.setLength = function(value) { this.soundLength = 64 - (value & 0x3F); }; Channel1.prototype.setEnvelopeVolume = function(volume) { this.envelopeCheck = volume > 0 && volume < 16 ? true : false; this.envelopeVolume = volume; this.gainNode.gain.value = this.envelopeVolume * 1/100; }; Channel1.prototype.disable = function() { this.oscillator.disconnect(); }; Channel1.prototype.enable = function() { this.oscillator.connect(this.gainNode); }; GameboyJS.Channel1 = Channel1; }(GameboyJS || (GameboyJS = {}))); var GameboyJS; (function (GameboyJS) { "use strict"; var Channel3 = function(apu, channelNumber, audioContext) { this.apu = apu; this.channelNumber = channelNumber; this.playing = false; this.soundLength = 0; this.soundLengthUnit = 0x4000; // 1 / 256 second of instructions this.lengthCheck = false; this.clockLength = 0; this.buffer = new Float32Array(32); var gainNode = audioContext.createGain(); gainNode.gain.value = 1; this.gainNode = gainNode; this.baseSpeed = 65536; var waveBuffer = audioContext.createBuffer(1, 32, this.baseSpeed); var bufferSource = audioContext.createBufferSource(); bufferSource.buffer = waveBuffer; bufferSource.loop = true; bufferSource.connect(gainNode); bufferSource.start(0); this.audioContext = audioContext; this.waveBuffer = waveBuffer; this.bufferSource = bufferSource; }; Channel3.prototype.play = function() { if (this.playing) return; this.playing = true; this.apu.setSoundFlag(this.channelNumber, 1); this.waveBuffer.copyToChannel(this.buffer, 0, 0); this.gainNode.connect(this.audioContext.destination); this.clockLength = 0; }; Channel3.prototype.stop = function() { this.playing = false; this.apu.setSoundFlag(this.channelNumber, 0); this.gainNode.disconnect(); }; Channel3.prototype.update = function(clockElapsed) { if (this.lengthCheck){ this.clockLength += clockElapsed; if (this.clockLength > this.soundLengthUnit) { this.soundLength--; this.clockLength -= this.soundLengthUnit; if (this.soundLength == 0) { this.setLength(0); this.stop(); } } } }; Channel3.prototype.setFrequency = function(value) { value = 65536 / (2048 - value); this.bufferSource.playbackRate.value = value / this.baseSpeed; }; Channel3.prototype.getFrequency = function() { var freq = 2048 - 65536 / (this.bufferSource.playbackRate.value * this.baseSpeed); return freq | 1; }; Channel3.prototype.setLength = function(value) { this.soundLength = 256 - value; }; Channel3.prototype.setWaveBufferByte = function(index, value) { var bufferIndex = index * 2; this.buffer[bufferIndex] = (value >> 4) / 8 - 1; // value in buffer is in -1 -> 1 this.buffer[bufferIndex+1] = (value & 0x0F) / 8 - 1; }; Channel3.prototype.disable = function() { this.bufferSource.disconnect(); }; Channel3.prototype.enable = function() { this.bufferSource.connect(this.gainNode); }; GameboyJS.Channel3 = Channel3; }(GameboyJS || (GameboyJS = {}))); var GameboyJS; (function (GameboyJS) { "use strict"; var Channel4 = function(apu, channelNumber, audioContext) { this.apu = apu; this.channelNumber = channelNumber; this.playing = false; this.soundLengthUnit = 0x4000; // 1 / 256 second of instructions this.soundLength = 64; // defaults to 64 periods this.lengthCheck = false; this.clockLength = 0; this.audioContext = audioContext; }; Channel4.prototype.play = function() { if (this.playing) return; this.playing = true; this.apu.setSoundFlag(this.channelNumber, 1); this.clockLength = 0; }; Channel4.prototype.stop = function() { this.playing = false; this.apu.setSoundFlag(this.channelNumber, 0); }; Channel4.prototype.update = function(clockElapsed) { if (this.lengthCheck) { this.clockLength += clockElapsed; if (this.clockLength > this.soundLengthUnit) { this.soundLength--; this.clockLength -= this.soundLengthUnit; if (this.soundLength == 0) { this.setLength(0); this.stop(); } } } }; Channel4.prototype.setLength = function(value) { this.soundLength = 64 - (value & 0x3F); }; GameboyJS.Channel4 = Channel4; }(GameboyJS || (GameboyJS = {}))); var GameboyJS; (function (GameboyJS) { "use strict"; var Timer = function(cpu, memory) { this.cpu = cpu; this.memory = memory; this.DIV = 0xFF04; this.TIMA = 0xFF05; this.TMA = 0xFF06; this.TAC = 0xFF07; this.mainTime = 0; this.divTime = 0; }; Timer.prototype.update = function(clockElapsed) { this.updateDiv(clockElapsed); this.updateTimer(clockElapsed); }; Timer.prototype.updateTimer = function(clockElapsed) { if (!(this.memory.rb(this.TAC) & 0x4)) { return; } this.mainTime += clockElapsed; var threshold = 64; switch (this.memory.rb(this.TAC) & 3) { case 0: threshold=64; break; // 4KHz case 1: threshold=1; break; // 256KHz case 2: threshold=4; break; // 64KHz case 3: threshold=16; break; // 16KHz } threshold *= 16; while (this.mainTime >= threshold) { this.mainTime -= threshold; this.memory.wb(this.TIMA, this.memory.rb(this.TIMA) + 1); if (this.memory.rb(this.TIMA) > 0xFF) { this.memory.wb(this.TIMA, this.memory.rb(this.TMA)); this.cpu.requestInterrupt(GameboyJS.CPU.INTERRUPTS.TIMER); } } }; // Update the DIV register internal clock // Increment it if the clock threshold is elapsed and // reset it if its value overflows Timer.prototype.updateDiv = function(clockElapsed) { var divThreshold = 256; // DIV is 16KHz this.divTime += clockElapsed; if (this.divTime > divThreshold) { this.divTime -= divThreshold; var div = this.memory.rb(this.DIV) + 1; this.memory.wb(this.DIV, div&0xFF); } }; Timer.prototype.resetDiv = function() { this.divTime = 0; this.memory[this.DIV] = 0; // direct write to avoid looping }; GameboyJS.Timer = Timer; }(GameboyJS || (GameboyJS = {}))); var GameboyJS; (function (GameboyJS) { "use strict"; // Utility functions var Util = { // Add to the first argument the properties of all other arguments extend: function(target /*, source1, source2, etc. */) { var sources = Array.prototype.slice.call(arguments); for (var i in sources) { var source = sources[i]; for (var name in source) { target[name] = source[name]; } } return target; }, testFlag: function(p, cc) { var test=1; var mask=0x10; if (cc=='NZ'||cc=='NC') test=0; if (cc=='NZ'||cc=='Z') mask=0x80; return (test && p.r.F&mask) || (!test && !(p.r.F&mask)); }, getRegAddr: function(p, r1, r2) {return Util.makeword(p.r[r1], p.r[r2]);}, // make a 16 bits word from 2 bytes makeword: function(b1, b2) {return (b1 << 8) + b2;}, // return the integer signed value of a given byte getSignedValue: function(v) {return v & 0x80 ? v-256 : v;}, // extract a bit from a byte readBit: function(byte, index) { return (byte >> index) & 1; } }; GameboyJS.Util = Util; }(GameboyJS || (GameboyJS = {})));