Repository: juchi/gameboy.js Branch: master Commit: e1456e6e4ade Files: 33 Total size: 220.1 KB Directory structure: gitextract_nzcsu177/ ├── .gitignore ├── LICENSE ├── README.md ├── dist/ │ └── gameboy.js ├── package.json ├── src/ │ ├── cpu.ts │ ├── debug.ts │ ├── display/ │ │ ├── gpu.ts │ │ └── screen.ts │ ├── exception.ts │ ├── ext_ram.ts │ ├── input/ │ │ ├── gamepad.ts │ │ ├── input.ts │ │ └── keyboard.ts │ ├── instructions.ts │ ├── main.ts │ ├── mbc.ts │ ├── memory.ts │ ├── opcodes.ts │ ├── rom/ │ │ ├── ajax_reader.ts │ │ ├── drop_file_reader.ts │ │ ├── file_reader.ts │ │ └── rom.ts │ ├── serial.ts │ ├── sound/ │ │ ├── apu.ts │ │ ├── channel.ts │ │ ├── channel1.ts │ │ ├── channel3.ts │ │ └── channel4.ts │ ├── timer.ts │ └── util.ts ├── tsconfig.json └── webpack.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ node_modules ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015 Julien Chichignoud Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ Gameboy.js ========== Gameboy.js is a Gameboy emulator written in JavaScript. It's a work in progress, see the Features section. ## Browser support Tested on: * Firefox * Chrome * Safari ## Usage ### Run now You can try the emulator directly on the [demo page](http://juchi.github.io/gameboy.js/). Many tests ROM can be downloaded [here](https://github.com/c-sp/game-boy-test-roms) and are runnable, but some tests fail. See the Tests section for more details. Currently, most tested games run (tested with Tetris, Super Mario, Pokémon Red) but have some glitches. ### Run on a custom page You can directly use the distributed compiled file in a custom HTML page of your own, and create a new Gameboy object. It will expect a Canvas element and an optional options object. ```javascript var canvas = document.getElementById('canvas'); new GameboyJS.Gameboy(canvas); ``` ### Options You can customize the configuration by passing a list of options to the Gameboy. ```javascript var options, canvas; //... new GameboyJS.Gameboy(canvas, options); ``` * `pad`: Object representing the pad to use as a physical gamepad. The `class` key is mandatory and should contain the class implementing the device you want to play with. You can implement any kind of pad as long as this class implements the init() method. See GameboyJS.Keyboard class for an example of implementation. You may also provide a `mapping` object that will be used if you choose the GameboyJS.Gamepad class. Default is `{class: GameboyJS.Keyboard, mapping: null}` * `zoom`: The zoom level as an integer. Default is 1 * `romReaders`: An array of ROM reader objects that can read a ROM file and send the data to the Gameboy. Default is empty (`[]`), leading to a GameboyJS.RomFileReader to be created. * `statusContainerId`: ID of the HTML element for status display. Default is 'status'. * `gameNameContainerId`: ID of the HTML element for game name display. Default is 'game-name'. * `errorContainerId`: ID of the HTML element for error display. Default is 'error'. ### Build from source If you want to build the compiled JavaScript file from source to be sure you have the latest updates, just clone the repository and run the build script: ``` git clone https://github.com/juchi/gameboy.js npm install npm run build ``` ## Features ### Devices The LCD screen is working but still has some sprite glitches. User input is available : arrow keys are mapped to the keyboard arrows, and A, B, START and SELECT are mapped to G, B, H, N respectively. Also, it's possible to use a gamepad using a custom `pad` option. Game saves are working fine and are stored as serialized data in the LocalStorage. Sound is partially implemented (the noise channel is not done yet) and is quite good on Firefox (latest release) but really bad on Chrome and Safari. This seems to be due to the implementation of the Web Audio API in webkit. The serial port can be used by the program as an output, the received bytes are displayed in the console (this is mainly used for tests). The ROM files are accessed using an explorer on your computer. Other methods may be included (AJAX and Drag & Drop are supported). ### Internal processes There is no boot program provided (nor supported). The execution starts automatically at address 0x0100 which is the start address of all ROMs. All the standard Gameboy instructions are implemented. Super Gameboy and Gameboy Color are not supported. The following features are in progress or partially working: * sprites (some glitches) * MBC (only MBC 1, MBC 3 and MBC 5 are partially supported) * sound (ok on Firefox, poor on Chrome) The following features are not currently supported at all: * boot program ## Tests The tests perform as follow : | Test | status | |--------------------|:--------:| | CPU instructions | pass | | instruction timing | pass | | memory timing | fail | | DMG sound | fail | | OAM bug | fail | ================================================ FILE: dist/gameboy.js ================================================ 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 = {}))); ================================================ FILE: package.json ================================================ { "name": "gameboy-js", "author": "Julien Chichignoud", "description": "JavaScript Gameboy emulator", "repository": { "type": "git", "url": "https://github.com/juchi/gameboy.js" }, "homepage": "https://juchi.github.io/gameboy.js/", "license": "MIT", "scripts": { "build": "webpack" }, "devDependencies": { "ts-loader": "^9.5.2", "webpack": "^5.99.9", "webpack-cli": "^6.0.1" } } ================================================ FILE: src/cpu.ts ================================================ import Memory from './memory'; import Timer from './timer'; import APU from './sound/apu'; import Screen from './display/screen'; import GPU from './display/gpu'; import Util from './util'; import { ConsoleSerial, SerialInterface } from './serial' import {cpuOps} from './instructions' import {opcodeMap} from './opcodes' // CPU class class CPU { gameboy; r; clock; gpu: GPU; apu: APU; input; timer: Timer; memory: Memory; IME = false; isHalted = false; isPaused = false; usingBootRom = false; SERIAL_INTERNAL_INSTR = 512; // instr to wait per bit if internal clock enableSerial = 0; serialHandler: SerialInterface; nextFrameTimer: ReturnType; constructor(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.clock = {c: 0, serial: 0}; this.createDevices(); } static INTERRUPTS = { VBLANK: 0, LCDC: 1, TIMER: 2, SERIAL: 3, HILO: 4 }; static interruptRoutines = { 0: function(p){cpuOps.RSTn(p, 0x40);}, 1: function(p){cpuOps.RSTn(p, 0x48);}, 2: function(p){cpuOps.RSTn(p, 0x50);}, 3: function(p){cpuOps.RSTn(p, 0x58);}, 4: function(p){cpuOps.RSTn(p, 0x60);} } createDevices() { this.memory = new Memory(this); this.timer = new Timer(this, this.memory); this.apu = new APU(this.memory); this.enableSerial = 0; this.serialHandler = new ConsoleSerial; } reset() { this.memory.reset(); this.r = {A:0x01, F: 0, B:0xFF, C:0x13, D:0, E:0xC1, H:0x84, L:0x03, pc:0, sp:0xFFFE}; } loadRom(data) { this.memory.setRomData(data); } getRamSize() { let 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; } getGameName() { 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 run() { if (this.usingBootRom) { this.r.pc = 0x0000; } else { this.r.pc = 0x0100; } this.frame(); } stop() { 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 frame() { if (!this.isPaused) { this.nextFrameTimer = setTimeout(this.frame.bind(this), 1000 / Screen.physics.FREQUENCY); } try { var vblank = false; while (!vblank) { var oldInstrCount = this.clock.c; if (!this.isHalted) { let opcode = this.fetchOpcode(); 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); } } fetchOpcode(): number { let opcode = this.memory.rb(this.r.pc++); if (!opcodeMap[opcode]) { this.stop(); throw 'Unknown opcode '+opcode.toString(16)+' at address '+(this.r.pc-1).toString(16)+', stopping execution...'; } return opcode; } // read register rr(register) { return this.r[register]; } // write register wr(register, value) { this.r[register] = value; } halt() { this.isHalted = true; } unhalt() { this.isHalted = false; } pause() { this.isPaused = true; } unpause() { if (this.isPaused) { this.isPaused = false; this.frame(); } } // Look for interrupt flags checkInterrupt() { if (!this.IME) { return; } for (var i = 0; i < 5 && this.IME; i++) { var IFval = this.memory.rb(0xFF0F); if (Util.readBit(IFval, i) && this.isInterruptEnable(i)) { IFval &= (0xFF - (1<> (7-pixel)) + ((b2 & mask) >> (7-pixel))*2; pixelData[line * 8 + pixel] = colorValue; } } let i = 0; while (pixelData.length) { console.log(i++ + ' ' + pixelData.splice(0, 8).join('')); } }; Debug.list_visible_sprites = function(gameboy: Gameboy) { let memory = gameboy.cpu.memory; let indexes: Array = []; for (let i = 0xFE00; i < 0xFE9F; i += 4) { let x = memory.oamram(i + 1); let y = memory.oamram(i); let tileIndex = memory.oamram(i + 2); if (x == 0 || x >= 168) { continue; } indexes.push({oamIndex:i, x:x, y:y, tileIndex:tileIndex}); } return indexes; }; export default Debug; ================================================ FILE: src/display/gpu.ts ================================================ import Screen from './screen'; import CPU from '../cpu'; import Util from '../util'; class GPU { LCDC= 0xFF40; STAT= 0xFF41; SCY = 0xFF42; SCX = 0xFF43; LY = 0xFF44; LYC = 0xFF45; BGP = 0xFF47; OBP0= 0xFF48; OBP1= 0xFF49; WY = 0xFF4A; WX = 0xFF4B; OAM_START = 0xFE00; OAM_END = 0xFE9F; VBLANK_TIME = 70224; cpu: CPU; screen: Screen; vram: Function; deviceram: Function; oamram: Function; clock: number; mode: number; line: number; buffer: number[]; tileBuffer: number[]; constructor(screen, cpu) { this.cpu = cpu; this.screen = screen; this.vram = cpu.memory.vram.bind(cpu.memory); this.deviceram = cpu.memory.deviceram.bind(cpu.memory); this.oamram = cpu.memory.oamram.bind(cpu.memory); this.clock = 0; this.mode = 2; this.line = 0; this.buffer = new Array(Screen.physics.WIDTH * Screen.physics.HEIGHT); this.tileBuffer = new Array(8); } static tilemap = { HEIGHT: 32, WIDTH: 32, START_0: 0x9800, START_1: 0x9C00, LENGTH: 0x0400 // 1024 bytes = 32*32 }; update(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(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; } updateLY() { 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(CPU.INTERRUPTS.LCDC); } } else { this.deviceram(this.STAT, STAT & (0xFF - (1 << 2))); } } setMode(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(CPU.INTERRUPTS.LCDC); } } } // Push one scanline into the main buffer drawScanLine(line) { var LCDC = this.deviceram(this.LCDC); var enable = 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 } } drawFrame() { var LCDC = this.deviceram(this.LCDC); var enable = Util.readBit(LCDC, 7); if (enable) { //this.drawSprites(LCDC); this.drawWindow(LCDC); } this.screen.render(this.buffer); } drawBackground(LCDC, line, lineBuffer) { if (!Util.readBit(LCDC, 0)) { return; } var mapStart = Util.readBit(LCDC, 3) ? GPU.tilemap.START_1 : GPU.tilemap.START_0; var dataStart, signedIndex = false; if (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 = Util.getSignedValue(tileIndex) + 128; } var tileData = 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 copyBGTileLine(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 copyLineToBuffer(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 drawTileLine(tileData, line: number, xflip = 0, 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; } } drawSprites(LCDC, line, bgLineBuffer) { if (!Util.readBit(LCDC, 1)) { return; } var spriteHeight = 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); if (spriteHeight === 16) index = index & 0xFE; 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}) } sprites.sort((a, b) => a.x - b.x); 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 = Util.readBit(sprite.flags, 4); var xflip = Util.readBit(sprite.flags, 5); var yflip = Util.readBit(sprite.flags, 6); var priority = Util.readBit(sprite.flags, 7); 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, priority, bgLineBuffer); } this.copySpriteLineToBuffer(spriteLineBuffer, line); } // Copy a tile line from a tileBuffer to a line buffer, at a given x position copySpriteTileLine = function(lineBuffer, tileBuffer, x, palette, priority, bgLineBuffer) { // copy tile line to buffer for (var k = 0; k < 8; k++, x++) { if (x < 0 || x >= Screen.physics.WIDTH || tileBuffer[k] == 0) continue; if (lineBuffer[x]) continue; if (priority === 1 && bgLineBuffer[x] > 0) { lineBuffer[x] = {color:0, palette: palette}; continue; } lineBuffer[x] = {color:tileBuffer[k], palette: palette}; } } // Copy a sprite scanline into the main buffer copySpriteLineToBuffer(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]); } } drawTile(tileData, x, y, buffer, bufferWidth, xflip = 0, yflip = 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; 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) readTileData(tileIndex: number, dataStart: number, tileSize?: number) { 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; } drawWindow(LCDC) { if (!Util.readBit(LCDC, 5)) { return; } var buffer = new Array(256*256); var mapStart = Util.readBit(LCDC, 6) ? GPU.tilemap.START_1 : GPU.tilemap.START_0; var dataStart, signedIndex = false; if (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 = 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); } } } drawPixel(x, y, color) { this.buffer[y * 160 + x] = color; } getPixel(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 static getPalette(paletteByte) { let palette: number[] = []; for (var i = 0; i < 8; i += 2) { let shade = (paletteByte & (3 << i)) >> i; palette.push(shade); } return palette; } } export default GPU; ================================================ FILE: src/display/screen.ts ================================================ // Screen device class Screen { canvas: HTMLCanvasElement; context: CanvasRenderingContext2D; pixelSize: number; imageData: ImageData; constructor(canvas: HTMLCanvasElement, pixelSize: number) { this.context = canvas.getContext('2d') as CanvasRenderingContext2D; this.canvas = canvas; this.pixelSize = pixelSize || 1; this.initImageData(); } // Palette colors (RGB) static colors = [ [0xFF, 0xFF, 0xFF], [0xAA, 0xAA, 0xAA], [0x55, 0x55, 0x55], [0x00, 0x00, 0x00] ]; static physics = { WIDTH : 160, HEIGHT : 144, FREQUENCY: 60 }; setPixelSize(pixelSize: number) { this.pixelSize = pixelSize; this.initImageData(); } initImageData() { 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); for (var i = 0; i < this.imageData.data.length; i++) { this.imageData.data[i] = 255; } } clearScreen() { this.context.fillStyle = '#FFF'; this.context.fillRect(0, 0, Screen.physics.WIDTH * this.pixelSize, Screen.physics.HEIGHT * this.pixelSize); } fillImageData(buffer) { for (var y = 0; y < Screen.physics.HEIGHT; y++) { for (var py = 0; py < this.pixelSize; py++) { var yOffset = (y * this.pixelSize + py) * this.canvas.width; for (var x = 0; x < Screen.physics.WIDTH; x++) { for (var px = 0; px < this.pixelSize; px++) { var offset = yOffset + (x * this.pixelSize + px); var v = Screen.colors[buffer[y * Screen.physics.WIDTH + x] | 0]; // set RGB values this.imageData.data[offset * 4] = v[0]; this.imageData.data[offset * 4 + 1] = v[1]; this.imageData.data[offset * 4 + 2] = v[2]; } } } } } render(buffer) { this.fillImageData(buffer); this.context.putImageData(this.imageData, 0, 0); } } export default Screen; ================================================ FILE: src/exception.ts ================================================ // This exception should be thrown whenever a critical feature that // has not been implemented is requested class UnimplementedException extends Error { fatal: boolean; constructor(message: string, fatal?: boolean) { super(); this.message = message; if (fatal === undefined) { fatal = true; } this.fatal = fatal || false; } } export default UnimplementedException; ================================================ FILE: src/ext_ram.ts ================================================ // Object for mapping the cartridge RAM class ExtRam { gameName: string; extRam: number[]; ramSize: number; ramBanksize: number; ramBank: number; constructor() { this.ramSize = 0; this.ramBank = 0; } loadRam(game: string, size: number) { this.gameName = game; this.ramSize = size; this.ramBanksize = this.ramSize >= 0x2000 ? 8192 : 2048; let key = this.getStorageKey(); let 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.'); } } } setRamBank(bank: number) { this.ramBank = bank; } manageWrite(offset: number, value: number) { this.extRam[this.ramBank * 8192 + offset] = value; } manageRead(offset: number) { return this.extRam[this.ramBank * 8192 + offset]; } getStorageKey() { return this.gameName + '_EXTRAM'; } // Actually save the RAM in the physical storage (localStorage) saveRamData() { localStorage.setItem(this.getStorageKey(), JSON.stringify(this.extRam)); } } export default ExtRam; ================================================ FILE: src/input/gamepad.ts ================================================ import {JoypadDevice} from './input'; // This is the default buttons mapping for the Gamepad API // // 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 let standardMapping = { 0: 'A', 1: 'B', 8: 'SELECT', 9: 'START', 12: 'UP', 13: 'DOWN', 14: 'LEFT', 15: 'RIGHT', }; // 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) class Gamepad implements JoypadDevice { gamepad: globalThis.Gamepad; state = {A:0,B:0,START:0,SELECT:0,LEFT:0,RIGHT:0,UP:0,DOWN:0}; pullInterval: ReturnType; onPress: Function; onRelease: Function; buttonMapping: object; constructor(mapping?: object) { this.buttonMapping = mapping || standardMapping; } // Initialize the keyboard listeners and set up the callbacks // for button press / release init(canvas: HTMLElement, onPress: Function, onRelease: Function) { this.onPress = onPress; this.onRelease = onRelease; let self = this; window.addEventListener('gamepadconnected', function(e) { self.gamepad = e.gamepad; self.activatePull(); }); window.addEventListener('gamepaddisconnected', function(e) { self.deactivatePull(); }); } activatePull() { this.deactivatePull(); this.pullInterval = setInterval(this.pullState.bind(this), 100); } deactivatePull() { clearInterval(this.pullInterval); } // Check the state of the current gamepad in order to detect any press/release action pullState() { for (let index in this.buttonMapping) { let button = this.buttonMapping[index]; let 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); } } } managePress(key) { this.onPress(key); } manageRelease(key) { this.onRelease(key); } } export default Gamepad; ================================================ FILE: src/input/input.ts ================================================ import CPU from '../cpu'; import Memory from '../memory'; // 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) export interface JoypadDevice { init(canvas: HTMLElement, onPress: Function, onRelease: Function): void; } class Input { cpu: CPU; memory: Memory; P1: number; state: number; interruptQueue: Array; constructor(cpu: CPU, pad: JoypadDevice, canvas) { this.cpu = cpu; this.memory = cpu.memory; this.P1 = 0xFF00; this.state = 0; this.interruptQueue = []; pad.init(canvas, this.pressKey.bind(this), this.releaseKey.bind(this)); } pressKey(key) { this.delayInterrupt(key); } releaseKey(key) { var mask = 0xFF - Input.keys[key]; this.state &= mask; } // do not send the interrupt right away, due to the way javascript works : // the key event fires when no other code is running, meaning when the frame() // in the GPU has finished rendering. This means the interrupt will always run // at LY = 144, which prevents the game to generate entropy for the key press actions // // the event is stored in a queue which is processed the next time the LY register is // at the randomly determined value delayInterrupt(key) { let ly = (Math.random() * 153) | 0; this.interruptQueue.push({ly: ly, key: key}); } update() { if (this.interruptQueue.length > 0) { // check for interrupt to fire if (this.interruptQueue[0].ly === this.memory.rb(this.cpu.gpu.LY)) { let v = this.interruptQueue.shift(); this.state |= Input.keys[v.key]; this.cpu.requestInterrupt(CPU.INTERRUPTS.HILO); } } 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; } static keys = { START: 0x80, SELECT: 0x40, B: 0x20, A: 0x10, DOWN: 0x08, UP: 0x04, LEFT: 0x02, RIGHT: 0x01 }; } export default Input; ================================================ FILE: src/input/keyboard.ts ================================================ import {JoypadDevice} from './input'; // Keyboard listener // Does the mapping between the keyboard and the Input class class Keyboard implements JoypadDevice { onPress: Function; onRelease: Function; // Initialize the keyboard listeners and set up the callbacks // for button press / release init(canvas: HTMLElement, onPress: Function, onRelease: Function) { this.onPress = onPress; this.onRelease = onRelease; if (canvas.getAttribute('tabIndex') === null) { canvas.setAttribute('tabIndex', '1'); } let self = this; canvas.addEventListener('keydown', function(e) { self.managePress(e.keyCode); if (e.keyCode !== 9) // only keep Tab active e.preventDefault(); }); canvas.addEventListener('keyup', function(e) { self.manageRelease(e.keyCode); if (e.keyCode !== 9) // only keep Tab active e.preventDefault(); }); } managePress(keycode) { let key = this.translateKey(keycode); if (key) { this.onPress(key); } } manageRelease(keycode) { let key = this.translateKey(keycode); if (key) { this.onRelease(key); } } // Transform a keyboard keycode into a key of the Input.keys object translateKey(keycode) { let key = ''; 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; } } export default Keyboard; ================================================ FILE: src/instructions.ts ================================================ import Util from './util'; import {opcodeCbmap} from './opcodes'; // 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 let 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, Util.getRegAddr(p, r1, r2), p.r[r3]);p.clock.c += 8;}, LDrrra: function(p, r1, r2, r3) {p.wr(r1, p.memory.rb(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', 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=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 = 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 = 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 = 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 = 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(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(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(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(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(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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 (Util.testFlag(p, cc)){var v=p.memory.rb(p.r.pc++);v=Util.getSignedValue(v);p.r.pc += v;p.clock.c+=4;}else{p.r.pc++;}p.clock.c += 8;}, JPccnn: function(p, cc) {if (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=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 (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 (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++); opcodeCbmap[opcode](p); p.clock.c+=4;} }; export {ops as cpuOps}; ================================================ FILE: src/main.ts ================================================ import Rom from './rom/rom'; import RomFileReader from './rom/file_reader'; import RomDropFileReader from './rom/drop_file_reader'; import RomAjaxReader from './rom/ajax_reader'; import Keyboard from './input/keyboard'; import Util from './util'; import CPU from './cpu'; import GPU from './display/gpu'; import Screen from './display/screen'; import Input, { JoypadDevice } from './input/input' import UnimplementedException from './exception'; import Debug from './debug'; let defaultOptions = { pad: {class: 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 class Gameboy { options; cpu: CPU; screen: Screen; input: Input; pad: JoypadDevice; statusContainer: HTMLElement; gameNameContainer: HTMLElement; errorContainer: HTMLElement; constructor(canvas, options) { options = options || {}; this.options = Util.extend({}, defaultOptions, options); var cpu = new CPU(this); var screen = new Screen(canvas, this.options.zoom); var gpu = new GPU(screen, cpu); cpu.gpu = gpu; var pad = new this.options.pad.class(this.options.pad.mapping); var input = new Input(cpu, pad, canvas); 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 createRom(readers: any[]) { var rom = new Rom(this); if (readers.length == 0) { // add the default rom reader var romReader = new RomFileReader(); rom.addReader(romReader); } else { for (var i in readers) { if (readers.hasOwnProperty(i)) { rom.addReader(readers[i]); } } } } startRom(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(); this.screen.canvas.focus(); } catch (e) { this.handleException(e); } } pause(value) { if (value) { this.setStatus('Game Paused :'); this.cpu.pause(); } else { this.setStatus('Game Running :'); this.cpu.unpause(); } } error(message) { this.setStatus('Error during execution'); this.setError('An error occurred during execution:' + message); this.cpu.stop(); } setStatus(status) { this.statusContainer.innerHTML = status; } // Display an error message setError(message) { this.errorContainer.classList.remove('hide'); this.errorContainer.innerHTML = message; } // Display the name of the game running setGameName(name) { this.gameNameContainer.innerHTML = name; } setSoundEnabled(value) { if (value) { this.cpu.apu.connect(); } else { this.cpu.apu.disconnect(); } } setScreenZoom(value) { this.screen.setPixelSize(value); } handleException(e) { if (e instanceof UnimplementedException) { if (e.fatal) { this.error('This cartridge is not supported ('+ e.message +')'); } else { console.error(e.message); } } else { throw e; } } } export { Gameboy, RomFileReader, RomDropFileReader, RomAjaxReader, Util, Debug }; ================================================ FILE: src/mbc.ts ================================================ import ExtRam from './ext_ram'; import Memory from './memory'; import UnimplementedException from './exception'; // Memory bank controllers abstract class MBC { memory: Memory; extRam: ExtRam; constructor(memory: Memory) { this.memory = memory; this.extRam = new ExtRam(); } // Create an MBC instance depending on the type specified in the cartridge static getMbcInstance(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 UnimplementedException('MBC type not supported'); } return instance; } } class MBC1 extends MBC { romBankNumber = 1; mode = 0; // mode 0 = ROM, mode 1 = RAM ramEnabled = true; loadRam(game, size) { this.extRam.loadRam(game, size); } manageWrite(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; } } readRam(addr) { return this.extRam.manageRead(addr - 0xA000); } } class MBC3 extends MBC { romBankNumber = 1; ramEnabled = true; loadRam(game, size) { this.extRam.loadRam(game, size); } manageWrite(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 UnimplementedException('cartridge clock not supported', false); break; case 0xA000: case 0xB000: this.extRam.manageWrite(addr - 0xA000, value); break; } } readRam(addr) { return this.extRam.manageRead(addr - 0xA000); } } // declare MBC5 for compatibility with most cartriges // does not support rumble feature let MBC5 = MBC3; // MBC0 exists for consistency and manages the no-MBC cartriges class MBC0 extends MBC { manageWrite(addr, value) { this.memory.loadRomBank(value); if (addr >= 0xA000 && addr < 0xC000) { this.extRam.manageWrite(addr - 0xA000, value); this.extRam.saveRamData(); } } readRam(addr) { return this.extRam.manageRead(addr - 0xA000); } loadRam(game, size) { this.extRam.loadRam(game, size); } } export default MBC; ================================================ FILE: src/memory.ts ================================================ import MBC from './mbc'; import CPU from './cpu'; // Memory unit class Memory extends Array { MEM_SIZE = 65536; // 64KB MBCtype = 0; banksize = 0x4000; rom: Uint8Array; mbc; cpu: CPU; constructor(cpu: CPU) { super(); this.cpu = cpu; } static 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 }; reset() { this.length = this.MEM_SIZE; for (let i = Memory.addresses.VRAM_START; i <= Memory.addresses.VRAM_END; i++) { this[i] = 0; } for (let i = Memory.addresses.DEVICE_START; i <= Memory.addresses.DEVICE_END; i++) { this[i] = 0; } this[0xFFFF] = 0; this[0xFF47] = 0xFC; this[0xFF04] = 0x18; } setRomData(data: Uint8Array) { this.rom = data; this.loadRomBank(0); this.mbc = MBC.getMbcInstance(this, this[0x147]); this.loadRomBank(1); this.mbc.loadRam(this.cpu.getGameName(), this.cpu.getRamSize()); } loadRomBank(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 vram(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 oamram(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 deviceram(address: number, value?: number) { if (address < Memory.addresses.DEVICE_START || address > Memory.addresses.DEVICE_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 rb(addr: number): number { 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]; } // Memory write proxy function // Used to centralize memory writes and delegate specific behaviour // to the correct units wb(addr: number, value: number) { 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) dmaTransfer(startAddressPrefix) { var startAddress = (startAddressPrefix << 8); for (var i = 0; i < 0xA0; i++) { this[Memory.addresses.OAM_START + i] = this[startAddress + i]; } } } // Bitmasks for audio addresses reads let 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 ]; export default Memory; ================================================ FILE: src/opcodes.ts ================================================ import {cpuOps as ops} from './instructions'; // 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 let 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');} }; export {map as opcodeMap, cbmap as opcodeCbmap}; ================================================ FILE: src/rom/ajax_reader.ts ================================================ import { RomReader } from './rom' // A RomAjaxReader is able to load a file through an AJAX request class RomAjaxReader implements RomReader { callback: Function; // The callback argument will be called when a file is successfully // read, with the data as argument (Uint8Array) setCallback(onLoadCallback: Function) { 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 loadFromUrl(url: string) { if (!url) { throw 'No url has been set in order to load a ROM file.'; } let cb = this.callback; let xhr = new XMLHttpRequest(); xhr.open('GET', url, true); xhr.responseType = "arraybuffer"; xhr.onload = function() { let rom = new Uint8Array(xhr.response); cb && cb(rom); }; xhr.send(); } } export default RomAjaxReader; ================================================ FILE: src/rom/drop_file_reader.ts ================================================ import { RomReader } from './rom' // A RomDropFileReader is able to load a drag and dropped file class RomDropFileReader implements RomReader { dropElement: HTMLElement; callback: Function; constructor(el) { this.dropElement = el; if (!this.dropElement) { throw 'The RomDropFileReader needs a drop zone.'; } let self = this; this.dropElement.addEventListener('dragenter', function(e) { e.preventDefault(); if (e.target !== self.dropElement) { return; } self.dropElement.classList.add('drag-active'); }); this.dropElement.addEventListener('dragleave', function(e) { e.preventDefault(); if (e.target !== self.dropElement) { return; } self.dropElement.classList.remove('drag-active'); }); this.dropElement.addEventListener('dragover', function(e) { e.preventDefault(); self.dropElement.classList.add('drag-active'); }); this.dropElement.addEventListener('drop', function (e) { self.dropElement.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) setCallback(onLoadCallback: Function) { this.callback = onLoadCallback; } // The file loading logic is the same as the regular file reader loadFromFile(file) { if (file === undefined) { return; } let fr = new FileReader(); let cb = this.callback; fr.onload = function() { cb && cb(new Uint8Array(fr.result as ArrayBuffer)); }; fr.onerror = function(e) { console.log('Error reading the file', e.target.error.code) }; fr.readAsArrayBuffer(file); } } export default RomDropFileReader; ================================================ FILE: src/rom/file_reader.ts ================================================ import { RomReader } from './rom'; // 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 class RomFileReader implements RomReader { domElement: HTMLElement; callback: Function; constructor(el?: HTMLElement) { this.domElement = el || document.getElementById('file'); if (!this.domElement) { throw 'The RomFileReader needs a valid input element.'; } let self = this; this.domElement.addEventListener('change', function(e) { self.loadFromFile((e.target as HTMLInputElement).files[0]); }); } // The callback argument will be called when a file is successfully // read, with the data as argument (Uint8Array) setCallback(onLoadCallback: Function) { this.callback = onLoadCallback; } // Automatically called when the DOM input is provided with a file loadFromFile(file) { if (file === undefined) { return; } let fr = new FileReader(); let cb = this.callback; fr.onload = function() { cb && cb(new Uint8Array(fr.result as ArrayBuffer)); }; fr.onerror = function(e) { console.log('Error reading the file', e.target.error.code) }; fr.readAsArrayBuffer(file); } } export default RomFileReader; ================================================ FILE: src/rom/rom.ts ================================================ class Rom { gameboy; data: Uint8Array; constructor(gameboy, romReader?) { this.gameboy = gameboy; if (romReader) { this.addReader(romReader); } } addReader(romReader: RomReader) { let self = this; romReader.setCallback(function(data: Uint8Array) { 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: Uint8Array) { let hash = 0; for (let i = 0x134; i <= 0x14C; i++) { hash = hash - data[i] - 1; } return (hash & 0xFF) == data[0x14D]; } export interface RomReader { setCallback(fn: Function): void; } export default Rom; ================================================ FILE: src/serial.ts ================================================ // Handlers for the Serial port of the Gameboy interface SerialInterface { out(data: number): void; in(): number; } // 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 class ConsoleSerial implements SerialInterface { current: string = ''; timeout: ReturnType; out(data: number): void { this.current += String.fromCharCode(data); if (data == 10) { this.print(); } else { clearTimeout(this.timeout); this.timeout = setTimeout(this.print.bind(this), 500); } } in(): number { return 0xFF; } print(): void { clearTimeout(this.timeout); console.log('serial: ' + this.current); this.current = ''; } } // A DummySerial outputs nothing and always inputs 0xFF class DummySerial implements SerialInterface { out(): void {} in(): number { return 0xFF; } } export {ConsoleSerial, DummySerial, SerialInterface}; ================================================ FILE: src/sound/apu.ts ================================================ import Channel1 from './channel1'; import Channel3 from './channel3'; import Channel4 from './channel4'; import Memory from '../memory'; // Audio Processing unit // Listens the write accesses to the audio-reserved memory addresses // and dispatches the data to the sound channels class APU { memory: Memory; enabled = false; channel1; channel2; channel3; channel4; constructor(memory: Memory) { this.memory = memory; this.enabled = false; var audioContext = new AudioContext(); this.channel1 = new Channel1(this, 1, audioContext); this.channel2 = new Channel1(this, 2, audioContext); this.channel3 = new Channel3(this, 3, audioContext); this.channel4 = new Channel4(this, 4, audioContext); } connect() { this.channel1.enable(); this.channel2.enable(); this.channel3.enable(); } disconnect() { this.channel1.disable(); this.channel2.disable(); this.channel3.disable(); } // Updates the states of each channel given the elapsed time // (in instructions) since last update update(clockElapsed) { if (this.enabled == false) return; this.channel1.update(clockElapsed); this.channel2.update(clockElapsed); this.channel3.update(clockElapsed); this.channel4.update(clockElapsed); } setSoundFlag(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 manageWrite(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); this.channel1.updateDAC(value); 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); this.channel2.updateDAC(value); 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 this.channel3.updateDAC(value); 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 this.channel4.updateDAC(value); 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; } } static 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 }; } export default APU; ================================================ FILE: src/sound/channel.ts ================================================ import APU from './apu' interface AudioChannel { play(): void; stop(): void; updateDAC(controlRegister: number): void; update(clockElapsed: number): void; } abstract class AbstractAudioChannel implements AudioChannel { apu: APU; channelNumber: number; audioContext: AudioContext; playing: boolean = false; dac: boolean = false; lengthCheck = false; clockLength = 0; soundLength = 64; // defaults to 64 periods soundLengthUnit = 0x4000; // 1 / 256 second of instructions abstract play(): void; abstract stop(): void; abstract updateDAC(controlRegister: number): void; abstract update(clockElapsed: number): void; abstract setLength(value: number): void; checkLength(clockElapsed: number) { 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(); } } } } protected setDAC(value: boolean) { this.dac = value; if (!value) this.stop(); } } export {AudioChannel, AbstractAudioChannel}; ================================================ FILE: src/sound/channel1.ts ================================================ import APU from './apu'; import {AbstractAudioChannel} from './channel'; class Channel1 extends AbstractAudioChannel { soundLength = 64; // defaults to 64 periods sweepTime = 0; // from 0 to 7 sweepStepLength = 0x8000; // 1 / 128 seconds of instructions sweepCount = 0; sweepShifts = 0; sweepSign = 1; // +1 / -1 for increase / decrease freq frequency = 0; envelopeStep = 0; envelopeStepLength = 0x10000;// 1 / 64 seconds of instructions envelopeCheck = false; envelopeSign = 1; envelopeVolume; clockEnvelop = 0; clockSweep = 0; gainNode; oscillator; constructor(apu: APU, channelNumber, audioContext) { super(); this.apu = apu; this.channelNumber = channelNumber; 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; } play() { if (this.playing || !this.dac) 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(); } stop() { this.playing = false; this.apu.setSoundFlag(this.channelNumber, 0); this.gainNode.disconnect(); } updateDAC(controlRegister: number): void { this.setDAC((controlRegister & 0xF8) > 0); } checkFreqSweep() { var oldFreq = this.getFrequency(); var newFreq = oldFreq + this.sweepSign * (oldFreq >> this.sweepShifts); if (newFreq > 0x7FF) { newFreq = 0; this.stop(); } return newFreq; } update(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; } } this.checkLength(clockElapsed); } setFrequency(value) { this.frequency = value; this.oscillator.frequency.value = 131072 / (2048 - this.frequency); } getFrequency() { return this.frequency; } setLength(value) { this.soundLength = 64 - (value & 0x3F); } setEnvelopeVolume(volume) { this.envelopeCheck = volume > 0 && volume < 16; this.envelopeVolume = volume; this.gainNode.gain.value = this.envelopeVolume * 1/100; } disable() { this.oscillator.disconnect(); } enable() { this.oscillator.connect(this.gainNode); } } export default Channel1; ================================================ FILE: src/sound/channel3.ts ================================================ import APU from './apu'; import {AbstractAudioChannel} from './channel'; class Channel3 extends AbstractAudioChannel { soundLengthUnit = 0x4000; // 1 / 256 second of instructions buffer; gainNode; baseSpeed = 65536; waveBuffer; bufferSource; constructor(apu: APU, channelNumber, audioContext) { super(); this.apu = apu; this.channelNumber = channelNumber; this.buffer = new Float32Array(32); var gainNode = audioContext.createGain(); gainNode.gain.value = 1; this.gainNode = gainNode; 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; } play() { if (this.playing || !this.dac) 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; } stop() { this.playing = false; this.apu.setSoundFlag(this.channelNumber, 0); this.gainNode.disconnect(); }; updateDAC(controlRegister) { this.setDAC((controlRegister & 0x80) > 0); } update(clockElapsed) { this.checkLength(clockElapsed); } setFrequency(value) { value = 65536 / (2048 - value); this.bufferSource.playbackRate.value = value / this.baseSpeed; } getFrequency() { var freq = 2048 - 65536 / (this.bufferSource.playbackRate.value * this.baseSpeed); return freq | 1; } setLength(value) { this.soundLength = 256 - value; } setWaveBufferByte(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; } disable() { this.bufferSource.disconnect(); } enable() { this.bufferSource.connect(this.gainNode); } } export default Channel3; ================================================ FILE: src/sound/channel4.ts ================================================ import APU from './apu'; import {AbstractAudioChannel} from './channel'; class Channel4 extends AbstractAudioChannel { constructor(apu, channelNumber, audioContext) { super(); this.apu = apu; this.channelNumber = channelNumber; this.audioContext = audioContext; } play() { if (this.playing || !this.dac) return; this.playing = true; this.apu.setSoundFlag(this.channelNumber, 1); this.clockLength = 0; } stop() { this.playing = false; this.apu.setSoundFlag(this.channelNumber, 0); } updateDAC(controlRegister) { this.setDAC((controlRegister & 0xF8) > 0); } update(clockElapsed) { this.checkLength(clockElapsed); } setLength(value) { this.soundLength = 64 - (value & 0x3F); } } export default Channel4; ================================================ FILE: src/timer.ts ================================================ import CPU from './cpu'; import Memory from './memory' class Timer { DIV = 0xFF04; TIMA = 0xFF05; TMA = 0xFF06; TAC = 0xFF07; mainTime = 0; divTime = 0; cpu: CPU; memory: Memory; constructor(cpu: CPU, memory: Memory) { this.cpu = cpu; this.memory = memory; } update(clockElapsed: number) { this.updateDiv(clockElapsed); this.updateTimer(clockElapsed); } updateTimer(clockElapsed: number) { if (!(this.memory.rb(this.TAC) & 0x4)) { return; } this.mainTime += clockElapsed; let 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(CPU.INTERRUPTS.TIMER); } } } // Update the DIV register internal clock // Increment it if the clock threshold is elapsed and // reset it if its value overflows updateDiv(clockElapsed: number) { let divThreshold = 256; // DIV is 16KHz this.divTime += clockElapsed; if (this.divTime > divThreshold) { this.divTime -= divThreshold; let div = this.memory.rb(this.DIV) + 1; this.memory.wb(this.DIV, div&0xFF); } } resetDiv() { this.divTime = 0; this.memory[this.DIV] = 0; // direct write to avoid looping } } export default Timer; ================================================ FILE: src/util.ts ================================================ import CPU from './cpu'; // Utility functions let Util = { // Add to the first argument the properties of all other arguments extend: function(target , ...sources/*, source1, source2, etc. */) { for (let i in sources) { let source = sources[i]; for (let name in source) { target[name] = source[name]; } } return target; }, testFlag: function(p: CPU, cc: string) { let test: number = 1; let mask: number = 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: CPU, r1: string, r2: string) { return Util.makeword(p.r[r1], p.r[r2]); }, // make a 16 bits word from 2 bytes makeword: function(b1: number, b2: number) { return (b1 << 8) + b2; }, // return the integer signed value of a given byte getSignedValue: function(v: number) { return v & 0x80 ? v-256 : v; }, // extract a bit from a byte readBit: function(byte: number, index: number) { return (byte >> index) & 1; } }; export default Util; ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "outDir": "./dist/", "allowJs": true, "target": "es6", "moduleResolution": "node", "noImplicitAny": false } } ================================================ FILE: webpack.config.js ================================================ const path = require('path'); module.exports = { entry: './src/main.ts', module: { rules: [ { test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/, }, ], }, resolve: { extensions: ['.tsx', '.ts', '.js'], }, output: { filename: 'gameboy.js', path: path.resolve(__dirname, 'dist'), library: { name: 'GameboyJS', type: 'global', }, }, optimization: { minimize: false }, mode: 'none' };