Holy-Unblocker/public/assets/js/gb.js
2020-10-07 20:51:19 -07:00

3406 lines
No EOL
123 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 << i));
this.memory.wb(0xFF0F, IFval);
this.disableInterrupts();
this.clock.c += 4; // 20 clocks to serve interrupt, with 16 for RSTn
CPU.interruptRoutines[i](this);
break;
}
}
};
// Set an interrupt flag
CPU.prototype.requestInterrupt = function(type) {
var IFval = this.memory.rb(0xFF0F);
IFval |= (1 << type)
this.memory.wb(0xFF0F, IFval);
this.unhalt();
};
CPU.prototype.isInterruptEnable = function(type) {
return GameboyJS.Util.readBit(this.memory.rb(0xFFFF), type) != 0;
};
CPU.prototype.enableInterrupts = function() {
this.IME = true;
};
CPU.prototype.disableInterrupts = function() {
this.IME = false;
};
CPU.prototype.enableSerialTransfer = function() {
this.enableSerial = 1;
this.clock.serial = 0;
};
CPU.prototype.endSerialTransfer = function() {
this.enableSerial = 0;
var data = this.memory.rb(0xFF01);
this.memory.wb(0xFF02, 0);
this.serialHandler.out(data);
this.memory.wb(0xFF01, this.serialHandler.in());
};
CPU.prototype.resetDivTimer = function() {
this.timer.resetDiv();
};
GameboyJS.CPU = CPU;
}(GameboyJS || (GameboyJS = {})));
var GameboyJS;
(function(GameboyJS) {
"use strict";
var Debug = {};
// Output a range of 16 memory addresses
Debug.view_memory = function(addr, gameboy) {
var memory = gameboy.cpu.memory;
addr = addr & 0xFFF0;
var pad = '00';
var str = addr.toString(16) + ':';
for (var i = addr; i < addr + 0x10; i++) {
if ((i & 0x1) == 0) {
str += ' ';
}
var val = memory[i] || 0;
val = val.toString(16);
str += pad.substring(val.length) + val;
}
return str;
};
Debug.view_tile = function(gameboy, index, dataStart) {
var memory = gameboy.cpu.memory;
var screen = gameboy.screen;
var LCDC = screen.deviceram(screen.LCDC);
if (typeof dataStart === 'undefined') {
dataStart = 0x8000;
if (!GameboyJS.Util.readBit(LCDC, 4)) {
dataStart = 0x8800;
index = GameboyJS.cpuOps._getSignedValue(index) + 128;
}
}
var tileData = screen.readTileData(index, dataStart);
var pixelData = new Array(8 * 8)
for (var line = 0; line < 8; line++) {
var b1 = tileData.shift();
var b2 = tileData.shift();
for (var pixel = 0; pixel < 8; pixel++) {
var mask = (1 << (7 - pixel));
var colorValue = ((b1 & mask) >> (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 << i; var z = (p.r[r1] & mask) ? 0 : 1; var f = p.r.F & 0x10;
f |= 0x20; if (z) f |= 0x80;
p.r.F = f;
p.clock.c += 4; },
BITirra: function(p, i, r1, r2) { var addr = GameboyJS.Util.getRegAddr(p, r1, r2); var mask = 1 << i; var z = (p.memory.rb(addr) & mask) ? 0 : 1; var f = p.r.F & 0x10;
f |= 0x20; if (z) f |= 0x80;
p.r.F = f;
p.clock.c += 8; },
SETir: function(p, i, r1) { var mask = 1 << i;
p.r[r1] |= mask;
p.clock.c += 4; },
SETirra: function(p, i, r1, r2) { var addr = GameboyJS.Util.getRegAddr(p, r1, r2); var mask = 1 << i;
p.memory.wb(addr, p.memory.rb(addr) | mask);
p.clock.c += 12; },
RESir: function(p, i, r1) { var mask = 0xFF - (1 << i);
p.r[r1] &= mask;
p.clock.c += 4; },
RESirra: function(p, i, r1, r2) { var addr = GameboyJS.Util.getRegAddr(p, r1, r2); var mask = 0xFF - (1 << i);
p.memory.wb(addr, p.memory.rb(addr) & mask);
p.clock.c += 12; },
SWAPr: function(p, r1) { p.r[r1] = ops._SWAPn(p, p.r[r1]);
p.clock.c += 4; },
SWAPrra: function(p, r1, r2) { var addr = (p.r[r1] << 8) + p.r[r2];
p.memory.wb(addr, ops._SWAPn(p, p.memory.rb(addr)));
p.clock.c += 12; },
_SWAPn: function(p, n) { p.r.F = n == 0 ? 0x80 : 0; return ((n & 0xF0) >> 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 = {})));