diff --git a/public/Repo/jsnes-ninjapad/index.html b/public/Repo/jsnes-ninjapad/index.html
new file mode 100644
index 0000000..f6c0a51
--- /dev/null
+++ b/public/Repo/jsnes-ninjapad/index.html
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/Repo/jsnes-ninjapad/jsnes.js b/public/Repo/jsnes-ninjapad/jsnes.js
new file mode 100644
index 0000000..f6129ba
--- /dev/null
+++ b/public/Repo/jsnes-ninjapad/jsnes.js
@@ -0,0 +1,7556 @@
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.jsnes = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i 0x4017) {
+ this.nes.cpu.mem[address] = value;
+ if (address >= 0x6000 && address < 0x8000) {
+ // Write to persistent RAM
+ this.nes.opts.onBatteryRamWrite(address, value);
+ }
+ } else if (address > 0x2007 && address < 0x4000) {
+ this.regWrite(0x2000 + (address & 0x7), value);
+ } else {
+ this.regWrite(address, value);
+ }
+ },
+
+ writelow: function (address, value) {
+ if (address < 0x2000) {
+ // Mirroring of RAM:
+ this.nes.cpu.mem[address & 0x7ff] = value;
+ } else if (address > 0x4017) {
+ this.nes.cpu.mem[address] = value;
+ } else if (address > 0x2007 && address < 0x4000) {
+ this.regWrite(0x2000 + (address & 0x7), value);
+ } else {
+ this.regWrite(address, value);
+ }
+ },
+
+ load: function (address) {
+ // Wrap around:
+ address &= 0xffff;
+
+ // Check address range:
+ if (address > 0x4017) {
+ // ROM:
+ return this.nes.cpu.mem[address];
+ } else if (address >= 0x2000) {
+ // I/O Ports.
+ return this.regLoad(address);
+ } else {
+ // RAM (mirrored)
+ return this.nes.cpu.mem[address & 0x7ff];
+ }
+ },
+
+ regLoad: function (address) {
+ switch (
+ address >> 12 // use fourth nibble (0xF000)
+ ) {
+ case 0:
+ break;
+
+ case 1:
+ break;
+
+ case 2:
+ // Fall through to case 3
+ case 3:
+ // PPU Registers
+ switch (address & 0x7) {
+ case 0x0:
+ // 0x2000:
+ // PPU Control Register 1.
+ // (the value is stored both
+ // in main memory and in the
+ // PPU as flags):
+ // (not in the real NES)
+ return this.nes.cpu.mem[0x2000];
+
+ case 0x1:
+ // 0x2001:
+ // PPU Control Register 2.
+ // (the value is stored both
+ // in main memory and in the
+ // PPU as flags):
+ // (not in the real NES)
+ return this.nes.cpu.mem[0x2001];
+
+ case 0x2:
+ // 0x2002:
+ // PPU Status Register.
+ // The value is stored in
+ // main memory in addition
+ // to as flags in the PPU.
+ // (not in the real NES)
+ return this.nes.ppu.readStatusRegister();
+
+ case 0x3:
+ return 0;
+
+ case 0x4:
+ // 0x2004:
+ // Sprite Memory read.
+ return this.nes.ppu.sramLoad();
+ case 0x5:
+ return 0;
+
+ case 0x6:
+ return 0;
+
+ case 0x7:
+ // 0x2007:
+ // VRAM read:
+ return this.nes.ppu.vramLoad();
+ }
+ break;
+ case 4:
+ // Sound+Joypad registers
+ switch (address - 0x4015) {
+ case 0:
+ // 0x4015:
+ // Sound channel enable, DMC Status
+ return this.nes.papu.readReg(address);
+
+ case 1:
+ // 0x4016:
+ // Joystick 1 + Strobe
+ return this.joy1Read();
+
+ case 2:
+ // 0x4017:
+ // Joystick 2 + Strobe
+ // https://wiki.nesdev.com/w/index.php/Zapper
+ var w;
+
+ if (
+ this.zapperX !== null &&
+ this.zapperY !== null &&
+ this.nes.ppu.isPixelWhite(this.zapperX, this.zapperY)
+ ) {
+ w = 0;
+ } else {
+ w = 0x1 << 3;
+ }
+
+ if (this.zapperFired) {
+ w |= 0x1 << 4;
+ }
+ return (this.joy2Read() | w) & 0xffff;
+ }
+ break;
+ }
+ return 0;
+ },
+
+ regWrite: function (address, value) {
+ switch (address) {
+ case 0x2000:
+ // PPU Control register 1
+ this.nes.cpu.mem[address] = value;
+ this.nes.ppu.updateControlReg1(value);
+ break;
+
+ case 0x2001:
+ // PPU Control register 2
+ this.nes.cpu.mem[address] = value;
+ this.nes.ppu.updateControlReg2(value);
+ break;
+
+ case 0x2003:
+ // Set Sprite RAM address:
+ this.nes.ppu.writeSRAMAddress(value);
+ break;
+
+ case 0x2004:
+ // Write to Sprite RAM:
+ this.nes.ppu.sramWrite(value);
+ break;
+
+ case 0x2005:
+ // Screen Scroll offsets:
+ this.nes.ppu.scrollWrite(value);
+ break;
+
+ case 0x2006:
+ // Set VRAM address:
+ this.nes.ppu.writeVRAMAddress(value);
+ break;
+
+ case 0x2007:
+ // Write to VRAM:
+ this.nes.ppu.vramWrite(value);
+ break;
+
+ case 0x4014:
+ // Sprite Memory DMA Access
+ this.nes.ppu.sramDMA(value);
+ break;
+
+ case 0x4015:
+ // Sound Channel Switch, DMC Status
+ this.nes.papu.writeReg(address, value);
+ break;
+
+ case 0x4016:
+ // Joystick 1 + Strobe
+ if ((value & 1) === 0 && (this.joypadLastWrite & 1) === 1) {
+ this.joy1StrobeState = 0;
+ this.joy2StrobeState = 0;
+ }
+ this.joypadLastWrite = value;
+ break;
+
+ case 0x4017:
+ // Sound channel frame sequencer:
+ this.nes.papu.writeReg(address, value);
+ break;
+
+ default:
+ // Sound registers
+ // console.log("write to sound reg");
+ if (address >= 0x4000 && address <= 0x4017) {
+ this.nes.papu.writeReg(address, value);
+ }
+ }
+ },
+
+ joy1Read: function () {
+ var ret;
+
+ switch (this.joy1StrobeState) {
+ case 0:
+ case 1:
+ case 2:
+ case 3:
+ case 4:
+ case 5:
+ case 6:
+ case 7:
+ ret = this.nes.controllers[1].state[this.joy1StrobeState];
+ break;
+ case 8:
+ case 9:
+ case 10:
+ case 11:
+ case 12:
+ case 13:
+ case 14:
+ case 15:
+ case 16:
+ case 17:
+ case 18:
+ ret = 0;
+ break;
+ case 19:
+ ret = 1;
+ break;
+ default:
+ ret = 0;
+ }
+
+ this.joy1StrobeState++;
+ if (this.joy1StrobeState === 24) {
+ this.joy1StrobeState = 0;
+ }
+
+ return ret;
+ },
+
+ joy2Read: function () {
+ var ret;
+
+ switch (this.joy2StrobeState) {
+ case 0:
+ case 1:
+ case 2:
+ case 3:
+ case 4:
+ case 5:
+ case 6:
+ case 7:
+ ret = this.nes.controllers[2].state[this.joy2StrobeState];
+ break;
+ case 8:
+ case 9:
+ case 10:
+ case 11:
+ case 12:
+ case 13:
+ case 14:
+ case 15:
+ case 16:
+ case 17:
+ case 18:
+ ret = 0;
+ break;
+ case 19:
+ ret = 1;
+ break;
+ default:
+ ret = 0;
+ }
+
+ this.joy2StrobeState++;
+ if (this.joy2StrobeState === 24) {
+ this.joy2StrobeState = 0;
+ }
+
+ return ret;
+ },
+
+ loadROM: function () {
+ if (!this.nes.rom.valid || this.nes.rom.romCount < 1) {
+ throw new Error("NoMapper: Invalid ROM! Unable to load.");
+ }
+
+ // Load ROM into memory:
+ this.loadPRGROM();
+
+ // Load CHR-ROM:
+ this.loadCHRROM();
+
+ // Load Battery RAM (if present):
+ this.loadBatteryRam();
+
+ // Reset IRQ:
+ //nes.getCpu().doResetInterrupt();
+ this.nes.cpu.requestIrq(this.nes.cpu.IRQ_RESET);
+ },
+
+ loadPRGROM: function () {
+ if (this.nes.rom.romCount > 1) {
+ // Load the two first banks into memory.
+ this.loadRomBank(0, 0x8000);
+ this.loadRomBank(1, 0xc000);
+ } else {
+ // Load the one bank into both memory locations:
+ this.loadRomBank(0, 0x8000);
+ this.loadRomBank(0, 0xc000);
+ }
+ },
+
+ loadCHRROM: function () {
+ // console.log("Loading CHR ROM..");
+ if (this.nes.rom.vromCount > 0) {
+ if (this.nes.rom.vromCount === 1) {
+ this.loadVromBank(0, 0x0000);
+ this.loadVromBank(0, 0x1000);
+ } else {
+ this.loadVromBank(0, 0x0000);
+ this.loadVromBank(1, 0x1000);
+ }
+ } else {
+ //System.out.println("There aren't any CHR-ROM banks..");
+ }
+ },
+
+ loadBatteryRam: function () {
+ if (this.nes.rom.batteryRam) {
+ var ram = this.nes.rom.batteryRam;
+ if (ram !== null && ram.length === 0x2000) {
+ // Load Battery RAM into memory:
+ utils.copyArrayElements(ram, 0, this.nes.cpu.mem, 0x6000, 0x2000);
+ }
+ }
+ },
+
+ loadRomBank: function (bank, address) {
+ // Loads a ROM bank into the specified address.
+ bank %= this.nes.rom.romCount;
+ //var data = this.nes.rom.rom[bank];
+ //cpuMem.write(address,data,data.length);
+ utils.copyArrayElements(
+ this.nes.rom.rom[bank],
+ 0,
+ this.nes.cpu.mem,
+ address,
+ 16384
+ );
+ },
+
+ loadVromBank: function (bank, address) {
+ if (this.nes.rom.vromCount === 0) {
+ return;
+ }
+ this.nes.ppu.triggerRendering();
+
+ utils.copyArrayElements(
+ this.nes.rom.vrom[bank % this.nes.rom.vromCount],
+ 0,
+ this.nes.ppu.vramMem,
+ address,
+ 4096
+ );
+
+ var vromTile = this.nes.rom.vromTile[bank % this.nes.rom.vromCount];
+ utils.copyArrayElements(
+ vromTile,
+ 0,
+ this.nes.ppu.ptTile,
+ address >> 4,
+ 256
+ );
+ },
+
+ load32kRomBank: function (bank, address) {
+ this.loadRomBank((bank * 2) % this.nes.rom.romCount, address);
+ this.loadRomBank((bank * 2 + 1) % this.nes.rom.romCount, address + 16384);
+ },
+
+ load8kVromBank: function (bank4kStart, address) {
+ if (this.nes.rom.vromCount === 0) {
+ return;
+ }
+ this.nes.ppu.triggerRendering();
+
+ this.loadVromBank(bank4kStart % this.nes.rom.vromCount, address);
+ this.loadVromBank(
+ (bank4kStart + 1) % this.nes.rom.vromCount,
+ address + 4096
+ );
+ },
+
+ load1kVromBank: function (bank1k, address) {
+ if (this.nes.rom.vromCount === 0) {
+ return;
+ }
+ this.nes.ppu.triggerRendering();
+
+ var bank4k = Math.floor(bank1k / 4) % this.nes.rom.vromCount;
+ var bankoffset = (bank1k % 4) * 1024;
+ utils.copyArrayElements(
+ this.nes.rom.vrom[bank4k],
+ bankoffset,
+ this.nes.ppu.vramMem,
+ address,
+ 1024
+ );
+
+ // Update tiles:
+ var vromTile = this.nes.rom.vromTile[bank4k];
+ var baseIndex = address >> 4;
+ for (var i = 0; i < 64; i++) {
+ this.nes.ppu.ptTile[baseIndex + i] = vromTile[(bank1k % 4 << 6) + i];
+ }
+ },
+
+ load2kVromBank: function (bank2k, address) {
+ if (this.nes.rom.vromCount === 0) {
+ return;
+ }
+ this.nes.ppu.triggerRendering();
+
+ var bank4k = Math.floor(bank2k / 2) % this.nes.rom.vromCount;
+ var bankoffset = (bank2k % 2) * 2048;
+ utils.copyArrayElements(
+ this.nes.rom.vrom[bank4k],
+ bankoffset,
+ this.nes.ppu.vramMem,
+ address,
+ 2048
+ );
+
+ // Update tiles:
+ var vromTile = this.nes.rom.vromTile[bank4k];
+ var baseIndex = address >> 4;
+ for (var i = 0; i < 128; i++) {
+ this.nes.ppu.ptTile[baseIndex + i] = vromTile[(bank2k % 2 << 7) + i];
+ }
+ },
+
+ load8kRomBank: function (bank8k, address) {
+ var bank16k = Math.floor(bank8k / 2) % this.nes.rom.romCount;
+ var offset = (bank8k % 2) * 8192;
+
+ //this.nes.cpu.mem.write(address,this.nes.rom.rom[bank16k],offset,8192);
+ utils.copyArrayElements(
+ this.nes.rom.rom[bank16k],
+ offset,
+ this.nes.cpu.mem,
+ address,
+ 8192
+ );
+ },
+
+ clockIrqCounter: function () {
+ // Does nothing. This is used by the MMC3 mapper.
+ },
+
+ // eslint-disable-next-line no-unused-vars
+ latchAccess: function (address) {
+ // Does nothing. This is used by MMC2.
+ },
+
+ toJSON: function () {
+ return {
+ joy1StrobeState: this.joy1StrobeState,
+ joy2StrobeState: this.joy2StrobeState,
+ joypadLastWrite: this.joypadLastWrite,
+ };
+ },
+
+ fromJSON: function (s) {
+ this.joy1StrobeState = s.joy1StrobeState;
+ this.joy2StrobeState = s.joy2StrobeState;
+ this.joypadLastWrite = s.joypadLastWrite;
+ },
+};
+
+Mappers[1] = function (nes) {
+ this.nes = nes;
+};
+
+Mappers[1].prototype = new Mappers[0]();
+
+Mappers[1].prototype.reset = function () {
+ Mappers[0].prototype.reset.apply(this);
+
+ // 5-bit buffer:
+ this.regBuffer = 0;
+ this.regBufferCounter = 0;
+
+ // Register 0:
+ this.mirroring = 0;
+ this.oneScreenMirroring = 0;
+ this.prgSwitchingArea = 1;
+ this.prgSwitchingSize = 1;
+ this.vromSwitchingSize = 0;
+
+ // Register 1:
+ this.romSelectionReg0 = 0;
+
+ // Register 2:
+ this.romSelectionReg1 = 0;
+
+ // Register 3:
+ this.romBankSelect = 0;
+};
+
+Mappers[1].prototype.write = function (address, value) {
+ // Writes to addresses other than MMC registers are handled by NoMapper.
+ if (address < 0x8000) {
+ Mappers[0].prototype.write.apply(this, arguments);
+ return;
+ }
+
+ // See what should be done with the written value:
+ if ((value & 128) !== 0) {
+ // Reset buffering:
+ this.regBufferCounter = 0;
+ this.regBuffer = 0;
+
+ // Reset register:
+ if (this.getRegNumber(address) === 0) {
+ this.prgSwitchingArea = 1;
+ this.prgSwitchingSize = 1;
+ }
+ } else {
+ // Continue buffering:
+ //regBuffer = (regBuffer & (0xFF-(1<> 2) & 1;
+
+ // PRG Switching Size:
+ this.prgSwitchingSize = (value >> 3) & 1;
+
+ // VROM Switching Size:
+ this.vromSwitchingSize = (value >> 4) & 1;
+
+ break;
+
+ case 1:
+ // ROM selection:
+ this.romSelectionReg0 = (value >> 4) & 1;
+
+ // Check whether the cart has VROM:
+ if (this.nes.rom.vromCount > 0) {
+ // Select VROM bank at 0x0000:
+ if (this.vromSwitchingSize === 0) {
+ // Swap 8kB VROM:
+ if (this.romSelectionReg0 === 0) {
+ this.load8kVromBank(value & 0xf, 0x0000);
+ } else {
+ this.load8kVromBank(
+ Math.floor(this.nes.rom.vromCount / 2) + (value & 0xf),
+ 0x0000
+ );
+ }
+ } else {
+ // Swap 4kB VROM:
+ if (this.romSelectionReg0 === 0) {
+ this.loadVromBank(value & 0xf, 0x0000);
+ } else {
+ this.loadVromBank(
+ Math.floor(this.nes.rom.vromCount / 2) + (value & 0xf),
+ 0x0000
+ );
+ }
+ }
+ }
+
+ break;
+
+ case 2:
+ // ROM selection:
+ this.romSelectionReg1 = (value >> 4) & 1;
+
+ // Check whether the cart has VROM:
+ if (this.nes.rom.vromCount > 0) {
+ // Select VROM bank at 0x1000:
+ if (this.vromSwitchingSize === 1) {
+ // Swap 4kB of VROM:
+ if (this.romSelectionReg1 === 0) {
+ this.loadVromBank(value & 0xf, 0x1000);
+ } else {
+ this.loadVromBank(
+ Math.floor(this.nes.rom.vromCount / 2) + (value & 0xf),
+ 0x1000
+ );
+ }
+ }
+ }
+ break;
+
+ default:
+ // Select ROM bank:
+ // -------------------------
+ tmp = value & 0xf;
+ var bank;
+ var baseBank = 0;
+
+ if (this.nes.rom.romCount >= 32) {
+ // 1024 kB cart
+ if (this.vromSwitchingSize === 0) {
+ if (this.romSelectionReg0 === 1) {
+ baseBank = 16;
+ }
+ } else {
+ baseBank =
+ (this.romSelectionReg0 | (this.romSelectionReg1 << 1)) << 3;
+ }
+ } else if (this.nes.rom.romCount >= 16) {
+ // 512 kB cart
+ if (this.romSelectionReg0 === 1) {
+ baseBank = 8;
+ }
+ }
+
+ if (this.prgSwitchingSize === 0) {
+ // 32kB
+ bank = baseBank + (value & 0xf);
+ this.load32kRomBank(bank, 0x8000);
+ } else {
+ // 16kB
+ bank = baseBank * 2 + (value & 0xf);
+ if (this.prgSwitchingArea === 0) {
+ this.loadRomBank(bank, 0xc000);
+ } else {
+ this.loadRomBank(bank, 0x8000);
+ }
+ }
+ }
+};
+
+// Returns the register number from the address written to:
+Mappers[1].prototype.getRegNumber = function (address) {
+ if (address >= 0x8000 && address <= 0x9fff) {
+ return 0;
+ } else if (address >= 0xa000 && address <= 0xbfff) {
+ return 1;
+ } else if (address >= 0xc000 && address <= 0xdfff) {
+ return 2;
+ } else {
+ return 3;
+ }
+};
+
+Mappers[1].prototype.loadROM = function () {
+ if (!this.nes.rom.valid) {
+ throw new Error("MMC1: Invalid ROM! Unable to load.");
+ }
+
+ // Load PRG-ROM:
+ this.loadRomBank(0, 0x8000); // First ROM bank..
+ this.loadRomBank(this.nes.rom.romCount - 1, 0xc000); // ..and last ROM bank.
+
+ // Load CHR-ROM:
+ this.loadCHRROM();
+
+ // Load Battery RAM (if present):
+ this.loadBatteryRam();
+
+ // Do Reset-Interrupt:
+ this.nes.cpu.requestIrq(this.nes.cpu.IRQ_RESET);
+};
+
+// eslint-disable-next-line no-unused-vars
+Mappers[1].prototype.switchLowHighPrgRom = function (oldSetting) {
+ // not yet.
+};
+
+Mappers[1].prototype.switch16to32 = function () {
+ // not yet.
+};
+
+Mappers[1].prototype.switch32to16 = function () {
+ // not yet.
+};
+
+Mappers[1].prototype.toJSON = function () {
+ var s = Mappers[0].prototype.toJSON.apply(this);
+ s.mirroring = this.mirroring;
+ s.oneScreenMirroring = this.oneScreenMirroring;
+ s.prgSwitchingArea = this.prgSwitchingArea;
+ s.prgSwitchingSize = this.prgSwitchingSize;
+ s.vromSwitchingSize = this.vromSwitchingSize;
+ s.romSelectionReg0 = this.romSelectionReg0;
+ s.romSelectionReg1 = this.romSelectionReg1;
+ s.romBankSelect = this.romBankSelect;
+ s.regBuffer = this.regBuffer;
+ s.regBufferCounter = this.regBufferCounter;
+ return s;
+};
+
+Mappers[1].prototype.fromJSON = function (s) {
+ Mappers[0].prototype.fromJSON.apply(this, arguments);
+ this.mirroring = s.mirroring;
+ this.oneScreenMirroring = s.oneScreenMirroring;
+ this.prgSwitchingArea = s.prgSwitchingArea;
+ this.prgSwitchingSize = s.prgSwitchingSize;
+ this.vromSwitchingSize = s.vromSwitchingSize;
+ this.romSelectionReg0 = s.romSelectionReg0;
+ this.romSelectionReg1 = s.romSelectionReg1;
+ this.romBankSelect = s.romBankSelect;
+ this.regBuffer = s.regBuffer;
+ this.regBufferCounter = s.regBufferCounter;
+};
+
+Mappers[2] = function (nes) {
+ this.nes = nes;
+};
+
+Mappers[2].prototype = new Mappers[0]();
+
+Mappers[2].prototype.write = function (address, value) {
+ // Writes to addresses other than MMC registers are handled by NoMapper.
+ if (address < 0x8000) {
+ Mappers[0].prototype.write.apply(this, arguments);
+ return;
+ } else {
+ // This is a ROM bank select command.
+ // Swap in the given ROM bank at 0x8000:
+ this.loadRomBank(value, 0x8000);
+ }
+};
+
+Mappers[2].prototype.loadROM = function () {
+ if (!this.nes.rom.valid) {
+ throw new Error("UNROM: Invalid ROM! Unable to load.");
+ }
+
+ // Load PRG-ROM:
+ this.loadRomBank(0, 0x8000);
+ this.loadRomBank(this.nes.rom.romCount - 1, 0xc000);
+
+ // Load CHR-ROM:
+ this.loadCHRROM();
+
+ // Do Reset-Interrupt:
+ this.nes.cpu.requestIrq(this.nes.cpu.IRQ_RESET);
+};
+
+/**
+ * Mapper 003 (CNROM)
+ *
+ * @constructor
+ * @example Solomon's Key, Arkanoid, Arkista's Ring, Bump 'n' Jump, Cybernoid
+ * @description http://wiki.nesdev.com/w/index.php/INES_Mapper_003
+ */
+Mappers[3] = function (nes) {
+ this.nes = nes;
+};
+
+Mappers[3].prototype = new Mappers[0]();
+
+Mappers[3].prototype.write = function (address, value) {
+ // Writes to addresses other than MMC registers are handled by NoMapper.
+ if (address < 0x8000) {
+ Mappers[0].prototype.write.apply(this, arguments);
+ return;
+ } else {
+ // This is a ROM bank select command.
+ // Swap in the given ROM bank at 0x8000:
+ // This is a VROM bank select command.
+ // Swap in the given VROM bank at 0x0000:
+ var bank = (value % (this.nes.rom.vromCount / 2)) * 2;
+ this.loadVromBank(bank, 0x0000);
+ this.loadVromBank(bank + 1, 0x1000);
+ this.load8kVromBank(value * 2, 0x0000);
+ }
+};
+
+Mappers[4] = function (nes) {
+ this.nes = nes;
+
+ this.CMD_SEL_2_1K_VROM_0000 = 0;
+ this.CMD_SEL_2_1K_VROM_0800 = 1;
+ this.CMD_SEL_1K_VROM_1000 = 2;
+ this.CMD_SEL_1K_VROM_1400 = 3;
+ this.CMD_SEL_1K_VROM_1800 = 4;
+ this.CMD_SEL_1K_VROM_1C00 = 5;
+ this.CMD_SEL_ROM_PAGE1 = 6;
+ this.CMD_SEL_ROM_PAGE2 = 7;
+
+ this.command = null;
+ this.prgAddressSelect = null;
+ this.chrAddressSelect = null;
+ this.pageNumber = null;
+ this.irqCounter = null;
+ this.irqLatchValue = null;
+ this.irqEnable = null;
+ this.prgAddressChanged = false;
+};
+
+Mappers[4].prototype = new Mappers[0]();
+
+Mappers[4].prototype.write = function (address, value) {
+ // Writes to addresses other than MMC registers are handled by NoMapper.
+ if (address < 0x8000) {
+ Mappers[0].prototype.write.apply(this, arguments);
+ return;
+ }
+
+ switch (address) {
+ case 0x8000:
+ // Command/Address Select register
+ this.command = value & 7;
+ var tmp = (value >> 6) & 1;
+ if (tmp !== this.prgAddressSelect) {
+ this.prgAddressChanged = true;
+ }
+ this.prgAddressSelect = tmp;
+ this.chrAddressSelect = (value >> 7) & 1;
+ break;
+
+ case 0x8001:
+ // Page number for command
+ this.executeCommand(this.command, value);
+ break;
+
+ case 0xa000:
+ // Mirroring select
+ if ((value & 1) !== 0) {
+ this.nes.ppu.setMirroring(this.nes.rom.HORIZONTAL_MIRRORING);
+ } else {
+ this.nes.ppu.setMirroring(this.nes.rom.VERTICAL_MIRRORING);
+ }
+ break;
+
+ case 0xa001:
+ // SaveRAM Toggle
+ // TODO
+ //nes.getRom().setSaveState((value&1)!=0);
+ break;
+
+ case 0xc000:
+ // IRQ Counter register
+ this.irqCounter = value;
+ //nes.ppu.mapperIrqCounter = 0;
+ break;
+
+ case 0xc001:
+ // IRQ Latch register
+ this.irqLatchValue = value;
+ break;
+
+ case 0xe000:
+ // IRQ Control Reg 0 (disable)
+ //irqCounter = irqLatchValue;
+ this.irqEnable = 0;
+ break;
+
+ case 0xe001:
+ // IRQ Control Reg 1 (enable)
+ this.irqEnable = 1;
+ break;
+
+ default:
+ // Not a MMC3 register.
+ // The game has probably crashed,
+ // since it tries to write to ROM..
+ // IGNORE.
+ }
+};
+
+Mappers[4].prototype.executeCommand = function (cmd, arg) {
+ switch (cmd) {
+ case this.CMD_SEL_2_1K_VROM_0000:
+ // Select 2 1KB VROM pages at 0x0000:
+ if (this.chrAddressSelect === 0) {
+ this.load1kVromBank(arg, 0x0000);
+ this.load1kVromBank(arg + 1, 0x0400);
+ } else {
+ this.load1kVromBank(arg, 0x1000);
+ this.load1kVromBank(arg + 1, 0x1400);
+ }
+ break;
+
+ case this.CMD_SEL_2_1K_VROM_0800:
+ // Select 2 1KB VROM pages at 0x0800:
+ if (this.chrAddressSelect === 0) {
+ this.load1kVromBank(arg, 0x0800);
+ this.load1kVromBank(arg + 1, 0x0c00);
+ } else {
+ this.load1kVromBank(arg, 0x1800);
+ this.load1kVromBank(arg + 1, 0x1c00);
+ }
+ break;
+
+ case this.CMD_SEL_1K_VROM_1000:
+ // Select 1K VROM Page at 0x1000:
+ if (this.chrAddressSelect === 0) {
+ this.load1kVromBank(arg, 0x1000);
+ } else {
+ this.load1kVromBank(arg, 0x0000);
+ }
+ break;
+
+ case this.CMD_SEL_1K_VROM_1400:
+ // Select 1K VROM Page at 0x1400:
+ if (this.chrAddressSelect === 0) {
+ this.load1kVromBank(arg, 0x1400);
+ } else {
+ this.load1kVromBank(arg, 0x0400);
+ }
+ break;
+
+ case this.CMD_SEL_1K_VROM_1800:
+ // Select 1K VROM Page at 0x1800:
+ if (this.chrAddressSelect === 0) {
+ this.load1kVromBank(arg, 0x1800);
+ } else {
+ this.load1kVromBank(arg, 0x0800);
+ }
+ break;
+
+ case this.CMD_SEL_1K_VROM_1C00:
+ // Select 1K VROM Page at 0x1C00:
+ if (this.chrAddressSelect === 0) {
+ this.load1kVromBank(arg, 0x1c00);
+ } else {
+ this.load1kVromBank(arg, 0x0c00);
+ }
+ break;
+
+ case this.CMD_SEL_ROM_PAGE1:
+ if (this.prgAddressChanged) {
+ // Load the two hardwired banks:
+ if (this.prgAddressSelect === 0) {
+ this.load8kRomBank((this.nes.rom.romCount - 1) * 2, 0xc000);
+ } else {
+ this.load8kRomBank((this.nes.rom.romCount - 1) * 2, 0x8000);
+ }
+ this.prgAddressChanged = false;
+ }
+
+ // Select first switchable ROM page:
+ if (this.prgAddressSelect === 0) {
+ this.load8kRomBank(arg, 0x8000);
+ } else {
+ this.load8kRomBank(arg, 0xc000);
+ }
+ break;
+
+ case this.CMD_SEL_ROM_PAGE2:
+ // Select second switchable ROM page:
+ this.load8kRomBank(arg, 0xa000);
+
+ // hardwire appropriate bank:
+ if (this.prgAddressChanged) {
+ // Load the two hardwired banks:
+ if (this.prgAddressSelect === 0) {
+ this.load8kRomBank((this.nes.rom.romCount - 1) * 2, 0xc000);
+ } else {
+ this.load8kRomBank((this.nes.rom.romCount - 1) * 2, 0x8000);
+ }
+ this.prgAddressChanged = false;
+ }
+ }
+};
+
+Mappers[4].prototype.loadROM = function () {
+ if (!this.nes.rom.valid) {
+ throw new Error("MMC3: Invalid ROM! Unable to load.");
+ }
+
+ // Load hardwired PRG banks (0xC000 and 0xE000):
+ this.load8kRomBank((this.nes.rom.romCount - 1) * 2, 0xc000);
+ this.load8kRomBank((this.nes.rom.romCount - 1) * 2 + 1, 0xe000);
+
+ // Load swappable PRG banks (0x8000 and 0xA000):
+ this.load8kRomBank(0, 0x8000);
+ this.load8kRomBank(1, 0xa000);
+
+ // Load CHR-ROM:
+ this.loadCHRROM();
+
+ // Load Battery RAM (if present):
+ this.loadBatteryRam();
+
+ // Do Reset-Interrupt:
+ this.nes.cpu.requestIrq(this.nes.cpu.IRQ_RESET);
+};
+
+Mappers[4].prototype.clockIrqCounter = function () {
+ if (this.irqEnable === 1) {
+ this.irqCounter--;
+ if (this.irqCounter < 0) {
+ // Trigger IRQ:
+ //nes.getCpu().doIrq();
+ this.nes.cpu.requestIrq(this.nes.cpu.IRQ_NORMAL);
+ this.irqCounter = this.irqLatchValue;
+ }
+ }
+};
+
+Mappers[4].prototype.toJSON = function () {
+ var s = Mappers[0].prototype.toJSON.apply(this);
+ s.command = this.command;
+ s.prgAddressSelect = this.prgAddressSelect;
+ s.chrAddressSelect = this.chrAddressSelect;
+ s.pageNumber = this.pageNumber;
+ s.irqCounter = this.irqCounter;
+ s.irqLatchValue = this.irqLatchValue;
+ s.irqEnable = this.irqEnable;
+ s.prgAddressChanged = this.prgAddressChanged;
+ return s;
+};
+
+Mappers[4].prototype.fromJSON = function (s) {
+ Mappers[0].prototype.fromJSON.apply(this, arguments);
+ this.command = s.command;
+ this.prgAddressSelect = s.prgAddressSelect;
+ this.chrAddressSelect = s.chrAddressSelect;
+ this.pageNumber = s.pageNumber;
+ this.irqCounter = s.irqCounter;
+ this.irqLatchValue = s.irqLatchValue;
+ this.irqEnable = s.irqEnable;
+ this.prgAddressChanged = s.prgAddressChanged;
+};
+
+/**
+ * Mapper005 (MMC5,ExROM)
+ *
+ * @example Castlevania 3, Just Breed, Uncharted Waters, Romance of the 3 Kingdoms 2, Laser Invasion, Metal Slader Glory, Uchuu Keibitai SDF, Shin 4 Nin Uchi Mahjong - Yakuman Tengoku
+ * @description http://wiki.nesdev.com/w/index.php/INES_Mapper_005
+ * @constructor
+ */
+Mappers[5] = function (nes) {
+ this.nes = nes;
+};
+
+Mappers[5].prototype = new Mappers[0]();
+
+Mappers[5].prototype.write = function (address, value) {
+ // Writes to addresses other than MMC registers are handled by NoMapper.
+ if (address < 0x8000) {
+ Mappers[0].prototype.write.apply(this, arguments);
+ } else {
+ this.load8kVromBank(value, 0x0000);
+ }
+};
+
+Mappers[5].prototype.write = function (address, value) {
+ // Writes to addresses other than MMC registers are handled by NoMapper.
+ if (address < 0x5000) {
+ Mappers[0].prototype.write.apply(this, arguments);
+ return;
+ }
+
+ switch (address) {
+ case 0x5100:
+ this.prg_size = value & 3;
+ break;
+ case 0x5101:
+ this.chr_size = value & 3;
+ break;
+ case 0x5102:
+ this.sram_we_a = value & 3;
+ break;
+ case 0x5103:
+ this.sram_we_b = value & 3;
+ break;
+ case 0x5104:
+ this.graphic_mode = value & 3;
+ break;
+ case 0x5105:
+ this.nametable_mode = value;
+ this.nametable_type[0] = value & 3;
+ this.load1kVromBank(value & 3, 0x2000);
+ value >>= 2;
+ this.nametable_type[1] = value & 3;
+ this.load1kVromBank(value & 3, 0x2400);
+ value >>= 2;
+ this.nametable_type[2] = value & 3;
+ this.load1kVromBank(value & 3, 0x2800);
+ value >>= 2;
+ this.nametable_type[3] = value & 3;
+ this.load1kVromBank(value & 3, 0x2c00);
+ break;
+ case 0x5106:
+ this.fill_chr = value;
+ break;
+ case 0x5107:
+ this.fill_pal = value & 3;
+ break;
+ case 0x5113:
+ this.SetBank_SRAM(3, value & 3);
+ break;
+ case 0x5114:
+ case 0x5115:
+ case 0x5116:
+ case 0x5117:
+ this.SetBank_CPU(address, value);
+ break;
+ case 0x5120:
+ case 0x5121:
+ case 0x5122:
+ case 0x5123:
+ case 0x5124:
+ case 0x5125:
+ case 0x5126:
+ case 0x5127:
+ this.chr_mode = 0;
+ this.chr_page[0][address & 7] = value;
+ this.SetBank_PPU();
+ break;
+ case 0x5128:
+ case 0x5129:
+ case 0x512a:
+ case 0x512b:
+ this.chr_mode = 1;
+ this.chr_page[1][(address & 3) + 0] = value;
+ this.chr_page[1][(address & 3) + 4] = value;
+ this.SetBank_PPU();
+ break;
+ case 0x5200:
+ this.split_control = value;
+ break;
+ case 0x5201:
+ this.split_scroll = value;
+ break;
+ case 0x5202:
+ this.split_page = value & 0x3f;
+ break;
+ case 0x5203:
+ this.irq_line = value;
+ this.nes.cpu.ClearIRQ();
+ break;
+ case 0x5204:
+ this.irq_enable = value;
+ this.nes.cpu.ClearIRQ();
+ break;
+ case 0x5205:
+ this.mult_a = value;
+ break;
+ case 0x5206:
+ this.mult_b = value;
+ break;
+ default:
+ if (address >= 0x5000 && address <= 0x5015) {
+ this.nes.papu.exWrite(address, value);
+ } else if (address >= 0x5c00 && address <= 0x5fff) {
+ if (this.graphic_mode === 2) {
+ // ExRAM
+ // vram write
+ } else if (this.graphic_mode !== 3) {
+ // Split,ExGraphic
+ if (this.irq_status & 0x40) {
+ // vram write
+ } else {
+ // vram write
+ }
+ }
+ } else if (address >= 0x6000 && address <= 0x7fff) {
+ if (this.sram_we_a === 2 && this.sram_we_b === 1) {
+ // additional ram write
+ }
+ }
+ break;
+ }
+};
+
+Mappers[5].prototype.loadROM = function () {
+ if (!this.nes.rom.valid) {
+ throw new Error("UNROM: Invalid ROM! Unable to load.");
+ }
+
+ // Load PRG-ROM:
+ this.load8kRomBank(this.nes.rom.romCount * 2 - 1, 0x8000);
+ this.load8kRomBank(this.nes.rom.romCount * 2 - 1, 0xa000);
+ this.load8kRomBank(this.nes.rom.romCount * 2 - 1, 0xc000);
+ this.load8kRomBank(this.nes.rom.romCount * 2 - 1, 0xe000);
+
+ // Load CHR-ROM:
+ this.loadCHRROM();
+
+ // Do Reset-Interrupt:
+ this.nes.cpu.requestIrq(this.nes.cpu.IRQ_RESET);
+};
+
+/**
+ * Mapper007 (AxROM)
+ * @example Battletoads, Time Lord, Marble Madness
+ * @description http://wiki.nesdev.com/w/index.php/INES_Mapper_007
+ * @constructor
+ */
+Mappers[7] = function (nes) {
+ this.nes = nes;
+};
+
+Mappers[7].prototype = new Mappers[0]();
+
+Mappers[7].prototype.write = function (address, value) {
+ // Writes to addresses other than MMC registers are handled by NoMapper.
+ if (address < 0x8000) {
+ Mappers[0].prototype.write.apply(this, arguments);
+ } else {
+ this.load32kRomBank(value & 0x7, 0x8000);
+ if (value & 0x10) {
+ this.nes.ppu.setMirroring(this.nes.rom.SINGLESCREEN_MIRRORING2);
+ } else {
+ this.nes.ppu.setMirroring(this.nes.rom.SINGLESCREEN_MIRRORING);
+ }
+ }
+};
+
+Mappers[7].prototype.loadROM = function () {
+ if (!this.nes.rom.valid) {
+ throw new Error("AOROM: Invalid ROM! Unable to load.");
+ }
+
+ // Load PRG-ROM:
+ this.loadPRGROM();
+
+ // Load CHR-ROM:
+ this.loadCHRROM();
+
+ // Do Reset-Interrupt:
+ this.nes.cpu.requestIrq(this.nes.cpu.IRQ_RESET);
+};
+
+/**
+ * Mapper 011 (Color Dreams)
+ *
+ * @description http://wiki.nesdev.com/w/index.php/Color_Dreams
+ * @example Crystal Mines, Metal Fighter
+ * @constructor
+ */
+Mappers[11] = function (nes) {
+ this.nes = nes;
+};
+
+Mappers[11].prototype = new Mappers[0]();
+
+Mappers[11].prototype.write = function (address, value) {
+ if (address < 0x8000) {
+ Mappers[0].prototype.write.apply(this, arguments);
+ return;
+ } else {
+ // Swap in the given PRG-ROM bank:
+ var prgbank1 = ((value & 0xf) * 2) % this.nes.rom.romCount;
+ var prgbank2 = ((value & 0xf) * 2 + 1) % this.nes.rom.romCount;
+
+ this.loadRomBank(prgbank1, 0x8000);
+ this.loadRomBank(prgbank2, 0xc000);
+
+ if (this.nes.rom.vromCount > 0) {
+ // Swap in the given VROM bank at 0x0000:
+ var bank = ((value >> 4) * 2) % this.nes.rom.vromCount;
+ this.loadVromBank(bank, 0x0000);
+ this.loadVromBank(bank + 1, 0x1000);
+ }
+ }
+};
+
+/**
+ * Mapper 034 (BNROM, NINA-01)
+ *
+ * @description http://wiki.nesdev.com/w/index.php/INES_Mapper_034
+ * @example Darkseed, Mashou, Mission Impossible 2
+ * @constructor
+ */
+Mappers[34] = function (nes) {
+ this.nes = nes;
+};
+
+Mappers[34].prototype = new Mappers[0]();
+
+Mappers[34].prototype.write = function (address, value) {
+ if (address < 0x8000) {
+ Mappers[0].prototype.write.apply(this, arguments);
+ return;
+ } else {
+ this.load32kRomBank(value, 0x8000);
+ }
+};
+
+/**
+ * Mapper 038
+ *
+ * @description http://wiki.nesdev.com/w/index.php/INES_Mapper_038
+ * @example Crime Busters
+ * @constructor
+ */
+Mappers[38] = function (nes) {
+ this.nes = nes;
+};
+
+Mappers[38].prototype = new Mappers[0]();
+
+Mappers[38].prototype.write = function (address, value) {
+ if (address < 0x7000 || address > 0x7fff) {
+ Mappers[0].prototype.write.apply(this, arguments);
+ return;
+ } else {
+ // Swap in the given PRG-ROM bank at 0x8000:
+ this.load32kRomBank(value & 3, 0x8000);
+
+ // Swap in the given VROM bank at 0x0000:
+ this.load8kVromBank(((value >> 2) & 3) * 2, 0x0000);
+ }
+};
+
+/**
+ * Mapper 066 (GxROM)
+ *
+ * @description http://wiki.nesdev.com/w/index.php/INES_Mapper_066
+ * @example Doraemon, Dragon Power, Gumshoe, Thunder & Lightning,
+ * Super Mario Bros. + Duck Hunt
+ * @constructor
+ */
+Mappers[66] = function (nes) {
+ this.nes = nes;
+};
+
+Mappers[66].prototype = new Mappers[0]();
+
+Mappers[66].prototype.write = function (address, value) {
+ if (address < 0x8000) {
+ Mappers[0].prototype.write.apply(this, arguments);
+ return;
+ } else {
+ // Swap in the given PRG-ROM bank at 0x8000:
+ this.load32kRomBank((value >> 4) & 3, 0x8000);
+
+ // Swap in the given VROM bank at 0x0000:
+ this.load8kVromBank((value & 3) * 2, 0x0000);
+ }
+};
+
+/**
+ * Mapper 094 (UN1ROM)
+ *
+ * @description http://wiki.nesdev.com/w/index.php/INES_Mapper_094
+ * @example Senjou no Ookami
+ * @constructor
+ */
+Mappers[94] = function (nes) {
+ this.nes = nes;
+};
+
+Mappers[94].prototype = new Mappers[0]();
+
+Mappers[94].prototype.write = function (address, value) {
+ // Writes to addresses other than MMC registers are handled by NoMapper.
+ if (address < 0x8000) {
+ Mappers[0].prototype.write.apply(this, arguments);
+ return;
+ } else {
+ // This is a ROM bank select command.
+ // Swap in the given ROM bank at 0x8000:
+ this.loadRomBank(value >> 2, 0x8000);
+ }
+};
+
+Mappers[94].prototype.loadROM = function () {
+ if (!this.nes.rom.valid) {
+ throw new Error("UN1ROM: Invalid ROM! Unable to load.");
+ }
+
+ // Load PRG-ROM:
+ this.loadRomBank(0, 0x8000);
+ this.loadRomBank(this.nes.rom.romCount - 1, 0xc000);
+
+ // Load CHR-ROM:
+ this.loadCHRROM();
+
+ // Do Reset-Interrupt:
+ this.nes.cpu.requestIrq(this.nes.cpu.IRQ_RESET);
+};
+
+/**
+ * Mapper 140
+ *
+ * @description http://wiki.nesdev.com/w/index.php/INES_Mapper_140
+ * @example Bio Senshi Dan - Increaser Tono Tatakai
+ * @constructor
+ */
+Mappers[140] = function (nes) {
+ this.nes = nes;
+};
+
+Mappers[140].prototype = new Mappers[0]();
+
+Mappers[140].prototype.write = function (address, value) {
+ if (address < 0x6000 || address > 0x7fff) {
+ Mappers[0].prototype.write.apply(this, arguments);
+ return;
+ } else {
+ // Swap in the given PRG-ROM bank at 0x8000:
+ this.load32kRomBank((value >> 4) & 3, 0x8000);
+
+ // Swap in the given VROM bank at 0x0000:
+ this.load8kVromBank((value & 0xf) * 2, 0x0000);
+ }
+};
+
+/**
+ * Mapper 180
+ *
+ * @description http://wiki.nesdev.com/w/index.php/INES_Mapper_180
+ * @example Crazy Climber
+ * @constructor
+ */
+Mappers[180] = function (nes) {
+ this.nes = nes;
+};
+
+Mappers[180].prototype = new Mappers[0]();
+
+Mappers[180].prototype.write = function (address, value) {
+ // Writes to addresses other than MMC registers are handled by NoMapper.
+ if (address < 0x8000) {
+ Mappers[0].prototype.write.apply(this, arguments);
+ return;
+ } else {
+ // This is a ROM bank select command.
+ // Swap in the given ROM bank at 0xc000:
+ this.loadRomBank(value, 0xc000);
+ }
+};
+
+Mappers[180].prototype.loadROM = function () {
+ if (!this.nes.rom.valid) {
+ throw new Error("Mapper 180: Invalid ROM! Unable to load.");
+ }
+
+ // Load PRG-ROM:
+ this.loadRomBank(0, 0x8000);
+ this.loadRomBank(this.nes.rom.romCount - 1, 0xc000);
+
+ // Load CHR-ROM:
+ this.loadCHRROM();
+
+ // Do Reset-Interrupt:
+ this.nes.cpu.requestIrq(this.nes.cpu.IRQ_RESET);
+};
+
+module.exports = Mappers;
+
+},{"./utils":10}],5:[function(require,module,exports){
+var CPU = require("./cpu");
+var Controller = require("./controller");
+var PPU = require("./ppu");
+var PAPU = require("./papu");
+var ROM = require("./rom");
+
+var NES = function (opts) {
+ this.opts = {
+ onFrame: function () {},
+ onAudioSample: null,
+ onStatusUpdate: function () {},
+ onBatteryRamWrite: function () {},
+
+ // FIXME: not actually used except for in PAPU
+ preferredFrameRate: 60,
+
+ emulateSound: true,
+ sampleRate: 48000, // Sound sample rate in hz
+ };
+ if (typeof opts !== "undefined") {
+ var key;
+ for (key in this.opts) {
+ if (typeof opts[key] !== "undefined") {
+ this.opts[key] = opts[key];
+ }
+ }
+ }
+
+ this.frameTime = 1000 / this.opts.preferredFrameRate;
+
+ this.ui = {
+ writeFrame: this.opts.onFrame,
+ updateStatus: this.opts.onStatusUpdate,
+ };
+ this.cpu = new CPU(this);
+ this.ppu = new PPU(this);
+ this.papu = new PAPU(this);
+ this.mmap = null; // set in loadROM()
+ this.controllers = {
+ 1: new Controller(),
+ 2: new Controller(),
+ };
+
+ this.ui.updateStatus("Ready to load a ROM.");
+
+ this.frame = this.frame.bind(this);
+ this.buttonDown = this.buttonDown.bind(this);
+ this.buttonUp = this.buttonUp.bind(this);
+ this.zapperMove = this.zapperMove.bind(this);
+ this.zapperFireDown = this.zapperFireDown.bind(this);
+ this.zapperFireUp = this.zapperFireUp.bind(this);
+};
+
+NES.prototype = {
+ fpsFrameCount: 0,
+ romData: null,
+ break: false,
+
+ // Set break to true to stop frame loop.
+ stop: function () {
+ this.break = true;
+ },
+
+ // Resets the system
+ reset: function () {
+ if (this.mmap !== null) {
+ this.mmap.reset();
+ }
+
+ this.cpu.reset();
+ this.ppu.reset();
+ this.papu.reset();
+
+ this.lastFpsTime = null;
+ this.fpsFrameCount = 0;
+
+ this.break = false;
+ },
+
+ frame: function () {
+ this.ppu.startFrame();
+ var cycles = 0;
+ var emulateSound = this.opts.emulateSound;
+ var cpu = this.cpu;
+ var ppu = this.ppu;
+ var papu = this.papu;
+ FRAMELOOP: for (;;) {
+ if (this.break) break;
+ if (cpu.cyclesToHalt === 0) {
+ // Execute a CPU instruction
+ cycles = cpu.emulate();
+ if (emulateSound) {
+ papu.clockFrameCounter(cycles);
+ }
+ cycles *= 3;
+ } else {
+ if (cpu.cyclesToHalt > 8) {
+ cycles = 24;
+ if (emulateSound) {
+ papu.clockFrameCounter(8);
+ }
+ cpu.cyclesToHalt -= 8;
+ } else {
+ cycles = cpu.cyclesToHalt * 3;
+ if (emulateSound) {
+ papu.clockFrameCounter(cpu.cyclesToHalt);
+ }
+ cpu.cyclesToHalt = 0;
+ }
+ }
+
+ for (; cycles > 0; cycles--) {
+ if (
+ ppu.curX === ppu.spr0HitX &&
+ ppu.f_spVisibility === 1 &&
+ ppu.scanline - 21 === ppu.spr0HitY
+ ) {
+ // Set sprite 0 hit flag:
+ ppu.setStatusFlag(ppu.STATUS_SPRITE0HIT, true);
+ }
+
+ if (ppu.requestEndFrame) {
+ ppu.nmiCounter--;
+ if (ppu.nmiCounter === 0) {
+ ppu.requestEndFrame = false;
+ ppu.startVBlank();
+ break FRAMELOOP;
+ }
+ }
+
+ ppu.curX++;
+ if (ppu.curX === 341) {
+ ppu.curX = 0;
+ ppu.endScanline();
+ }
+ }
+ }
+ this.fpsFrameCount++;
+ },
+
+ buttonDown: function (controller, button) {
+ this.controllers[controller].buttonDown(button);
+ },
+
+ buttonUp: function (controller, button) {
+ this.controllers[controller].buttonUp(button);
+ },
+
+ zapperMove: function (x, y) {
+ if (!this.mmap) return;
+ this.mmap.zapperX = x;
+ this.mmap.zapperY = y;
+ },
+
+ zapperFireDown: function () {
+ if (!this.mmap) return;
+ this.mmap.zapperFired = true;
+ },
+
+ zapperFireUp: function () {
+ if (!this.mmap) return;
+ this.mmap.zapperFired = false;
+ },
+
+ getFPS: function () {
+ var now = +new Date();
+ var fps = null;
+ if (this.lastFpsTime) {
+ fps = this.fpsFrameCount / ((now - this.lastFpsTime) / 1000);
+ }
+ this.fpsFrameCount = 0;
+ this.lastFpsTime = now;
+ return fps;
+ },
+
+ reloadROM: function () {
+ if (this.romData !== null) {
+ this.loadROM(this.romData);
+ }
+ },
+
+ // Loads a ROM file into the CPU and PPU.
+ // The ROM file is validated first.
+ loadROM: function (data) {
+ // Load ROM file:
+ this.rom = new ROM(this);
+ this.rom.load(data);
+
+ this.reset();
+ this.mmap = this.rom.createMapper();
+ this.mmap.loadROM();
+ this.ppu.setMirroring(this.rom.getMirroringType());
+ this.romData = data;
+ },
+
+ setFramerate: function (rate) {
+ this.opts.preferredFrameRate = rate;
+ this.frameTime = 1000 / rate;
+ this.papu.setSampleRate(this.opts.sampleRate, false);
+ },
+
+ toJSON: function () {
+ return {
+ // romData: this.romData,
+ cpu: this.cpu.toJSON(),
+ mmap: this.mmap.toJSON(),
+ ppu: this.ppu.toJSON(),
+ papu: this.papu.toJSON(),
+ };
+ },
+
+ fromJSON: function (s) {
+ this.reset();
+ // this.romData = s.romData;
+ this.cpu.fromJSON(s.cpu);
+ this.mmap.fromJSON(s.mmap);
+ this.ppu.fromJSON(s.ppu);
+ this.papu.fromJSON(s.papu);
+ },
+};
+
+module.exports = NES;
+
+},{"./controller":1,"./cpu":2,"./papu":6,"./ppu":7,"./rom":8}],6:[function(require,module,exports){
+var utils = require("./utils");
+
+var CPU_FREQ_NTSC = 1789772.5; //1789772.72727272d;
+// var CPU_FREQ_PAL = 1773447.4;
+
+var PAPU = function (nes) {
+ this.nes = nes;
+
+ this.square1 = new ChannelSquare(this, true);
+ this.square2 = new ChannelSquare(this, false);
+ this.triangle = new ChannelTriangle(this);
+ this.noise = new ChannelNoise(this);
+ this.dmc = new ChannelDM(this);
+
+ this.frameIrqCounter = null;
+ this.frameIrqCounterMax = 4;
+ this.initCounter = 2048;
+ this.channelEnableValue = null;
+
+ this.sampleRate = 44100;
+
+ this.lengthLookup = null;
+ this.dmcFreqLookup = null;
+ this.noiseWavelengthLookup = null;
+ this.square_table = null;
+ this.tnd_table = null;
+
+ this.frameIrqEnabled = false;
+ this.frameIrqActive = null;
+ this.frameClockNow = null;
+ this.startedPlaying = false;
+ this.recordOutput = false;
+ this.initingHardware = false;
+
+ this.masterFrameCounter = null;
+ this.derivedFrameCounter = null;
+ this.countSequence = null;
+ this.sampleTimer = null;
+ this.frameTime = null;
+ this.sampleTimerMax = null;
+ this.sampleCount = null;
+ this.triValue = 0;
+
+ this.smpSquare1 = null;
+ this.smpSquare2 = null;
+ this.smpTriangle = null;
+ this.smpDmc = null;
+ this.accCount = null;
+
+ // DC removal vars:
+ this.prevSampleL = 0;
+ this.prevSampleR = 0;
+ this.smpAccumL = 0;
+ this.smpAccumR = 0;
+
+ // DAC range:
+ this.dacRange = 0;
+ this.dcValue = 0;
+
+ // Master volume:
+ this.masterVolume = 256;
+
+ // Stereo positioning:
+ this.stereoPosLSquare1 = null;
+ this.stereoPosLSquare2 = null;
+ this.stereoPosLTriangle = null;
+ this.stereoPosLNoise = null;
+ this.stereoPosLDMC = null;
+ this.stereoPosRSquare1 = null;
+ this.stereoPosRSquare2 = null;
+ this.stereoPosRTriangle = null;
+ this.stereoPosRNoise = null;
+ this.stereoPosRDMC = null;
+
+ this.extraCycles = null;
+
+ this.maxSample = null;
+ this.minSample = null;
+
+ // Panning:
+ this.panning = [80, 170, 100, 150, 128];
+ this.setPanning(this.panning);
+
+ // Initialize lookup tables:
+ this.initLengthLookup();
+ this.initDmcFrequencyLookup();
+ this.initNoiseWavelengthLookup();
+ this.initDACtables();
+
+ // Init sound registers:
+ for (var i = 0; i < 0x14; i++) {
+ if (i === 0x10) {
+ this.writeReg(0x4010, 0x10);
+ } else {
+ this.writeReg(0x4000 + i, 0);
+ }
+ }
+
+ this.reset();
+};
+
+PAPU.prototype = {
+ reset: function () {
+ this.sampleRate = this.nes.opts.sampleRate;
+ this.sampleTimerMax = Math.floor(
+ (1024.0 * CPU_FREQ_NTSC * this.nes.opts.preferredFrameRate) /
+ (this.sampleRate * 60.0)
+ );
+
+ this.frameTime = Math.floor(
+ (14915.0 * this.nes.opts.preferredFrameRate) / 60.0
+ );
+
+ this.sampleTimer = 0;
+
+ this.updateChannelEnable(0);
+ this.masterFrameCounter = 0;
+ this.derivedFrameCounter = 0;
+ this.countSequence = 0;
+ this.sampleCount = 0;
+ this.initCounter = 2048;
+ this.frameIrqEnabled = false;
+ this.initingHardware = false;
+
+ this.resetCounter();
+
+ this.square1.reset();
+ this.square2.reset();
+ this.triangle.reset();
+ this.noise.reset();
+ this.dmc.reset();
+
+ this.accCount = 0;
+ this.smpSquare1 = 0;
+ this.smpSquare2 = 0;
+ this.smpTriangle = 0;
+ this.smpDmc = 0;
+
+ this.frameIrqEnabled = false;
+ this.frameIrqCounterMax = 4;
+
+ this.channelEnableValue = 0xff;
+ this.startedPlaying = false;
+ this.prevSampleL = 0;
+ this.prevSampleR = 0;
+ this.smpAccumL = 0;
+ this.smpAccumR = 0;
+
+ this.maxSample = -500000;
+ this.minSample = 500000;
+ },
+
+ // eslint-disable-next-line no-unused-vars
+ readReg: function (address) {
+ // Read 0x4015:
+ var tmp = 0;
+ tmp |= this.square1.getLengthStatus();
+ tmp |= this.square2.getLengthStatus() << 1;
+ tmp |= this.triangle.getLengthStatus() << 2;
+ tmp |= this.noise.getLengthStatus() << 3;
+ tmp |= this.dmc.getLengthStatus() << 4;
+ tmp |= (this.frameIrqActive && this.frameIrqEnabled ? 1 : 0) << 6;
+ tmp |= this.dmc.getIrqStatus() << 7;
+
+ this.frameIrqActive = false;
+ this.dmc.irqGenerated = false;
+
+ return tmp & 0xffff;
+ },
+
+ writeReg: function (address, value) {
+ if (address >= 0x4000 && address < 0x4004) {
+ // Square Wave 1 Control
+ this.square1.writeReg(address, value);
+ // console.log("Square Write");
+ } else if (address >= 0x4004 && address < 0x4008) {
+ // Square 2 Control
+ this.square2.writeReg(address, value);
+ } else if (address >= 0x4008 && address < 0x400c) {
+ // Triangle Control
+ this.triangle.writeReg(address, value);
+ } else if (address >= 0x400c && address <= 0x400f) {
+ // Noise Control
+ this.noise.writeReg(address, value);
+ } else if (address === 0x4010) {
+ // DMC Play mode & DMA frequency
+ this.dmc.writeReg(address, value);
+ } else if (address === 0x4011) {
+ // DMC Delta Counter
+ this.dmc.writeReg(address, value);
+ } else if (address === 0x4012) {
+ // DMC Play code starting address
+ this.dmc.writeReg(address, value);
+ } else if (address === 0x4013) {
+ // DMC Play code length
+ this.dmc.writeReg(address, value);
+ } else if (address === 0x4015) {
+ // Channel enable
+ this.updateChannelEnable(value);
+
+ if (value !== 0 && this.initCounter > 0) {
+ // Start hardware initialization
+ this.initingHardware = true;
+ }
+
+ // DMC/IRQ Status
+ this.dmc.writeReg(address, value);
+ } else if (address === 0x4017) {
+ // Frame counter control
+ this.countSequence = (value >> 7) & 1;
+ this.masterFrameCounter = 0;
+ this.frameIrqActive = false;
+
+ if (((value >> 6) & 0x1) === 0) {
+ this.frameIrqEnabled = true;
+ } else {
+ this.frameIrqEnabled = false;
+ }
+
+ if (this.countSequence === 0) {
+ // NTSC:
+ this.frameIrqCounterMax = 4;
+ this.derivedFrameCounter = 4;
+ } else {
+ // PAL:
+ this.frameIrqCounterMax = 5;
+ this.derivedFrameCounter = 0;
+ this.frameCounterTick();
+ }
+ }
+ },
+
+ resetCounter: function () {
+ if (this.countSequence === 0) {
+ this.derivedFrameCounter = 4;
+ } else {
+ this.derivedFrameCounter = 0;
+ }
+ },
+
+ // Updates channel enable status.
+ // This is done on writes to the
+ // channel enable register (0x4015),
+ // and when the user enables/disables channels
+ // in the GUI.
+ updateChannelEnable: function (value) {
+ this.channelEnableValue = value & 0xffff;
+ this.square1.setEnabled((value & 1) !== 0);
+ this.square2.setEnabled((value & 2) !== 0);
+ this.triangle.setEnabled((value & 4) !== 0);
+ this.noise.setEnabled((value & 8) !== 0);
+ this.dmc.setEnabled((value & 16) !== 0);
+ },
+
+ // Clocks the frame counter. It should be clocked at
+ // twice the cpu speed, so the cycles will be
+ // divided by 2 for those counters that are
+ // clocked at cpu speed.
+ clockFrameCounter: function (nCycles) {
+ if (this.initCounter > 0) {
+ if (this.initingHardware) {
+ this.initCounter -= nCycles;
+ if (this.initCounter <= 0) {
+ this.initingHardware = false;
+ }
+ return;
+ }
+ }
+
+ // Don't process ticks beyond next sampling:
+ nCycles += this.extraCycles;
+ var maxCycles = this.sampleTimerMax - this.sampleTimer;
+ if (nCycles << 10 > maxCycles) {
+ this.extraCycles = ((nCycles << 10) - maxCycles) >> 10;
+ nCycles -= this.extraCycles;
+ } else {
+ this.extraCycles = 0;
+ }
+
+ var dmc = this.dmc;
+ var triangle = this.triangle;
+ var square1 = this.square1;
+ var square2 = this.square2;
+ var noise = this.noise;
+
+ // Clock DMC:
+ if (dmc.isEnabled) {
+ dmc.shiftCounter -= nCycles << 3;
+ while (dmc.shiftCounter <= 0 && dmc.dmaFrequency > 0) {
+ dmc.shiftCounter += dmc.dmaFrequency;
+ dmc.clockDmc();
+ }
+ }
+
+ // Clock Triangle channel Prog timer:
+ if (triangle.progTimerMax > 0) {
+ triangle.progTimerCount -= nCycles;
+ while (triangle.progTimerCount <= 0) {
+ triangle.progTimerCount += triangle.progTimerMax + 1;
+ if (triangle.linearCounter > 0 && triangle.lengthCounter > 0) {
+ triangle.triangleCounter++;
+ triangle.triangleCounter &= 0x1f;
+
+ if (triangle.isEnabled) {
+ if (triangle.triangleCounter >= 0x10) {
+ // Normal value.
+ triangle.sampleValue = triangle.triangleCounter & 0xf;
+ } else {
+ // Inverted value.
+ triangle.sampleValue = 0xf - (triangle.triangleCounter & 0xf);
+ }
+ triangle.sampleValue <<= 4;
+ }
+ }
+ }
+ }
+
+ // Clock Square channel 1 Prog timer:
+ square1.progTimerCount -= nCycles;
+ if (square1.progTimerCount <= 0) {
+ square1.progTimerCount += (square1.progTimerMax + 1) << 1;
+
+ square1.squareCounter++;
+ square1.squareCounter &= 0x7;
+ square1.updateSampleValue();
+ }
+
+ // Clock Square channel 2 Prog timer:
+ square2.progTimerCount -= nCycles;
+ if (square2.progTimerCount <= 0) {
+ square2.progTimerCount += (square2.progTimerMax + 1) << 1;
+
+ square2.squareCounter++;
+ square2.squareCounter &= 0x7;
+ square2.updateSampleValue();
+ }
+
+ // Clock noise channel Prog timer:
+ var acc_c = nCycles;
+ if (noise.progTimerCount - acc_c > 0) {
+ // Do all cycles at once:
+ noise.progTimerCount -= acc_c;
+ noise.accCount += acc_c;
+ noise.accValue += acc_c * noise.sampleValue;
+ } else {
+ // Slow-step:
+ while (acc_c-- > 0) {
+ if (--noise.progTimerCount <= 0 && noise.progTimerMax > 0) {
+ // Update noise shift register:
+ noise.shiftReg <<= 1;
+ noise.tmp =
+ ((noise.shiftReg << (noise.randomMode === 0 ? 1 : 6)) ^
+ noise.shiftReg) &
+ 0x8000;
+ if (noise.tmp !== 0) {
+ // Sample value must be 0.
+ noise.shiftReg |= 0x01;
+ noise.randomBit = 0;
+ noise.sampleValue = 0;
+ } else {
+ // Find sample value:
+ noise.randomBit = 1;
+ if (noise.isEnabled && noise.lengthCounter > 0) {
+ noise.sampleValue = noise.masterVolume;
+ } else {
+ noise.sampleValue = 0;
+ }
+ }
+
+ noise.progTimerCount += noise.progTimerMax;
+ }
+
+ noise.accValue += noise.sampleValue;
+ noise.accCount++;
+ }
+ }
+
+ // Frame IRQ handling:
+ if (this.frameIrqEnabled && this.frameIrqActive) {
+ this.nes.cpu.requestIrq(this.nes.cpu.IRQ_NORMAL);
+ }
+
+ // Clock frame counter at double CPU speed:
+ this.masterFrameCounter += nCycles << 1;
+ if (this.masterFrameCounter >= this.frameTime) {
+ // 240Hz tick:
+ this.masterFrameCounter -= this.frameTime;
+ this.frameCounterTick();
+ }
+
+ // Accumulate sample value:
+ this.accSample(nCycles);
+
+ // Clock sample timer:
+ this.sampleTimer += nCycles << 10;
+ if (this.sampleTimer >= this.sampleTimerMax) {
+ // Sample channels:
+ this.sample();
+ this.sampleTimer -= this.sampleTimerMax;
+ }
+ },
+
+ accSample: function (cycles) {
+ // Special treatment for triangle channel - need to interpolate.
+ if (this.triangle.sampleCondition) {
+ this.triValue = Math.floor(
+ (this.triangle.progTimerCount << 4) / (this.triangle.progTimerMax + 1)
+ );
+ if (this.triValue > 16) {
+ this.triValue = 16;
+ }
+ if (this.triangle.triangleCounter >= 16) {
+ this.triValue = 16 - this.triValue;
+ }
+
+ // Add non-interpolated sample value:
+ this.triValue += this.triangle.sampleValue;
+ }
+
+ // Now sample normally:
+ if (cycles === 2) {
+ this.smpTriangle += this.triValue << 1;
+ this.smpDmc += this.dmc.sample << 1;
+ this.smpSquare1 += this.square1.sampleValue << 1;
+ this.smpSquare2 += this.square2.sampleValue << 1;
+ this.accCount += 2;
+ } else if (cycles === 4) {
+ this.smpTriangle += this.triValue << 2;
+ this.smpDmc += this.dmc.sample << 2;
+ this.smpSquare1 += this.square1.sampleValue << 2;
+ this.smpSquare2 += this.square2.sampleValue << 2;
+ this.accCount += 4;
+ } else {
+ this.smpTriangle += cycles * this.triValue;
+ this.smpDmc += cycles * this.dmc.sample;
+ this.smpSquare1 += cycles * this.square1.sampleValue;
+ this.smpSquare2 += cycles * this.square2.sampleValue;
+ this.accCount += cycles;
+ }
+ },
+
+ frameCounterTick: function () {
+ this.derivedFrameCounter++;
+ if (this.derivedFrameCounter >= this.frameIrqCounterMax) {
+ this.derivedFrameCounter = 0;
+ }
+
+ if (this.derivedFrameCounter === 1 || this.derivedFrameCounter === 3) {
+ // Clock length & sweep:
+ this.triangle.clockLengthCounter();
+ this.square1.clockLengthCounter();
+ this.square2.clockLengthCounter();
+ this.noise.clockLengthCounter();
+ this.square1.clockSweep();
+ this.square2.clockSweep();
+ }
+
+ if (this.derivedFrameCounter >= 0 && this.derivedFrameCounter < 4) {
+ // Clock linear & decay:
+ this.square1.clockEnvDecay();
+ this.square2.clockEnvDecay();
+ this.noise.clockEnvDecay();
+ this.triangle.clockLinearCounter();
+ }
+
+ if (this.derivedFrameCounter === 3 && this.countSequence === 0) {
+ // Enable IRQ:
+ this.frameIrqActive = true;
+ }
+
+ // End of 240Hz tick
+ },
+
+ // Samples the channels, mixes the output together, then writes to buffer.
+ sample: function () {
+ var sq_index, tnd_index;
+
+ if (this.accCount > 0) {
+ this.smpSquare1 <<= 4;
+ this.smpSquare1 = Math.floor(this.smpSquare1 / this.accCount);
+
+ this.smpSquare2 <<= 4;
+ this.smpSquare2 = Math.floor(this.smpSquare2 / this.accCount);
+
+ this.smpTriangle = Math.floor(this.smpTriangle / this.accCount);
+
+ this.smpDmc <<= 4;
+ this.smpDmc = Math.floor(this.smpDmc / this.accCount);
+
+ this.accCount = 0;
+ } else {
+ this.smpSquare1 = this.square1.sampleValue << 4;
+ this.smpSquare2 = this.square2.sampleValue << 4;
+ this.smpTriangle = this.triangle.sampleValue;
+ this.smpDmc = this.dmc.sample << 4;
+ }
+
+ var smpNoise = Math.floor((this.noise.accValue << 4) / this.noise.accCount);
+ this.noise.accValue = smpNoise >> 4;
+ this.noise.accCount = 1;
+
+ // Stereo sound.
+
+ // Left channel:
+ sq_index =
+ (this.smpSquare1 * this.stereoPosLSquare1 +
+ this.smpSquare2 * this.stereoPosLSquare2) >>
+ 8;
+ tnd_index =
+ (3 * this.smpTriangle * this.stereoPosLTriangle +
+ (smpNoise << 1) * this.stereoPosLNoise +
+ this.smpDmc * this.stereoPosLDMC) >>
+ 8;
+ if (sq_index >= this.square_table.length) {
+ sq_index = this.square_table.length - 1;
+ }
+ if (tnd_index >= this.tnd_table.length) {
+ tnd_index = this.tnd_table.length - 1;
+ }
+ var sampleValueL =
+ this.square_table[sq_index] + this.tnd_table[tnd_index] - this.dcValue;
+
+ // Right channel:
+ sq_index =
+ (this.smpSquare1 * this.stereoPosRSquare1 +
+ this.smpSquare2 * this.stereoPosRSquare2) >>
+ 8;
+ tnd_index =
+ (3 * this.smpTriangle * this.stereoPosRTriangle +
+ (smpNoise << 1) * this.stereoPosRNoise +
+ this.smpDmc * this.stereoPosRDMC) >>
+ 8;
+ if (sq_index >= this.square_table.length) {
+ sq_index = this.square_table.length - 1;
+ }
+ if (tnd_index >= this.tnd_table.length) {
+ tnd_index = this.tnd_table.length - 1;
+ }
+ var sampleValueR =
+ this.square_table[sq_index] + this.tnd_table[tnd_index] - this.dcValue;
+
+ // Remove DC from left channel:
+ var smpDiffL = sampleValueL - this.prevSampleL;
+ this.prevSampleL += smpDiffL;
+ this.smpAccumL += smpDiffL - (this.smpAccumL >> 10);
+ sampleValueL = this.smpAccumL;
+
+ // Remove DC from right channel:
+ var smpDiffR = sampleValueR - this.prevSampleR;
+ this.prevSampleR += smpDiffR;
+ this.smpAccumR += smpDiffR - (this.smpAccumR >> 10);
+ sampleValueR = this.smpAccumR;
+
+ // Write:
+ if (sampleValueL > this.maxSample) {
+ this.maxSample = sampleValueL;
+ }
+ if (sampleValueL < this.minSample) {
+ this.minSample = sampleValueL;
+ }
+
+ if (this.nes.opts.onAudioSample) {
+ this.nes.opts.onAudioSample(sampleValueL / 32768, sampleValueR / 32768);
+ }
+
+ // Reset sampled values:
+ this.smpSquare1 = 0;
+ this.smpSquare2 = 0;
+ this.smpTriangle = 0;
+ this.smpDmc = 0;
+ },
+
+ getLengthMax: function (value) {
+ return this.lengthLookup[value >> 3];
+ },
+
+ getDmcFrequency: function (value) {
+ if (value >= 0 && value < 0x10) {
+ return this.dmcFreqLookup[value];
+ }
+ return 0;
+ },
+
+ getNoiseWaveLength: function (value) {
+ if (value >= 0 && value < 0x10) {
+ return this.noiseWavelengthLookup[value];
+ }
+ return 0;
+ },
+
+ setPanning: function (pos) {
+ for (var i = 0; i < 5; i++) {
+ this.panning[i] = pos[i];
+ }
+ this.updateStereoPos();
+ },
+
+ setMasterVolume: function (value) {
+ if (value < 0) {
+ value = 0;
+ }
+ if (value > 256) {
+ value = 256;
+ }
+ this.masterVolume = value;
+ this.updateStereoPos();
+ },
+
+ updateStereoPos: function () {
+ this.stereoPosLSquare1 = (this.panning[0] * this.masterVolume) >> 8;
+ this.stereoPosLSquare2 = (this.panning[1] * this.masterVolume) >> 8;
+ this.stereoPosLTriangle = (this.panning[2] * this.masterVolume) >> 8;
+ this.stereoPosLNoise = (this.panning[3] * this.masterVolume) >> 8;
+ this.stereoPosLDMC = (this.panning[4] * this.masterVolume) >> 8;
+
+ this.stereoPosRSquare1 = this.masterVolume - this.stereoPosLSquare1;
+ this.stereoPosRSquare2 = this.masterVolume - this.stereoPosLSquare2;
+ this.stereoPosRTriangle = this.masterVolume - this.stereoPosLTriangle;
+ this.stereoPosRNoise = this.masterVolume - this.stereoPosLNoise;
+ this.stereoPosRDMC = this.masterVolume - this.stereoPosLDMC;
+ },
+
+ initLengthLookup: function () {
+ // prettier-ignore
+ this.lengthLookup = [
+ 0x0A, 0xFE,
+ 0x14, 0x02,
+ 0x28, 0x04,
+ 0x50, 0x06,
+ 0xA0, 0x08,
+ 0x3C, 0x0A,
+ 0x0E, 0x0C,
+ 0x1A, 0x0E,
+ 0x0C, 0x10,
+ 0x18, 0x12,
+ 0x30, 0x14,
+ 0x60, 0x16,
+ 0xC0, 0x18,
+ 0x48, 0x1A,
+ 0x10, 0x1C,
+ 0x20, 0x1E
+ ];
+ },
+
+ initDmcFrequencyLookup: function () {
+ this.dmcFreqLookup = new Array(16);
+
+ this.dmcFreqLookup[0x0] = 0xd60;
+ this.dmcFreqLookup[0x1] = 0xbe0;
+ this.dmcFreqLookup[0x2] = 0xaa0;
+ this.dmcFreqLookup[0x3] = 0xa00;
+ this.dmcFreqLookup[0x4] = 0x8f0;
+ this.dmcFreqLookup[0x5] = 0x7f0;
+ this.dmcFreqLookup[0x6] = 0x710;
+ this.dmcFreqLookup[0x7] = 0x6b0;
+ this.dmcFreqLookup[0x8] = 0x5f0;
+ this.dmcFreqLookup[0x9] = 0x500;
+ this.dmcFreqLookup[0xa] = 0x470;
+ this.dmcFreqLookup[0xb] = 0x400;
+ this.dmcFreqLookup[0xc] = 0x350;
+ this.dmcFreqLookup[0xd] = 0x2a0;
+ this.dmcFreqLookup[0xe] = 0x240;
+ this.dmcFreqLookup[0xf] = 0x1b0;
+ //for(int i=0;i<16;i++)dmcFreqLookup[i]/=8;
+ },
+
+ initNoiseWavelengthLookup: function () {
+ this.noiseWavelengthLookup = new Array(16);
+
+ this.noiseWavelengthLookup[0x0] = 0x004;
+ this.noiseWavelengthLookup[0x1] = 0x008;
+ this.noiseWavelengthLookup[0x2] = 0x010;
+ this.noiseWavelengthLookup[0x3] = 0x020;
+ this.noiseWavelengthLookup[0x4] = 0x040;
+ this.noiseWavelengthLookup[0x5] = 0x060;
+ this.noiseWavelengthLookup[0x6] = 0x080;
+ this.noiseWavelengthLookup[0x7] = 0x0a0;
+ this.noiseWavelengthLookup[0x8] = 0x0ca;
+ this.noiseWavelengthLookup[0x9] = 0x0fe;
+ this.noiseWavelengthLookup[0xa] = 0x17c;
+ this.noiseWavelengthLookup[0xb] = 0x1fc;
+ this.noiseWavelengthLookup[0xc] = 0x2fa;
+ this.noiseWavelengthLookup[0xd] = 0x3f8;
+ this.noiseWavelengthLookup[0xe] = 0x7f2;
+ this.noiseWavelengthLookup[0xf] = 0xfe4;
+ },
+
+ initDACtables: function () {
+ var value, ival, i;
+ var max_sqr = 0;
+ var max_tnd = 0;
+
+ this.square_table = new Array(32 * 16);
+ this.tnd_table = new Array(204 * 16);
+
+ for (i = 0; i < 32 * 16; i++) {
+ value = 95.52 / (8128.0 / (i / 16.0) + 100.0);
+ value *= 0.98411;
+ value *= 50000.0;
+ ival = Math.floor(value);
+
+ this.square_table[i] = ival;
+ if (ival > max_sqr) {
+ max_sqr = ival;
+ }
+ }
+
+ for (i = 0; i < 204 * 16; i++) {
+ value = 163.67 / (24329.0 / (i / 16.0) + 100.0);
+ value *= 0.98411;
+ value *= 50000.0;
+ ival = Math.floor(value);
+
+ this.tnd_table[i] = ival;
+ if (ival > max_tnd) {
+ max_tnd = ival;
+ }
+ }
+
+ this.dacRange = max_sqr + max_tnd;
+ this.dcValue = this.dacRange / 2;
+ },
+
+ JSON_PROPERTIES: [
+ "frameIrqCounter",
+ "frameIrqCounterMax",
+ "initCounter",
+ "channelEnableValue",
+ "sampleRate",
+ "frameIrqEnabled",
+ "frameIrqActive",
+ "frameClockNow",
+ "startedPlaying",
+ "recordOutput",
+ "initingHardware",
+ "masterFrameCounter",
+ "derivedFrameCounter",
+ "countSequence",
+ "sampleTimer",
+ "frameTime",
+ "sampleTimerMax",
+ "sampleCount",
+ "triValue",
+ "smpSquare1",
+ "smpSquare2",
+ "smpTriangle",
+ "smpDmc",
+ "accCount",
+ "prevSampleL",
+ "prevSampleR",
+ "smpAccumL",
+ "smpAccumR",
+ "masterVolume",
+ "stereoPosLSquare1",
+ "stereoPosLSquare2",
+ "stereoPosLTriangle",
+ "stereoPosLNoise",
+ "stereoPosLDMC",
+ "stereoPosRSquare1",
+ "stereoPosRSquare2",
+ "stereoPosRTriangle",
+ "stereoPosRNoise",
+ "stereoPosRDMC",
+ "extraCycles",
+ "maxSample",
+ "minSample",
+ "panning",
+ ],
+
+ toJSON: function () {
+ let obj = utils.toJSON(this);
+ obj.dmc = this.dmc.toJSON();
+ obj.noise = this.noise.toJSON();
+ obj.square1 = this.square1.toJSON();
+ obj.square2 = this.square2.toJSON();
+ obj.triangle = this.triangle.toJSON();
+ return obj;
+ },
+
+ fromJSON: function (s) {
+ utils.fromJSON(this, s);
+ this.dmc.fromJSON(s.dmc);
+ this.noise.fromJSON(s.noise);
+ this.square1.fromJSON(s.square1);
+ this.square2.fromJSON(s.square2);
+ this.triangle.fromJSON(s.triangle);
+ },
+};
+
+var ChannelDM = function (papu) {
+ this.papu = papu;
+
+ this.MODE_NORMAL = 0;
+ this.MODE_LOOP = 1;
+ this.MODE_IRQ = 2;
+
+ this.isEnabled = null;
+ this.hasSample = null;
+ this.irqGenerated = false;
+
+ this.playMode = null;
+ this.dmaFrequency = null;
+ this.dmaCounter = null;
+ this.deltaCounter = null;
+ this.playStartAddress = null;
+ this.playAddress = null;
+ this.playLength = null;
+ this.playLengthCounter = null;
+ this.shiftCounter = null;
+ this.reg4012 = null;
+ this.reg4013 = null;
+ this.sample = null;
+ this.dacLsb = null;
+ this.data = null;
+
+ this.reset();
+};
+
+ChannelDM.prototype = {
+ clockDmc: function () {
+ // Only alter DAC value if the sample buffer has data:
+ if (this.hasSample) {
+ if ((this.data & 1) === 0) {
+ // Decrement delta:
+ if (this.deltaCounter > 0) {
+ this.deltaCounter--;
+ }
+ } else {
+ // Increment delta:
+ if (this.deltaCounter < 63) {
+ this.deltaCounter++;
+ }
+ }
+
+ // Update sample value:
+ this.sample = this.isEnabled ? (this.deltaCounter << 1) + this.dacLsb : 0;
+
+ // Update shift register:
+ this.data >>= 1;
+ }
+
+ this.dmaCounter--;
+ if (this.dmaCounter <= 0) {
+ // No more sample bits.
+ this.hasSample = false;
+ this.endOfSample();
+ this.dmaCounter = 8;
+ }
+
+ if (this.irqGenerated) {
+ this.papu.nes.cpu.requestIrq(this.papu.nes.cpu.IRQ_NORMAL);
+ }
+ },
+
+ endOfSample: function () {
+ if (this.playLengthCounter === 0 && this.playMode === this.MODE_LOOP) {
+ // Start from beginning of sample:
+ this.playAddress = this.playStartAddress;
+ this.playLengthCounter = this.playLength;
+ }
+
+ if (this.playLengthCounter > 0) {
+ // Fetch next sample:
+ this.nextSample();
+
+ if (this.playLengthCounter === 0) {
+ // Last byte of sample fetched, generate IRQ:
+ if (this.playMode === this.MODE_IRQ) {
+ // Generate IRQ:
+ this.irqGenerated = true;
+ }
+ }
+ }
+ },
+
+ nextSample: function () {
+ // Fetch byte:
+ this.data = this.papu.nes.mmap.load(this.playAddress);
+ this.papu.nes.cpu.haltCycles(4);
+
+ this.playLengthCounter--;
+ this.playAddress++;
+ if (this.playAddress > 0xffff) {
+ this.playAddress = 0x8000;
+ }
+
+ this.hasSample = true;
+ },
+
+ writeReg: function (address, value) {
+ if (address === 0x4010) {
+ // Play mode, DMA Frequency
+ if (value >> 6 === 0) {
+ this.playMode = this.MODE_NORMAL;
+ } else if (((value >> 6) & 1) === 1) {
+ this.playMode = this.MODE_LOOP;
+ } else if (value >> 6 === 2) {
+ this.playMode = this.MODE_IRQ;
+ }
+
+ if ((value & 0x80) === 0) {
+ this.irqGenerated = false;
+ }
+
+ this.dmaFrequency = this.papu.getDmcFrequency(value & 0xf);
+ } else if (address === 0x4011) {
+ // Delta counter load register:
+ this.deltaCounter = (value >> 1) & 63;
+ this.dacLsb = value & 1;
+ this.sample = (this.deltaCounter << 1) + this.dacLsb; // update sample value
+ } else if (address === 0x4012) {
+ // DMA address load register
+ this.playStartAddress = (value << 6) | 0x0c000;
+ this.playAddress = this.playStartAddress;
+ this.reg4012 = value;
+ } else if (address === 0x4013) {
+ // Length of play code
+ this.playLength = (value << 4) + 1;
+ this.playLengthCounter = this.playLength;
+ this.reg4013 = value;
+ } else if (address === 0x4015) {
+ // DMC/IRQ Status
+ if (((value >> 4) & 1) === 0) {
+ // Disable:
+ this.playLengthCounter = 0;
+ } else {
+ // Restart:
+ this.playAddress = this.playStartAddress;
+ this.playLengthCounter = this.playLength;
+ }
+ this.irqGenerated = false;
+ }
+ },
+
+ setEnabled: function (value) {
+ if (!this.isEnabled && value) {
+ this.playLengthCounter = this.playLength;
+ }
+ this.isEnabled = value;
+ },
+
+ getLengthStatus: function () {
+ return this.playLengthCounter === 0 || !this.isEnabled ? 0 : 1;
+ },
+
+ getIrqStatus: function () {
+ return this.irqGenerated ? 1 : 0;
+ },
+
+ reset: function () {
+ this.isEnabled = false;
+ this.irqGenerated = false;
+ this.playMode = this.MODE_NORMAL;
+ this.dmaFrequency = 0;
+ this.dmaCounter = 0;
+ this.deltaCounter = 0;
+ this.playStartAddress = 0;
+ this.playAddress = 0;
+ this.playLength = 0;
+ this.playLengthCounter = 0;
+ this.sample = 0;
+ this.dacLsb = 0;
+ this.shiftCounter = 0;
+ this.reg4012 = 0;
+ this.reg4013 = 0;
+ this.data = 0;
+ },
+
+ JSON_PROPERTIES: [
+ "MODE_NORMAL",
+ "MODE_LOOP",
+ "MODE_IRQ",
+ "isEnabled",
+ "hasSample",
+ "irqGenerated",
+ "playMode",
+ "dmaFrequency",
+ "dmaCounter",
+ "deltaCounter",
+ "playStartAddress",
+ "playAddress",
+ "playLength",
+ "playLengthCounter",
+ "shiftCounter",
+ "reg4012",
+ "reg4013",
+ "sample",
+ "dacLsb",
+ "data",
+ ],
+
+ toJSON: function () {
+ return utils.toJSON(this);
+ },
+
+ fromJSON: function (s) {
+ utils.fromJSON(this, s);
+ },
+};
+
+var ChannelNoise = function (papu) {
+ this.papu = papu;
+
+ this.isEnabled = null;
+ this.envDecayDisable = null;
+ this.envDecayLoopEnable = null;
+ this.lengthCounterEnable = null;
+ this.envReset = null;
+ this.shiftNow = null;
+
+ this.lengthCounter = null;
+ this.progTimerCount = null;
+ this.progTimerMax = null;
+ this.envDecayRate = null;
+ this.envDecayCounter = null;
+ this.envVolume = null;
+ this.masterVolume = null;
+ this.shiftReg = 1 << 14;
+ this.randomBit = null;
+ this.randomMode = null;
+ this.sampleValue = null;
+ this.accValue = 0;
+ this.accCount = 1;
+ this.tmp = null;
+
+ this.reset();
+};
+
+ChannelNoise.prototype = {
+ reset: function () {
+ this.progTimerCount = 0;
+ this.progTimerMax = 0;
+ this.isEnabled = false;
+ this.lengthCounter = 0;
+ this.lengthCounterEnable = false;
+ this.envDecayDisable = false;
+ this.envDecayLoopEnable = false;
+ this.shiftNow = false;
+ this.envDecayRate = 0;
+ this.envDecayCounter = 0;
+ this.envVolume = 0;
+ this.masterVolume = 0;
+ this.shiftReg = 1;
+ this.randomBit = 0;
+ this.randomMode = 0;
+ this.sampleValue = 0;
+ this.tmp = 0;
+ },
+
+ clockLengthCounter: function () {
+ if (this.lengthCounterEnable && this.lengthCounter > 0) {
+ this.lengthCounter--;
+ if (this.lengthCounter === 0) {
+ this.updateSampleValue();
+ }
+ }
+ },
+
+ clockEnvDecay: function () {
+ if (this.envReset) {
+ // Reset envelope:
+ this.envReset = false;
+ this.envDecayCounter = this.envDecayRate + 1;
+ this.envVolume = 0xf;
+ } else if (--this.envDecayCounter <= 0) {
+ // Normal handling:
+ this.envDecayCounter = this.envDecayRate + 1;
+ if (this.envVolume > 0) {
+ this.envVolume--;
+ } else {
+ this.envVolume = this.envDecayLoopEnable ? 0xf : 0;
+ }
+ }
+ if (this.envDecayDisable) {
+ this.masterVolume = this.envDecayRate;
+ } else {
+ this.masterVolume = this.envVolume;
+ }
+ this.updateSampleValue();
+ },
+
+ updateSampleValue: function () {
+ if (this.isEnabled && this.lengthCounter > 0) {
+ this.sampleValue = this.randomBit * this.masterVolume;
+ }
+ },
+
+ writeReg: function (address, value) {
+ if (address === 0x400c) {
+ // Volume/Envelope decay:
+ this.envDecayDisable = (value & 0x10) !== 0;
+ this.envDecayRate = value & 0xf;
+ this.envDecayLoopEnable = (value & 0x20) !== 0;
+ this.lengthCounterEnable = (value & 0x20) === 0;
+ if (this.envDecayDisable) {
+ this.masterVolume = this.envDecayRate;
+ } else {
+ this.masterVolume = this.envVolume;
+ }
+ } else if (address === 0x400e) {
+ // Programmable timer:
+ this.progTimerMax = this.papu.getNoiseWaveLength(value & 0xf);
+ this.randomMode = value >> 7;
+ } else if (address === 0x400f) {
+ // Length counter
+ this.lengthCounter = this.papu.getLengthMax(value & 248);
+ this.envReset = true;
+ }
+ // Update:
+ //updateSampleValue();
+ },
+
+ setEnabled: function (value) {
+ this.isEnabled = value;
+ if (!value) {
+ this.lengthCounter = 0;
+ }
+ this.updateSampleValue();
+ },
+
+ getLengthStatus: function () {
+ return this.lengthCounter === 0 || !this.isEnabled ? 0 : 1;
+ },
+
+ JSON_PROPERTIES: [
+ "isEnabled",
+ "envDecayDisable",
+ "envDecayLoopEnable",
+ "lengthCounterEnable",
+ "envReset",
+ "shiftNow",
+ "lengthCounter",
+ "progTimerCount",
+ "progTimerMax",
+ "envDecayRate",
+ "envDecayCounter",
+ "envVolume",
+ "masterVolume",
+ "shiftReg",
+ "randomBit",
+ "randomMode",
+ "sampleValue",
+ "accValue",
+ "accCount",
+ "tmp",
+ ],
+
+ toJSON: function () {
+ return utils.toJSON(this);
+ },
+
+ fromJSON: function (s) {
+ utils.fromJSON(this, s);
+ },
+};
+
+var ChannelSquare = function (papu, square1) {
+ this.papu = papu;
+
+ // prettier-ignore
+ this.dutyLookup = [
+ 0, 1, 0, 0, 0, 0, 0, 0,
+ 0, 1, 1, 0, 0, 0, 0, 0,
+ 0, 1, 1, 1, 1, 0, 0, 0,
+ 1, 0, 0, 1, 1, 1, 1, 1
+ ];
+ // prettier-ignore
+ this.impLookup = [
+ 1,-1, 0, 0, 0, 0, 0, 0,
+ 1, 0,-1, 0, 0, 0, 0, 0,
+ 1, 0, 0, 0,-1, 0, 0, 0,
+ -1, 0, 1, 0, 0, 0, 0, 0
+ ];
+
+ this.sqr1 = square1;
+ this.isEnabled = null;
+ this.lengthCounterEnable = null;
+ this.sweepActive = null;
+ this.envDecayDisable = null;
+ this.envDecayLoopEnable = null;
+ this.envReset = null;
+ this.sweepCarry = null;
+ this.updateSweepPeriod = null;
+
+ this.progTimerCount = null;
+ this.progTimerMax = null;
+ this.lengthCounter = null;
+ this.squareCounter = null;
+ this.sweepCounter = null;
+ this.sweepCounterMax = null;
+ this.sweepMode = null;
+ this.sweepShiftAmount = null;
+ this.envDecayRate = null;
+ this.envDecayCounter = null;
+ this.envVolume = null;
+ this.masterVolume = null;
+ this.dutyMode = null;
+ this.sweepResult = null;
+ this.sampleValue = null;
+ this.vol = null;
+
+ this.reset();
+};
+
+ChannelSquare.prototype = {
+ reset: function () {
+ this.progTimerCount = 0;
+ this.progTimerMax = 0;
+ this.lengthCounter = 0;
+ this.squareCounter = 0;
+ this.sweepCounter = 0;
+ this.sweepCounterMax = 0;
+ this.sweepMode = 0;
+ this.sweepShiftAmount = 0;
+ this.envDecayRate = 0;
+ this.envDecayCounter = 0;
+ this.envVolume = 0;
+ this.masterVolume = 0;
+ this.dutyMode = 0;
+ this.vol = 0;
+
+ this.isEnabled = false;
+ this.lengthCounterEnable = false;
+ this.sweepActive = false;
+ this.sweepCarry = false;
+ this.envDecayDisable = false;
+ this.envDecayLoopEnable = false;
+ },
+
+ clockLengthCounter: function () {
+ if (this.lengthCounterEnable && this.lengthCounter > 0) {
+ this.lengthCounter--;
+ if (this.lengthCounter === 0) {
+ this.updateSampleValue();
+ }
+ }
+ },
+
+ clockEnvDecay: function () {
+ if (this.envReset) {
+ // Reset envelope:
+ this.envReset = false;
+ this.envDecayCounter = this.envDecayRate + 1;
+ this.envVolume = 0xf;
+ } else if (--this.envDecayCounter <= 0) {
+ // Normal handling:
+ this.envDecayCounter = this.envDecayRate + 1;
+ if (this.envVolume > 0) {
+ this.envVolume--;
+ } else {
+ this.envVolume = this.envDecayLoopEnable ? 0xf : 0;
+ }
+ }
+
+ if (this.envDecayDisable) {
+ this.masterVolume = this.envDecayRate;
+ } else {
+ this.masterVolume = this.envVolume;
+ }
+ this.updateSampleValue();
+ },
+
+ clockSweep: function () {
+ if (--this.sweepCounter <= 0) {
+ this.sweepCounter = this.sweepCounterMax + 1;
+ if (
+ this.sweepActive &&
+ this.sweepShiftAmount > 0 &&
+ this.progTimerMax > 7
+ ) {
+ // Calculate result from shifter:
+ this.sweepCarry = false;
+ if (this.sweepMode === 0) {
+ this.progTimerMax += this.progTimerMax >> this.sweepShiftAmount;
+ if (this.progTimerMax > 4095) {
+ this.progTimerMax = 4095;
+ this.sweepCarry = true;
+ }
+ } else {
+ this.progTimerMax =
+ this.progTimerMax -
+ ((this.progTimerMax >> this.sweepShiftAmount) -
+ (this.sqr1 ? 1 : 0));
+ }
+ }
+ }
+
+ if (this.updateSweepPeriod) {
+ this.updateSweepPeriod = false;
+ this.sweepCounter = this.sweepCounterMax + 1;
+ }
+ },
+
+ updateSampleValue: function () {
+ if (this.isEnabled && this.lengthCounter > 0 && this.progTimerMax > 7) {
+ if (
+ this.sweepMode === 0 &&
+ this.progTimerMax + (this.progTimerMax >> this.sweepShiftAmount) > 4095
+ ) {
+ //if (this.sweepCarry) {
+ this.sampleValue = 0;
+ } else {
+ this.sampleValue =
+ this.masterVolume *
+ this.dutyLookup[(this.dutyMode << 3) + this.squareCounter];
+ }
+ } else {
+ this.sampleValue = 0;
+ }
+ },
+
+ writeReg: function (address, value) {
+ var addrAdd = this.sqr1 ? 0 : 4;
+ if (address === 0x4000 + addrAdd) {
+ // Volume/Envelope decay:
+ this.envDecayDisable = (value & 0x10) !== 0;
+ this.envDecayRate = value & 0xf;
+ this.envDecayLoopEnable = (value & 0x20) !== 0;
+ this.dutyMode = (value >> 6) & 0x3;
+ this.lengthCounterEnable = (value & 0x20) === 0;
+ if (this.envDecayDisable) {
+ this.masterVolume = this.envDecayRate;
+ } else {
+ this.masterVolume = this.envVolume;
+ }
+ this.updateSampleValue();
+ } else if (address === 0x4001 + addrAdd) {
+ // Sweep:
+ this.sweepActive = (value & 0x80) !== 0;
+ this.sweepCounterMax = (value >> 4) & 7;
+ this.sweepMode = (value >> 3) & 1;
+ this.sweepShiftAmount = value & 7;
+ this.updateSweepPeriod = true;
+ } else if (address === 0x4002 + addrAdd) {
+ // Programmable timer:
+ this.progTimerMax &= 0x700;
+ this.progTimerMax |= value;
+ } else if (address === 0x4003 + addrAdd) {
+ // Programmable timer, length counter
+ this.progTimerMax &= 0xff;
+ this.progTimerMax |= (value & 0x7) << 8;
+
+ if (this.isEnabled) {
+ this.lengthCounter = this.papu.getLengthMax(value & 0xf8);
+ }
+
+ this.envReset = true;
+ }
+ },
+
+ setEnabled: function (value) {
+ this.isEnabled = value;
+ if (!value) {
+ this.lengthCounter = 0;
+ }
+ this.updateSampleValue();
+ },
+
+ getLengthStatus: function () {
+ return this.lengthCounter === 0 || !this.isEnabled ? 0 : 1;
+ },
+
+ JSON_PROPERTIES: [
+ "isEnabled",
+ "lengthCounterEnable",
+ "sweepActive",
+ "envDecayDisable",
+ "envDecayLoopEnable",
+ "envReset",
+ "sweepCarry",
+ "updateSweepPeriod",
+ "progTimerCount",
+ "progTimerMax",
+ "lengthCounter",
+ "squareCounter",
+ "sweepCounter",
+ "sweepCounterMax",
+ "sweepMode",
+ "sweepShiftAmount",
+ "envDecayRate",
+ "envDecayCounter",
+ "envVolume",
+ "masterVolume",
+ "dutyMode",
+ "sweepResult",
+ "sampleValue",
+ "vol",
+ ],
+
+ toJSON: function () {
+ return utils.toJSON(this);
+ },
+
+ fromJSON: function (s) {
+ utils.fromJSON(this, s);
+ },
+};
+
+var ChannelTriangle = function (papu) {
+ this.papu = papu;
+
+ this.isEnabled = null;
+ this.sampleCondition = null;
+ this.lengthCounterEnable = null;
+ this.lcHalt = null;
+ this.lcControl = null;
+
+ this.progTimerCount = null;
+ this.progTimerMax = null;
+ this.triangleCounter = null;
+ this.lengthCounter = null;
+ this.linearCounter = null;
+ this.lcLoadValue = null;
+ this.sampleValue = null;
+ this.tmp = null;
+
+ this.reset();
+};
+
+ChannelTriangle.prototype = {
+ reset: function () {
+ this.progTimerCount = 0;
+ this.progTimerMax = 0;
+ this.triangleCounter = 0;
+ this.isEnabled = false;
+ this.sampleCondition = false;
+ this.lengthCounter = 0;
+ this.lengthCounterEnable = false;
+ this.linearCounter = 0;
+ this.lcLoadValue = 0;
+ this.lcHalt = true;
+ this.lcControl = false;
+ this.tmp = 0;
+ this.sampleValue = 0xf;
+ },
+
+ clockLengthCounter: function () {
+ if (this.lengthCounterEnable && this.lengthCounter > 0) {
+ this.lengthCounter--;
+ if (this.lengthCounter === 0) {
+ this.updateSampleCondition();
+ }
+ }
+ },
+
+ clockLinearCounter: function () {
+ if (this.lcHalt) {
+ // Load:
+ this.linearCounter = this.lcLoadValue;
+ this.updateSampleCondition();
+ } else if (this.linearCounter > 0) {
+ // Decrement:
+ this.linearCounter--;
+ this.updateSampleCondition();
+ }
+ if (!this.lcControl) {
+ // Clear halt flag:
+ this.lcHalt = false;
+ }
+ },
+
+ getLengthStatus: function () {
+ return this.lengthCounter === 0 || !this.isEnabled ? 0 : 1;
+ },
+
+ // eslint-disable-next-line no-unused-vars
+ readReg: function (address) {
+ return 0;
+ },
+
+ writeReg: function (address, value) {
+ if (address === 0x4008) {
+ // New values for linear counter:
+ this.lcControl = (value & 0x80) !== 0;
+ this.lcLoadValue = value & 0x7f;
+
+ // Length counter enable:
+ this.lengthCounterEnable = !this.lcControl;
+ } else if (address === 0x400a) {
+ // Programmable timer:
+ this.progTimerMax &= 0x700;
+ this.progTimerMax |= value;
+ } else if (address === 0x400b) {
+ // Programmable timer, length counter
+ this.progTimerMax &= 0xff;
+ this.progTimerMax |= (value & 0x07) << 8;
+ this.lengthCounter = this.papu.getLengthMax(value & 0xf8);
+ this.lcHalt = true;
+ }
+
+ this.updateSampleCondition();
+ },
+
+ clockProgrammableTimer: function (nCycles) {
+ if (this.progTimerMax > 0) {
+ this.progTimerCount += nCycles;
+ while (
+ this.progTimerMax > 0 &&
+ this.progTimerCount >= this.progTimerMax
+ ) {
+ this.progTimerCount -= this.progTimerMax;
+ if (
+ this.isEnabled &&
+ this.lengthCounter > 0 &&
+ this.linearCounter > 0
+ ) {
+ this.clockTriangleGenerator();
+ }
+ }
+ }
+ },
+
+ clockTriangleGenerator: function () {
+ this.triangleCounter++;
+ this.triangleCounter &= 0x1f;
+ },
+
+ setEnabled: function (value) {
+ this.isEnabled = value;
+ if (!value) {
+ this.lengthCounter = 0;
+ }
+ this.updateSampleCondition();
+ },
+
+ updateSampleCondition: function () {
+ this.sampleCondition =
+ this.isEnabled &&
+ this.progTimerMax > 7 &&
+ this.linearCounter > 0 &&
+ this.lengthCounter > 0;
+ },
+
+ JSON_PROPERTIES: [
+ "isEnabled",
+ "sampleCondition",
+ "lengthCounterEnable",
+ "lcHalt",
+ "lcControl",
+ "progTimerCount",
+ "progTimerMax",
+ "triangleCounter",
+ "lengthCounter",
+ "linearCounter",
+ "lcLoadValue",
+ "sampleValue",
+ "tmp",
+ ],
+
+ toJSON: function () {
+ return utils.toJSON(this);
+ },
+
+ fromJSON: function (s) {
+ utils.fromJSON(this, s);
+ },
+};
+
+module.exports = PAPU;
+
+},{"./utils":10}],7:[function(require,module,exports){
+var Tile = require("./tile");
+var utils = require("./utils");
+
+var PPU = function (nes) {
+ this.nes = nes;
+
+ // Keep Chrome happy
+ this.vramMem = null;
+ this.spriteMem = null;
+ this.vramAddress = null;
+ this.vramTmpAddress = null;
+ this.vramBufferedReadValue = null;
+ this.firstWrite = null;
+ this.sramAddress = null;
+ this.currentMirroring = null;
+ this.requestEndFrame = null;
+ this.nmiOk = null;
+ this.dummyCycleToggle = null;
+ this.validTileData = null;
+ this.nmiCounter = null;
+ this.scanlineAlreadyRendered = null;
+ this.f_nmiOnVblank = null;
+ this.f_spriteSize = null;
+ this.f_bgPatternTable = null;
+ this.f_spPatternTable = null;
+ this.f_addrInc = null;
+ this.f_nTblAddress = null;
+ this.f_color = null;
+ this.f_spVisibility = null;
+ this.f_bgVisibility = null;
+ this.f_spClipping = null;
+ this.f_bgClipping = null;
+ this.f_dispType = null;
+ this.cntFV = null;
+ this.cntV = null;
+ this.cntH = null;
+ this.cntVT = null;
+ this.cntHT = null;
+ this.regFV = null;
+ this.regV = null;
+ this.regH = null;
+ this.regVT = null;
+ this.regHT = null;
+ this.regFH = null;
+ this.regS = null;
+ this.curNt = null;
+ this.attrib = null;
+ this.buffer = null;
+ this.bgbuffer = null;
+ this.pixrendered = null;
+
+ this.validTileData = null;
+ this.scantile = null;
+ this.scanline = null;
+ this.lastRenderedScanline = null;
+ this.curX = null;
+ this.sprX = null;
+ this.sprY = null;
+ this.sprTile = null;
+ this.sprCol = null;
+ this.vertFlip = null;
+ this.horiFlip = null;
+ this.bgPriority = null;
+ this.spr0HitX = null;
+ this.spr0HitY = null;
+ this.hitSpr0 = null;
+ this.sprPalette = null;
+ this.imgPalette = null;
+ this.ptTile = null;
+ this.ntable1 = null;
+ this.currentMirroring = null;
+ this.nameTable = null;
+ this.vramMirrorTable = null;
+ this.palTable = null;
+
+ // Rendering Options:
+ this.showSpr0Hit = false;
+ this.clipToTvSize = true;
+
+ this.reset();
+};
+
+PPU.prototype = {
+ // Status flags:
+ STATUS_VRAMWRITE: 4,
+ STATUS_SLSPRITECOUNT: 5,
+ STATUS_SPRITE0HIT: 6,
+ STATUS_VBLANK: 7,
+
+ reset: function () {
+ var i;
+
+ // Memory
+ this.vramMem = new Array(0x8000);
+ this.spriteMem = new Array(0x100);
+ for (i = 0; i < this.vramMem.length; i++) {
+ this.vramMem[i] = 0;
+ }
+ for (i = 0; i < this.spriteMem.length; i++) {
+ this.spriteMem[i] = 0;
+ }
+
+ // VRAM I/O:
+ this.vramAddress = null;
+ this.vramTmpAddress = null;
+ this.vramBufferedReadValue = 0;
+ this.firstWrite = true; // VRAM/Scroll Hi/Lo latch
+
+ // SPR-RAM I/O:
+ this.sramAddress = 0; // 8-bit only.
+
+ this.currentMirroring = -1;
+ this.requestEndFrame = false;
+ this.nmiOk = false;
+ this.dummyCycleToggle = false;
+ this.validTileData = false;
+ this.nmiCounter = 0;
+ this.scanlineAlreadyRendered = null;
+
+ // Control Flags Register 1:
+ this.f_nmiOnVblank = 0; // NMI on VBlank. 0=disable, 1=enable
+ this.f_spriteSize = 0; // Sprite size. 0=8x8, 1=8x16
+ this.f_bgPatternTable = 0; // Background Pattern Table address. 0=0x0000,1=0x1000
+ this.f_spPatternTable = 0; // Sprite Pattern Table address. 0=0x0000,1=0x1000
+ this.f_addrInc = 0; // PPU Address Increment. 0=1,1=32
+ this.f_nTblAddress = 0; // Name Table Address. 0=0x2000,1=0x2400,2=0x2800,3=0x2C00
+
+ // Control Flags Register 2:
+ this.f_color = 0; // Background color. 0=black, 1=blue, 2=green, 4=red
+ this.f_spVisibility = 0; // Sprite visibility. 0=not displayed,1=displayed
+ this.f_bgVisibility = 0; // Background visibility. 0=Not Displayed,1=displayed
+ this.f_spClipping = 0; // Sprite clipping. 0=Sprites invisible in left 8-pixel column,1=No clipping
+ this.f_bgClipping = 0; // Background clipping. 0=BG invisible in left 8-pixel column, 1=No clipping
+ this.f_dispType = 0; // Display type. 0=color, 1=monochrome
+
+ // Counters:
+ this.cntFV = 0;
+ this.cntV = 0;
+ this.cntH = 0;
+ this.cntVT = 0;
+ this.cntHT = 0;
+
+ // Registers:
+ this.regFV = 0;
+ this.regV = 0;
+ this.regH = 0;
+ this.regVT = 0;
+ this.regHT = 0;
+ this.regFH = 0;
+ this.regS = 0;
+
+ // These are temporary variables used in rendering and sound procedures.
+ // Their states outside of those procedures can be ignored.
+ // TODO: the use of this is a bit weird, investigate
+ this.curNt = null;
+
+ // Variables used when rendering:
+ this.attrib = new Array(32);
+ this.buffer = new Array(256 * 240);
+ this.bgbuffer = new Array(256 * 240);
+ this.pixrendered = new Array(256 * 240);
+
+ this.validTileData = null;
+
+ this.scantile = new Array(32);
+
+ // Initialize misc vars:
+ this.scanline = 0;
+ this.lastRenderedScanline = -1;
+ this.curX = 0;
+
+ // Sprite data:
+ this.sprX = new Array(64); // X coordinate
+ this.sprY = new Array(64); // Y coordinate
+ this.sprTile = new Array(64); // Tile Index (into pattern table)
+ this.sprCol = new Array(64); // Upper two bits of color
+ this.vertFlip = new Array(64); // Vertical Flip
+ this.horiFlip = new Array(64); // Horizontal Flip
+ this.bgPriority = new Array(64); // Background priority
+ this.spr0HitX = 0; // Sprite #0 hit X coordinate
+ this.spr0HitY = 0; // Sprite #0 hit Y coordinate
+ this.hitSpr0 = false;
+
+ // Palette data:
+ this.sprPalette = new Array(16);
+ this.imgPalette = new Array(16);
+
+ // Create pattern table tile buffers:
+ this.ptTile = new Array(512);
+ for (i = 0; i < 512; i++) {
+ this.ptTile[i] = new Tile();
+ }
+
+ // Create nametable buffers:
+ // Name table data:
+ this.ntable1 = new Array(4);
+ this.currentMirroring = -1;
+ this.nameTable = new Array(4);
+ for (i = 0; i < 4; i++) {
+ this.nameTable[i] = new NameTable(32, 32, "Nt" + i);
+ }
+
+ // Initialize mirroring lookup table:
+ this.vramMirrorTable = new Array(0x8000);
+ for (i = 0; i < 0x8000; i++) {
+ this.vramMirrorTable[i] = i;
+ }
+
+ this.palTable = new PaletteTable();
+ this.palTable.loadNTSCPalette();
+ //this.palTable.loadDefaultPalette();
+
+ this.updateControlReg1(0);
+ this.updateControlReg2(0);
+ },
+
+ // Sets Nametable mirroring.
+ setMirroring: function (mirroring) {
+ if (mirroring === this.currentMirroring) {
+ return;
+ }
+
+ this.currentMirroring = mirroring;
+ this.triggerRendering();
+
+ // Remove mirroring:
+ if (this.vramMirrorTable === null) {
+ this.vramMirrorTable = new Array(0x8000);
+ }
+ for (var i = 0; i < 0x8000; i++) {
+ this.vramMirrorTable[i] = i;
+ }
+
+ // Palette mirroring:
+ this.defineMirrorRegion(0x3f20, 0x3f00, 0x20);
+ this.defineMirrorRegion(0x3f40, 0x3f00, 0x20);
+ this.defineMirrorRegion(0x3f80, 0x3f00, 0x20);
+ this.defineMirrorRegion(0x3fc0, 0x3f00, 0x20);
+
+ // Additional mirroring:
+ this.defineMirrorRegion(0x3000, 0x2000, 0xf00);
+ this.defineMirrorRegion(0x4000, 0x0000, 0x4000);
+
+ if (mirroring === this.nes.rom.HORIZONTAL_MIRRORING) {
+ // Horizontal mirroring.
+
+ this.ntable1[0] = 0;
+ this.ntable1[1] = 0;
+ this.ntable1[2] = 1;
+ this.ntable1[3] = 1;
+
+ this.defineMirrorRegion(0x2400, 0x2000, 0x400);
+ this.defineMirrorRegion(0x2c00, 0x2800, 0x400);
+ } else if (mirroring === this.nes.rom.VERTICAL_MIRRORING) {
+ // Vertical mirroring.
+
+ this.ntable1[0] = 0;
+ this.ntable1[1] = 1;
+ this.ntable1[2] = 0;
+ this.ntable1[3] = 1;
+
+ this.defineMirrorRegion(0x2800, 0x2000, 0x400);
+ this.defineMirrorRegion(0x2c00, 0x2400, 0x400);
+ } else if (mirroring === this.nes.rom.SINGLESCREEN_MIRRORING) {
+ // Single Screen mirroring
+
+ this.ntable1[0] = 0;
+ this.ntable1[1] = 0;
+ this.ntable1[2] = 0;
+ this.ntable1[3] = 0;
+
+ this.defineMirrorRegion(0x2400, 0x2000, 0x400);
+ this.defineMirrorRegion(0x2800, 0x2000, 0x400);
+ this.defineMirrorRegion(0x2c00, 0x2000, 0x400);
+ } else if (mirroring === this.nes.rom.SINGLESCREEN_MIRRORING2) {
+ this.ntable1[0] = 1;
+ this.ntable1[1] = 1;
+ this.ntable1[2] = 1;
+ this.ntable1[3] = 1;
+
+ this.defineMirrorRegion(0x2400, 0x2400, 0x400);
+ this.defineMirrorRegion(0x2800, 0x2400, 0x400);
+ this.defineMirrorRegion(0x2c00, 0x2400, 0x400);
+ } else {
+ // Assume Four-screen mirroring.
+
+ this.ntable1[0] = 0;
+ this.ntable1[1] = 1;
+ this.ntable1[2] = 2;
+ this.ntable1[3] = 3;
+ }
+ },
+
+ // Define a mirrored area in the address lookup table.
+ // Assumes the regions don't overlap.
+ // The 'to' region is the region that is physically in memory.
+ defineMirrorRegion: function (fromStart, toStart, size) {
+ for (var i = 0; i < size; i++) {
+ this.vramMirrorTable[fromStart + i] = toStart + i;
+ }
+ },
+
+ startVBlank: function () {
+ // Do NMI:
+ this.nes.cpu.requestIrq(this.nes.cpu.IRQ_NMI);
+
+ // Make sure everything is rendered:
+ if (this.lastRenderedScanline < 239) {
+ this.renderFramePartially(
+ this.lastRenderedScanline + 1,
+ 240 - this.lastRenderedScanline
+ );
+ }
+
+ // End frame:
+ this.endFrame();
+
+ // Reset scanline counter:
+ this.lastRenderedScanline = -1;
+ },
+
+ endScanline: function () {
+ switch (this.scanline) {
+ case 19:
+ // Dummy scanline.
+ // May be variable length:
+ if (this.dummyCycleToggle) {
+ // Remove dead cycle at end of scanline,
+ // for next scanline:
+ this.curX = 1;
+ this.dummyCycleToggle = !this.dummyCycleToggle;
+ }
+ break;
+
+ case 20:
+ // Clear VBlank flag:
+ this.setStatusFlag(this.STATUS_VBLANK, false);
+
+ // Clear Sprite #0 hit flag:
+ this.setStatusFlag(this.STATUS_SPRITE0HIT, false);
+ this.hitSpr0 = false;
+ this.spr0HitX = -1;
+ this.spr0HitY = -1;
+
+ if (this.f_bgVisibility === 1 || this.f_spVisibility === 1) {
+ // Update counters:
+ this.cntFV = this.regFV;
+ this.cntV = this.regV;
+ this.cntH = this.regH;
+ this.cntVT = this.regVT;
+ this.cntHT = this.regHT;
+
+ if (this.f_bgVisibility === 1) {
+ // Render dummy scanline:
+ this.renderBgScanline(false, 0);
+ }
+ }
+
+ if (this.f_bgVisibility === 1 && this.f_spVisibility === 1) {
+ // Check sprite 0 hit for first scanline:
+ this.checkSprite0(0);
+ }
+
+ if (this.f_bgVisibility === 1 || this.f_spVisibility === 1) {
+ // Clock mapper IRQ Counter:
+ this.nes.mmap.clockIrqCounter();
+ }
+ break;
+
+ case 261:
+ // Dead scanline, no rendering.
+ // Set VINT:
+ this.setStatusFlag(this.STATUS_VBLANK, true);
+ this.requestEndFrame = true;
+ this.nmiCounter = 9;
+
+ // Wrap around:
+ this.scanline = -1; // will be incremented to 0
+
+ break;
+
+ default:
+ if (this.scanline >= 21 && this.scanline <= 260) {
+ // Render normally:
+ if (this.f_bgVisibility === 1) {
+ if (!this.scanlineAlreadyRendered) {
+ // update scroll:
+ this.cntHT = this.regHT;
+ this.cntH = this.regH;
+ this.renderBgScanline(true, this.scanline + 1 - 21);
+ }
+ this.scanlineAlreadyRendered = false;
+
+ // Check for sprite 0 (next scanline):
+ if (!this.hitSpr0 && this.f_spVisibility === 1) {
+ if (
+ this.sprX[0] >= -7 &&
+ this.sprX[0] < 256 &&
+ this.sprY[0] + 1 <= this.scanline - 20 &&
+ this.sprY[0] + 1 + (this.f_spriteSize === 0 ? 8 : 16) >=
+ this.scanline - 20
+ ) {
+ if (this.checkSprite0(this.scanline - 20)) {
+ this.hitSpr0 = true;
+ }
+ }
+ }
+ }
+
+ if (this.f_bgVisibility === 1 || this.f_spVisibility === 1) {
+ // Clock mapper IRQ Counter:
+ this.nes.mmap.clockIrqCounter();
+ }
+ }
+ }
+
+ this.scanline++;
+ this.regsToAddress();
+ this.cntsToAddress();
+ },
+
+ startFrame: function () {
+ // Set background color:
+ var bgColor = 0;
+
+ if (this.f_dispType === 0) {
+ // Color display.
+ // f_color determines color emphasis.
+ // Use first entry of image palette as BG color.
+ bgColor = this.imgPalette[0];
+ } else {
+ // Monochrome display.
+ // f_color determines the bg color.
+ switch (this.f_color) {
+ case 0:
+ // Black
+ bgColor = 0x00000;
+ break;
+ case 1:
+ // Green
+ bgColor = 0x00ff00;
+ break;
+ case 2:
+ // Blue
+ bgColor = 0xff0000;
+ break;
+ case 3:
+ // Invalid. Use black.
+ bgColor = 0x000000;
+ break;
+ case 4:
+ // Red
+ bgColor = 0x0000ff;
+ break;
+ default:
+ // Invalid. Use black.
+ bgColor = 0x0;
+ }
+ }
+
+ var buffer = this.buffer;
+ var i;
+ for (i = 0; i < 256 * 240; i++) {
+ buffer[i] = bgColor;
+ }
+ var pixrendered = this.pixrendered;
+ for (i = 0; i < pixrendered.length; i++) {
+ pixrendered[i] = 65;
+ }
+ },
+
+ endFrame: function () {
+ var i, x, y;
+ var buffer = this.buffer;
+
+ // Draw spr#0 hit coordinates:
+ if (this.showSpr0Hit) {
+ // Spr 0 position:
+ if (
+ this.sprX[0] >= 0 &&
+ this.sprX[0] < 256 &&
+ this.sprY[0] >= 0 &&
+ this.sprY[0] < 240
+ ) {
+ for (i = 0; i < 256; i++) {
+ buffer[(this.sprY[0] << 8) + i] = 0xff5555;
+ }
+ for (i = 0; i < 240; i++) {
+ buffer[(i << 8) + this.sprX[0]] = 0xff5555;
+ }
+ }
+ // Hit position:
+ if (
+ this.spr0HitX >= 0 &&
+ this.spr0HitX < 256 &&
+ this.spr0HitY >= 0 &&
+ this.spr0HitY < 240
+ ) {
+ for (i = 0; i < 256; i++) {
+ buffer[(this.spr0HitY << 8) + i] = 0x55ff55;
+ }
+ for (i = 0; i < 240; i++) {
+ buffer[(i << 8) + this.spr0HitX] = 0x55ff55;
+ }
+ }
+ }
+
+ // This is a bit lazy..
+ // if either the sprites or the background should be clipped,
+ // both are clipped after rendering is finished.
+ if (
+ this.clipToTvSize ||
+ this.f_bgClipping === 0 ||
+ this.f_spClipping === 0
+ ) {
+ // Clip left 8-pixels column:
+ for (y = 0; y < 240; y++) {
+ for (x = 0; x < 8; x++) {
+ buffer[(y << 8) + x] = 0;
+ }
+ }
+ }
+
+ if (this.clipToTvSize) {
+ // Clip right 8-pixels column too:
+ for (y = 0; y < 240; y++) {
+ for (x = 0; x < 8; x++) {
+ buffer[(y << 8) + 255 - x] = 0;
+ }
+ }
+ }
+
+ // Clip top and bottom 8 pixels:
+ if (this.clipToTvSize) {
+ for (y = 0; y < 8; y++) {
+ for (x = 0; x < 256; x++) {
+ buffer[(y << 8) + x] = 0;
+ buffer[((239 - y) << 8) + x] = 0;
+ }
+ }
+ }
+
+ this.nes.ui.writeFrame(buffer);
+ },
+
+ updateControlReg1: function (value) {
+ this.triggerRendering();
+
+ this.f_nmiOnVblank = (value >> 7) & 1;
+ this.f_spriteSize = (value >> 5) & 1;
+ this.f_bgPatternTable = (value >> 4) & 1;
+ this.f_spPatternTable = (value >> 3) & 1;
+ this.f_addrInc = (value >> 2) & 1;
+ this.f_nTblAddress = value & 3;
+
+ this.regV = (value >> 1) & 1;
+ this.regH = value & 1;
+ this.regS = (value >> 4) & 1;
+ },
+
+ updateControlReg2: function (value) {
+ this.triggerRendering();
+
+ this.f_color = (value >> 5) & 7;
+ this.f_spVisibility = (value >> 4) & 1;
+ this.f_bgVisibility = (value >> 3) & 1;
+ this.f_spClipping = (value >> 2) & 1;
+ this.f_bgClipping = (value >> 1) & 1;
+ this.f_dispType = value & 1;
+
+ if (this.f_dispType === 0) {
+ this.palTable.setEmphasis(this.f_color);
+ }
+ this.updatePalettes();
+ },
+
+ setStatusFlag: function (flag, value) {
+ var n = 1 << flag;
+ this.nes.cpu.mem[0x2002] =
+ (this.nes.cpu.mem[0x2002] & (255 - n)) | (value ? n : 0);
+ },
+
+ // CPU Register $2002:
+ // Read the Status Register.
+ readStatusRegister: function () {
+ var tmp = this.nes.cpu.mem[0x2002];
+
+ // Reset scroll & VRAM Address toggle:
+ this.firstWrite = true;
+
+ // Clear VBlank flag:
+ this.setStatusFlag(this.STATUS_VBLANK, false);
+
+ // Fetch status data:
+ return tmp;
+ },
+
+ // CPU Register $2003:
+ // Write the SPR-RAM address that is used for sramWrite (Register 0x2004 in CPU memory map)
+ writeSRAMAddress: function (address) {
+ this.sramAddress = address;
+ },
+
+ // CPU Register $2004 (R):
+ // Read from SPR-RAM (Sprite RAM).
+ // The address should be set first.
+ sramLoad: function () {
+ /*short tmp = sprMem.load(sramAddress);
+ sramAddress++; // Increment address
+ sramAddress%=0x100;
+ return tmp;*/
+ return this.spriteMem[this.sramAddress];
+ },
+
+ // CPU Register $2004 (W):
+ // Write to SPR-RAM (Sprite RAM).
+ // The address should be set first.
+ sramWrite: function (value) {
+ this.spriteMem[this.sramAddress] = value;
+ this.spriteRamWriteUpdate(this.sramAddress, value);
+ this.sramAddress++; // Increment address
+ this.sramAddress %= 0x100;
+ },
+
+ // CPU Register $2005:
+ // Write to scroll registers.
+ // The first write is the vertical offset, the second is the
+ // horizontal offset:
+ scrollWrite: function (value) {
+ this.triggerRendering();
+
+ if (this.firstWrite) {
+ // First write, horizontal scroll:
+ this.regHT = (value >> 3) & 31;
+ this.regFH = value & 7;
+ } else {
+ // Second write, vertical scroll:
+ this.regFV = value & 7;
+ this.regVT = (value >> 3) & 31;
+ }
+ this.firstWrite = !this.firstWrite;
+ },
+
+ // CPU Register $2006:
+ // Sets the adress used when reading/writing from/to VRAM.
+ // The first write sets the high byte, the second the low byte.
+ writeVRAMAddress: function (address) {
+ if (this.firstWrite) {
+ this.regFV = (address >> 4) & 3;
+ this.regV = (address >> 3) & 1;
+ this.regH = (address >> 2) & 1;
+ this.regVT = (this.regVT & 7) | ((address & 3) << 3);
+ } else {
+ this.triggerRendering();
+
+ this.regVT = (this.regVT & 24) | ((address >> 5) & 7);
+ this.regHT = address & 31;
+
+ this.cntFV = this.regFV;
+ this.cntV = this.regV;
+ this.cntH = this.regH;
+ this.cntVT = this.regVT;
+ this.cntHT = this.regHT;
+
+ this.checkSprite0(this.scanline - 20);
+ }
+
+ this.firstWrite = !this.firstWrite;
+
+ // Invoke mapper latch:
+ this.cntsToAddress();
+ if (this.vramAddress < 0x2000) {
+ this.nes.mmap.latchAccess(this.vramAddress);
+ }
+ },
+
+ // CPU Register $2007(R):
+ // Read from PPU memory. The address should be set first.
+ vramLoad: function () {
+ var tmp;
+
+ this.cntsToAddress();
+ this.regsToAddress();
+
+ // If address is in range 0x0000-0x3EFF, return buffered values:
+ if (this.vramAddress <= 0x3eff) {
+ tmp = this.vramBufferedReadValue;
+
+ // Update buffered value:
+ if (this.vramAddress < 0x2000) {
+ this.vramBufferedReadValue = this.vramMem[this.vramAddress];
+ } else {
+ this.vramBufferedReadValue = this.mirroredLoad(this.vramAddress);
+ }
+
+ // Mapper latch access:
+ if (this.vramAddress < 0x2000) {
+ this.nes.mmap.latchAccess(this.vramAddress);
+ }
+
+ // Increment by either 1 or 32, depending on d2 of Control Register 1:
+ this.vramAddress += this.f_addrInc === 1 ? 32 : 1;
+
+ this.cntsFromAddress();
+ this.regsFromAddress();
+
+ return tmp; // Return the previous buffered value.
+ }
+
+ // No buffering in this mem range. Read normally.
+ tmp = this.mirroredLoad(this.vramAddress);
+
+ // Increment by either 1 or 32, depending on d2 of Control Register 1:
+ this.vramAddress += this.f_addrInc === 1 ? 32 : 1;
+
+ this.cntsFromAddress();
+ this.regsFromAddress();
+
+ return tmp;
+ },
+
+ // CPU Register $2007(W):
+ // Write to PPU memory. The address should be set first.
+ vramWrite: function (value) {
+ this.triggerRendering();
+ this.cntsToAddress();
+ this.regsToAddress();
+
+ if (this.vramAddress >= 0x2000) {
+ // Mirroring is used.
+ this.mirroredWrite(this.vramAddress, value);
+ } else {
+ // Write normally.
+ this.writeMem(this.vramAddress, value);
+
+ // Invoke mapper latch:
+ this.nes.mmap.latchAccess(this.vramAddress);
+ }
+
+ // Increment by either 1 or 32, depending on d2 of Control Register 1:
+ this.vramAddress += this.f_addrInc === 1 ? 32 : 1;
+ this.regsFromAddress();
+ this.cntsFromAddress();
+ },
+
+ // CPU Register $4014:
+ // Write 256 bytes of main memory
+ // into Sprite RAM.
+ sramDMA: function (value) {
+ var baseAddress = value * 0x100;
+ var data;
+ for (var i = this.sramAddress; i < 256; i++) {
+ data = this.nes.cpu.mem[baseAddress + i];
+ this.spriteMem[i] = data;
+ this.spriteRamWriteUpdate(i, data);
+ }
+
+ this.nes.cpu.haltCycles(513);
+ },
+
+ // Updates the scroll registers from a new VRAM address.
+ regsFromAddress: function () {
+ var address = (this.vramTmpAddress >> 8) & 0xff;
+ this.regFV = (address >> 4) & 7;
+ this.regV = (address >> 3) & 1;
+ this.regH = (address >> 2) & 1;
+ this.regVT = (this.regVT & 7) | ((address & 3) << 3);
+
+ address = this.vramTmpAddress & 0xff;
+ this.regVT = (this.regVT & 24) | ((address >> 5) & 7);
+ this.regHT = address & 31;
+ },
+
+ // Updates the scroll registers from a new VRAM address.
+ cntsFromAddress: function () {
+ var address = (this.vramAddress >> 8) & 0xff;
+ this.cntFV = (address >> 4) & 3;
+ this.cntV = (address >> 3) & 1;
+ this.cntH = (address >> 2) & 1;
+ this.cntVT = (this.cntVT & 7) | ((address & 3) << 3);
+
+ address = this.vramAddress & 0xff;
+ this.cntVT = (this.cntVT & 24) | ((address >> 5) & 7);
+ this.cntHT = address & 31;
+ },
+
+ regsToAddress: function () {
+ var b1 = (this.regFV & 7) << 4;
+ b1 |= (this.regV & 1) << 3;
+ b1 |= (this.regH & 1) << 2;
+ b1 |= (this.regVT >> 3) & 3;
+
+ var b2 = (this.regVT & 7) << 5;
+ b2 |= this.regHT & 31;
+
+ this.vramTmpAddress = ((b1 << 8) | b2) & 0x7fff;
+ },
+
+ cntsToAddress: function () {
+ var b1 = (this.cntFV & 7) << 4;
+ b1 |= (this.cntV & 1) << 3;
+ b1 |= (this.cntH & 1) << 2;
+ b1 |= (this.cntVT >> 3) & 3;
+
+ var b2 = (this.cntVT & 7) << 5;
+ b2 |= this.cntHT & 31;
+
+ this.vramAddress = ((b1 << 8) | b2) & 0x7fff;
+ },
+
+ incTileCounter: function (count) {
+ for (var i = count; i !== 0; i--) {
+ this.cntHT++;
+ if (this.cntHT === 32) {
+ this.cntHT = 0;
+ this.cntVT++;
+ if (this.cntVT >= 30) {
+ this.cntH++;
+ if (this.cntH === 2) {
+ this.cntH = 0;
+ this.cntV++;
+ if (this.cntV === 2) {
+ this.cntV = 0;
+ this.cntFV++;
+ this.cntFV &= 0x7;
+ }
+ }
+ }
+ }
+ }
+ },
+
+ // Reads from memory, taking into account
+ // mirroring/mapping of address ranges.
+ mirroredLoad: function (address) {
+ return this.vramMem[this.vramMirrorTable[address]];
+ },
+
+ // Writes to memory, taking into account
+ // mirroring/mapping of address ranges.
+ mirroredWrite: function (address, value) {
+ if (address >= 0x3f00 && address < 0x3f20) {
+ // Palette write mirroring.
+ if (address === 0x3f00 || address === 0x3f10) {
+ this.writeMem(0x3f00, value);
+ this.writeMem(0x3f10, value);
+ } else if (address === 0x3f04 || address === 0x3f14) {
+ this.writeMem(0x3f04, value);
+ this.writeMem(0x3f14, value);
+ } else if (address === 0x3f08 || address === 0x3f18) {
+ this.writeMem(0x3f08, value);
+ this.writeMem(0x3f18, value);
+ } else if (address === 0x3f0c || address === 0x3f1c) {
+ this.writeMem(0x3f0c, value);
+ this.writeMem(0x3f1c, value);
+ } else {
+ this.writeMem(address, value);
+ }
+ } else {
+ // Use lookup table for mirrored address:
+ if (address < this.vramMirrorTable.length) {
+ this.writeMem(this.vramMirrorTable[address], value);
+ } else {
+ throw new Error("Invalid VRAM address: " + address.toString(16));
+ }
+ }
+ },
+
+ triggerRendering: function () {
+ if (this.scanline >= 21 && this.scanline <= 260) {
+ // Render sprites, and combine:
+ this.renderFramePartially(
+ this.lastRenderedScanline + 1,
+ this.scanline - 21 - this.lastRenderedScanline
+ );
+
+ // Set last rendered scanline:
+ this.lastRenderedScanline = this.scanline - 21;
+ }
+ },
+
+ renderFramePartially: function (startScan, scanCount) {
+ if (this.f_spVisibility === 1) {
+ this.renderSpritesPartially(startScan, scanCount, true);
+ }
+
+ if (this.f_bgVisibility === 1) {
+ var si = startScan << 8;
+ var ei = (startScan + scanCount) << 8;
+ if (ei > 0xf000) {
+ ei = 0xf000;
+ }
+ var buffer = this.buffer;
+ var bgbuffer = this.bgbuffer;
+ var pixrendered = this.pixrendered;
+ for (var destIndex = si; destIndex < ei; destIndex++) {
+ if (pixrendered[destIndex] > 0xff) {
+ buffer[destIndex] = bgbuffer[destIndex];
+ }
+ }
+ }
+
+ if (this.f_spVisibility === 1) {
+ this.renderSpritesPartially(startScan, scanCount, false);
+ }
+
+ this.validTileData = false;
+ },
+
+ renderBgScanline: function (bgbuffer, scan) {
+ var baseTile = this.regS === 0 ? 0 : 256;
+ var destIndex = (scan << 8) - this.regFH;
+
+ this.curNt = this.ntable1[this.cntV + this.cntV + this.cntH];
+
+ this.cntHT = this.regHT;
+ this.cntH = this.regH;
+ this.curNt = this.ntable1[this.cntV + this.cntV + this.cntH];
+
+ if (scan < 240 && scan - this.cntFV >= 0) {
+ var tscanoffset = this.cntFV << 3;
+ var scantile = this.scantile;
+ var attrib = this.attrib;
+ var ptTile = this.ptTile;
+ var nameTable = this.nameTable;
+ var imgPalette = this.imgPalette;
+ var pixrendered = this.pixrendered;
+ var targetBuffer = bgbuffer ? this.bgbuffer : this.buffer;
+
+ var t, tpix, att, col;
+
+ for (var tile = 0; tile < 32; tile++) {
+ if (scan >= 0) {
+ // Fetch tile & attrib data:
+ if (this.validTileData) {
+ // Get data from array:
+ t = scantile[tile];
+ if (typeof t === "undefined") {
+ continue;
+ }
+ tpix = t.pix;
+ att = attrib[tile];
+ } else {
+ // Fetch data:
+ t =
+ ptTile[
+ baseTile +
+ nameTable[this.curNt].getTileIndex(this.cntHT, this.cntVT)
+ ];
+ if (typeof t === "undefined") {
+ continue;
+ }
+ tpix = t.pix;
+ att = nameTable[this.curNt].getAttrib(this.cntHT, this.cntVT);
+ scantile[tile] = t;
+ attrib[tile] = att;
+ }
+
+ // Render tile scanline:
+ var sx = 0;
+ var x = (tile << 3) - this.regFH;
+
+ if (x > -8) {
+ if (x < 0) {
+ destIndex -= x;
+ sx = -x;
+ }
+ if (t.opaque[this.cntFV]) {
+ for (; sx < 8; sx++) {
+ targetBuffer[destIndex] =
+ imgPalette[tpix[tscanoffset + sx] + att];
+ pixrendered[destIndex] |= 256;
+ destIndex++;
+ }
+ } else {
+ for (; sx < 8; sx++) {
+ col = tpix[tscanoffset + sx];
+ if (col !== 0) {
+ targetBuffer[destIndex] = imgPalette[col + att];
+ pixrendered[destIndex] |= 256;
+ }
+ destIndex++;
+ }
+ }
+ }
+ }
+
+ // Increase Horizontal Tile Counter:
+ if (++this.cntHT === 32) {
+ this.cntHT = 0;
+ this.cntH++;
+ this.cntH %= 2;
+ this.curNt = this.ntable1[(this.cntV << 1) + this.cntH];
+ }
+ }
+
+ // Tile data for one row should now have been fetched,
+ // so the data in the array is valid.
+ this.validTileData = true;
+ }
+
+ // update vertical scroll:
+ this.cntFV++;
+ if (this.cntFV === 8) {
+ this.cntFV = 0;
+ this.cntVT++;
+ if (this.cntVT === 30) {
+ this.cntVT = 0;
+ this.cntV++;
+ this.cntV %= 2;
+ this.curNt = this.ntable1[(this.cntV << 1) + this.cntH];
+ } else if (this.cntVT === 32) {
+ this.cntVT = 0;
+ }
+
+ // Invalidate fetched data:
+ this.validTileData = false;
+ }
+ },
+
+ renderSpritesPartially: function (startscan, scancount, bgPri) {
+ if (this.f_spVisibility === 1) {
+ for (var i = 0; i < 64; i++) {
+ if (
+ this.bgPriority[i] === bgPri &&
+ this.sprX[i] >= 0 &&
+ this.sprX[i] < 256 &&
+ this.sprY[i] + 8 >= startscan &&
+ this.sprY[i] < startscan + scancount
+ ) {
+ // Show sprite.
+ if (this.f_spriteSize === 0) {
+ // 8x8 sprites
+
+ this.srcy1 = 0;
+ this.srcy2 = 8;
+
+ if (this.sprY[i] < startscan) {
+ this.srcy1 = startscan - this.sprY[i] - 1;
+ }
+
+ if (this.sprY[i] + 8 > startscan + scancount) {
+ this.srcy2 = startscan + scancount - this.sprY[i] + 1;
+ }
+
+ if (this.f_spPatternTable === 0) {
+ this.ptTile[this.sprTile[i]].render(
+ this.buffer,
+ 0,
+ this.srcy1,
+ 8,
+ this.srcy2,
+ this.sprX[i],
+ this.sprY[i] + 1,
+ this.sprCol[i],
+ this.sprPalette,
+ this.horiFlip[i],
+ this.vertFlip[i],
+ i,
+ this.pixrendered
+ );
+ } else {
+ this.ptTile[this.sprTile[i] + 256].render(
+ this.buffer,
+ 0,
+ this.srcy1,
+ 8,
+ this.srcy2,
+ this.sprX[i],
+ this.sprY[i] + 1,
+ this.sprCol[i],
+ this.sprPalette,
+ this.horiFlip[i],
+ this.vertFlip[i],
+ i,
+ this.pixrendered
+ );
+ }
+ } else {
+ // 8x16 sprites
+ var top = this.sprTile[i];
+ if ((top & 1) !== 0) {
+ top = this.sprTile[i] - 1 + 256;
+ }
+
+ var srcy1 = 0;
+ var srcy2 = 8;
+
+ if (this.sprY[i] < startscan) {
+ srcy1 = startscan - this.sprY[i] - 1;
+ }
+
+ if (this.sprY[i] + 8 > startscan + scancount) {
+ srcy2 = startscan + scancount - this.sprY[i];
+ }
+
+ this.ptTile[top + (this.vertFlip[i] ? 1 : 0)].render(
+ this.buffer,
+ 0,
+ srcy1,
+ 8,
+ srcy2,
+ this.sprX[i],
+ this.sprY[i] + 1,
+ this.sprCol[i],
+ this.sprPalette,
+ this.horiFlip[i],
+ this.vertFlip[i],
+ i,
+ this.pixrendered
+ );
+
+ srcy1 = 0;
+ srcy2 = 8;
+
+ if (this.sprY[i] + 8 < startscan) {
+ srcy1 = startscan - (this.sprY[i] + 8 + 1);
+ }
+
+ if (this.sprY[i] + 16 > startscan + scancount) {
+ srcy2 = startscan + scancount - (this.sprY[i] + 8);
+ }
+
+ this.ptTile[top + (this.vertFlip[i] ? 0 : 1)].render(
+ this.buffer,
+ 0,
+ srcy1,
+ 8,
+ srcy2,
+ this.sprX[i],
+ this.sprY[i] + 1 + 8,
+ this.sprCol[i],
+ this.sprPalette,
+ this.horiFlip[i],
+ this.vertFlip[i],
+ i,
+ this.pixrendered
+ );
+ }
+ }
+ }
+ }
+ },
+
+ checkSprite0: function (scan) {
+ this.spr0HitX = -1;
+ this.spr0HitY = -1;
+
+ var toffset;
+ var tIndexAdd = this.f_spPatternTable === 0 ? 0 : 256;
+ var x, y, t, i;
+ var bufferIndex;
+
+ x = this.sprX[0];
+ y = this.sprY[0] + 1;
+
+ if (this.f_spriteSize === 0) {
+ // 8x8 sprites.
+
+ // Check range:
+ if (y <= scan && y + 8 > scan && x >= -7 && x < 256) {
+ // Sprite is in range.
+ // Draw scanline:
+ t = this.ptTile[this.sprTile[0] + tIndexAdd];
+
+ if (this.vertFlip[0]) {
+ toffset = 7 - (scan - y);
+ } else {
+ toffset = scan - y;
+ }
+ toffset *= 8;
+
+ bufferIndex = scan * 256 + x;
+ if (this.horiFlip[0]) {
+ for (i = 7; i >= 0; i--) {
+ if (x >= 0 && x < 256) {
+ if (
+ bufferIndex >= 0 &&
+ bufferIndex < 61440 &&
+ this.pixrendered[bufferIndex] !== 0
+ ) {
+ if (t.pix[toffset + i] !== 0) {
+ this.spr0HitX = bufferIndex % 256;
+ this.spr0HitY = scan;
+ return true;
+ }
+ }
+ }
+ x++;
+ bufferIndex++;
+ }
+ } else {
+ for (i = 0; i < 8; i++) {
+ if (x >= 0 && x < 256) {
+ if (
+ bufferIndex >= 0 &&
+ bufferIndex < 61440 &&
+ this.pixrendered[bufferIndex] !== 0
+ ) {
+ if (t.pix[toffset + i] !== 0) {
+ this.spr0HitX = bufferIndex % 256;
+ this.spr0HitY = scan;
+ return true;
+ }
+ }
+ }
+ x++;
+ bufferIndex++;
+ }
+ }
+ }
+ } else {
+ // 8x16 sprites:
+
+ // Check range:
+ if (y <= scan && y + 16 > scan && x >= -7 && x < 256) {
+ // Sprite is in range.
+ // Draw scanline:
+
+ if (this.vertFlip[0]) {
+ toffset = 15 - (scan - y);
+ } else {
+ toffset = scan - y;
+ }
+
+ if (toffset < 8) {
+ // first half of sprite.
+ t = this.ptTile[
+ this.sprTile[0] +
+ (this.vertFlip[0] ? 1 : 0) +
+ ((this.sprTile[0] & 1) !== 0 ? 255 : 0)
+ ];
+ } else {
+ // second half of sprite.
+ t = this.ptTile[
+ this.sprTile[0] +
+ (this.vertFlip[0] ? 0 : 1) +
+ ((this.sprTile[0] & 1) !== 0 ? 255 : 0)
+ ];
+ if (this.vertFlip[0]) {
+ toffset = 15 - toffset;
+ } else {
+ toffset -= 8;
+ }
+ }
+ toffset *= 8;
+
+ bufferIndex = scan * 256 + x;
+ if (this.horiFlip[0]) {
+ for (i = 7; i >= 0; i--) {
+ if (x >= 0 && x < 256) {
+ if (
+ bufferIndex >= 0 &&
+ bufferIndex < 61440 &&
+ this.pixrendered[bufferIndex] !== 0
+ ) {
+ if (t.pix[toffset + i] !== 0) {
+ this.spr0HitX = bufferIndex % 256;
+ this.spr0HitY = scan;
+ return true;
+ }
+ }
+ }
+ x++;
+ bufferIndex++;
+ }
+ } else {
+ for (i = 0; i < 8; i++) {
+ if (x >= 0 && x < 256) {
+ if (
+ bufferIndex >= 0 &&
+ bufferIndex < 61440 &&
+ this.pixrendered[bufferIndex] !== 0
+ ) {
+ if (t.pix[toffset + i] !== 0) {
+ this.spr0HitX = bufferIndex % 256;
+ this.spr0HitY = scan;
+ return true;
+ }
+ }
+ }
+ x++;
+ bufferIndex++;
+ }
+ }
+ }
+ }
+
+ return false;
+ },
+
+ // This will write to PPU memory, and
+ // update internally buffered data
+ // appropriately.
+ writeMem: function (address, value) {
+ this.vramMem[address] = value;
+
+ // Update internally buffered data:
+ if (address < 0x2000) {
+ this.vramMem[address] = value;
+ this.patternWrite(address, value);
+ } else if (address >= 0x2000 && address < 0x23c0) {
+ this.nameTableWrite(this.ntable1[0], address - 0x2000, value);
+ } else if (address >= 0x23c0 && address < 0x2400) {
+ this.attribTableWrite(this.ntable1[0], address - 0x23c0, value);
+ } else if (address >= 0x2400 && address < 0x27c0) {
+ this.nameTableWrite(this.ntable1[1], address - 0x2400, value);
+ } else if (address >= 0x27c0 && address < 0x2800) {
+ this.attribTableWrite(this.ntable1[1], address - 0x27c0, value);
+ } else if (address >= 0x2800 && address < 0x2bc0) {
+ this.nameTableWrite(this.ntable1[2], address - 0x2800, value);
+ } else if (address >= 0x2bc0 && address < 0x2c00) {
+ this.attribTableWrite(this.ntable1[2], address - 0x2bc0, value);
+ } else if (address >= 0x2c00 && address < 0x2fc0) {
+ this.nameTableWrite(this.ntable1[3], address - 0x2c00, value);
+ } else if (address >= 0x2fc0 && address < 0x3000) {
+ this.attribTableWrite(this.ntable1[3], address - 0x2fc0, value);
+ } else if (address >= 0x3f00 && address < 0x3f20) {
+ this.updatePalettes();
+ }
+ },
+
+ // Reads data from $3f00 to $f20
+ // into the two buffered palettes.
+ updatePalettes: function () {
+ var i;
+
+ for (i = 0; i < 16; i++) {
+ if (this.f_dispType === 0) {
+ this.imgPalette[i] = this.palTable.getEntry(
+ this.vramMem[0x3f00 + i] & 63
+ );
+ } else {
+ this.imgPalette[i] = this.palTable.getEntry(
+ this.vramMem[0x3f00 + i] & 32
+ );
+ }
+ }
+ for (i = 0; i < 16; i++) {
+ if (this.f_dispType === 0) {
+ this.sprPalette[i] = this.palTable.getEntry(
+ this.vramMem[0x3f10 + i] & 63
+ );
+ } else {
+ this.sprPalette[i] = this.palTable.getEntry(
+ this.vramMem[0x3f10 + i] & 32
+ );
+ }
+ }
+ },
+
+ // Updates the internal pattern
+ // table buffers with this new byte.
+ // In vNES, there is a version of this with 4 arguments which isn't used.
+ patternWrite: function (address, value) {
+ var tileIndex = Math.floor(address / 16);
+ var leftOver = address % 16;
+ if (leftOver < 8) {
+ this.ptTile[tileIndex].setScanline(
+ leftOver,
+ value,
+ this.vramMem[address + 8]
+ );
+ } else {
+ this.ptTile[tileIndex].setScanline(
+ leftOver - 8,
+ this.vramMem[address - 8],
+ value
+ );
+ }
+ },
+
+ // Updates the internal name table buffers
+ // with this new byte.
+ nameTableWrite: function (index, address, value) {
+ this.nameTable[index].tile[address] = value;
+
+ // Update Sprite #0 hit:
+ //updateSpr0Hit();
+ this.checkSprite0(this.scanline - 20);
+ },
+
+ // Updates the internal pattern
+ // table buffers with this new attribute
+ // table byte.
+ attribTableWrite: function (index, address, value) {
+ this.nameTable[index].writeAttrib(address, value);
+ },
+
+ // Updates the internally buffered sprite
+ // data with this new byte of info.
+ spriteRamWriteUpdate: function (address, value) {
+ var tIndex = Math.floor(address / 4);
+
+ if (tIndex === 0) {
+ //updateSpr0Hit();
+ this.checkSprite0(this.scanline - 20);
+ }
+
+ if (address % 4 === 0) {
+ // Y coordinate
+ this.sprY[tIndex] = value;
+ } else if (address % 4 === 1) {
+ // Tile index
+ this.sprTile[tIndex] = value;
+ } else if (address % 4 === 2) {
+ // Attributes
+ this.vertFlip[tIndex] = (value & 0x80) !== 0;
+ this.horiFlip[tIndex] = (value & 0x40) !== 0;
+ this.bgPriority[tIndex] = (value & 0x20) !== 0;
+ this.sprCol[tIndex] = (value & 3) << 2;
+ } else if (address % 4 === 3) {
+ // X coordinate
+ this.sprX[tIndex] = value;
+ }
+ },
+
+ doNMI: function () {
+ // Set VBlank flag:
+ this.setStatusFlag(this.STATUS_VBLANK, true);
+ //nes.getCpu().doNonMaskableInterrupt();
+ this.nes.cpu.requestIrq(this.nes.cpu.IRQ_NMI);
+ },
+
+ isPixelWhite: function (x, y) {
+ this.triggerRendering();
+ return this.nes.ppu.buffer[(y << 8) + x] === 0xffffff;
+ },
+
+ JSON_PROPERTIES: [
+ // Memory
+ "vramMem",
+ "spriteMem",
+ // Counters
+ "cntFV",
+ "cntV",
+ "cntH",
+ "cntVT",
+ "cntHT",
+ // Registers
+ "regFV",
+ "regV",
+ "regH",
+ "regVT",
+ "regHT",
+ "regFH",
+ "regS",
+ // VRAM addr
+ "vramAddress",
+ "vramTmpAddress",
+ // Control/Status registers
+ "f_nmiOnVblank",
+ "f_spriteSize",
+ "f_bgPatternTable",
+ "f_spPatternTable",
+ "f_addrInc",
+ "f_nTblAddress",
+ "f_color",
+ "f_spVisibility",
+ "f_bgVisibility",
+ "f_spClipping",
+ "f_bgClipping",
+ "f_dispType",
+ // VRAM I/O
+ "vramBufferedReadValue",
+ "firstWrite",
+ // Mirroring
+ "currentMirroring",
+ "vramMirrorTable",
+ "ntable1",
+ // SPR-RAM I/O
+ "sramAddress",
+ // Sprites. Most sprite data is rebuilt from spriteMem
+ "hitSpr0",
+ // Palettes
+ "sprPalette",
+ "imgPalette",
+ // Rendering progression
+ "curX",
+ "scanline",
+ "lastRenderedScanline",
+ "curNt",
+ "scantile",
+ // Used during rendering
+ "attrib",
+ "buffer",
+ "bgbuffer",
+ "pixrendered",
+ // Misc
+ "requestEndFrame",
+ "nmiOk",
+ "dummyCycleToggle",
+ "nmiCounter",
+ "validTileData",
+ "scanlineAlreadyRendered",
+ ],
+
+ toJSON: function () {
+ var i;
+ var state = utils.toJSON(this);
+
+ state.nameTable = [];
+ for (i = 0; i < this.nameTable.length; i++) {
+ state.nameTable[i] = this.nameTable[i].toJSON();
+ }
+
+ state.ptTile = [];
+ for (i = 0; i < this.ptTile.length; i++) {
+ state.ptTile[i] = this.ptTile[i].toJSON();
+ }
+
+ return state;
+ },
+
+ fromJSON: function (state) {
+ var i;
+
+ utils.fromJSON(this, state);
+
+ for (i = 0; i < this.nameTable.length; i++) {
+ this.nameTable[i].fromJSON(state.nameTable[i]);
+ }
+
+ for (i = 0; i < this.ptTile.length; i++) {
+ this.ptTile[i].fromJSON(state.ptTile[i]);
+ }
+
+ // Sprite data:
+ for (i = 0; i < this.spriteMem.length; i++) {
+ this.spriteRamWriteUpdate(i, this.spriteMem[i]);
+ }
+ },
+};
+
+var NameTable = function (width, height, name) {
+ this.width = width;
+ this.height = height;
+ this.name = name;
+
+ this.tile = new Array(width * height);
+ this.attrib = new Array(width * height);
+ for (var i = 0; i < width * height; i++) {
+ this.tile[i] = 0;
+ this.attrib[i] = 0;
+ }
+};
+
+NameTable.prototype = {
+ getTileIndex: function (x, y) {
+ return this.tile[y * this.width + x];
+ },
+
+ getAttrib: function (x, y) {
+ return this.attrib[y * this.width + x];
+ },
+
+ writeAttrib: function (index, value) {
+ var basex = (index % 8) * 4;
+ var basey = Math.floor(index / 8) * 4;
+ var add;
+ var tx, ty;
+ var attindex;
+
+ for (var sqy = 0; sqy < 2; sqy++) {
+ for (var sqx = 0; sqx < 2; sqx++) {
+ add = (value >> (2 * (sqy * 2 + sqx))) & 3;
+ for (var y = 0; y < 2; y++) {
+ for (var x = 0; x < 2; x++) {
+ tx = basex + sqx * 2 + x;
+ ty = basey + sqy * 2 + y;
+ attindex = ty * this.width + tx;
+ this.attrib[attindex] = (add << 2) & 12;
+ }
+ }
+ }
+ }
+ },
+
+ toJSON: function () {
+ return {
+ tile: this.tile,
+ attrib: this.attrib,
+ };
+ },
+
+ fromJSON: function (s) {
+ this.tile = s.tile;
+ this.attrib = s.attrib;
+ },
+};
+
+var PaletteTable = function () {
+ this.curTable = new Array(64);
+ this.emphTable = new Array(8);
+ this.currentEmph = -1;
+};
+
+PaletteTable.prototype = {
+ reset: function () {
+ this.setEmphasis(0);
+ },
+
+ loadNTSCPalette: function () {
+ // prettier-ignore
+ this.curTable = [0x525252, 0xB40000, 0xA00000, 0xB1003D, 0x740069, 0x00005B, 0x00005F, 0x001840, 0x002F10, 0x084A08, 0x006700, 0x124200, 0x6D2800, 0x000000, 0x000000, 0x000000, 0xC4D5E7, 0xFF4000, 0xDC0E22, 0xFF476B, 0xD7009F, 0x680AD7, 0x0019BC, 0x0054B1, 0x006A5B, 0x008C03, 0x00AB00, 0x2C8800, 0xA47200, 0x000000, 0x000000, 0x000000, 0xF8F8F8, 0xFFAB3C, 0xFF7981, 0xFF5BC5, 0xFF48F2, 0xDF49FF, 0x476DFF, 0x00B4F7, 0x00E0FF, 0x00E375, 0x03F42B, 0x78B82E, 0xE5E218, 0x787878, 0x000000, 0x000000, 0xFFFFFF, 0xFFF2BE, 0xF8B8B8, 0xF8B8D8, 0xFFB6FF, 0xFFC3FF, 0xC7D1FF, 0x9ADAFF, 0x88EDF8, 0x83FFDD, 0xB8F8B8, 0xF5F8AC, 0xFFFFB0, 0xF8D8F8, 0x000000, 0x000000];
+ this.makeTables();
+ this.setEmphasis(0);
+ },
+
+ loadPALPalette: function () {
+ // prettier-ignore
+ this.curTable = [0x525252, 0xB40000, 0xA00000, 0xB1003D, 0x740069, 0x00005B, 0x00005F, 0x001840, 0x002F10, 0x084A08, 0x006700, 0x124200, 0x6D2800, 0x000000, 0x000000, 0x000000, 0xC4D5E7, 0xFF4000, 0xDC0E22, 0xFF476B, 0xD7009F, 0x680AD7, 0x0019BC, 0x0054B1, 0x006A5B, 0x008C03, 0x00AB00, 0x2C8800, 0xA47200, 0x000000, 0x000000, 0x000000, 0xF8F8F8, 0xFFAB3C, 0xFF7981, 0xFF5BC5, 0xFF48F2, 0xDF49FF, 0x476DFF, 0x00B4F7, 0x00E0FF, 0x00E375, 0x03F42B, 0x78B82E, 0xE5E218, 0x787878, 0x000000, 0x000000, 0xFFFFFF, 0xFFF2BE, 0xF8B8B8, 0xF8B8D8, 0xFFB6FF, 0xFFC3FF, 0xC7D1FF, 0x9ADAFF, 0x88EDF8, 0x83FFDD, 0xB8F8B8, 0xF5F8AC, 0xFFFFB0, 0xF8D8F8, 0x000000, 0x000000];
+ this.makeTables();
+ this.setEmphasis(0);
+ },
+
+ makeTables: function () {
+ var r, g, b, col, i, rFactor, gFactor, bFactor;
+
+ // Calculate a table for each possible emphasis setting:
+ for (var emph = 0; emph < 8; emph++) {
+ // Determine color component factors:
+ rFactor = 1.0;
+ gFactor = 1.0;
+ bFactor = 1.0;
+
+ if ((emph & 1) !== 0) {
+ rFactor = 0.75;
+ bFactor = 0.75;
+ }
+ if ((emph & 2) !== 0) {
+ rFactor = 0.75;
+ gFactor = 0.75;
+ }
+ if ((emph & 4) !== 0) {
+ gFactor = 0.75;
+ bFactor = 0.75;
+ }
+
+ this.emphTable[emph] = new Array(64);
+
+ // Calculate table:
+ for (i = 0; i < 64; i++) {
+ col = this.curTable[i];
+ r = Math.floor(this.getRed(col) * rFactor);
+ g = Math.floor(this.getGreen(col) * gFactor);
+ b = Math.floor(this.getBlue(col) * bFactor);
+ this.emphTable[emph][i] = this.getRgb(r, g, b);
+ }
+ }
+ },
+
+ setEmphasis: function (emph) {
+ if (emph !== this.currentEmph) {
+ this.currentEmph = emph;
+ for (var i = 0; i < 64; i++) {
+ this.curTable[i] = this.emphTable[emph][i];
+ }
+ }
+ },
+
+ getEntry: function (yiq) {
+ return this.curTable[yiq];
+ },
+
+ getRed: function (rgb) {
+ return (rgb >> 16) & 0xff;
+ },
+
+ getGreen: function (rgb) {
+ return (rgb >> 8) & 0xff;
+ },
+
+ getBlue: function (rgb) {
+ return rgb & 0xff;
+ },
+
+ getRgb: function (r, g, b) {
+ return (r << 16) | (g << 8) | b;
+ },
+
+ loadDefaultPalette: function () {
+ this.curTable[0] = this.getRgb(117, 117, 117);
+ this.curTable[1] = this.getRgb(39, 27, 143);
+ this.curTable[2] = this.getRgb(0, 0, 171);
+ this.curTable[3] = this.getRgb(71, 0, 159);
+ this.curTable[4] = this.getRgb(143, 0, 119);
+ this.curTable[5] = this.getRgb(171, 0, 19);
+ this.curTable[6] = this.getRgb(167, 0, 0);
+ this.curTable[7] = this.getRgb(127, 11, 0);
+ this.curTable[8] = this.getRgb(67, 47, 0);
+ this.curTable[9] = this.getRgb(0, 71, 0);
+ this.curTable[10] = this.getRgb(0, 81, 0);
+ this.curTable[11] = this.getRgb(0, 63, 23);
+ this.curTable[12] = this.getRgb(27, 63, 95);
+ this.curTable[13] = this.getRgb(0, 0, 0);
+ this.curTable[14] = this.getRgb(0, 0, 0);
+ this.curTable[15] = this.getRgb(0, 0, 0);
+ this.curTable[16] = this.getRgb(188, 188, 188);
+ this.curTable[17] = this.getRgb(0, 115, 239);
+ this.curTable[18] = this.getRgb(35, 59, 239);
+ this.curTable[19] = this.getRgb(131, 0, 243);
+ this.curTable[20] = this.getRgb(191, 0, 191);
+ this.curTable[21] = this.getRgb(231, 0, 91);
+ this.curTable[22] = this.getRgb(219, 43, 0);
+ this.curTable[23] = this.getRgb(203, 79, 15);
+ this.curTable[24] = this.getRgb(139, 115, 0);
+ this.curTable[25] = this.getRgb(0, 151, 0);
+ this.curTable[26] = this.getRgb(0, 171, 0);
+ this.curTable[27] = this.getRgb(0, 147, 59);
+ this.curTable[28] = this.getRgb(0, 131, 139);
+ this.curTable[29] = this.getRgb(0, 0, 0);
+ this.curTable[30] = this.getRgb(0, 0, 0);
+ this.curTable[31] = this.getRgb(0, 0, 0);
+ this.curTable[32] = this.getRgb(255, 255, 255);
+ this.curTable[33] = this.getRgb(63, 191, 255);
+ this.curTable[34] = this.getRgb(95, 151, 255);
+ this.curTable[35] = this.getRgb(167, 139, 253);
+ this.curTable[36] = this.getRgb(247, 123, 255);
+ this.curTable[37] = this.getRgb(255, 119, 183);
+ this.curTable[38] = this.getRgb(255, 119, 99);
+ this.curTable[39] = this.getRgb(255, 155, 59);
+ this.curTable[40] = this.getRgb(243, 191, 63);
+ this.curTable[41] = this.getRgb(131, 211, 19);
+ this.curTable[42] = this.getRgb(79, 223, 75);
+ this.curTable[43] = this.getRgb(88, 248, 152);
+ this.curTable[44] = this.getRgb(0, 235, 219);
+ this.curTable[45] = this.getRgb(0, 0, 0);
+ this.curTable[46] = this.getRgb(0, 0, 0);
+ this.curTable[47] = this.getRgb(0, 0, 0);
+ this.curTable[48] = this.getRgb(255, 255, 255);
+ this.curTable[49] = this.getRgb(171, 231, 255);
+ this.curTable[50] = this.getRgb(199, 215, 255);
+ this.curTable[51] = this.getRgb(215, 203, 255);
+ this.curTable[52] = this.getRgb(255, 199, 255);
+ this.curTable[53] = this.getRgb(255, 199, 219);
+ this.curTable[54] = this.getRgb(255, 191, 179);
+ this.curTable[55] = this.getRgb(255, 219, 171);
+ this.curTable[56] = this.getRgb(255, 231, 163);
+ this.curTable[57] = this.getRgb(227, 255, 163);
+ this.curTable[58] = this.getRgb(171, 243, 191);
+ this.curTable[59] = this.getRgb(179, 255, 207);
+ this.curTable[60] = this.getRgb(159, 255, 243);
+ this.curTable[61] = this.getRgb(0, 0, 0);
+ this.curTable[62] = this.getRgb(0, 0, 0);
+ this.curTable[63] = this.getRgb(0, 0, 0);
+
+ this.makeTables();
+ this.setEmphasis(0);
+ },
+};
+
+module.exports = PPU;
+
+},{"./tile":9,"./utils":10}],8:[function(require,module,exports){
+var Mappers = require("./mappers");
+var Tile = require("./tile");
+
+var ROM = function (nes) {
+ this.nes = nes;
+
+ this.mapperName = new Array(92);
+
+ for (var i = 0; i < 92; i++) {
+ this.mapperName[i] = "Unknown Mapper";
+ }
+ this.mapperName[0] = "Direct Access";
+ this.mapperName[1] = "Nintendo MMC1";
+ this.mapperName[2] = "UNROM";
+ this.mapperName[3] = "CNROM";
+ this.mapperName[4] = "Nintendo MMC3";
+ this.mapperName[5] = "Nintendo MMC5";
+ this.mapperName[6] = "FFE F4xxx";
+ this.mapperName[7] = "AOROM";
+ this.mapperName[8] = "FFE F3xxx";
+ this.mapperName[9] = "Nintendo MMC2";
+ this.mapperName[10] = "Nintendo MMC4";
+ this.mapperName[11] = "Color Dreams Chip";
+ this.mapperName[12] = "FFE F6xxx";
+ this.mapperName[15] = "100-in-1 switch";
+ this.mapperName[16] = "Bandai chip";
+ this.mapperName[17] = "FFE F8xxx";
+ this.mapperName[18] = "Jaleco SS8806 chip";
+ this.mapperName[19] = "Namcot 106 chip";
+ this.mapperName[20] = "Famicom Disk System";
+ this.mapperName[21] = "Konami VRC4a";
+ this.mapperName[22] = "Konami VRC2a";
+ this.mapperName[23] = "Konami VRC2a";
+ this.mapperName[24] = "Konami VRC6";
+ this.mapperName[25] = "Konami VRC4b";
+ this.mapperName[32] = "Irem G-101 chip";
+ this.mapperName[33] = "Taito TC0190/TC0350";
+ this.mapperName[34] = "32kB ROM switch";
+
+ this.mapperName[64] = "Tengen RAMBO-1 chip";
+ this.mapperName[65] = "Irem H-3001 chip";
+ this.mapperName[66] = "GNROM switch";
+ this.mapperName[67] = "SunSoft3 chip";
+ this.mapperName[68] = "SunSoft4 chip";
+ this.mapperName[69] = "SunSoft5 FME-7 chip";
+ this.mapperName[71] = "Camerica chip";
+ this.mapperName[78] = "Irem 74HC161/32-based";
+ this.mapperName[91] = "Pirate HK-SF3 chip";
+};
+
+ROM.prototype = {
+ // Mirroring types:
+ VERTICAL_MIRRORING: 0,
+ HORIZONTAL_MIRRORING: 1,
+ FOURSCREEN_MIRRORING: 2,
+ SINGLESCREEN_MIRRORING: 3,
+ SINGLESCREEN_MIRRORING2: 4,
+ SINGLESCREEN_MIRRORING3: 5,
+ SINGLESCREEN_MIRRORING4: 6,
+ CHRROM_MIRRORING: 7,
+
+ header: null,
+ rom: null,
+ vrom: null,
+ vromTile: null,
+
+ romCount: null,
+ vromCount: null,
+ mirroring: null,
+ batteryRam: null,
+ trainer: null,
+ fourScreen: null,
+ mapperType: null,
+ valid: false,
+
+ load: function (data) {
+ var i, j, v;
+
+ if (data.indexOf("NES\x1a") === -1) {
+ throw new Error("Not a valid NES ROM.");
+ }
+ this.header = new Array(16);
+ for (i = 0; i < 16; i++) {
+ this.header[i] = data.charCodeAt(i) & 0xff;
+ }
+ this.romCount = this.header[4];
+ this.vromCount = this.header[5] * 2; // Get the number of 4kB banks, not 8kB
+ this.mirroring = (this.header[6] & 1) !== 0 ? 1 : 0;
+ this.batteryRam = (this.header[6] & 2) !== 0;
+ this.trainer = (this.header[6] & 4) !== 0;
+ this.fourScreen = (this.header[6] & 8) !== 0;
+ this.mapperType = (this.header[6] >> 4) | (this.header[7] & 0xf0);
+ /* TODO
+ if (this.batteryRam)
+ this.loadBatteryRam();*/
+ // Check whether byte 8-15 are zero's:
+ var foundError = false;
+ for (i = 8; i < 16; i++) {
+ if (this.header[i] !== 0) {
+ foundError = true;
+ break;
+ }
+ }
+ if (foundError) {
+ this.mapperType &= 0xf; // Ignore byte 7
+ }
+ // Load PRG-ROM banks:
+ this.rom = new Array(this.romCount);
+ var offset = 16;
+ for (i = 0; i < this.romCount; i++) {
+ this.rom[i] = new Array(16384);
+ for (j = 0; j < 16384; j++) {
+ if (offset + j >= data.length) {
+ break;
+ }
+ this.rom[i][j] = data.charCodeAt(offset + j) & 0xff;
+ }
+ offset += 16384;
+ }
+ // Load CHR-ROM banks:
+ this.vrom = new Array(this.vromCount);
+ for (i = 0; i < this.vromCount; i++) {
+ this.vrom[i] = new Array(4096);
+ for (j = 0; j < 4096; j++) {
+ if (offset + j >= data.length) {
+ break;
+ }
+ this.vrom[i][j] = data.charCodeAt(offset + j) & 0xff;
+ }
+ offset += 4096;
+ }
+
+ // Create VROM tiles:
+ this.vromTile = new Array(this.vromCount);
+ for (i = 0; i < this.vromCount; i++) {
+ this.vromTile[i] = new Array(256);
+ for (j = 0; j < 256; j++) {
+ this.vromTile[i][j] = new Tile();
+ }
+ }
+
+ // Convert CHR-ROM banks to tiles:
+ var tileIndex;
+ var leftOver;
+ for (v = 0; v < this.vromCount; v++) {
+ for (i = 0; i < 4096; i++) {
+ tileIndex = i >> 4;
+ leftOver = i % 16;
+ if (leftOver < 8) {
+ this.vromTile[v][tileIndex].setScanline(
+ leftOver,
+ this.vrom[v][i],
+ this.vrom[v][i + 8]
+ );
+ } else {
+ this.vromTile[v][tileIndex].setScanline(
+ leftOver - 8,
+ this.vrom[v][i - 8],
+ this.vrom[v][i]
+ );
+ }
+ }
+ }
+
+ this.valid = true;
+ },
+
+ getMirroringType: function () {
+ if (this.fourScreen) {
+ return this.FOURSCREEN_MIRRORING;
+ }
+ if (this.mirroring === 0) {
+ return this.HORIZONTAL_MIRRORING;
+ }
+ return this.VERTICAL_MIRRORING;
+ },
+
+ getMapperName: function () {
+ if (this.mapperType >= 0 && this.mapperType < this.mapperName.length) {
+ return this.mapperName[this.mapperType];
+ }
+ return "Unknown Mapper, " + this.mapperType;
+ },
+
+ mapperSupported: function () {
+ return typeof Mappers[this.mapperType] !== "undefined";
+ },
+
+ createMapper: function () {
+ if (this.mapperSupported()) {
+ return new Mappers[this.mapperType](this.nes);
+ } else {
+ throw new Error(
+ "This ROM uses a mapper not supported by JSNES: " +
+ this.getMapperName() +
+ "(" +
+ this.mapperType +
+ ")"
+ );
+ }
+ },
+};
+
+module.exports = ROM;
+
+},{"./mappers":4,"./tile":9}],9:[function(require,module,exports){
+var Tile = function () {
+ // Tile data:
+ this.pix = new Array(64);
+
+ this.fbIndex = null;
+ this.tIndex = null;
+ this.x = null;
+ this.y = null;
+ this.w = null;
+ this.h = null;
+ this.incX = null;
+ this.incY = null;
+ this.palIndex = null;
+ this.tpri = null;
+ this.c = null;
+ this.initialized = false;
+ this.opaque = new Array(8);
+};
+
+Tile.prototype = {
+ setBuffer: function (scanline) {
+ for (this.y = 0; this.y < 8; this.y++) {
+ this.setScanline(this.y, scanline[this.y], scanline[this.y + 8]);
+ }
+ },
+
+ setScanline: function (sline, b1, b2) {
+ this.initialized = true;
+ this.tIndex = sline << 3;
+ for (this.x = 0; this.x < 8; this.x++) {
+ this.pix[this.tIndex + this.x] =
+ ((b1 >> (7 - this.x)) & 1) + (((b2 >> (7 - this.x)) & 1) << 1);
+ if (this.pix[this.tIndex + this.x] === 0) {
+ this.opaque[sline] = false;
+ }
+ }
+ },
+
+ render: function (
+ buffer,
+ srcx1,
+ srcy1,
+ srcx2,
+ srcy2,
+ dx,
+ dy,
+ palAdd,
+ palette,
+ flipHorizontal,
+ flipVertical,
+ pri,
+ priTable
+ ) {
+ if (dx < -7 || dx >= 256 || dy < -7 || dy >= 240) {
+ return;
+ }
+
+ this.w = srcx2 - srcx1;
+ this.h = srcy2 - srcy1;
+
+ if (dx < 0) {
+ srcx1 -= dx;
+ }
+ if (dx + srcx2 >= 256) {
+ srcx2 = 256 - dx;
+ }
+
+ if (dy < 0) {
+ srcy1 -= dy;
+ }
+ if (dy + srcy2 >= 240) {
+ srcy2 = 240 - dy;
+ }
+
+ if (!flipHorizontal && !flipVertical) {
+ this.fbIndex = (dy << 8) + dx;
+ this.tIndex = 0;
+ for (this.y = 0; this.y < 8; this.y++) {
+ for (this.x = 0; this.x < 8; this.x++) {
+ if (
+ this.x >= srcx1 &&
+ this.x < srcx2 &&
+ this.y >= srcy1 &&
+ this.y < srcy2
+ ) {
+ this.palIndex = this.pix[this.tIndex];
+ this.tpri = priTable[this.fbIndex];
+ if (this.palIndex !== 0 && pri <= (this.tpri & 0xff)) {
+ //console.log("Rendering upright tile to buffer");
+ buffer[this.fbIndex] = palette[this.palIndex + palAdd];
+ this.tpri = (this.tpri & 0xf00) | pri;
+ priTable[this.fbIndex] = this.tpri;
+ }
+ }
+ this.fbIndex++;
+ this.tIndex++;
+ }
+ this.fbIndex -= 8;
+ this.fbIndex += 256;
+ }
+ } else if (flipHorizontal && !flipVertical) {
+ this.fbIndex = (dy << 8) + dx;
+ this.tIndex = 7;
+ for (this.y = 0; this.y < 8; this.y++) {
+ for (this.x = 0; this.x < 8; this.x++) {
+ if (
+ this.x >= srcx1 &&
+ this.x < srcx2 &&
+ this.y >= srcy1 &&
+ this.y < srcy2
+ ) {
+ this.palIndex = this.pix[this.tIndex];
+ this.tpri = priTable[this.fbIndex];
+ if (this.palIndex !== 0 && pri <= (this.tpri & 0xff)) {
+ buffer[this.fbIndex] = palette[this.palIndex + palAdd];
+ this.tpri = (this.tpri & 0xf00) | pri;
+ priTable[this.fbIndex] = this.tpri;
+ }
+ }
+ this.fbIndex++;
+ this.tIndex--;
+ }
+ this.fbIndex -= 8;
+ this.fbIndex += 256;
+ this.tIndex += 16;
+ }
+ } else if (flipVertical && !flipHorizontal) {
+ this.fbIndex = (dy << 8) + dx;
+ this.tIndex = 56;
+ for (this.y = 0; this.y < 8; this.y++) {
+ for (this.x = 0; this.x < 8; this.x++) {
+ if (
+ this.x >= srcx1 &&
+ this.x < srcx2 &&
+ this.y >= srcy1 &&
+ this.y < srcy2
+ ) {
+ this.palIndex = this.pix[this.tIndex];
+ this.tpri = priTable[this.fbIndex];
+ if (this.palIndex !== 0 && pri <= (this.tpri & 0xff)) {
+ buffer[this.fbIndex] = palette[this.palIndex + palAdd];
+ this.tpri = (this.tpri & 0xf00) | pri;
+ priTable[this.fbIndex] = this.tpri;
+ }
+ }
+ this.fbIndex++;
+ this.tIndex++;
+ }
+ this.fbIndex -= 8;
+ this.fbIndex += 256;
+ this.tIndex -= 16;
+ }
+ } else {
+ this.fbIndex = (dy << 8) + dx;
+ this.tIndex = 63;
+ for (this.y = 0; this.y < 8; this.y++) {
+ for (this.x = 0; this.x < 8; this.x++) {
+ if (
+ this.x >= srcx1 &&
+ this.x < srcx2 &&
+ this.y >= srcy1 &&
+ this.y < srcy2
+ ) {
+ this.palIndex = this.pix[this.tIndex];
+ this.tpri = priTable[this.fbIndex];
+ if (this.palIndex !== 0 && pri <= (this.tpri & 0xff)) {
+ buffer[this.fbIndex] = palette[this.palIndex + palAdd];
+ this.tpri = (this.tpri & 0xf00) | pri;
+ priTable[this.fbIndex] = this.tpri;
+ }
+ }
+ this.fbIndex++;
+ this.tIndex--;
+ }
+ this.fbIndex -= 8;
+ this.fbIndex += 256;
+ }
+ }
+ },
+
+ isTransparent: function (x, y) {
+ return this.pix[(y << 3) + x] === 0;
+ },
+
+ toJSON: function () {
+ return {
+ opaque: this.opaque,
+ pix: this.pix,
+ };
+ },
+
+ fromJSON: function (s) {
+ this.opaque = s.opaque;
+ this.pix = s.pix;
+ },
+};
+
+module.exports = Tile;
+
+},{}],10:[function(require,module,exports){
+module.exports = {
+ copyArrayElements: function (src, srcPos, dest, destPos, length) {
+ for (var i = 0; i < length; ++i) {
+ dest[destPos + i] = src[srcPos + i];
+ }
+ },
+
+ copyArray: function (src) {
+ return src.slice(0);
+ },
+
+ fromJSON: function (obj, state) {
+ for (var i = 0; i < obj.JSON_PROPERTIES.length; i++) {
+ obj[obj.JSON_PROPERTIES[i]] = state[obj.JSON_PROPERTIES[i]];
+ }
+ },
+
+ toJSON: function (obj) {
+ var state = {};
+ for (var i = 0; i < obj.JSON_PROPERTIES.length; i++) {
+ state[obj.JSON_PROPERTIES[i]] = obj[obj.JSON_PROPERTIES[i]];
+ }
+ return state;
+ },
+};
+
+},{}]},{},[3])(3)
+});
diff --git a/public/Repo/jsnes-ninjapad/ninjapad-config.js b/public/Repo/jsnes-ninjapad/ninjapad-config.js
new file mode 100644
index 0000000..8b6554e
--- /dev/null
+++ b/public/Repo/jsnes-ninjapad/ninjapad-config.js
@@ -0,0 +1,7 @@
+var DEBUG = false;
+var DEADZONE = 2;
+var DISPLAY = "emu-canvas";
+var SCREEN = "emu-screen";
+var EMULATOR = "jsnes";
+var SINGLE_ROM = true;
+var DEFAULT_ROM = JSON.parse(new URLSearchParams(window.location.hash).get('#')).RomUrl;
diff --git a/public/Repo/jsnes-ninjapad/ninjapad/gamepad.js b/public/Repo/jsnes-ninjapad/ninjapad/gamepad.js
new file mode 100644
index 0000000..36f3b66
--- /dev/null
+++ b/public/Repo/jsnes-ninjapad/ninjapad/gamepad.js
@@ -0,0 +1,219 @@
+ninjapad.gamepad = function() {
+ // Handle single-touch multiple button presses
+ const MULTIPRESS = {
+ "UR": ["BUTTON_UP", "BUTTON_RIGHT"],
+ "DR": ["BUTTON_DOWN", "BUTTON_RIGHT"],
+ "UL": ["BUTTON_UP", "BUTTON_LEFT" ],
+ "DL": ["BUTTON_DOWN", "BUTTON_LEFT" ],
+ "AB": ["BUTTON_A", "BUTTON_B" ]
+ };
+
+ const DPAD_BUTTONS = [
+ ["BUTTON_LEFT" ],
+ ["BUTTON_UP", "BUTTON_LEFT" ],
+ ["BUTTON_UP", ],
+ ["BUTTON_UP", "BUTTON_RIGHT"],
+ ["BUTTON_RIGHT" ],
+ ["BUTTON_DOWN", "BUTTON_RIGHT"],
+ ["BUTTON_DOWN" ],
+ ["BUTTON_DOWN", "BUTTON_LEFT" ],
+ ]
+
+ // This object is necessary to handle the user
+ // sliding their finger from one button to another
+ var childButton = {};
+
+ var analog = {
+ active: false,
+ touchX: undefined,
+ touchY: undefined,
+ deltaX: undefined,
+ deltaY: undefined,
+ padBtn: undefined
+ };
+
+ function isButtonDown(eventType) {
+ return eventType.endsWith("start") || eventType.endsWith("move");
+ }
+
+ function fnButtonPress(eventType) {
+ return isButtonDown(eventType) ? ninjapad.emulator.buttonDown : ninjapad.emulator.buttonUp;
+ }
+
+ function pressButtons(fn, buttons) {
+ for (const b of buttons) fn(b);
+ }
+
+ function analogReset(element) {
+ element.css("transform", "translate(0, 0)");
+ }
+
+ return {
+ analogTouch: function(event) {
+ event.preventDefault();
+ event.stopPropagation();
+ for (const touch of event.changedTouches) {
+ // Ignore any touches where the target
+ // element doesn't match the source element
+ if (touch.target.id != event.target.id) continue;
+
+ switch (event.type) {
+ case "touchstart":
+ analog.touchX = touch.clientX;
+ analog.touchY = touch.clientY;
+ break;
+
+ case "touchmove":
+ analog.deltaX = touch.clientX - analog.touchX;
+ analog.deltaY = touch.clientY - analog.touchY;
+
+ let r = ninjapad.utils.angle(analog.deltaX, analog.deltaY);
+ let d = Math.min(ninjapad.utils.vw(10), ninjapad.utils.dist(analog.deltaX, analog.deltaY));
+
+ let dx = Math.cos(r) * d;
+ let dy = Math.sin(r) * d;
+ ninjapad.jQElement.analogStick.css(
+ "transform",
+ "translate(" + dx + "px, " + dy + "px)"
+ );
+ let btnIndex = Math.floor(((180 + (45/2) + (r * 180 / Math.PI)) % 360) / 45);
+ analog.padBtn && pressButtons(ninjapad.emulator.buttonUp, analog.padBtn);
+ analog.padBtn = d < ninjapad.utils.vw(DEADZONE) ? null : DPAD_BUTTONS[btnIndex];
+ analog.padBtn && pressButtons(ninjapad.emulator.buttonDown, analog.padBtn);
+ break;
+
+ default:
+ analog.padBtn && pressButtons(ninjapad.emulator.buttonUp, analog.padBtn);
+ analogReset(ninjapad.jQElement.analogStick);
+ }
+ }
+ },
+
+ buttonPress: function(event) {
+ // Prevent all the shenanigans that happen with a "long-press" on mobile
+ event.preventDefault();
+
+ // Get the source element
+ target = event.target;
+
+ // Handle the touch
+ for (const touch of event.changedTouches) {
+ // Ignore any touches where the target
+ // element doesn't match the source element
+ if (touch.target.id != target.id) continue;
+ // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+ // Get the element (either a button or the empty area of the gamepad)
+ // the user is physically touching right now
+ let element = $(document.elementFromPoint(touch.clientX, touch.clientY))[0];
+
+ // If it's a new touch, set the child button to its parent
+ if (event.type == "touchstart") {
+ childButton[target.id] = element;
+ }
+ // Otherwise, if the user is sliding its finger from one button to another
+ // or simply stops touching the screen with that finger
+ else if (childButton[target.id].id != element.id) {
+ //else if (element.id && childButton[target.id].id != element.id) {
+ // Check which button (if any) the user had its finger on previously
+ let lastButton = childButton[target.id];
+ // If the user was actually pressing a button before
+ if (lastButton.id.startsWith("BUTTON")) {
+ // Tell the emulator to release that button
+ ninjapad.emulator.buttonUp(lastButton.id);
+ $(lastButton).css("border-style", "outset");
+ DEBUG && console.log("Released", lastButton.id); // Debug
+ }
+ // Otherwise, if it was a multipress
+ else if (lastButton.id.startsWith("MULTI")) {
+ // Get buttons
+ let key = lastButton.id.split("_").pop();
+ for (const d of MULTIPRESS[key]) {
+ ninjapad.emulator.buttonUp(d);
+ }
+ $(lastButton).css("background-color", "transparent");
+ DEBUG && console.log("Released", lastButton.id); // Debug
+ }
+ // Update the child button to be the one the user is touching right now
+ childButton[target.id] = element;
+ }
+
+ // If the user is actually interacting a button right now
+ if (element.id.startsWith("BUTTON")) {
+
+ // Press / release that button
+ fnButtonPress(event.type)(element.id);
+
+ // Show button presses / releases
+ if (isButtonDown(event.type)) {
+ $(element).css("border-style", "inset");
+ DEBUG && console.log("Pressed", element.id); // Debug
+ }
+ else {
+ $(element).css("border-style", "outset");
+ DEBUG && console.log("Released", element.id); // Debug
+ }
+ }
+ // Otherwise, if it's actually two buttons at the same time
+ else if (element.id.startsWith("MULTI")) {
+
+ // Get the correct function call
+ let fn = fnButtonPress(event.type)
+
+ // Get buttons and press / release them
+ let key = element.id.split("_").pop();
+ for (const d of MULTIPRESS[key]) {
+ fn(d);
+ }
+
+ // Resume emulation and show button presses / releases
+ if (isButtonDown(event.type)) {
+ $(element).css("background-color", "#444");
+ DEBUG && console.log("Pressed", element.id); // Debug
+ }
+ else {
+ $(element).css("background-color", "transparent");
+ DEBUG && console.log("Released", element.id); // Debug
+ }
+ }
+ }
+ },
+
+ analogSwitch: function(event) {
+ event.preventDefault();
+ if (event.type == "touchstart") {
+ ninjapad.jQElement.analogSwitch.css("border-style", "inset");
+ return;
+ }
+ ninjapad.jQElement.analogSwitch.css("border-style", "outset");
+
+ if (ninjapad.jQElement.analog.css("display") == "none") {
+ analog.active = true;
+ ninjapad.jQElement.dpad.hide();
+ ninjapad.jQElement.analog.show();
+ analogReset(ninjapad.jQElement.analog);
+ return;
+ }
+ analog.active = false;
+ ninjapad.jQElement.analog.hide();
+ ninjapad.jQElement.dpad.show();
+ },
+
+ toggleMenu: function(event) {
+ event.preventDefault();
+ if (event.type == "touchstart") {
+ ninjapad.jQElement.menu.css("border-style", "inset");
+ return;
+ }
+ ninjapad.jQElement.menu.css("border-style", "outset");
+ ninjapad.menu.toggleMenu();
+ },
+
+ // Doesn't work on iOS
+ toggleFullScreen: function(event) {
+ event.preventDefault();
+ let element = document.getElementById("ninjaPad");
+ ninjapad.utils.isFullScreen() ? ninjapad.utils.exitFullScreen() : ninjapad.utils.enterFullscreen(element);
+ },
+ }
+}();
diff --git a/public/Repo/jsnes-ninjapad/ninjapad/index.js b/public/Repo/jsnes-ninjapad/ninjapad/index.js
new file mode 100644
index 0000000..0129b7d
--- /dev/null
+++ b/public/Repo/jsnes-ninjapad/ninjapad/index.js
@@ -0,0 +1 @@
+const ninjapad = {};
diff --git a/public/Repo/jsnes-ninjapad/ninjapad/interface.js b/public/Repo/jsnes-ninjapad/ninjapad/interface.js
new file mode 100644
index 0000000..0da6fd7
--- /dev/null
+++ b/public/Repo/jsnes-ninjapad/ninjapad/interface.js
@@ -0,0 +1,430 @@
+ninjapad.interface = {
+ jsnes: function() {
+ const BUTTON_A = 0;
+ const BUTTON_B = 2;
+
+ const SCREEN_WIDTH = 256;
+ const SCREEN_HEIGHT = 240;
+ const FRAMEBUFFER_SIZE = SCREEN_WIDTH*SCREEN_HEIGHT;
+
+ const AUDIO_BUFFERING = 512;
+ const SAMPLE_COUNT = 4*1024;
+ const SAMPLE_MASK = SAMPLE_COUNT - 1;
+
+ var canvas_ctx, image;
+ var framebuffer_u8, framebuffer_u32;
+
+ var audio_ctx, script_processor;
+ var audio_samples_L = new Float32Array(SAMPLE_COUNT);
+ var audio_samples_R = new Float32Array(SAMPLE_COUNT);
+ var audio_write_cursor = 0, audio_read_cursor = 0;
+
+ const nes = new jsnes.NES({
+ onFrame: function(framebuffer_24){
+ for(var i = 0; i < FRAMEBUFFER_SIZE; i++) framebuffer_u32[i] = 0xFF000000 | framebuffer_24[i];
+ },
+ onAudioSample: function(l, r){
+ audio_samples_L[audio_write_cursor] = l;
+ audio_samples_R[audio_write_cursor] = r;
+ audio_write_cursor = (audio_write_cursor + 1) & SAMPLE_MASK;
+ },
+ // sampleRate: getSampleRate()
+ });
+
+ function getSampleRate()
+ {
+ if (!window.AudioContext) {
+ return 44100;
+ }
+ let myCtx = new window.AudioContext();
+ let sampleRate = myCtx.sampleRate;
+ myCtx.close();
+ return sampleRate;
+ }
+
+ function onAnimationFrame(){
+ window.setTimeout(onAnimationFrame, 1000/60);
+ image.data.set(framebuffer_u8);
+ canvas_ctx.putImageData(image, 0, 0);
+ nes.frame();
+ }
+
+ function audio_remain(){
+ return (audio_write_cursor - audio_read_cursor) & SAMPLE_MASK;
+ }
+
+ function audio_callback(event) {
+ if (nes.rom == null) return;
+
+ var dst = event.outputBuffer;
+ var len = dst.length;
+
+ // Attempt to avoid buffer underruns.
+ if(audio_remain() < AUDIO_BUFFERING) nes.frame();
+
+ var dst_l = dst.getChannelData(0);
+ var dst_r = dst.getChannelData(1);
+ for(var i = 0; i < len; i++){
+ var src_idx = (audio_read_cursor + i) & SAMPLE_MASK;
+ dst_l[i] = audio_samples_L[src_idx];
+ dst_r[i] = audio_samples_R[src_idx];
+ }
+
+ audio_read_cursor = (audio_read_cursor + len) & SAMPLE_MASK;
+ }
+
+ function keyboard(callback, event) {
+ var player = 1;
+ var prevent = true;
+ switch(event.keyCode){
+ case 38: // UP
+ case 87: // W
+ callback(player, jsnes.Controller.BUTTON_UP); break;
+ case 40: // Down
+ case 83: // S
+ callback(player, jsnes.Controller.BUTTON_DOWN); break;
+ case 37: // Left
+ case 65: // A
+ callback(player, jsnes.Controller.BUTTON_LEFT); break;
+ case 39: // Right
+ case 68: // D
+ callback(player, jsnes.Controller.BUTTON_RIGHT); break;
+ case 18: // 'alt'
+ case 88: // 'x'
+ callback(player, jsnes.Controller.BUTTON_A); break;
+ case 90: // 'z'
+ case 17: // 'ctrl'
+ callback(player, jsnes.Controller.BUTTON_B); break;
+ case 32: // Space
+ case 16: // Right Shift
+ callback(player, jsnes.Controller.BUTTON_SELECT); break;
+ case 13: // Return
+ callback(player, jsnes.Controller.BUTTON_START); break;
+ default:
+ prevent = false; break;
+ }
+
+ if (prevent){
+ event.preventDefault();
+ }
+ }
+
+ function nes_init(canvas_id){
+ var canvas = document.getElementById(canvas_id);
+ canvas_ctx = canvas.getContext("2d");
+ image = canvas_ctx.getImageData(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
+
+ canvas_ctx.fillStyle = "black";
+ canvas_ctx.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
+
+ // Allocate framebuffer array.
+ var buffer = new ArrayBuffer(image.data.length);
+ framebuffer_u8 = new Uint8ClampedArray(buffer);
+ framebuffer_u32 = new Uint32Array(buffer);
+
+ // Setup audio.
+ audio_ctx = new window.AudioContext();
+ script_processor = audio_ctx.createScriptProcessor(AUDIO_BUFFERING, 0, 2);
+ script_processor.onaudioprocess = audio_callback;
+ script_processor.connect(audio_ctx.destination);
+ document.addEventListener('touchstart', () => { audio_ctx.resume() });
+ document.addEventListener('keydown', () => { audio_ctx.resume() });
+ }
+
+ function nes_boot(rom_data){
+ nes.loadROM(rom_data);
+ window.requestAnimationFrame(onAnimationFrame);
+ }
+
+ function nes_load_data(canvas_id, rom_data){
+ nes_init(canvas_id);
+ nes_boot(rom_data);
+ }
+
+ function nes_load_url(canvas_id, path){
+ nes_init(canvas_id);
+
+ var req = new XMLHttpRequest();
+ req.open("GET", path);
+ req.overrideMimeType("text/plain; charset=x-user-defined");
+ req.onerror = () => console.log(`Error loading ${path}: ${req.statusText}`);
+
+ req.onload = function() {
+ if (this.status === 200) {
+ nes_boot(this.responseText);
+ } else if (this.status === 0) {
+ // Aborted, so ignore error
+ } else {
+ req.onerror();
+ }
+ };
+
+ req.send();
+ }
+
+ document.addEventListener('keydown', (event) => {keyboard(nes.buttonDown, event)});
+ document.addEventListener('keyup', (event) => {keyboard(nes.buttonUp, event)});
+
+ /////////////////////
+ // GAMEPAD SUPPORT
+ //
+ // Based on documentation here: https://developer.mozilla.org/en-US/docs/Web/API/Gamepad_API/Using_the_Gamepad_API
+ /////////////////////
+
+ var haveEvents = 'ongamepadconnected' in window;
+ var controllers = {};
+
+ // Once the presses a button on one of the controllers, this will store
+ // the index of that controller so that only that controller is checked
+ // each frame. This is to avoid additional controllers triggering key_up
+ // events when they are just sitting there inactive.
+ var cur_controller_index = -1;
+
+ function connecthandler(e) {
+ addgamepad(e.gamepad);
+ }
+
+ function addgamepad(gamepad) {
+ controllers[gamepad.index] = gamepad;
+ requestAnimationFrame(updateStatus);
+ }
+
+ function disconnecthandler(e) {
+ removegamepad(e.gamepad);
+ }
+
+ function removegamepad(gamepad) {
+ delete controllers[gamepad.index];
+ }
+
+ // Check all controllers to see if player has pressed any buttons.
+ // If they have, store that as the current controller.
+ function findController()
+ {
+ var i = 0;
+ var j;
+
+ for (j in controllers)
+ {
+ var controller = controllers[j];
+
+ for (i = 0; i < controller.buttons.length; i++)
+ {
+ var val = controller.buttons[i];
+ var pressed = val == 1.0;
+ if (typeof(val) == "object")
+ {
+ pressed = val.pressed;
+ val = val.value;
+ }
+
+ if (pressed)
+ {
+ cur_controller_index = j;
+ }
+ }
+ }
+ }
+
+ function updateStatus()
+ {
+ if (!haveEvents)
+ {
+ scangamepads();
+ }
+
+ // If a controller has not yet been chosen, check for one now.
+ if (cur_controller_index == -1)
+ {
+ findController();
+ }
+
+ // Allow for case where controller was chosen this frame
+ if (cur_controller_index != -1)
+ {
+ var i = 0;
+ var j;
+
+ var controller = controllers[cur_controller_index];
+
+ for (i = 0; i < controller.buttons.length; i++)
+ {
+ var val = controller.buttons[i];
+ var pressed = val == 1.0;
+ if (typeof(val) == "object")
+ {
+ pressed = val.pressed;
+ val = val.value;
+ }
+
+ var player = 1 //parseInt(j,10) + 1;
+
+ if (pressed)
+ {
+ if (audio_ctx) audio_ctx.resume();
+ var callback = nes.buttonDown;
+ switch(i)
+ {
+ case 12: // UP
+ callback(player, jsnes.Controller.BUTTON_UP); break;
+ case 13: // Down
+ callback(player, jsnes.Controller.BUTTON_DOWN); break;
+ case 14: // Left
+ callback(player, jsnes.Controller.BUTTON_LEFT); break;
+ case 15: // Right
+ callback(player, jsnes.Controller.BUTTON_RIGHT); break;
+ case BUTTON_A: // 'A'
+ callback(player, jsnes.Controller.BUTTON_A); break;
+ case BUTTON_B: // 'B'
+ callback(player, jsnes.Controller.BUTTON_B); break;
+ case 8: // Select
+ callback(player, jsnes.Controller.BUTTON_SELECT); break;
+ case 9: // Start
+ callback(player, jsnes.Controller.BUTTON_START); break;
+ }
+ }
+ else
+ {
+ var callback = nes.buttonUp;
+ switch(i)
+ {
+ case 12: // UP
+ callback(player, jsnes.Controller.BUTTON_UP); break;
+ case 13: // Down
+ callback(player, jsnes.Controller.BUTTON_DOWN); break;
+ case 14: // Left
+ callback(player, jsnes.Controller.BUTTON_LEFT); break;
+ case 15: // Right
+ callback(player, jsnes.Controller.BUTTON_RIGHT); break;
+ case BUTTON_A: // 'A'
+ callback(player, jsnes.Controller.BUTTON_A); break;
+ case BUTTON_B: // 'B'
+ callback(player, jsnes.Controller.BUTTON_B); break;
+ case 8: // Select
+ callback(player, jsnes.Controller.BUTTON_SELECT); break;
+ case 9: // Start
+ callback(player, jsnes.Controller.BUTTON_START); break;
+ }
+ }
+
+ // var axes = d.getElementsByClassName("axis");
+ // for (i = 0; i < controller.axes.length; i++)
+ // {
+ // var a = axes[i];
+ // a.innerHTML = i + ": " + controller.axes[i].toFixed(4);
+ // a.setAttribute("value", controller.axes[i] + 1);
+ // }
+ }
+ }
+
+ requestAnimationFrame(updateStatus);
+ }
+
+ function scangamepads() {
+ var gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);
+ for (var i = 0; i < gamepads.length; i++) {
+ if (gamepads[i]) {
+ if (gamepads[i].index in controllers) {
+ controllers[gamepads[i].index] = gamepads[i];
+ } else {
+ addgamepad(gamepads[i]);
+ }
+ }
+ }
+ }
+
+ window.addEventListener("gamepadconnected", connecthandler);
+ window.addEventListener("gamepaddisconnected", disconnecthandler);
+
+ if (!haveEvents) {
+ setInterval(scangamepads, 500);
+ }
+
+
+ return {
+ display: {
+ width: 256,
+ height: 240
+ },
+
+ core: function() {
+ return nes;
+ }(),
+
+ buttonDown: function(b) {
+ nes.buttonDown(1, eval("jsnes.Controller." + b));
+ },
+
+ buttonUp: function(b) {
+ nes.buttonUp(1, eval("jsnes.Controller." + b));
+ },
+
+ pause: function() {
+ function _pause() {
+ if (nes.break) return;
+ // - - - - - - - - - - - - - - - - - - - - - - -
+ if (audio_ctx && audio_ctx.suspend) {
+ audio_ctx.suspend();
+ }
+ audio_ctx = {
+ resume: function(){},
+ isNull: true
+ };
+ nes.break = true;
+ if (typeof enforcePause === 'undefined') {
+ enforcePause = setInterval(_pause, 16);
+ }
+ }
+ _pause();
+ },
+
+ resume: function() {
+ if (!nes.rom.header) return;
+ clearInterval(enforcePause);
+ enforcePause = undefined;
+ if (audio_ctx.isNull) {
+ audio_ctx = new window.AudioContext();
+ script_processor = audio_ctx.createScriptProcessor(AUDIO_BUFFERING, 0, 2);
+ script_processor.onaudioprocess = audio_callback;
+ script_processor.connect(audio_ctx.destination);
+ }
+ audio_ctx.resume();
+ nes.break = false;
+ },
+
+ loadROMData: function(d) {
+ nes.loadROM(d);
+ },
+
+ reloadROM: function() {
+ nes.reloadROM();
+ },
+
+ getROMData: function() {
+ return nes.romData;
+ },
+
+ saveState: function() {
+ const o = nes.toJSON();
+ const s = JSON.stringify(o);
+ return ninjapad.utils.zip(s);
+ },
+
+ loadState: function(z) {
+ const s = ninjapad.utils.unzip(z);
+ const o = JSON.parse(s);
+ nes.fromJSON(o);
+ },
+
+ isROMLoaded: function() {
+ return !!nes.rom.header
+ },
+
+ initialize: function() {
+ nes_load_url(DISPLAY, DEFAULT_ROM);
+ }
+
+ // ...
+ };
+ }()
+};
diff --git a/public/Repo/jsnes-ninjapad/ninjapad/layout.js b/public/Repo/jsnes-ninjapad/ninjapad/layout.js
new file mode 100644
index 0000000..b06ba82
--- /dev/null
+++ b/public/Repo/jsnes-ninjapad/ninjapad/layout.js
@@ -0,0 +1,117 @@
+ninjapad.layout = function() {
+ var coldStart = true;
+
+ function setOSDLayout() {
+ ninjapad.jQElement.osd.empty();
+ ninjapad.jQElement.osd.detach().appendTo(ninjapad.jQElement.screen);
+ ninjapad.jQElement.osd.css("top", 0);
+ ninjapad.jQElement.osd.css("left", 0);
+ ninjapad.jQElement.osd.css("height", ninjapad.jQElement.screen.height());
+ ninjapad.jQElement.osd.css("width", ninjapad.jQElement.screen.width());
+ ninjapad.jQElement.osd.css("visibility", ninjapad.pause.pauseScreen.visibility);
+ ninjapad.jQElement.osd.append(ninjapad.pause.pauseScreen.content);
+ }
+
+ function setEmulationScreenLayout() {
+ ninjapad.jQElement.screen.removeAttr("style");
+ ninjapad.jQElement.screen.css("width", ninjapad.emulator.display.width);
+ ninjapad.jQElement.screen.css("height", ninjapad.emulator.display.height);
+ ninjapad.jQElement.screen.css("margin", "auto");
+ ninjapad.jQElement.screen.css("position", "relative");
+ }
+
+ function setDesktopLayout() {
+ DEBUG && console.log("Desktop mode");
+
+ let useJQuery = !ninjapad.utils.isFullScreen() || ninjapad.utils.isIOSDevice();
+ let width = useJQuery ? $(window).width() : window.innerWidth;
+ let height = useJQuery ? $(window).height() : window.innerHeight;
+
+ if (width > height) {
+ ninjapad.jQElement.screen.height("100%");
+ let newHeight = ninjapad.jQElement.screen.height();
+ ninjapad.jQElement.screen.width(256 * (newHeight / 240));
+ }
+ else {
+ ninjapad.jQElement.screen.width("100%");
+ let newWidth = ninjapad.jQElement.screen.width();
+ ninjapad.jQElement.screen.height(240 * (newWidth / 256));
+ }
+ ninjapad.jQElement.gamepad.height("0%");
+ ninjapad.jQElement.controller.hide();
+ }
+
+ function setMobileLayout() {
+ DEBUG && console.log("Mobile mode");
+
+ if (coldStart) {
+ DEBUG && console.log("Cold start");
+ $("#ninjaPad").css("height", "100%");
+ $("body").removeAttr("style").css("margin", "0%");
+ setEmulationScreenLayout();
+ ninjapad.jQElement.screen.detach().appendTo("#SCREEN");
+ $("body *").not("#ninjaPad *").not("#ninjaPad").remove();
+ coldStart = false;
+ }
+
+ let useJQuery = !ninjapad.utils.isFullScreen() || ninjapad.utils.isIOSDevice();
+ let width = useJQuery ? $(window).width() : window.innerWidth;
+ let height = useJQuery ? $(window).height() : window.innerHeight;
+
+ if (height >= width || window.matchMedia("(orientation: portrait)").matches) {
+ let opacity = 1;
+ let bottom = "auto";
+
+ ninjapad.jQElement.screen.width("100%");
+ let newWidth = ninjapad.jQElement.screen.width();
+ ninjapad.jQElement.screen.height(240 * (newWidth / 256));
+
+ let padHeight = ninjapad.utils.vw(47.5);
+ let remainingHeight = height - ninjapad.jQElement.screen.height();
+ ninjapad.jQElement.gamepad.height(Math.max(padHeight, remainingHeight));
+
+ let difference = remainingHeight - padHeight;
+ if (difference < 0) {
+ opacity += (difference / (padHeight * 2));
+ bottom = 0;
+ }
+ ninjapad.jQElement.gamepad.css("bottom", bottom);
+ ninjapad.jQElement.gamepad.css("display", "block");
+
+ ninjapad.jQElement.controller.css("opacity", opacity);
+ ninjapad.jQElement.controller.show();
+
+ if (ninjapad.pause.state.cannotResume) {
+ ninjapad.pause.state.cannotResume = false;
+ ninjapad.pause.pauseEmulation();
+ }
+ DEBUG && console.log("Show touch controls");
+ }
+ else {
+ setDesktopLayout();
+ handleLandscapeMode();
+ DEBUG && console.log("Hide touch controls");
+ }
+ }
+
+ function handleLandscapeMode() {
+ ninjapad.pause.state.cannotResume = true;
+ ninjapad.pause.pauseEmulation(
+ ninjapad.utils.html(
+ "span", "pauseScreenContent",
+ `Landscape mode
+ not supported yet
+
+ Turn your device
+ upright to play`
+ )
+ );
+ }
+
+ return {
+ setPageLayout: function() {
+ ninjapad.utils.isMobileDevice() ? setMobileLayout() : setDesktopLayout();
+ setOSDLayout();
+ }
+ };
+}();
diff --git a/public/Repo/jsnes-ninjapad/ninjapad/lib/fflate.min.js b/public/Repo/jsnes-ninjapad/ninjapad/lib/fflate.min.js
new file mode 100644
index 0000000..dd1a046
--- /dev/null
+++ b/public/Repo/jsnes-ninjapad/ninjapad/lib/fflate.min.js
@@ -0,0 +1,24 @@
+/*
+MIT License
+
+Copyright (c) 2020 Arjun Barrett
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+!function(f){typeof module!='undefined'&&typeof exports=='object'?module.exports=f():typeof define!='undefined'&&define.amd?define(['fflate',f]):(typeof self!='undefined'?self:this).fflate=f()}(function(){var _e={};"use strict";var t=(typeof module!='undefined'&&typeof exports=='object'?function(_f){"use strict";var e,t=";var __w=require('worker_threads');__w.parentPort.on('message',function(m){onmessage({data:m})}),postMessage=function(m,t){__w.parentPort.postMessage(m,t)},close=process.exit;self=global";try{e=require("worker_threads").Worker}catch(e){}exports.default=e?function(r,n,o,a,s){var u=!1,i=new e(r+t,{eval:!0}).on("error",(function(e){return s(e,null)})).on("message",(function(e){return s(null,e)})).on("exit",(function(e){e&&!u&&s(Error("exited with code "+e),null)}));return i.postMessage(o,a),i.terminate=function(){return u=!0,e.prototype.terminate.call(i)},i}:function(e,t,r,n,o){setImmediate((function(){return o(Error("async operations unsupported - update to Node 12+ (or Node 10-11 with the --experimental-worker CLI flag)"),null)}));var a=function(){};return{terminate:a,postMessage:a}};return _f}:function(_f){"use strict";var e={};_f.default=function(r,t,s,a,n){var o=new Worker(e[t]||(e[t]=URL.createObjectURL(new Blob([r+';addEventListener("error",function(e){e=e.error;postMessage({$e$:[e.message,e.code,e.stack]})})'],{type:"text/javascript"}))));return o.onmessage=function(e){var r=e.data,t=r.$e$;if(t){var s=Error(t[0]);s.code=t[1],s.stack=t[2],n(s,null)}else n(null,r)},o.postMessage(s,a),o};return _f})({}),n=Uint8Array,r=Uint16Array,e=Uint32Array,i=new n([0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0,0]),o=new n([0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13,0,0]),a=new n([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]),s=function(t,n){for(var i=new r(31),o=0;o<31;++o)i[o]=n+=1<>>1|(21845&d)<<1;v[d]=((65280&(g=(61680&(g=(52428&g)>>>2|(13107&g)<<2))>>>4|(3855&g)<<4))>>>8|(255&g)<<8)>>>1}var y=function(t,n,e){for(var i=t.length,o=0,a=new r(n);o>>f]=h}else for(s=new r(i),o=0;o>>15-t[o]);return s},m=new n(288);for(d=0;d<144;++d)m[d]=8;for(d=144;d<256;++d)m[d]=9;for(d=256;d<280;++d)m[d]=7;for(d=280;d<288;++d)m[d]=8;var w=new n(32);for(d=0;d<32;++d)w[d]=5;var x=y(m,9,0),b=y(m,9,1),z=y(w,5,0),k=y(w,5,1),M=function(t){for(var n=t[0],r=1;rn&&(n=t[r]);return n},S=function(t,n,r){var e=n/8|0;return(t[e]|t[e+1]<<8)>>(7&n)&r},A=function(t,n){var r=n/8|0;return(t[r]|t[r+1]<<8|t[r+2]<<16)>>(7&n)},T=function(t){return(t+7)/8|0},D=function(t,i,o){(null==i||i<0)&&(i=0),(null==o||o>t.length)&&(o=t.length);var a=new(t instanceof r?r:t instanceof e?e:n)(o-i);return a.set(t.subarray(i,o)),a};_e.FlateErrorCode={UnexpectedEOF:0,InvalidBlockType:1,InvalidLengthLiteral:2,InvalidDistance:3,StreamFinished:4,NoStreamHandler:5,InvalidHeader:6,NoCallback:7,InvalidUTF8:8,ExtraFieldTooLong:9,InvalidDate:10,FilenameTooLong:11,StreamFinishing:12,InvalidZipData:13,UnknownCompressionMethod:14};var C=["unexpected EOF","invalid block type","invalid length/literal","invalid distance","stream finished","no stream handler",,"no callback","invalid UTF-8 data","extra field too long","date not in range 1980-2099","filename too long","stream finishing","invalid zip data"],U=function(t,n,r){var e=Error(n||C[t]);if(e.code=t,Error.captureStackTrace&&Error.captureStackTrace(e,U),!r)throw e;return e},I=function(t,r,e){var s=t.length;if(!s||e&&e.f&&!e.l)return r||new n(0);var u=!r||e,h=!e||e.i;e||(e={}),r||(r=new n(3*s));var c=function(t){var e=r.length;if(t>e){var i=new n(Math.max(2*e,t));i.set(r),r=i}},p=e.f||0,v=e.p||0,d=e.b||0,g=e.l,m=e.d,w=e.m,x=e.n,z=8*s;do{if(!g){p=S(t,v,1);var C=S(t,v+1,3);if(v+=3,!C){var I=t[(P=T(v)+4)-4]|t[P-3]<<8,F=P+I;if(F>s){h&&U(0);break}u&&c(d+I),r.set(t.subarray(P,F),d),e.b=d+=I,e.p=v=8*F,e.f=p;continue}if(1==C)g=b,m=k,w=9,x=5;else if(2==C){var E=S(t,v,31)+257,Z=S(t,v+10,15)+4,O=E+S(t,v+5,31)+1;v+=14;for(var G=new n(O),L=new n(19),q=0;q>>4)<16)G[q++]=P;else{var Y=0,J=0;for(16==P?(J=3+S(t,v,3),v+=2,Y=G[q-1]):17==P?(J=3+S(t,v,7),v+=3):18==P&&(J=11+S(t,v,127),v+=7);J--;)G[q++]=Y}}var K=G.subarray(0,E),Q=G.subarray(E);w=M(K),x=M(Q),g=y(K,w,1),m=y(Q,x,1)}else U(1);if(v>z){h&&U(0);break}}u&&c(d+131072);for(var R=(1<>>4;if((v+=15&Y)>z){h&&U(0);break}if(Y||U(2),X<256)r[d++]=X;else{if(256==X){W=v,g=null;break}var $=X-254;X>264&&($=S(t,v,(1<<(nt=i[q=X-257]))-1)+f[q],v+=nt);var _=m[A(t,v)&V],tt=_>>>4;if(_||U(3),v+=15&_,Q=l[tt],tt>3){var nt=o[tt];Q+=A(t,v)&(1<z){h&&U(0);break}u&&c(d+131072);for(var rt=d+$;d