From b32a7982d06852909f2da933b398a6fec49c13df Mon Sep 17 00:00:00 2001 From: octospacc Date: Sun, 3 Dec 2023 17:15:59 +0100 Subject: [PATCH] jsnes-ninjapad --- public/Repo/jsnes-ninjapad/index.html | 108 + public/Repo/jsnes-ninjapad/jsnes.js | 7556 +++++++++++++++++ public/Repo/jsnes-ninjapad/ninjapad-config.js | 7 + .../Repo/jsnes-ninjapad/ninjapad/gamepad.js | 219 + public/Repo/jsnes-ninjapad/ninjapad/index.js | 1 + .../Repo/jsnes-ninjapad/ninjapad/interface.js | 430 + public/Repo/jsnes-ninjapad/ninjapad/layout.js | 117 + .../jsnes-ninjapad/ninjapad/lib/fflate.min.js | 24 + .../jsnes-ninjapad/ninjapad/lib/sha256.js | 468 + .../ninjapad/lib/uint8-to-utf16.js | 103 + public/Repo/jsnes-ninjapad/ninjapad/main.js | 56 + public/Repo/jsnes-ninjapad/ninjapad/menu.js | 140 + .../Repo/jsnes-ninjapad/ninjapad/ninjapad.css | 237 + public/Repo/jsnes-ninjapad/ninjapad/pause.js | 48 + .../Repo/jsnes-ninjapad/ninjapad/res/ka1.ttf | Bin 0 -> 56908 bytes public/Repo/jsnes-ninjapad/ninjapad/utils.js | 162 + 16 files changed, 9676 insertions(+) create mode 100644 public/Repo/jsnes-ninjapad/index.html create mode 100644 public/Repo/jsnes-ninjapad/jsnes.js create mode 100644 public/Repo/jsnes-ninjapad/ninjapad-config.js create mode 100644 public/Repo/jsnes-ninjapad/ninjapad/gamepad.js create mode 100644 public/Repo/jsnes-ninjapad/ninjapad/index.js create mode 100644 public/Repo/jsnes-ninjapad/ninjapad/interface.js create mode 100644 public/Repo/jsnes-ninjapad/ninjapad/layout.js create mode 100644 public/Repo/jsnes-ninjapad/ninjapad/lib/fflate.min.js create mode 100644 public/Repo/jsnes-ninjapad/ninjapad/lib/sha256.js create mode 100644 public/Repo/jsnes-ninjapad/ninjapad/lib/uint8-to-utf16.js create mode 100644 public/Repo/jsnes-ninjapad/ninjapad/main.js create mode 100644 public/Repo/jsnes-ninjapad/ninjapad/menu.js create mode 100644 public/Repo/jsnes-ninjapad/ninjapad/ninjapad.css create mode 100644 public/Repo/jsnes-ninjapad/ninjapad/pause.js create mode 100644 public/Repo/jsnes-ninjapad/ninjapad/res/ka1.ttf create mode 100644 public/Repo/jsnes-ninjapad/ninjapad/utils.js 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>>8},E=function(t,n,r){var e=n/8|0;t[e]|=r<<=7&n,t[e+1]|=r>>>8,t[e+2]|=r>>>16},Z=function(t,e){for(var i=[],o=0;ov&&(v=s[o].s);var d=new r(v+1),g=O(i[l-1],d,0);if(g>e){o=0;var y=0,m=g-e,w=1<e))break;y+=w-(1<>>=m;y>0;){var b=s[o].s;d[b]=0&&y;--o){var z=s[o].s;d[z]==e&&(--d[z],++y)}g=e}return[new n(d),g]},O=function(t,n,r){return-1==t.s?Math.max(O(t.l,n,r+1),O(t.r,n,r+1)):n[t.s]=r},G=function(t){for(var n=t.length;n&&!t[--n];);for(var e=new r(++n),i=0,o=t[0],a=1,s=function(t){e[i++]=t},u=1;u<=n;++u)if(t[u]==o&&u!=n)++a;else{if(!o&&a>2){for(;a>138;a-=138)s(32754);a>2&&(s(a>10?a-11<<5|28690:a-3<<5|12305),a=0)}else if(a>3){for(s(o),--a;a>6;a-=6)s(8304);a>2&&(s(a-3<<5|8208),a=0)}for(;a--;)s(o);a=1,o=t[u]}return[e.subarray(0,i),n]},L=function(t,n){for(var r=0,e=0;e>>8,t[i+2]=255^t[i],t[i+3]=255^t[i+1];for(var o=0;o4&&!N[a[B-1]];--B);var Y,J,K,Q,R=p+5<<3,V=L(u,m)+L(f,w)+h,W=L(u,g)+L(f,M)+h+14+3*B+L(O,N)+(2*O[16]+3*O[17]+7*O[18]);if(R<=V&&R<=W)return q(n,v,t.subarray(l,l+p));if(F(n,v,1+(W15&&(F(n,v,tt[H]>>>5&127),v+=tt[H]>>>12)}}else Y=x,J=m,K=z,Q=w;for(H=0;H255){var nt;E(n,v,Y[257+(nt=s[H]>>>18&31)]),v+=J[nt+257],nt>7&&(F(n,v,s[H]>>>23&31),v+=i[nt]);var rt=31&s[H];E(n,v,K[rt]),v+=Q[rt],rt>3&&(E(n,v,s[H]>>>5&8191),v+=o[rt])}else E(n,v,Y[s[H]]),v+=J[s[H]];return E(n,v,Y[256]),v+J[256]},j=new e([65540,131080,131088,131104,262176,1048704,1048832,2114560,2117632]),N=new n(0),P=function(t,a,s,u,f,c){var l=t.length,v=new n(u+l+5*(1+Math.ceil(l/7e3))+f),d=v.subarray(u,v.length-f),g=0;if(!a||l<8)for(var y=0;y<=l;y+=65535){var m=y+65535;m>>13,b=8191&w,z=(1<7e3||O>24576)&&J>423){g=H(t,d,0,U,I,F,Z,O,L,y-L,g),O=E=Z=0,L=y;for(var K=0;K<286;++K)I[K]=0;for(K=0;K<30;++K)F[K]=0}var Q=2,R=0,V=b,W=B-Y&32767;if(J>2&&P==C(y-W))for(var X=Math.min(x,J)-1,$=Math.min(32767,y),_=Math.min(258,J);W<=$&&--V&&B!=Y;){if(t[y+Q]==t[y+Q-W]){for(var tt=0;tt<_&&t[y+tt]==t[y+tt-W];++tt);if(tt>Q){if(Q=tt,R=W,tt>X)break;var nt=Math.min(W,tt-2),rt=0;for(K=0;Krt&&(rt=it,Y=et)}}}W+=(B=Y)-(Y=k[B])+32768&32767}if(R){U[O++]=268435456|h[Q]<<18|p[R];var ot=31&h[Q],at=31&p[R];Z+=i[ot]+o[at],++I[257+ot],++F[at],G=y+Q,++E}else U[O++]=t[y],++I[t[y]]}}g=H(t,d,c,U,I,F,Z,O,L,y-L,g),!c&&7&g&&(g=q(d,g+1,N))}return D(v,0,u+T(g)+f)},B=function(){for(var t=new Int32Array(256),n=0;n<256;++n){for(var r=n,e=9;--e;)r=(1&r&&-306674912)^r>>>1;t[n]=r}return t}(),Y=function(){var t=-1;return{p:function(n){for(var r=t,e=0;e>>8;t=r},d:function(){return~t}}},J=function(){var t=1,n=0;return{p:function(r){for(var e=t,i=n,o=0|r.length,a=0;a!=o;){for(var s=Math.min(a+2655,o);a>16),i=(65535&i)+15*(i>>16)}t=e,n=i},d:function(){return(255&(t%=65521))<<24|t>>>8<<16|(255&(n%=65521))<<8|n>>>8}}},K=function(t,n,r,e,i){return P(t,null==n.level?6:n.level,null==n.mem?Math.ceil(1.5*Math.max(8,Math.min(13,Math.log(t.length)))):12+n.mem,r,e,!i)},Q=function(t,n){var r={};for(var e in t)r[e]=t[e];for(var e in n)r[e]=n[e];return r},R=function(t,n,r){for(var e=t(),i=""+t,o=i.slice(i.indexOf("[")+1,i.lastIndexOf("]")).replace(/ /g,"").split(","),a=0;a>>0},ct=function(t,n){return ht(t,n)+4294967296*ht(t,n+4)},lt=function(t,n,r){for(;r;++n)t[n]=r,r>>>=8},pt=function(t,n){var r=n.filename;if(t[0]=31,t[1]=139,t[2]=8,t[8]=n.level<2?4:9==n.level?2:0,t[9]=3,0!=n.mtime&<(t,4,Math.floor(new Date(n.mtime||Date.now())/1e3)),r){t[3]=8;for(var e=0;e<=r.length;++e)t[e+10]=r.charCodeAt(e)}},vt=function(t){31==t[0]&&139==t[1]&&8==t[2]||U(6,"invalid gzip data");var n=t[3],r=10;4&n&&(r+=t[10]|2+(t[11]<<8));for(var e=(n>>3&1)+(n>>4&1);e>0;e-=!t[r++]);return r+(2&n)},dt=function(t){var n=t.length;return(t[n-4]|t[n-3]<<8|t[n-2]<<16|t[n-1]<<24)>>>0},gt=function(t){return 10+(t.filename&&t.filename.length+1||0)},yt=function(t,n){var r=n.level,e=0==r?0:r<6?1:9==r?3:2;t[0]=120,t[1]=e<<6|(e?32-2*e:1)},mt=function(t){(8!=(15&t[0])||t[0]>>>4>7||(t[0]<<8|t[1])%31)&&U(6,"invalid zlib data"),32&t[1]&&U(6,"invalid zlib data: preset dictionaries not supported")};function wt(t,n){return n||"function"!=typeof t||(n=t,t={}),this.ondata=n,t}var xt=function(){function t(t,n){n||"function"!=typeof t||(n=t,t={}),this.ondata=n,this.o=t||{}}return t.prototype.p=function(t,n){this.ondata(K(t,this.o,0,0,!n),n)},t.prototype.push=function(t,n){this.ondata||U(5),this.d&&U(4),this.d=n,this.p(t,n||!1)},t}();_e.Deflate=xt;var bt=function(){return function(t,n){ut([_,function(){return[st,xt]}],this,wt.call(this,t,n),(function(t){var n=new xt(t.data);onmessage=st(n)}),6)}}();function zt(t,n,r){return r||(r=n,n={}),"function"!=typeof r&&U(7),at(t,n,[_],(function(t){return it(kt(t.data[0],t.data[1]))}),0,r)}function kt(t,n){return K(t,n||{},0,0)}_e.AsyncDeflate=bt,_e.deflate=zt,_e.deflateSync=kt;var Mt=function(){function t(t){this.s={},this.p=new n(0),this.ondata=t}return t.prototype.e=function(t){this.ondata||U(5),this.d&&U(4);var r=this.p.length,e=new n(r+t.length);e.set(this.p),e.set(t,r),this.p=e},t.prototype.c=function(t){this.d=this.s.i=t||!1;var n=this.s.b,r=I(this.p,this.o,this.s);this.ondata(D(r,n,this.s.b),this.d),this.o=D(r,this.s.b-32768),this.s.b=this.o.length,this.p=D(this.p,this.s.p/8|0),this.s.p&=7},t.prototype.push=function(t,n){this.e(t),this.c(n)},t}();_e.Inflate=Mt;var St=function(){return function(t){this.ondata=t,ut([$,function(){return[st,Mt]}],this,0,(function(){var t=new Mt;onmessage=st(t)}),7)}}();function At(t,n,r){return r||(r=n,n={}),"function"!=typeof r&&U(7),at(t,n,[$],(function(t){return it(Tt(t.data[0],ot(t.data[1])))}),1,r)}function Tt(t,n){return I(t,n)}_e.AsyncInflate=St,_e.inflate=At,_e.inflateSync=Tt;var Dt=function(){function t(t,n){this.c=Y(),this.l=0,this.v=1,xt.call(this,t,n)}return t.prototype.push=function(t,n){xt.prototype.push.call(this,t,n)},t.prototype.p=function(t,n){this.c.p(t),this.l+=t.length;var r=K(t,this.o,this.v&>(this.o),n&&8,!n);this.v&&(pt(r,this.o),this.v=0),n&&(lt(r,r.length-8,this.c.d()),lt(r,r.length-4,this.l)),this.ondata(r,n)},t}();_e.Gzip=Dt,_e.Compress=Dt;var Ct=function(){return function(t,n){ut([_,tt,function(){return[st,xt,Dt]}],this,wt.call(this,t,n),(function(t){var n=new Dt(t.data);onmessage=st(n)}),8)}}();function Ut(t,n,r){return r||(r=n,n={}),"function"!=typeof r&&U(7),at(t,n,[_,tt,function(){return[It]}],(function(t){return it(It(t.data[0],t.data[1]))}),2,r)}function It(t,n){n||(n={});var r=Y(),e=t.length;r.p(t);var i=K(t,n,gt(n),8),o=i.length;return pt(i,n),lt(i,o-8,r.d()),lt(i,o-4,e),i}_e.AsyncGzip=Ct,_e.AsyncCompress=Ct,_e.gzip=Ut,_e.compress=Ut,_e.gzipSync=It,_e.compressSync=It;var Ft=function(){function t(t){this.v=1,Mt.call(this,t)}return t.prototype.push=function(t,n){if(Mt.prototype.e.call(this,t),this.v){var r=this.p.length>3?vt(this.p):4;if(r>=this.p.length&&!n)return;this.p=this.p.subarray(r),this.v=0}n&&(this.p.length<8&&U(6,"invalid gzip data"),this.p=this.p.subarray(0,-8)),Mt.prototype.c.call(this,n)},t}();_e.Gunzip=Ft;var Et=function(){return function(t){this.ondata=t,ut([$,nt,function(){return[st,Mt,Ft]}],this,0,(function(){var t=new Ft;onmessage=st(t)}),9)}}();function Zt(t,n,r){return r||(r=n,n={}),"function"!=typeof r&&U(7),at(t,n,[$,nt,function(){return[Ot]}],(function(t){return it(Ot(t.data[0]))}),3,r)}function Ot(t,r){return I(t.subarray(vt(t),-8),r||new n(dt(t)))}_e.AsyncGunzip=Et,_e.gunzip=Zt,_e.gunzipSync=Ot;var Gt=function(){function t(t,n){this.c=J(),this.v=1,xt.call(this,t,n)}return t.prototype.push=function(t,n){xt.prototype.push.call(this,t,n)},t.prototype.p=function(t,n){this.c.p(t);var r=K(t,this.o,this.v&&2,n&&4,!n);this.v&&(yt(r,this.o),this.v=0),n&<(r,r.length-4,this.c.d()),this.ondata(r,n)},t}();_e.Zlib=Gt;var Lt=function(){return function(t,n){ut([_,rt,function(){return[st,xt,Gt]}],this,wt.call(this,t,n),(function(t){var n=new Gt(t.data);onmessage=st(n)}),10)}}();function qt(t,n,r){return r||(r=n,n={}),"function"!=typeof r&&U(7),at(t,n,[_,rt,function(){return[Ht]}],(function(t){return it(Ht(t.data[0],t.data[1]))}),4,r)}function Ht(t,n){n||(n={});var r=J();r.p(t);var e=K(t,n,2,4);return yt(e,n),lt(e,e.length-4,r.d()),e}_e.AsyncZlib=Lt,_e.zlib=qt,_e.zlibSync=Ht;var jt=function(){function t(t){this.v=1,Mt.call(this,t)}return t.prototype.push=function(t,n){if(Mt.prototype.e.call(this,t),this.v){if(this.p.length<2&&!n)return;this.p=this.p.subarray(2),this.v=0}n&&(this.p.length<4&&U(6,"invalid zlib data"),this.p=this.p.subarray(0,-4)),Mt.prototype.c.call(this,n)},t}();_e.Unzlib=jt;var Nt=function(){return function(t){this.ondata=t,ut([$,et,function(){return[st,Mt,jt]}],this,0,(function(){var t=new jt;onmessage=st(t)}),11)}}();function Pt(t,n,r){return r||(r=n,n={}),"function"!=typeof r&&U(7),at(t,n,[$,et,function(){return[Bt]}],(function(t){return it(Bt(t.data[0],ot(t.data[1])))}),5,r)}function Bt(t,n){return I((mt(t),t.subarray(2,-4)),n)}_e.AsyncUnzlib=Nt,_e.unzlib=Pt,_e.unzlibSync=Bt;var Yt=function(){function t(t){this.G=Ft,this.I=Mt,this.Z=jt,this.ondata=t}return t.prototype.push=function(t,r){if(this.ondata||U(5),this.s)this.s.push(t,r);else{if(this.p&&this.p.length){var e=new n(this.p.length+t.length);e.set(this.p),e.set(t,this.p.length)}else this.p=t;if(this.p.length>2){var i=this,o=function(){i.ondata.apply(i,arguments)};this.s=31==this.p[0]&&139==this.p[1]&&8==this.p[2]?new this.G(o):8!=(15&this.p[0])||this.p[0]>>4>7||(this.p[0]<<8|this.p[1])%31?new this.I(o):new this.Z(o),this.s.push(this.p,r),this.p=null}}},t}();_e.Decompress=Yt;var Jt=function(){function t(t){this.G=Et,this.I=St,this.Z=Nt,this.ondata=t}return t.prototype.push=function(t,n){Yt.prototype.push.call(this,t,n)},t}();function Kt(t,n,r){return r||(r=n,n={}),"function"!=typeof r&&U(7),31==t[0]&&139==t[1]&&8==t[2]?Zt(t,n,r):8!=(15&t[0])||t[0]>>4>7||(t[0]<<8|t[1])%31?At(t,n,r):Pt(t,n,r)}function Qt(t,n){return 31==t[0]&&139==t[1]&&8==t[2]?Ot(t,n):8!=(15&t[0])||t[0]>>4>7||(t[0]<<8|t[1])%31?Tt(t,n):Bt(t,n)}_e.AsyncDecompress=Jt,_e.decompress=Kt,_e.decompressSync=Qt;var Rt=function(t,r,e,i){for(var o in t){var a=t[o],s=r+o;a instanceof n?e[s]=[a,i]:Array.isArray(a)?e[s]=[a[0],Q(i,a[1])]:Rt(a,s+"/",e,i)}},Vt="undefined"!=typeof TextEncoder&&new TextEncoder,Wt="undefined"!=typeof TextDecoder&&new TextDecoder,Xt=0;try{Wt.decode(N,{stream:!0}),Xt=1}catch(t){}var $t=function(t){for(var n="",r=0;;){var e=t[r++],i=(e>127)+(e>223)+(e>239);if(r+i>t.length)return[n,D(t,r-1)];i?3==i?(e=((15&e)<<18|(63&t[r++])<<12|(63&t[r++])<<6|63&t[r++])-65536,n+=String.fromCharCode(55296|e>>10,56320|1023&e)):n+=String.fromCharCode(1&i?(31&e)<<6|63&t[r++]:(15&e)<<12|(63&t[r++])<<6|63&t[r++]):n+=String.fromCharCode(e)}},_t=function(){function t(t){this.ondata=t,Xt?this.t=new TextDecoder:this.p=N}return t.prototype.push=function(t,r){if(this.ondata||U(5),r=!!r,this.t)return this.ondata(this.t.decode(t,{stream:!0}),r),void(r&&(this.t.decode().length&&U(8),this.t=null));this.p||U(4);var e=new n(this.p.length+t.length);e.set(this.p),e.set(t,this.p.length);var i=$t(e),o=i[0],a=i[1];r?(a.length&&U(8),this.p=null):this.p=a,this.ondata(o,r)},t}();_e.DecodeUTF8=_t;var tn=function(){function t(t){this.ondata=t}return t.prototype.push=function(t,n){this.ondata||U(5),this.d&&U(4),this.ondata(nn(t),this.d=n||!1)},t}();function nn(t,r){if(r){for(var e=new n(t.length),i=0;i>1)),s=0,u=function(t){a[s++]=t};for(i=0;ia.length){var f=new n(s+8+(o-i<<1));f.set(a),a=f}var h=t.charCodeAt(i);h<128||r?u(h):h<2048?(u(192|h>>6),u(128|63&h)):h>55295&&h<57344?(u(240|(h=65536+(1047552&h)|1023&t.charCodeAt(++i))>>18),u(128|h>>12&63),u(128|h>>6&63),u(128|63&h)):(u(224|h>>12),u(128|h>>6&63),u(128|63&h))}return D(a,0,s)}function rn(t,n){if(n){for(var r="",e=0;e65535&&U(9),n+=e+4}return n},fn=function(t,n,r,e,i,o,a,s){var u=e.length,f=r.extra,h=s&&s.length,c=un(f);lt(t,n,null!=a?33639248:67324752),n+=4,null!=a&&(t[n++]=20,t[n++]=r.os),t[n]=20,n+=2,t[n++]=r.flag<<1|(null==o&&8),t[n++]=i&&8,t[n++]=255&r.compression,t[n++]=r.compression>>8;var l=new Date(null==r.mtime?Date.now():r.mtime),p=l.getFullYear()-1980;if((p<0||p>119)&&U(10),lt(t,n,p<<25|l.getMonth()+1<<21|l.getDate()<<16|l.getHours()<<11|l.getMinutes()<<5|l.getSeconds()>>>1),n+=4,null!=o&&(lt(t,n,r.crc),lt(t,n+4,o),lt(t,n+8,r.size)),lt(t,n+12,u),lt(t,n+14,c),n+=16,null!=a&&(lt(t,n,h),lt(t,n+6,r.attrs),lt(t,n+10,a),n+=14),t.set(e,n),n+=u,c)for(var v in f){var d=f[v],g=d.length;lt(t,n,+v),lt(t,n+2,g),t.set(d,n+4),n+=4+g}return h&&(t.set(s,n),n+=h),n},hn=function(t,n,r,e,i){lt(t,n,101010256),lt(t,n+8,r),lt(t,n+10,r),lt(t,n+12,e),lt(t,n+16,i)},cn=function(){function t(t){this.filename=t,this.c=Y(),this.size=0,this.compression=0}return t.prototype.process=function(t,n){this.ondata(null,t,n)},t.prototype.push=function(t,n){this.ondata||U(5),this.c.p(t),this.size+=t.length,n&&(this.crc=this.c.d()),this.process(t,n||!1)},t}();_e.ZipPassThrough=cn;var ln=function(){function t(t,n){var r=this;n||(n={}),cn.call(this,t),this.d=new xt(n,(function(t,n){r.ondata(null,t,n)})),this.compression=8,this.flag=en(n.level)}return t.prototype.process=function(t,n){try{this.d.push(t,n)}catch(t){this.ondata(t,null,n)}},t.prototype.push=function(t,n){cn.prototype.push.call(this,t,n)},t}();_e.ZipDeflate=ln;var pn=function(){function t(t,n){var r=this;n||(n={}),cn.call(this,t),this.d=new bt(n,(function(t,n,e){r.ondata(t,n,e)})),this.compression=8,this.flag=en(n.level),this.terminate=this.d.terminate}return t.prototype.process=function(t,n){this.d.push(t,n)},t.prototype.push=function(t,n){cn.prototype.push.call(this,t,n)},t}();_e.AsyncZipDeflate=pn;var vn=function(){function t(t){this.ondata=t,this.u=[],this.d=1}return t.prototype.add=function(t){var r=this;if(this.ondata||U(5),2&this.d)this.ondata(U(4+8*(1&this.d),0,1),null,!1);else{var e=nn(t.filename),i=e.length,o=t.comment,a=o&&nn(o),s=i!=t.filename.length||a&&o.length!=a.length,u=i+un(t.extra)+30;i>65535&&this.ondata(U(11,0,1),null,!1);var f=new n(u);fn(f,0,t,e,s);var h=[f],c=function(){for(var t=0,n=h;t65535&&M(U(11,0,1),null),k)if(g<16e4)try{M(null,kt(e,f))}catch(t){M(t,null)}else c.push(zt(e,f,M));else M(null,e)},g=0;g65535&&U(11);var y=c?kt(f,h):f,m=y.length,w=Y();w.p(f),i.push(Q(h,{size:f.length,crc:w.d(),c:y,f:M,m:v,u:l!=s.length||v&&p.length!=d,o:o,compression:c})),o+=30+l+g+m,a+=76+2*(l+g)+(d||0)+m}for(var x=new n(a+22),b=o,z=a-o,k=0;k0){var i=Math.min(this.c,t.length),o=t.subarray(0,i);if(this.c-=i,this.d?this.d.push(o,!this.c):this.k[0].push(o),(t=t.subarray(i)).length)return this.push(t,r)}else{var a=0,s=0,u=void 0,f=void 0;this.p.length?t.length?((f=new n(this.p.length+t.length)).set(this.p),f.set(t,this.p.length)):f=this.p:f=t;for(var h=f.length,c=this.c,l=c&&this.d,p=function(){var t,n=ht(f,s);if(67324752==n){a=1,u=s,v.d=null,v.c=0;var r=ft(f,s+6),i=ft(f,s+8),o=2048&r,l=8&r,p=ft(f,s+26),d=ft(f,s+28);if(h>s+30+p+d){var g=[];v.k.unshift(g),a=2;var y,m=ht(f,s+18),w=ht(f,s+22),x=rn(f.subarray(s+30,s+=30+p),!o);4294967295==m?(t=l?[-2]:sn(f,s),m=t[0],w=t[1]):l&&(m=-1),s+=d,v.c=m;var b={name:x,compression:i,start:function(){if(b.ondata||U(5),m){var t=e.o[i];t||b.ondata(U(14,"unknown compression type "+i,1),null,!1),(y=m<0?new t(x):new t(x,m,w)).ondata=function(t,n,r){b.ondata(t,n,r)};for(var n=0,r=g;n=0&&(b.size=m,b.originalSize=w),v.onfile(b)}return"break"}if(c){if(134695760==n)return u=s+=12+(-2==c&&8),a=3,v.c=0,"break";if(33639248==n)return u=s-=4,a=3,v.c=0,"break"}},v=this;s65558)return s(U(13,0,1),null),o;var f=ft(t,u+8);if(f){var h=f,c=ht(t,u+16),l=4294967295==c;if(l){if(u=ht(t,u-12),101075792!=ht(t,u))return s(U(13,0,1),null),o;h=f=ht(t,u+32),c=ht(t,u+48)}for(var p=r&&r.filter,v=function(r){var e=an(t,c,l),u=e[0],h=e[1],v=e[2],d=e[3],g=e[4],y=on(t,e[5]);c=g;var m=function(t,n){t?(o(),s(t,null)):(n&&(a[d]=n),--f||s(null,a))};if(!p||p({name:d,size:h,originalSize:v,compression:u}))if(u)if(8==u){var w=t.subarray(y,y+h);if(h<32e4)try{m(null,Tt(w,new n(v)))}catch(t){m(t,null)}else i.push(At(w,{size:v},m))}else m(U(14,"unknown compression type "+u,1),null);else m(null,D(t,y,y+h));else m(null,null)},d=0;d65558)&&U(13);var o=ft(t,i+8);if(!o)return{};var a=ht(t,i+16),s=4294967295==a;s&&(i=ht(t,i-12),101075792!=ht(t,i)&&U(13),o=ht(t,i+32),a=ht(t,i+48));for(var u=r&&r.filter,f=0;f> 2] |= message[index] << SHIFT[i++ & 3]; + } + } else { + for (i = this.start; index < length && i < 64; ++index) { + code = message.charCodeAt(index); + if (code < 0x80) { + blocks[i >> 2] |= code << SHIFT[i++ & 3]; + } else if (code < 0x800) { + blocks[i >> 2] |= (0xc0 | (code >> 6)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; + } else if (code < 0xd800 || code >= 0xe000) { + blocks[i >> 2] |= (0xe0 | (code >> 12)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; + } else { + code = 0x10000 + (((code & 0x3ff) << 10) | (message.charCodeAt(++index) & 0x3ff)); + blocks[i >> 2] |= (0xf0 | (code >> 18)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | ((code >> 12) & 0x3f)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; + } + } + } + this.lastByteIndex = i; + this.bytes += i - this.start; + if (i >= 64) { + this.block = blocks[16]; + this.start = i - 64; + this.hash(); + this.hashed = true; + } else { + this.start = i; + } + } + if (this.bytes > 4294967295) { + this.hBytes += this.bytes / 4294967296 << 0; + this.bytes = this.bytes % 4294967296; + } + return this; + }; + Sha256.prototype.finalize = function() { + if (this.finalized) { + return; + } + this.finalized = true; + var blocks = this.blocks, + i = this.lastByteIndex; + blocks[16] = this.block; + blocks[i >> 2] |= EXTRA[i & 3]; + this.block = blocks[16]; + if (i >= 56) { + if (!this.hashed) { + this.hash(); + } + blocks[0] = this.block; + blocks[16] = blocks[1] = blocks[2] = blocks[3] = blocks[4] = blocks[5] = blocks[6] = blocks[7] = blocks[8] = blocks[9] = blocks[10] = blocks[11] = blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0; + } + blocks[14] = this.hBytes << 3 | this.bytes >>> 29; + blocks[15] = this.bytes << 3; + this.hash(); + }; + Sha256.prototype.hash = function() { + var a = this.h0, + b = this.h1, + c = this.h2, + d = this.h3, + e = this.h4, + f = this.h5, + g = this.h6, + h = this.h7, + blocks = this.blocks, + j, s0, s1, maj, t1, t2, ch, ab, da, cd, bc; + for (j = 16; j < 64; ++j) { + // rightrotate + t1 = blocks[j - 15]; + s0 = ((t1 >>> 7) | (t1 << 25)) ^ ((t1 >>> 18) | (t1 << 14)) ^ (t1 >>> 3); + t1 = blocks[j - 2]; + s1 = ((t1 >>> 17) | (t1 << 15)) ^ ((t1 >>> 19) | (t1 << 13)) ^ (t1 >>> 10); + blocks[j] = blocks[j - 16] + s0 + blocks[j - 7] + s1 << 0; + } + bc = b & c; + for (j = 0; j < 64; j += 4) { + if (this.first) { + if (this.is224) { + ab = 300032; + t1 = blocks[0] - 1413257819; + h = t1 - 150054599 << 0; + d = t1 + 24177077 << 0; + } else { + ab = 704751109; + t1 = blocks[0] - 210244248; + h = t1 - 1521486534 << 0; + d = t1 + 143694565 << 0; + } + this.first = false; + } else { + s0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10)); + s1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7)); + ab = a & b; + maj = ab ^ (a & c) ^ bc; + ch = (e & f) ^ (~e & g); + t1 = h + s1 + ch + K[j] + blocks[j]; + t2 = s0 + maj; + h = d + t1 << 0; + d = t1 + t2 << 0; + } + s0 = ((d >>> 2) | (d << 30)) ^ ((d >>> 13) | (d << 19)) ^ ((d >>> 22) | (d << 10)); + s1 = ((h >>> 6) | (h << 26)) ^ ((h >>> 11) | (h << 21)) ^ ((h >>> 25) | (h << 7)); + da = d & a; + maj = da ^ (d & b) ^ ab; + ch = (h & e) ^ (~h & f); + t1 = g + s1 + ch + K[j + 1] + blocks[j + 1]; + t2 = s0 + maj; + g = c + t1 << 0; + c = t1 + t2 << 0; + s0 = ((c >>> 2) | (c << 30)) ^ ((c >>> 13) | (c << 19)) ^ ((c >>> 22) | (c << 10)); + s1 = ((g >>> 6) | (g << 26)) ^ ((g >>> 11) | (g << 21)) ^ ((g >>> 25) | (g << 7)); + cd = c & d; + maj = cd ^ (c & a) ^ da; + ch = (g & h) ^ (~g & e); + t1 = f + s1 + ch + K[j + 2] + blocks[j + 2]; + t2 = s0 + maj; + f = b + t1 << 0; + b = t1 + t2 << 0; + s0 = ((b >>> 2) | (b << 30)) ^ ((b >>> 13) | (b << 19)) ^ ((b >>> 22) | (b << 10)); + s1 = ((f >>> 6) | (f << 26)) ^ ((f >>> 11) | (f << 21)) ^ ((f >>> 25) | (f << 7)); + bc = b & c; + maj = bc ^ (b & d) ^ cd; + ch = (f & g) ^ (~f & h); + t1 = e + s1 + ch + K[j + 3] + blocks[j + 3]; + t2 = s0 + maj; + e = a + t1 << 0; + a = t1 + t2 << 0; + } + this.h0 = this.h0 + a << 0; + this.h1 = this.h1 + b << 0; + this.h2 = this.h2 + c << 0; + this.h3 = this.h3 + d << 0; + this.h4 = this.h4 + e << 0; + this.h5 = this.h5 + f << 0; + this.h6 = this.h6 + g << 0; + this.h7 = this.h7 + h << 0; + }; + Sha256.prototype.hex = function() { + this.finalize(); + var h0 = this.h0, + h1 = this.h1, + h2 = this.h2, + h3 = this.h3, + h4 = this.h4, + h5 = this.h5, + h6 = this.h6, + h7 = this.h7; + var hex = HEX_CHARS[(h0 >> 28) & 0x0F] + HEX_CHARS[(h0 >> 24) & 0x0F] + HEX_CHARS[(h0 >> 20) & 0x0F] + HEX_CHARS[(h0 >> 16) & 0x0F] + HEX_CHARS[(h0 >> 12) & 0x0F] + HEX_CHARS[(h0 >> 8) & 0x0F] + HEX_CHARS[(h0 >> 4) & 0x0F] + HEX_CHARS[h0 & 0x0F] + HEX_CHARS[(h1 >> 28) & 0x0F] + HEX_CHARS[(h1 >> 24) & 0x0F] + HEX_CHARS[(h1 >> 20) & 0x0F] + HEX_CHARS[(h1 >> 16) & 0x0F] + HEX_CHARS[(h1 >> 12) & 0x0F] + HEX_CHARS[(h1 >> 8) & 0x0F] + HEX_CHARS[(h1 >> 4) & 0x0F] + HEX_CHARS[h1 & 0x0F] + HEX_CHARS[(h2 >> 28) & 0x0F] + HEX_CHARS[(h2 >> 24) & 0x0F] + HEX_CHARS[(h2 >> 20) & 0x0F] + HEX_CHARS[(h2 >> 16) & 0x0F] + HEX_CHARS[(h2 >> 12) & 0x0F] + HEX_CHARS[(h2 >> 8) & 0x0F] + HEX_CHARS[(h2 >> 4) & 0x0F] + HEX_CHARS[h2 & 0x0F] + HEX_CHARS[(h3 >> 28) & 0x0F] + HEX_CHARS[(h3 >> 24) & 0x0F] + HEX_CHARS[(h3 >> 20) & 0x0F] + HEX_CHARS[(h3 >> 16) & 0x0F] + HEX_CHARS[(h3 >> 12) & 0x0F] + HEX_CHARS[(h3 >> 8) & 0x0F] + HEX_CHARS[(h3 >> 4) & 0x0F] + HEX_CHARS[h3 & 0x0F] + HEX_CHARS[(h4 >> 28) & 0x0F] + HEX_CHARS[(h4 >> 24) & 0x0F] + HEX_CHARS[(h4 >> 20) & 0x0F] + HEX_CHARS[(h4 >> 16) & 0x0F] + HEX_CHARS[(h4 >> 12) & 0x0F] + HEX_CHARS[(h4 >> 8) & 0x0F] + HEX_CHARS[(h4 >> 4) & 0x0F] + HEX_CHARS[h4 & 0x0F] + HEX_CHARS[(h5 >> 28) & 0x0F] + HEX_CHARS[(h5 >> 24) & 0x0F] + HEX_CHARS[(h5 >> 20) & 0x0F] + HEX_CHARS[(h5 >> 16) & 0x0F] + HEX_CHARS[(h5 >> 12) & 0x0F] + HEX_CHARS[(h5 >> 8) & 0x0F] + HEX_CHARS[(h5 >> 4) & 0x0F] + HEX_CHARS[h5 & 0x0F] + HEX_CHARS[(h6 >> 28) & 0x0F] + HEX_CHARS[(h6 >> 24) & 0x0F] + HEX_CHARS[(h6 >> 20) & 0x0F] + HEX_CHARS[(h6 >> 16) & 0x0F] + HEX_CHARS[(h6 >> 12) & 0x0F] + HEX_CHARS[(h6 >> 8) & 0x0F] + HEX_CHARS[(h6 >> 4) & 0x0F] + HEX_CHARS[h6 & 0x0F]; + if (!this.is224) { + hex += HEX_CHARS[(h7 >> 28) & 0x0F] + HEX_CHARS[(h7 >> 24) & 0x0F] + HEX_CHARS[(h7 >> 20) & 0x0F] + HEX_CHARS[(h7 >> 16) & 0x0F] + HEX_CHARS[(h7 >> 12) & 0x0F] + HEX_CHARS[(h7 >> 8) & 0x0F] + HEX_CHARS[(h7 >> 4) & 0x0F] + HEX_CHARS[h7 & 0x0F]; + } + return hex; + }; + Sha256.prototype.toString = Sha256.prototype.hex; + Sha256.prototype.digest = function() { + this.finalize(); + var h0 = this.h0, + h1 = this.h1, + h2 = this.h2, + h3 = this.h3, + h4 = this.h4, + h5 = this.h5, + h6 = this.h6, + h7 = this.h7; + var arr = [ + (h0 >> 24) & 0xFF, (h0 >> 16) & 0xFF, (h0 >> 8) & 0xFF, h0 & 0xFF, + (h1 >> 24) & 0xFF, (h1 >> 16) & 0xFF, (h1 >> 8) & 0xFF, h1 & 0xFF, + (h2 >> 24) & 0xFF, (h2 >> 16) & 0xFF, (h2 >> 8) & 0xFF, h2 & 0xFF, + (h3 >> 24) & 0xFF, (h3 >> 16) & 0xFF, (h3 >> 8) & 0xFF, h3 & 0xFF, + (h4 >> 24) & 0xFF, (h4 >> 16) & 0xFF, (h4 >> 8) & 0xFF, h4 & 0xFF, + (h5 >> 24) & 0xFF, (h5 >> 16) & 0xFF, (h5 >> 8) & 0xFF, h5 & 0xFF, + (h6 >> 24) & 0xFF, (h6 >> 16) & 0xFF, (h6 >> 8) & 0xFF, h6 & 0xFF + ]; + if (!this.is224) { + arr.push((h7 >> 24) & 0xFF, (h7 >> 16) & 0xFF, (h7 >> 8) & 0xFF, h7 & 0xFF); + } + return arr; + }; + Sha256.prototype.array = Sha256.prototype.digest; + Sha256.prototype.arrayBuffer = function() { + this.finalize(); + var buffer = new ArrayBuffer(this.is224 ? 28 : 32); + var dataView = new DataView(buffer); + dataView.setUint32(0, this.h0); + dataView.setUint32(4, this.h1); + dataView.setUint32(8, this.h2); + dataView.setUint32(12, this.h3); + dataView.setUint32(16, this.h4); + dataView.setUint32(20, this.h5); + dataView.setUint32(24, this.h6); + if (!this.is224) { + dataView.setUint32(28, this.h7); + } + return buffer; + }; + + function HmacSha256(key, is224, sharedMemory) { + var i, type = typeof key; + if (type === 'string') { + var bytes = [], + length = key.length, + index = 0, + code; + for (i = 0; i < length; ++i) { + code = key.charCodeAt(i); + if (code < 0x80) { + bytes[index++] = code; + } else if (code < 0x800) { + bytes[index++] = (0xc0 | (code >> 6)); + bytes[index++] = (0x80 | (code & 0x3f)); + } else if (code < 0xd800 || code >= 0xe000) { + bytes[index++] = (0xe0 | (code >> 12)); + bytes[index++] = (0x80 | ((code >> 6) & 0x3f)); + bytes[index++] = (0x80 | (code & 0x3f)); + } else { + code = 0x10000 + (((code & 0x3ff) << 10) | (key.charCodeAt(++i) & 0x3ff)); + bytes[index++] = (0xf0 | (code >> 18)); + bytes[index++] = (0x80 | ((code >> 12) & 0x3f)); + bytes[index++] = (0x80 | ((code >> 6) & 0x3f)); + bytes[index++] = (0x80 | (code & 0x3f)); + } + } + key = bytes; + } else { + if (type === 'object') { + if (key === null) { + throw new Error(ERROR); + } else if (ARRAY_BUFFER && key.constructor === ArrayBuffer) { + key = new Uint8Array(key); + } else if (!Array.isArray(key)) { + if (!ARRAY_BUFFER || !ArrayBuffer.isView(key)) { + throw new Error(ERROR); + } + } + } else { + throw new Error(ERROR); + } + } + if (key.length > 64) { + key = (new Sha256(is224, true)).update(key).array(); + } + var oKeyPad = [], + iKeyPad = []; + for (i = 0; i < 64; ++i) { + var b = key[i] || 0; + oKeyPad[i] = 0x5c ^ b; + iKeyPad[i] = 0x36 ^ b; + } + Sha256.call(this, is224, sharedMemory); + this.update(iKeyPad); + this.oKeyPad = oKeyPad; + this.inner = true; + this.sharedMemory = sharedMemory; + } + HmacSha256.prototype = new Sha256(); + HmacSha256.prototype.finalize = function() { + Sha256.prototype.finalize.call(this); + if (this.inner) { + this.inner = false; + var innerHash = this.array(); + Sha256.call(this, this.is224, this.sharedMemory); + this.update(this.oKeyPad); + this.update(innerHash); + Sha256.prototype.finalize.call(this); + } + }; + var exports = createMethod(); + exports.sha256 = exports; + exports.sha224 = createMethod(true); + exports.sha256.hmac = createHmacMethod(); + exports.sha224.hmac = createHmacMethod(true); + if (COMMON_JS) { + module.exports = exports; + } else { + root.sha256 = exports.sha256; + root.sha224 = exports.sha224; + if (AMD) { + define(function() { + return exports; + }); + } + } +})(); diff --git a/public/Repo/jsnes-ninjapad/ninjapad/lib/uint8-to-utf16.js b/public/Repo/jsnes-ninjapad/ninjapad/lib/uint8-to-utf16.js new file mode 100644 index 0000000..bff3590 --- /dev/null +++ b/public/Repo/jsnes-ninjapad/ninjapad/lib/uint8-to-utf16.js @@ -0,0 +1,103 @@ +/* +ISC License + +Copyright (c) 2020, Andrea Giammarchi, @WebReflection + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +*/ + +/* +Ninja Dynamics 20-Sep-2021: +- Modified both functions to allow for safe usage with localStorage +*/ + +var uint8ToUtf16 = (function (exports) { + 'use strict'; + + /* Added by Ninja Dynamics */ + function _isInvalidChar(char) { + /* https://stackoverflow.com/questions/11170716/are-there-any-characters-that-are-not-allowed-in-localstorage */ + return (char < 0x20 || (char >= 0xD800 && char < 0xE000) || char > 0xFFFD); + } + + /*! (c) Andrea Giammarchi @WebReflection */ + var ceil = Math.ceil; + var fromCharCode = String.fromCharCode; + + /* Modified by Ninja Dynamics */ + var encode = function encode(uint8array) { + var char; + var extra = 0; + var output = []; + var modified = ""; + var length = uint8array.length; + var len = ceil(length / 2); + + for (var j = 0, i = 0; i < len; i++) { + char = (uint8array[j++] << 8) + (j < length ? uint8array[j++] : extra++); + if (_isInvalidChar(char)) { + char = (char + 0x800).mod(0x10000); + modified += (output.length.toString(16).padStart(4, '0')); + } + output.push(fromCharCode(char)); + } + + if (_isInvalidChar(extra)) { + extra = (extra + 0x800).mod(0x10000); + modified += (output.length.toString(16).padStart(4, '0')); + } + output.push(fromCharCode(extra)); + + return modified.length + '!' + modified + output.join(''); + }; + + /* Modified by Ninja Dynamics */ + var decode = function decode(chars) { + var indexes = [] + var separator = chars.indexOf('!'); + var modifiedStart = separator + 1; + var modifiedLength = parseInt(chars.slice(0, separator)); + var modified = chars.slice(modifiedStart, modifiedStart + modifiedLength); + for (var i = 0; i < modifiedLength; i += 4) { + var hex = modified.slice(i, i + 4); + indexes.push(parseInt(hex, 16)); + } + + chars = chars.slice(modifiedStart + modifiedLength, chars.length); + + var codes = []; + var length = chars.length - 1; + for (var i = 0; i < length; i++) { + var c = chars.charCodeAt(i); + if (i == indexes[0]) { + c = (c - 0x800).mod(0x10000); + indexes.shift(); + } + codes.push(c >> 8, c & 0xFF); + } + + c = chars.charCodeAt(length); + if (i == indexes.shift()) { + c = (c - 0x800).mod(0x10000); + } + + if (c) codes.pop(); + return Uint8Array.from(codes); + }; + + exports.decode = decode; + exports.encode = encode; + + return exports; + +}({})); diff --git a/public/Repo/jsnes-ninjapad/ninjapad/main.js b/public/Repo/jsnes-ninjapad/ninjapad/main.js new file mode 100644 index 0000000..92456ca --- /dev/null +++ b/public/Repo/jsnes-ninjapad/ninjapad/main.js @@ -0,0 +1,56 @@ +ninjapad.emulator = null; +ninjapad.jQElement = null; +ninjapad.initialize = function() { + ninjapad.jQElement = { + gamepad: $("#GAMEPAD"), + controller: $("#GAMEPAD-BUTTONS"), + analogSwitch: $("#analogSwitch"), + menu: $("#menu"), + upload: $("#upload"), + analogStick: $("#ANALOG_STICK"), + analog: $("#ANALOG"), + dpad: $("#DPAD"), + osd: $("#OSD"), + screen: $("#" + SCREEN), + }; + + // Page setup + ninjapad.layout.setPageLayout(); + + // Assign function calls to touch events + ninjapad.utils.assign(ninjapad.gamepad.toggleMenu, "menu", "start", "end"); + ninjapad.utils.assign(ninjapad.gamepad.analogSwitch, "analogSwitch", "start", "end"); + ninjapad.utils.assign(ninjapad.gamepad.buttonPress, "GAMEPAD-BUTTONS", "start", "move", "end"); + ninjapad.utils.assign(ninjapad.gamepad.analogTouch, "ANALOG_STICK", "start", "move", "end"); + ninjapad.utils.assign(ninjapad.gamepad.toggleFullScreen, SCREEN, "end"); + ninjapad.utils.assign(null, "GAMEPAD"); +}; + +$(document).ready(function() { + DEBUG && console.log("Document ready event"); + + // Load emulator + ninjapad.emulator = ninjapad.interface[EMULATOR]; + + // Pause on loss of focus + $(window).blur(function() { + !ninjapad.pause.state.isEmulationPaused && + ninjapad.utils.isMobileDevice() && + ninjapad.pause.pauseEmulation(); + }); + + // Reload layout on orientation change + $(window).resize(function() { + DEBUG && console.log("Window resize event"); + ninjapad.initialize(); + }); + + // Use ESC key to open the menu + $(window).keyup(function(e) { + if (e.code == "Escape") ninjapad.menu.toggleMenu(); + }); + + // Load a ROM and setup the page layout + ninjapad.emulator.initialize(); + ninjapad.initialize(); +}); diff --git a/public/Repo/jsnes-ninjapad/ninjapad/menu.js b/public/Repo/jsnes-ninjapad/ninjapad/menu.js new file mode 100644 index 0000000..72a3381 --- /dev/null +++ b/public/Repo/jsnes-ninjapad/ninjapad/menu.js @@ -0,0 +1,140 @@ +ninjapad.menu = function() { + var state = { isOpen: false }; + + function allowUserInteraction(ontap=null) { + ninjapad.utils.allowInteraction("pauseScreenContent"); + ninjapad.utils.assignNoPropagation(ontap, "OSD", ontap && "end"); + } + + function preventUserInteraction(ontap=null) { + ninjapad.utils.assign(null, "pauseScreenContent"); + ninjapad.utils.assignNoPropagation(ontap, "OSD", ontap && "end"); + } + + function showError(msg) { + $("#pauseScreenContent").html( + ninjapad.utils.html("div", "error", msg) + ); + preventUserInteraction(returnToMainMenu); + } + + function mainMenu() { + const upload = "ninjapad.menu.uploadROM()"; + const save = "ninjapad.menu.saveState();"; + const load = "ninjapad.menu.loadState();"; + const reset = "ninjapad.menu.reset();" + const about = "ninjapad.menu.showCredits()"; + return ninjapad.utils.createMenu(null, + ninjapad.utils.link("Load ROM", js=upload, hide=SINGLE_ROM), + ninjapad.utils.link("Save State", js=save), + ninjapad.utils.link("Load State", js=load), + ninjapad.utils.link("Reset", js=reset), + ninjapad.utils.link("About", js=about) + ); + } + + function openMainMenu() { + ninjapad.pause.pauseEmulation( + ninjapad.utils.html("span", "pauseScreenContent", mainMenu()) + ); + allowUserInteraction(); + state.isOpen = true; + } + + function returnToMainMenu(event) { + event.stopPropagation(); + $("#pauseScreenContent").html( + mainMenu() + ); + allowUserInteraction(); + } + + return { + state: state, + + loadState: function() { + const hash = sha256(ninjapad.emulator.getROMData()); + const data = localStorage.getItem(hash); + if (!data) { + showError("No save data"); + return; + } + try { + ninjapad.emulator.loadState( + uint8ToUtf16.decode(data) + ); + ninjapad.pause.resumeEmulation(); + } + catch (e) { + showError(`Error

${e.message}`); + DEBUG && console.log(e); + } + }, + + saveState: function() { + const hash = sha256(ninjapad.emulator.getROMData()); + const data = ninjapad.emulator.saveState(); + try { + const optimizedData = uint8ToUtf16.encode(data); + localStorage.setItem(hash, optimizedData); + ninjapad.pause.resumeEmulation(); + } + catch (e) { + showError(`Error

${e.message}`); + DEBUG && console.log(e); + } + }, + + reset: function() { + ninjapad.emulator.reloadROM(); + ninjapad.pause.resumeEmulation(); + }, + + uploadROM: function() { + ninjapad.jQElement.upload.trigger("click"); + + const inputElement = document.getElementById("upload"); + inputElement.addEventListener("change", handleFiles, false); + + function handleFiles() { + let saveData = null; + if (ninjapad.emulator.isROMLoaded()) { + saveData = ninjapad.emulator.saveState(); + } + let f = document.getElementById('upload').files[0]; + let reader = new FileReader(); + reader.onload = function () { + try { + ninjapad.emulator.loadROMData(reader.result); + ninjapad.pause.resumeEmulation(); + } + catch (e) { + if (saveData) { + ninjapad.emulator.reloadROM(); + ninjapad.emulator.loadState(saveData); + } + showError(`Error

${e.message.strip(".")}`); + DEBUG && console.log(e); + } + } + reader.readAsBinaryString(f); + } + }, + + showCredits: function() { + $("#pauseScreenContent").html( + ninjapad.utils.html("div", "about", ABOUT) + ); + allowUserInteraction(returnToMainMenu) + }, + + toggleMenu: function() { + if (!ninjapad.pause.state.cannotResume && state.isOpen) { + ninjapad.pause.resumeEmulation(); + state.isOpen = false; + return; + } + openMainMenu(); + } + } +}(); diff --git a/public/Repo/jsnes-ninjapad/ninjapad/ninjapad.css b/public/Repo/jsnes-ninjapad/ninjapad/ninjapad.css new file mode 100644 index 0000000..1e22cae --- /dev/null +++ b/public/Repo/jsnes-ninjapad/ninjapad/ninjapad.css @@ -0,0 +1,237 @@ +#ninjaPad { + background-color: #222; + height: 0%; +} + +#GAMEPAD { + position: absolute; + width: 100%; + display: none; +} + +#OSD { + position: absolute; + text-align: center; + display: flex; + justify-content: center; + align-content: center; + flex-direction: column; + font-family: KarmaticArcade; + font-size: 5vmin; + box-sizing: border-box; + padding: 2em; + word-spacing: 0.5em; + color: red; + visibility: hidden; + background-color: rgba(0, 0, 0, 0.8); +} + +/* unvisited link */ +#OSD a:link { + color: red; + text-decoration: none; +} + +/* visited link */ +#OSD a:visited { + color: red; + text-decoration: none; +} + +/* mouse over link */ +#OSD a:hover { + color: red; + text-decoration: none; +} + +/* selected link */ +#OSD a:active { + color: yellow; + text-decoration: none; +} + +#DEADZONE { + background: #444; +} + +.dirButtons { + position: absolute; + left: 4%; + top: 10vw; + width: 35vw; + height: 35vw; + display: flex; + justify-content: center; + align-items: center; +} + +.actionButtons { + position: absolute; + right: 4%; + top: 10vw; + width: 35vw; + height: 35vw; + display: flex; + justify-content: center; + align-items: center; +} + +.middleButtons { + position: absolute; + left: 45%; + top: 10vw; + width: 10vw; + height: 35vw; + display: flex; + justify-content: center; + align-items: center; +} + +.dpad { + position: absolute; + width: 100%; + height: 100%; + display: grid; + grid-template-columns: 1fr 1fr 1fr; + grid-template-rows: 1fr 1fr 1fr; +} + +.analogRing { + position: absolute; + width: 100%; + height: 100%; + box-sizing: border-box; + border-radius: 50%; + background-color: transparent; + border: 2.5vw; + border-style: inset; +} + +.analogStick { + position: absolute; + width: 62%; + height: 62%; + box-sizing: border-box; + border-radius: 50%; + background-color: #666; + border: 2.5vw; + border-style: outset; +} + +.button_B { + position: absolute; + left: 0%; + top: 25%; + width: 50%; + height: 50%; + box-sizing: border-box; + border-radius: 50%; + + text-align: center; + display: flex; + justify-content: center; + align-content: center; + flex-direction: column; + font-family: monospace; +} + +.button_A { + position: absolute; + right: 0%; + top: -2.5%; + width: 50%; + height: 50%; + box-sizing: border-box; + border-radius: 50%; + + text-align: center; + display: flex; + justify-content: center; + align-content: center; + flex-direction: column; + font-family: monospace; +} + +.button_AB { + position: absolute; + right: 0%; + bottom: -2.5%; + width: 50%; + height: 50%; + box-sizing: border-box; + border-radius: 50%; + + text-align: center; + display: flex; + justify-content: center; + align-content: center; + flex-direction: column; + font-family: monospace; +} + +.button_SELECT { + position: absolute; + left: 0%; + top: 30%; + width: 100%; + height: 16%; + box-sizing: border-box; + align-items: center; + justify-content: center; +} + +.button_START { + position: absolute; + left: 0%; + top: 55%; + width: 100%; + height: 16%; + box-sizing: border-box; + align-items: center; + justify-content: center; +} + +.analogSwitchButton { + position: absolute; + left: 0%; + top: 0%; + width: 100%; + height: 16%; + box-sizing: border-box; + background-color: #666; + border: 1vw; + border-style: outset; + border-radius: 20%; +} + +.menuButton { + position: absolute; + left: 0%; + bottom: 0%; + width: 100%; + height: 16%; + box-sizing: border-box; + background-color: #666; + border: 1vw; + border-style: outset; + border-radius: 20%; +} + +.box { + display: flex; + align-items: center; + justify-content: center; +} + +[id^="BUTTON"] { + background-color: #BBB; + border: 1vw; + border-style: outset; +} + +@font-face { + font-family: 'KarmaticArcade'; + src: url('res/ka1.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} diff --git a/public/Repo/jsnes-ninjapad/ninjapad/pause.js b/public/Repo/jsnes-ninjapad/ninjapad/pause.js new file mode 100644 index 0000000..c2544e7 --- /dev/null +++ b/public/Repo/jsnes-ninjapad/ninjapad/pause.js @@ -0,0 +1,48 @@ +ninjapad.pause = function() { + var state = { + isEmulationPaused: false, + cannotResume: false + }; + + var pauseScreen = { + visibility: "hidden", + content: "" + }; + + function pauseText() { + let msg = "Emulation paused"; + let resumeMsg = ninjapad.utils.isMobileDevice() ? "Tap" : "Click"; + resumeMsg += " to resume"; + return ninjapad.utils.html("span", "pauseScreenContent", msg + "
" + resumeMsg); + } + + return { + state: state, + + pauseScreen: pauseScreen, + + pauseEmulation: function(content=null) { + ninjapad.emulator.pause(); + pauseScreen.visibility = "visible"; + pauseScreen.content = content || pauseText(); + ninjapad.jQElement.osd.empty(); + ninjapad.jQElement.osd.append(pauseScreen.content); + ninjapad.jQElement.osd.css("visibility", pauseScreen.visibility); + state.isEmulationPaused = true; + ninjapad.utils.assign(null, "pauseScreenContent"); + ninjapad.utils.assignNoPropagation(ninjapad.pause.resumeEmulation, "OSD", "end"); + DEBUG && console.log("Emulation paused"); + }, + + resumeEmulation: function(event) { + if (event) event.stopPropagation(); + if (state.cannotResume) return; + ninjapad.emulator.resume(); + pauseScreen.visibility = "hidden"; + ninjapad.jQElement.osd.css("visibility", pauseScreen.visibility); + state.isEmulationPaused = false; + ninjapad.menu.state.isOpen = false; + DEBUG && console.log("Emulation resumed"); + } + }; +}(); diff --git a/public/Repo/jsnes-ninjapad/ninjapad/res/ka1.ttf b/public/Repo/jsnes-ninjapad/ninjapad/res/ka1.ttf new file mode 100644 index 0000000000000000000000000000000000000000..d1df85235c97923dd54e9234999fe36452e309ba GIT binary patch literal 56908 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 Gf&PDRo=4yS literal 0 HcmV?d00001 diff --git a/public/Repo/jsnes-ninjapad/ninjapad/utils.js b/public/Repo/jsnes-ninjapad/ninjapad/utils.js new file mode 100644 index 0000000..aa5197b --- /dev/null +++ b/public/Repo/jsnes-ninjapad/ninjapad/utils.js @@ -0,0 +1,162 @@ +ninjapad.utils = function() { + const TOUCH_EVENTS = ["start", "move", "end"]; + + Number.prototype.mod = function(n) { + return ((this%n)+n)%n; + }; + + String.prototype.strip = function (string) { + var escaped = string.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1"); + return this.replace(RegExp("^[" + escaped + "]+|[" + escaped + "]+$", "gm"), ''); + }; + + return { + + preventDefaultWithoutPropagation: function(event) { + event.preventDefault(); + event.stopPropagation(); + }, + + preventDefault: function(event) { + event.preventDefault(); + }, + + stopPropagation: function(event) { + event.stopPropagation(); + }, + + isIOSDevice: function(){ + return !!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform); + }, + + isMobileDevice: function() { + return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); + }, + + isFullScreen: function() { + return ( + document.fullscreenElement || + document.mozFullScreenElement || + document.webkitFullscreenElement + ); + }, + + enterFullscreen: function(element) { + if (element.requestFullScreen) { + element.requestFullScreen(); + } else if (element.webkitRequestFullScreen) { + element.webkitRequestFullScreen(); + } else if (element.mozRequestFullScreen) { + element.mozRequestFullScreen(); + } else if (element.msRequestFullscreen) { + element.msRequestFullscreen(); + } else if (element.webkitEnterFullscreen) { + element.webkitEnterFullscreen(); //for iphone this code worked + } + }, + + exitFullScreen: function() { + if (document.cancelFullScreen) { + document.cancelFullScreen(); + } else if (document.mozCancelFullScreen) { + document.mozCancelFullScreen(); + } else if (document.webkitCancelFullScreen) { + document.webkitCancelFullScreen(); + } else if (document.msExitFullscreen) { + document.msExitFullscreen(); + } + }, + + html: function(obj, id, text) { + return `<${obj} id='${id}'>${text}`; + }, + + link: function(content, js, hide) { + js = `${js}; return false;`; + return hide || `${content}`; + }, + + createMenu: function(title, ...opts) { + opts = opts.filter(e => e !== true); + title = title ? `${title}
` : ""; + return ( + `
+ ${title} + ${opts.join("
")} +
` + ); + }, + + assign: function(fn, elementName, ...touchEvents) { + // Prevent default on all events + let element = document.getElementById(elementName); + for (const e of TOUCH_EVENTS) { + eval("element.ontouch" + e + " = ninjapad.utils.preventDefault"); + } + // Assign function call to events + for (const e of touchEvents) { + eval("element.ontouch" + e + " = fn"); + } + }, + + assignNoPropagation: function(fn, elementName, ...touchEvents) { + // Prevent default and stop propagation on all events + let element = document.getElementById(elementName); + for (const e of TOUCH_EVENTS) { + eval("element.ontouch" + e + " = ninjapad.utils.preventDefaultWithoutPropagation"); + } + // Assign function call to events + for (const e of touchEvents) { + eval("element.ontouch" + e + " = fn"); + } + }, + + allowInteraction: function(elementName) { + let element = document.getElementById(elementName); + for (const e of TOUCH_EVENTS) { + eval("element.ontouch" + e + " = ninjapad.utils.stopPropagation"); + } + }, + + zip: function(data) { + const buf = fflate.strToU8(data); + return fflate.compressSync(buf, { level: 9, mem: 8 }); + }, + + unzip: function(data) { + const decompressed = fflate.decompressSync(data); + return fflate.strFromU8(decompressed); + }, + + equal: function(buf1, buf2) { + var result = true; + if (buf1.byteLength != buf2.byteLength) { + DEBUG && console.log("size", buf1.byteLength, buf2.byteLength); + return false; + } + var dv1 = new Int8Array(buf1); + var dv2 = new Int8Array(buf2); + for (var i = 0 ; i != buf1.byteLength ; i++) + { + if (dv1[i] != dv2[i]) { + result = false; + DEBUG && console.log(i, dv1[i], dv2[i]); + } + } + return result; + }, + + vw: function(v) { + let w = Math.max(document.documentElement.clientWidth, window.innerWidth || 0); + return (v * w) / 100; + }, + + dist: function(dx, dy) { + return Math.sqrt((dx * dx) + (dy * dy)); + }, + + angle: function(dx, dy) { + return Math.atan2(dy, dx); + } + } +}();