From 663ae1cfda71371f46d8aacd4e948f5ddac5e328 Mon Sep 17 00:00:00 2001 From: Joseph Date: Sat, 18 Jul 2015 16:45:06 -0400 Subject: [PATCH 1/2] Added gamepad support to JSNES --- AUTHORS.md | 1 + index.html | 25 +++- lib/gamepad.js | 284 +++++++++++++++++++++++++++++++++++++ source/gamepad_keyboard.js | 182 ++++++++++++++++++++++++ source/mappers.js | 220 ++++++++++++++-------------- source/nes.js | 58 ++++---- source/ui.js | 99 +++++++++---- 7 files changed, 700 insertions(+), 169 deletions(-) create mode 100644 lib/gamepad.js create mode 100644 source/gamepad_keyboard.js diff --git a/AUTHORS.md b/AUTHORS.md index 56298129..7fba5905 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -11,3 +11,4 @@ Thanks to: * Jens Lindstrom for some optimisations. * Rafal Chlodnicki for an Opera fix. * Ecin Krispie for fixing player 2 controls. + * Joseph Lewis for adding gamepad support. diff --git a/index.html b/index.html index d6fc08e1..47aec172 100644 --- a/index.html +++ b/index.html @@ -7,56 +7,71 @@

JSNES

- +
- +

Controls

+ + + + + + + + +
Button Player 1 Player 2Gamepad
Left Left Num-4D-pad Left
Right Right Num-6D-pad Right
Up Up Num-8D-pad Up
Down Down Num-2D-pad Down
A X Num-7Button 1 (usually "A")
B Z/Y Num-9Button 2 (usually "B")
Start Enter Num-1Start
Select Ctrl Num-3Select (sometimes "back")
+ +

Note: The gamepad API is currently experimental, Firefox and Chrome + currently support it.

+ +

In Firefox some controllers are improperly mapped. + Try using Chrome if you experience issues.

@@ -64,6 +79,8 @@

Controls

+ + @@ -80,7 +97,7 @@

Controls

], "Working": [ ['Bubble Bobble', 'local-roms/Bubble Bobble (U).nes'], - + ['Contra', 'local-roms/Contra (U) [!].nes'], ['Donkey Kong', 'local-roms/Donkey Kong (JU).nes'], ['Dr. Mario', 'local-roms/Dr. Mario (JU).nes'], @@ -88,7 +105,7 @@

Controls

['The Legend of Zelda', 'local-roms/Legend of Zelda, The (U) (PRG1).nes'], ['Lemmings', 'local-roms/Lemmings (U).nes'], ['Lifeforce', 'local-roms/Lifeforce (U).nes'], - + ['Mario Bros.', 'local-roms/Mario Bros. (JU) [!].nes'], ['Mega Man', 'local-roms/Mega Man (U).nes'], ['Pac-Man', 'local-roms/Pac-Man (U) [!].nes'], diff --git a/lib/gamepad.js b/lib/gamepad.js new file mode 100644 index 00000000..c3eddde2 --- /dev/null +++ b/lib/gamepad.js @@ -0,0 +1,284 @@ +/** + * Copyright 2012 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @author mwichary@google.com (Marcin Wichary) + */ + +var gamepadSupport = { + // A number of typical buttons recognized by Gamepad API and mapped to + // standard controls. Any extraneous buttons will have larger indexes. + TYPICAL_BUTTON_COUNT: 16, + + // A number of typical axes recognized by Gamepad API and mapped to + // standard controls. Any extraneous buttons will have larger indexes. + TYPICAL_AXIS_COUNT: 4, + + // Whether we’re requestAnimationFrameing like it’s 1999. + ticking: false, + + // The canonical list of attached gamepads, without “holes” (always + // starting at [0]) and unified between Firefox and Chrome. + gamepads: [], + + // Remembers the connected gamepads at the last check; used in Chrome + // to figure out when gamepads get connected or disconnected, since no + // events are fired. + prevRawGamepadTypes: [], + + // Previous timestamps for gamepad state; used in Chrome to not bother with + // analyzing the polled data if nothing changed (timestamp is the same + // as last time). + prevTimestamps: [], + + /** + * Initialize support for Gamepad API. + */ + init: function() { + var gamepadSupportAvailable = navigator.getGamepads || + !!navigator.webkitGetGamepads || + !!navigator.webkitGamepads; + + if (!gamepadSupportAvailable) { + // It doesn’t seem Gamepad API is available – show a message telling + // the visitor about it. + tester.showNotSupported(); + } else { + // Check and see if gamepadconnected/gamepaddisconnected is supported. + // If so, listen for those events and don't start polling until a gamepad + // has been connected. + if ('ongamepadconnected' in window) { + window.addEventListener('gamepadconnected', + gamepadSupport.onGamepadConnect, false); + window.addEventListener('gamepaddisconnected', + gamepadSupport.onGamepadDisconnect, false); + } else { + // If connection events are not supported just start polling + gamepadSupport.startPolling(); + } + } + }, + + /** + * React to the gamepad being connected. + */ + onGamepadConnect: function(event) { + // Add the new gamepad on the list of gamepads to look after. + gamepadSupport.gamepads.push(event.gamepad); + + // Ask the tester to update the screen to show more gamepads. + tester.updateGamepads(gamepadSupport.gamepads); + + // Start the polling loop to monitor button changes. + gamepadSupport.startPolling(); + }, + + /** + * React to the gamepad being disconnected. + */ + onGamepadDisconnect: function(event) { + // Remove the gamepad from the list of gamepads to monitor. + for (var i in gamepadSupport.gamepads) { + if (gamepadSupport.gamepads[i].index == event.gamepad.index) { + gamepadSupport.gamepads.splice(i, 1); + break; + } + } + + // If no gamepads are left, stop the polling loop. + if (gamepadSupport.gamepads.length == 0) { + gamepadSupport.stopPolling(); + } + + // Ask the tester to update the screen to remove the gamepad. + tester.updateGamepads(gamepadSupport.gamepads); + }, + + /** + * Starts a polling loop to check for gamepad state. + */ + startPolling: function() { + // Don’t accidentally start a second loop, man. + if (!gamepadSupport.ticking) { + gamepadSupport.ticking = true; + gamepadSupport.tick(); + } + }, + + /** + * Stops a polling loop by setting a flag which will prevent the next + * requestAnimationFrame() from being scheduled. + */ + stopPolling: function() { + gamepadSupport.ticking = false; + }, + + /** + * A function called with each requestAnimationFrame(). Polls the gamepad + * status and schedules another poll. + */ + tick: function() { + gamepadSupport.pollStatus(); + gamepadSupport.scheduleNextTick(); + }, + + scheduleNextTick: function() { + // Only schedule the next frame if we haven’t decided to stop via + // stopPolling() before. + if (gamepadSupport.ticking) { + if (window.requestAnimationFrame) { + window.requestAnimationFrame(gamepadSupport.tick); + } else if (window.mozRequestAnimationFrame) { + window.mozRequestAnimationFrame(gamepadSupport.tick); + } else if (window.webkitRequestAnimationFrame) { + window.webkitRequestAnimationFrame(gamepadSupport.tick); + } + // Note lack of setTimeout since all the browsers that support + // Gamepad API are already supporting requestAnimationFrame(). + } + }, + + /** + * Checks for the gamepad status. Monitors the necessary data and notices + * the differences from previous state (buttons for Chrome/Firefox, + * new connects/disconnects for Chrome). If differences are noticed, asks + * to update the display accordingly. Should run as close to 60 frames per + * second as possible. + */ + pollStatus: function() { + // Poll to see if gamepads are connected or disconnected. Necessary + // only on Chrome. + gamepadSupport.pollGamepads(); + + for (var i in gamepadSupport.gamepads) { + var gamepad = gamepadSupport.gamepads[i]; + + // Don’t do anything if the current timestamp is the same as previous + // one, which means that the state of the gamepad hasn’t changed. + // This is only supported by Chrome right now, so the first check + // makes sure we’re not doing anything if the timestamps are empty + // or undefined. + if (gamepad.timestamp && + (gamepad.timestamp == gamepadSupport.prevTimestamps[i])) { + continue; + } + gamepadSupport.prevTimestamps[i] = gamepad.timestamp; + + gamepadSupport.updateDisplay(i); + } + }, + + // This function is called only on Chrome, which does not yet support + // connection/disconnection events, but requires you to monitor + // an array for changes. + pollGamepads: function() { + // Get the array of gamepads – the first method (getGamepads) + // is the most modern one and is supported by Firefox 28+ and + // Chrome 35+. The second one (webkitGetGamepads) is a deprecated method + // used by older Chrome builds. + var rawGamepads = + (navigator.getGamepads && navigator.getGamepads()) || + (navigator.webkitGetGamepads && navigator.webkitGetGamepads()); + + if (rawGamepads) { + // We don’t want to use rawGamepads coming straight from the browser, + // since it can have “holes” (e.g. if you plug two gamepads, and then + // unplug the first one, the remaining one will be at index [1]). + gamepadSupport.gamepads = []; + + // We only refresh the display when we detect some gamepads are new + // or removed; we do it by comparing raw gamepad table entries to + // “undefined.” + var gamepadsChanged = false; + + for (var i = 0; i < rawGamepads.length; i++) { + if (typeof rawGamepads[i] != gamepadSupport.prevRawGamepadTypes[i]) { + gamepadsChanged = true; + gamepadSupport.prevRawGamepadTypes[i] = typeof rawGamepads[i]; + } + + if (rawGamepads[i]) { + gamepadSupport.gamepads.push(rawGamepads[i]); + } + } + + // Ask the tester to refresh the visual representations of gamepads + // on the screen. + if (gamepadsChanged) { + tester.updateGamepads(gamepadSupport.gamepads); + } + } + }, + + // Call the tester with new state and ask it to update the visual + // representation of a given gamepad. + updateDisplay: function(gamepadId) { + var gamepad = gamepadSupport.gamepads[gamepadId]; + + // Update all the buttons (and their corresponding labels) on screen. + tester.updateButton(gamepad.buttons[0], gamepadId, 'button-1'); + tester.updateButton(gamepad.buttons[1], gamepadId, 'button-2'); + tester.updateButton(gamepad.buttons[2], gamepadId, 'button-3'); + tester.updateButton(gamepad.buttons[3], gamepadId, 'button-4'); + + tester.updateButton(gamepad.buttons[4], gamepadId, + 'button-left-shoulder-top'); + tester.updateButton(gamepad.buttons[6], gamepadId, + 'button-left-shoulder-bottom'); + tester.updateButton(gamepad.buttons[5], gamepadId, + 'button-right-shoulder-top'); + tester.updateButton(gamepad.buttons[7], gamepadId, + 'button-right-shoulder-bottom'); + + tester.updateButton(gamepad.buttons[8], gamepadId, 'button-select'); + tester.updateButton(gamepad.buttons[9], gamepadId, 'button-start'); + + tester.updateButton(gamepad.buttons[10], gamepadId, 'stick-1'); + tester.updateButton(gamepad.buttons[11], gamepadId, 'stick-2'); + + tester.updateButton(gamepad.buttons[12], gamepadId, 'button-dpad-top'); + tester.updateButton(gamepad.buttons[13], gamepadId, 'button-dpad-bottom'); + tester.updateButton(gamepad.buttons[14], gamepadId, 'button-dpad-left'); + tester.updateButton(gamepad.buttons[15], gamepadId, 'button-dpad-right'); + + // Update all the analogue sticks. + tester.updateAxis(gamepad.axes[0], gamepadId, + 'stick-1-axis-x', 'stick-1', true); + tester.updateAxis(gamepad.axes[1], gamepadId, + 'stick-1-axis-y', 'stick-1', false); + tester.updateAxis(gamepad.axes[2], gamepadId, + 'stick-2-axis-x', 'stick-2', true); + tester.updateAxis(gamepad.axes[3], gamepadId, + 'stick-2-axis-y', 'stick-2', false); + + // Update extraneous buttons. + var extraButtonId = gamepadSupport.TYPICAL_BUTTON_COUNT; + while (typeof gamepad.buttons[extraButtonId] != 'undefined') { + tester.updateButton(gamepad.buttons[extraButtonId], gamepadId, + 'extra-button-' + extraButtonId); + + extraButtonId++; + } + + // Update extraneous axes. + var extraAxisId = gamepadSupport.TYPICAL_AXIS_COUNT; + while (typeof gamepad.axes[extraAxisId] != 'undefined') { + tester.updateAxis(gamepad.axes[extraAxisId], gamepadId, + 'extra-axis-' + extraAxisId); + + extraAxisId++; + } + + } +}; diff --git a/source/gamepad_keyboard.js b/source/gamepad_keyboard.js new file mode 100644 index 00000000..78cc5c0a --- /dev/null +++ b/source/gamepad_keyboard.js @@ -0,0 +1,182 @@ +/** + * Modified File for JSNES Copyright 2015 Joseph Lewis + * Original File: Copyright 2012 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @author mwichary@google.com (Marcin Wichary) + */ + +var tester = { + // If the number exceeds this in any way, we treat the label as active + // and highlight it. + VISIBLE_THRESHOLD: 0.1, + + // How far can a stick move on screen. + STICK_OFFSET: 25, + + // How “deep” does an analogue button need to be depressed to consider it + // a button down. + ANALOGUE_BUTTON_THRESHOLD: .5, + + // From jsnes + i: 0, + + keys : { + KEY_A: 0, + KEY_B: 1, + KEY_SELECT: 2, + KEY_START: 3, + KEY_UP: 4, + KEY_DOWN: 5, + KEY_LEFT: 6, + KEY_RIGHT: 7 + }, + + state1:[0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40], + state2:[0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40], + + setKey: function(key, value) { + switch (key) { + case 88: this.state1[this.keys.KEY_A] = value; break; // X + case 89: this.state1[this.keys.KEY_B] = value; break; // Y (Central European keyboard) + case 90: this.state1[this.keys.KEY_B] = value; break; // Z + case 17: this.state1[this.keys.KEY_SELECT] = value; break; // Right Ctrl + case 13: this.state1[this.keys.KEY_START] = value; break; // Enter + case 38: this.state1[this.keys.KEY_UP] = value; break; // Up + case 40: this.state1[this.keys.KEY_DOWN] = value; break; // Down + case 37: this.state1[this.keys.KEY_LEFT] = value; break; // Left + case 39: this.state1[this.keys.KEY_RIGHT] = value; break; // Right + + case 103: this.state2[this.keys.KEY_A] = value; break; // Num-7 + case 105: this.state2[this.keys.KEY_B] = value; break; // Num-9 + case 99: this.state2[this.keys.KEY_SELECT] = value; break; // Num-3 + case 97: this.state2[this.keys.KEY_START] = value; break; // Num-1 + case 104: this.state2[this.keys.KEY_UP] = value; break; // Num-8 + case 98: this.state2[this.keys.KEY_DOWN] = value; break; // Num-2 + case 100: this.state2[this.keys.KEY_LEFT] = value; break; // Num-4 + case 102: this.state2[this.keys.KEY_RIGHT] = value; break; // Num-6 + default: return true; + } + return false; // preventDefault + }, + + keyDown: function(evt) { + if (!this.setKey(evt.keyCode, 0x41) && evt.preventDefault) { + evt.preventDefault(); + } + }, + + keyUp: function(evt) { + if (!this.setKey(evt.keyCode, 0x40) && evt.preventDefault) { + evt.preventDefault(); + } + }, + + keyPress: function(evt) { + evt.preventDefault(); + }, + + init: function() { + tester.updateMode(); + tester.updateGamepads(); + }, + + /** + * Tell the user the browser doesn’t support Gamepad API. + */ + showNotSupported: function() { + document.querySelector('#no-gamepad-support').classList.add('visible'); + }, + + /** + * Update the mode (visual vs. raw) if any of the radio buttons were + * pressed. + */ + updateMode: function() { + }, + + /** + * Update the gamepads on the screen, creating new elements from the + * template. + */ + updateGamepads: function(gamepads) { + }, + + /** + * Update a given button on the screen. + */ + updateButton: function(button, gamepadId, id) { + var value, pressed; + + // Older version of the gamepad API provided buttons as a floating point + // value from 0 to 1. Newer implementations provide GamepadButton objects, + // which contain an analog value and a pressed boolean. + if (typeof(button) == 'object') { + value = button.value; + pressed = button.pressed; + } else { + value = button; + pressed = button > tester.ANALOGUE_BUTTON_THRESHOLD; + } + + // translate the value to something the JSNES keyboard understands + value = (value == 1) ? 0x41 : 0x40; + + var keytype = -1; + if(value == 0x41) + console.log(id, value); + + switch(id) { + case "button-1": + keytype = this.keys.KEY_A; + break; + case "button-2": + keytype = this.keys.KEY_B; + break; + case "button-start": + keytype = this.keys.KEY_START; + break; + case "button-select": + keytype = this.keys.KEY_SELECT; + break; + case "button-dpad-top": + keytype = this.keys.KEY_UP; + break; + case "button-dpad-bottom": + keytype = this.keys.KEY_DOWN; + break; + case "button-dpad-left": + keytype = this.keys.KEY_LEFT; + break; + case "button-dpad-right": + keytype = this.keys.KEY_RIGHT; + break; + default: + return; + } + + if(gamepadId == 0) { + this.state1[keytype] = value; + } else { + this.state2[keytype] = value; + } + }, + + /** + * Update a given analogue stick on the screen. + */ + updateAxis: function(value, gamepadId, labelId, stickId, horizontal) { + // For snes we don't use the joysticks. + } +}; diff --git a/source/mappers.js b/source/mappers.js index 6d0ed9a5..98d3850f 100644 --- a/source/mappers.js +++ b/source/mappers.js @@ -27,17 +27,17 @@ JSNES.Mappers[0].prototype = { this.joy1StrobeState = 0; this.joy2StrobeState = 0; this.joypadLastWrite = 0; - + this.mousePressed = false; this.mouseX = null; this.mouseY = null; }, - + write: 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; @@ -55,7 +55,7 @@ JSNES.Mappers[0].prototype = { this.regWrite(address, value); } }, - + writelow: function(address, value) { if (address < 0x2000) { // Mirroring of RAM: @@ -75,7 +75,7 @@ JSNES.Mappers[0].prototype = { load: function(address) { // Wrap around: address &= 0xFFFF; - + // Check address range: if (address > 0x4017) { // ROM: @@ -95,10 +95,10 @@ JSNES.Mappers[0].prototype = { switch (address >> 12) { // use fourth nibble (0xF000) case 0: break; - + case 1: break; - + case 2: // Fall through to case 3 case 3: @@ -112,7 +112,7 @@ JSNES.Mappers[0].prototype = { // PPU as flags): // (not in the real NES) return this.nes.cpu.mem[0x2000]; - + case 0x1: // 0x2001: // PPU Control Register 2. @@ -121,7 +121,7 @@ JSNES.Mappers[0].prototype = { // PPU as flags): // (not in the real NES) return this.nes.cpu.mem[0x2001]; - + case 0x2: // 0x2002: // PPU Status Register. @@ -130,20 +130,20 @@ JSNES.Mappers[0].prototype = { // 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: @@ -157,27 +157,27 @@ JSNES.Mappers[0].prototype = { // 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 if (this.mousePressed) { - + // Check for white pixel nearby: var sx = Math.max(0, this.mouseX - 4); var ex = Math.min(256, this.mouseX + 4); var sy = Math.max(0, this.mouseY - 4); var ey = Math.min(240, this.mouseY + 4); var w = 0; - + for (var y=sy; y= 0x4000 && address <= 0x4017) { this.nes.papu.writeReg(address,value); } - + } }, joy1Read: function() { var ret; - + switch (this.joy1StrobeState) { case 0: case 1: @@ -305,18 +305,18 @@ JSNES.Mappers[0].prototype = { 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: @@ -352,7 +352,7 @@ JSNES.Mappers[0].prototype = { if (this.joy2StrobeState == 24) { this.joy2StrobeState = 0; } - + return ret; }, @@ -361,16 +361,16 @@ JSNES.Mappers[0].prototype = { alert("NoMapper: Invalid ROM! Unable to load."); return; } - + // 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); @@ -429,10 +429,10 @@ JSNES.Mappers[0].prototype = { return; } this.nes.ppu.triggerRendering(); - - JSNES.Utils.copyArrayElements(this.nes.rom.vrom[bank % this.nes.rom.vromCount], + + JSNES.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]; JSNES.Utils.copyArrayElements(vromTile, 0, this.nes.ppu.ptTile,address >> 4, 256); }, @@ -458,12 +458,12 @@ JSNES.Mappers[0].prototype = { return; } this.nes.ppu.triggerRendering(); - + var bank4k = Math.floor(bank1k / 4) % this.nes.rom.vromCount; var bankoffset = (bank1k % 4) * 1024; - JSNES.Utils.copyArrayElements(this.nes.rom.vrom[bank4k], 0, + JSNES.Utils.copyArrayElements(this.nes.rom.vrom[bank4k], 0, this.nes.ppu.vramMem, bankoffset, 1024); - + // Update tiles: var vromTile = this.nes.rom.vromTile[bank4k]; var baseIndex = address >> 4; @@ -477,12 +477,12 @@ JSNES.Mappers[0].prototype = { return; } this.nes.ppu.triggerRendering(); - + var bank4k = Math.floor(bank2k / 2) % this.nes.rom.vromCount; var bankoffset = (bank2k % 2) * 2048; JSNES.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; @@ -494,9 +494,9 @@ JSNES.Mappers[0].prototype = { 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); - JSNES.Utils.copyArrayElements(this.nes.rom.rom[bank16k], offset, + JSNES.Utils.copyArrayElements(this.nes.rom.rom[bank16k], offset, this.nes.cpu.mem, address, 8192); }, @@ -507,7 +507,7 @@ JSNES.Mappers[0].prototype = { latchAccess: function(address) { // Does nothing. This is used by MMC2. }, - + toJSON: function() { return { 'joy1StrobeState': this.joy1StrobeState, @@ -515,11 +515,13 @@ JSNES.Mappers[0].prototype = { 'joypadLastWrite': this.joypadLastWrite }; }, - + fromJSON: function(s) { - this.joy1StrobeState = s.joy1StrobeState; - this.joy2StrobeState = s.joy2StrobeState; - this.joypadLastWrite = s.joypadLastWrite; + if(s !== undefined) { + this.joy1StrobeState = s.joy1StrobeState; + this.joy2StrobeState = s.joy2StrobeState; + this.joypadLastWrite = s.joypadLastWrite; + } } }; @@ -532,7 +534,7 @@ JSNES.Mappers[1].prototype = new JSNES.Mappers[0](); JSNES.Mappers[1].prototype.reset = function() { JSNES.Mappers[0].prototype.reset.apply(this); - + // 5-bit buffer: this.regBuffer = 0; this.regBufferCounter = 0; @@ -567,26 +569,26 @@ JSNES.Mappers[1].prototype.write = function(address, value) { // 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); @@ -648,11 +650,11 @@ JSNES.Mappers[1].prototype.setReg = function(reg, value) { else { this.load8kVromBank( Math.floor(this.nes.rom.vromCount / 2) + - (value & 0xF), + (value & 0xF), 0x0000 ); } - + } else { // Swap 4kB VROM: @@ -668,16 +670,16 @@ JSNES.Mappers[1].prototype.setReg = function(reg, value) { } } } - + 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: @@ -694,14 +696,14 @@ JSNES.Mappers[1].prototype.setReg = function(reg, value) { } } 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) { @@ -710,7 +712,7 @@ JSNES.Mappers[1].prototype.setReg = function(reg, value) { } } else { - baseBank = (this.romSelectionReg0 + baseBank = (this.romSelectionReg0 | (this.romSelectionReg1 << 1)) << 3; } } @@ -720,7 +722,7 @@ JSNES.Mappers[1].prototype.setReg = function(reg, value) { baseBank = 8; } } - + if (this.prgSwitchingSize === 0) { // 32kB bank = baseBank + (value & 0xF); @@ -735,7 +737,7 @@ JSNES.Mappers[1].prototype.setReg = function(reg, value) { else { this.loadRomBank(bank, 0x8000); } - } + } } }; @@ -856,7 +858,7 @@ JSNES.Mappers[2].prototype.loadROM = function(rom) { JSNES.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; @@ -865,7 +867,7 @@ JSNES.Mappers[4] = function(nes) { 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; @@ -896,13 +898,13 @@ JSNES.Mappers[4].prototype.write = function(address, value) { this.prgAddressSelect = tmp; this.chrAddressSelect = (value >> 7) & 1; break; - + case 0x8001: // Page number for command this.executeCommand(this.command, value); break; - - case 0xA000: + + case 0xA000: // Mirroring select if ((value & 1) !== 0) { this.nes.ppu.setMirroring( @@ -913,35 +915,35 @@ JSNES.Mappers[4].prototype.write = function(address, value) { 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: + + case 0xE001: // IRQ Control Reg 1 (enable) this.irqEnable = 1; break; - + default: // Not a MMC3 register. // The game has probably crashed, @@ -963,8 +965,8 @@ JSNES.Mappers[4].prototype.executeCommand = function(cmd, arg) { this.load1kVromBank(arg + 1, 0x1400); } break; - - case this.CMD_SEL_2_1K_VROM_0800: + + case this.CMD_SEL_2_1K_VROM_0800: // Select 2 1KB VROM pages at 0x0800: if (this.chrAddressSelect === 0) { this.load1kVromBank(arg, 0x0800); @@ -975,8 +977,8 @@ JSNES.Mappers[4].prototype.executeCommand = function(cmd, arg) { this.load1kVromBank(arg + 1, 0x1C00); } break; - - case this.CMD_SEL_1K_VROM_1000: + + case this.CMD_SEL_1K_VROM_1000: // Select 1K VROM Page at 0x1000: if (this.chrAddressSelect === 0) { this.load1kVromBank(arg, 0x1000); @@ -985,8 +987,8 @@ JSNES.Mappers[4].prototype.executeCommand = function(cmd, arg) { this.load1kVromBank(arg, 0x0000); } break; - - case this.CMD_SEL_1K_VROM_1400: + + case this.CMD_SEL_1K_VROM_1400: // Select 1K VROM Page at 0x1400: if (this.chrAddressSelect === 0) { this.load1kVromBank(arg, 0x1400); @@ -995,7 +997,7 @@ JSNES.Mappers[4].prototype.executeCommand = function(cmd, arg) { this.load1kVromBank(arg, 0x0400); } break; - + case this.CMD_SEL_1K_VROM_1800: // Select 1K VROM Page at 0x1800: if (this.chrAddressSelect === 0) { @@ -1005,7 +1007,7 @@ JSNES.Mappers[4].prototype.executeCommand = function(cmd, arg) { this.load1kVromBank(arg, 0x0800); } break; - + case this.CMD_SEL_1K_VROM_1C00: // Select 1K VROM Page at 0x1C00: if (this.chrAddressSelect === 0) { @@ -1014,11 +1016,11 @@ JSNES.Mappers[4].prototype.executeCommand = function(cmd, arg) { this.load1kVromBank(arg, 0x0C00); } break; - + case this.CMD_SEL_ROM_PAGE1: if (this.prgAddressChanged) { // Load the two hardwired banks: - if (this.prgAddressSelect === 0) { + if (this.prgAddressSelect === 0) { this.load8kRomBank( ((this.nes.rom.romCount - 1) * 2), 0xC000 @@ -1032,7 +1034,7 @@ JSNES.Mappers[4].prototype.executeCommand = function(cmd, arg) { } this.prgAddressChanged = false; } - + // Select first switchable ROM page: if (this.prgAddressSelect === 0) { this.load8kRomBank(arg, 0x8000); @@ -1041,21 +1043,21 @@ JSNES.Mappers[4].prototype.executeCommand = function(cmd, arg) { 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) { + if (this.prgAddressSelect === 0) { this.load8kRomBank( ((this.nes.rom.romCount - 1) * 2), 0xC000 ); } - else { + else { this.load8kRomBank( ((this.nes.rom.romCount - 1) * 2), 0x8000 diff --git a/source/nes.js b/source/nes.js index dc6bd716..06d33758 100644 --- a/source/nes.js +++ b/source/nes.js @@ -20,14 +20,14 @@ var JSNES = function(opts) { this.opts = { ui: JSNES.DummyUI, swfPath: 'lib/', - + preferredFrameRate: 60, fpsInterval: 500, // Time between updating FPS in ms showDisplay: true, emulateSound: false, sampleRate: 44100, // Sound sample rate in hz - + CPU_FREQ_NTSC: 1789772.5, //1789772.72727272d; CPU_FREQ_PAL: 1773447.4 }; @@ -39,16 +39,24 @@ var JSNES = function(opts) { } } } - + this.frameTime = 1000 / this.opts.preferredFrameRate; - + this.ui = new this.opts.ui(this); this.cpu = new JSNES.CPU(this); this.ppu = new JSNES.PPU(this); this.papu = new JSNES.PAPU(this); this.mmap = null; // set in loadRom() - this.keyboard = new JSNES.Keyboard(); - + + // The original keyboard is here + //this.keyboard = new JSNES.Keyboard(); + + // This "keyboard" is used to both suck in keyboard signals + // and game controller signals + tester.init(); + gamepadSupport.init(); + this.keyboard = tester; + this.ui.updateStatus("Ready to load a ROM."); }; @@ -58,25 +66,25 @@ JSNES.prototype = { isRunning: false, fpsFrameCount: 0, romData: null, - + // Resets the system reset: function() { if (this.mmap !== null) { this.mmap.reset(); } - + this.cpu.reset(); this.ppu.reset(); this.papu.reset(); }, - + start: function() { var self = this; - + if (this.rom !== null && this.rom.valid) { if (!this.isRunning) { this.isRunning = true; - + this.frameInterval = setInterval(function() { self.frame(); }, this.frameTime); @@ -91,7 +99,7 @@ JSNES.prototype = { this.ui.updateStatus("There is no ROM loaded, or it is invalid."); } }, - + frame: function() { this.ppu.startFrame(); var cycles = 0; @@ -124,7 +132,7 @@ JSNES.prototype = { cpu.cyclesToHalt = 0; } } - + for (; cycles > 0; cycles--) { if(ppu.curX === ppu.spr0HitX && ppu.f_spVisibility === 1 && @@ -152,7 +160,7 @@ JSNES.prototype = { this.fpsFrameCount++; this.lastFrameTime = +new Date(); }, - + printFps: function() { var now = +new Date(); var s = 'Running'; @@ -165,32 +173,32 @@ JSNES.prototype = { this.fpsFrameCount = 0; this.lastFpsTime = now; }, - + stop: function() { clearInterval(this.frameInterval); clearInterval(this.fpsInterval); this.isRunning = false; }, - + 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) { if (this.isRunning) { this.stop(); } - + this.ui.updateStatus("Loading ROM..."); - + // Load ROM file: this.rom = new JSNES.ROM(this); this.rom.load(data); - + if (this.rom.valid) { this.reset(); this.mmap = this.rom.createMapper(); @@ -200,7 +208,7 @@ JSNES.prototype = { this.mmap.loadROM(); this.ppu.setMirroring(this.rom.getMirroringType()); this.romData = data; - + this.ui.updateStatus("Successfully loaded. Ready to be started."); } else { @@ -208,18 +216,18 @@ JSNES.prototype = { } return this.rom.valid; }, - + resetFps: function() { this.lastFpsTime = null; this.fpsFrameCount = 0; }, - + setFramerate: function(rate){ this.opts.preferredFrameRate = rate; this.frameTime = 1000 / rate; this.papu.setSampleRate(this.opts.sampleRate, false); }, - + toJSON: function() { return { 'romData': this.romData, @@ -228,7 +236,7 @@ JSNES.prototype = { 'ppu': this.ppu.toJSON() }; }, - + fromJSON: function(s) { this.loadRom(s.romData); this.cpu.fromJSON(s.cpu); diff --git a/source/ui.js b/source/ui.js index b2e94417..00840f18 100644 --- a/source/ui.js +++ b/source/ui.js @@ -31,38 +31,42 @@ if (typeof jQuery !== 'undefined') { var UI = function(nes) { var self = this; self.nes = nes; - + /* * Create UI */ self.root = $('
'); self.screen = $('').appendTo(self.root); - + if (!self.screen[0].getContext) { parent.html("Your browser doesn't support the <canvas> tag. Try Google Chrome, Safari, Opera or Firefox!"); return; } - + self.romContainer = $('
').appendTo(self.root); self.romSelect = $('').appendTo(self.romContainer); - + self.controls = $('
').appendTo(self.root); self.buttons = { pause: $('').appendTo(self.controls), restart: $('').appendTo(self.controls), sound: $('').appendTo(self.controls), - zoom: $('').appendTo(self.controls) + zoom: $('').appendTo(self.controls), + save: $('').appendTo(self.controls), + load: $('').appendTo(self.controls) }; self.status = $('

Booting up...

').appendTo(self.root); self.root.appendTo(parent); - + /* * ROM loading */ self.romSelect.change(function() { self.loadROM(); }); - + + self.currentRom = ""; + /* * Buttons */ @@ -77,12 +81,12 @@ if (typeof jQuery !== 'undefined') { self.buttons.pause.attr("value", "pause"); } }); - + self.buttons.restart.click(function() { self.nes.reloadRom(); self.nes.start(); }); - + self.buttons.sound.click(function() { if (self.nes.opts.emulateSound) { self.nes.opts.emulateSound = false; @@ -93,7 +97,34 @@ if (typeof jQuery !== 'undefined') { self.buttons.sound.attr("value", "disable sound"); } }); - + + self.buttons.save.click(function(){ + + var rompath = escape(self.currentRom); + console.log("saving " + rompath); + + var currData = self.nes.toJSON(); + var saveData = JSON.stringify(currData); + localStorage.setItem(rompath, saveData); + }); + + self.buttons.load.click(function() { + var rompath = escape(self.currentRom); + console.log("loading " + rompath); + + var saveData = localStorage.getItem(rompath); + if( saveData == null ) { + console.log("nothing to load"); + alert("You must save before loading."); + return; + } + + var decodedData = JSON.parse(saveData); + console.log(decodedData); + self.nes.fromJSON(decodedData); + self.nes.start(); + }); + self.zoomed = false; self.buttons.zoom.click(function() { if (self.zoomed) { @@ -101,6 +132,7 @@ if (typeof jQuery !== 'undefined') { width: '256px', height: '240px' }); + self.buttons.zoom.attr("value", "zoom in"); self.zoomed = false; } @@ -109,11 +141,12 @@ if (typeof jQuery !== 'undefined') { width: '512px', height: '480px' }); + self.buttons.zoom.attr("value", "zoom out"); self.zoomed = true; } }); - + /* * Lightgun experiments with mouse * (Requires jquery.dimensions.js) @@ -136,38 +169,38 @@ if (typeof jQuery !== 'undefined') { }, 500); }); } - + if (typeof roms != 'undefined') { self.setRoms(roms); } - + /* * Canvas */ self.canvasContext = self.screen[0].getContext('2d'); - + if (!self.canvasContext.getImageData) { parent.html("Your browser doesn't support writing pixels directly to the <canvas> tag. Try the latest versions of Google Chrome, Safari, Opera or Firefox!"); return; } - + self.canvasImageData = self.canvasContext.getImageData(0, 0, 256, 240); self.resetCanvas(); - + /* * Keyboard */ $(document). bind('keydown', function(evt) { - self.nes.keyboard.keyDown(evt); + self.nes.keyboard.keyDown(evt); }). bind('keyup', function(evt) { - self.nes.keyboard.keyUp(evt); + self.nes.keyboard.keyUp(evt); }). bind('keypress', function(evt) { self.nes.keyboard.keyPress(evt); }); - + /* * Sound */ @@ -175,13 +208,17 @@ if (typeof jQuery !== 'undefined') { swf: nes.opts.swfPath+'dynamicaudio.swf' }); }; - - UI.prototype = { + + UI.prototype = { loadROM: function() { + return this.loadSpecificROM(this.romSelect.val()); + }, + loadSpecificROM: function(romPath) { var self = this; + self.currentRom = romPath; self.updateStatus("Downloading..."); $.ajax({ - url: escape(self.romSelect.val()), + url: escape(romPath), xhr: function() { var xhr = $.ajaxSettings.xhr(); if (typeof xhr.overrideMimeType !== 'undefined') { @@ -198,7 +235,7 @@ if (typeof jQuery !== 'undefined') { xhr.responseBody ).toArray(); data = String.fromCharCode.apply( - undefined, + undefined, charCodes ); } @@ -211,7 +248,7 @@ if (typeof jQuery !== 'undefined') { } }); }, - + resetCanvas: function() { this.canvasContext.fillStyle = 'black'; // set alpha to opaque @@ -222,7 +259,7 @@ if (typeof jQuery !== 'undefined') { this.canvasImageData.data[i] = 0xFF; } }, - + /* * * nes.ui.screenshot() --> return element :) @@ -233,7 +270,7 @@ if (typeof jQuery !== 'undefined') { img.src = data; return img; }, - + /* * Enable and reset UI elements */ @@ -253,11 +290,11 @@ if (typeof jQuery !== 'undefined') { this.buttons.sound.attr("value", "enable sound"); } }, - + updateStatus: function(s) { this.status.text(s); }, - + setRoms: function(roms) { this.romSelect.children().remove(); $("").appendTo(this.romSelect); @@ -274,11 +311,11 @@ if (typeof jQuery !== 'undefined') { } } }, - + writeAudio: function(samples) { return this.dynamicaudio.writeInt(samples); }, - + writeFrame: function(buffer, prevBuffer) { var imageData = this.canvasImageData.data; var pixel, i, j; @@ -298,7 +335,7 @@ if (typeof jQuery !== 'undefined') { this.canvasContext.putImageData(this.canvasImageData, 0, 0); } }; - + return UI; }; })(jQuery); From f8edcf469a80ed4659c5904f4434535ac7804201 Mon Sep 17 00:00:00 2001 From: Joseph Date: Sat, 18 Jul 2015 16:50:58 -0400 Subject: [PATCH 2/2] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a1bab5cd..67fae5ac 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ JSNES ===== -A JavaScript NES emulator. +A JavaScript NES emulator that includes support for saving using localstorage and gamepads. Build -----