├── examples ├── AutoJump-2.js ├── README.md ├── AutoJump.mjs ├── nes_emulator │ ├── emu │ │ ├── controller.js │ │ ├── utils.js │ │ ├── nes.js │ │ ├── rom.js │ │ ├── tile.js │ │ ├── papu.js │ │ ├── mappers.js │ │ └── ppu.js │ └── main.js ├── translator.js └── macros.js ├── README.md └── LICENSE /examples/AutoJump-2.js: -------------------------------------------------------------------------------- 1 | const script = registerScript({ 2 | name: "AutoJump", 3 | version: "1.0.0", 4 | authors: ["1zuna"] 5 | }); 6 | 7 | script.registerModule({ 8 | name: "AutoJump", 9 | category: "Movement", // Movement, Combat, Render, ... 10 | description: "Jumps automatically for you." 11 | }, (mod) => { 12 | mod.on("movementInput", (event) => { 13 | event.setJumping(true); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | This directory contains example scripts that may be used as a reference when developing. 4 | 5 | The following examples are currently available: 6 | 7 | - [macros.js](macros.js): Allows binding client and server commands as well as chat messages to a key. 8 | 9 | - [Autojump.mjs](AutoJump.mjs): Jumps automatically on every tick. Made for LiquidBounce Nextgen. 10 | 11 | - [Autojump-2.js](AutoJump-2.js): Jumps automatically on every tick. Made for LiquidBounce Legacy. 12 | -------------------------------------------------------------------------------- /examples/AutoJump.mjs: -------------------------------------------------------------------------------- 1 | const script = registerScript({ 2 | name: "AutoJump", 3 | version: "1.0.0", 4 | authors: ["lagoon"] 5 | }); 6 | 7 | script.registerModule({ 8 | name: "AutoJump", 9 | category: "Movement", // Movement, Combat, Render, ... 10 | description: "Jumps automatically for you." 11 | }, (mod) => { 12 | mod.on("disable", () => { 13 | mc.options.jumpKey.setPressed(false); 14 | }); 15 | mod.on("playerTick", () => { 16 | mc.options.jumpKey.setPressed(true); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /examples/nes_emulator/emu/controller.js: -------------------------------------------------------------------------------- 1 | var Controller = function () { 2 | this.state = new Array(8); 3 | for (var i = 0; i < this.state.length; i++) { 4 | this.state[i] = 0x40; 5 | } 6 | }; 7 | 8 | Controller.BUTTON_A = 0; 9 | Controller.BUTTON_B = 1; 10 | Controller.BUTTON_SELECT = 2; 11 | Controller.BUTTON_START = 3; 12 | Controller.BUTTON_UP = 4; 13 | Controller.BUTTON_DOWN = 5; 14 | Controller.BUTTON_LEFT = 6; 15 | Controller.BUTTON_RIGHT = 7; 16 | 17 | Controller.prototype = { 18 | buttonDown: function (key) { 19 | this.state[key] = 0x41; 20 | }, 21 | 22 | buttonUp: function (key) { 23 | this.state[key] = 0x40; 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /examples/nes_emulator/emu/utils.js: -------------------------------------------------------------------------------- 1 | 2 | function copyArrayElements(src, srcPos, dest, destPos, length) { 3 | for (var i = 0; i < length; ++i) { 4 | dest[destPos + i] = src[srcPos + i]; 5 | } 6 | } 7 | 8 | function copyArray(src) { 9 | return src.slice(0); 10 | } 11 | 12 | function fromJSON(obj, state) { 13 | for (var i = 0; i < obj.JSON_PROPERTIES.length; i++) { 14 | obj[obj.JSON_PROPERTIES[i]] = state[obj.JSON_PROPERTIES[i]]; 15 | } 16 | } 17 | 18 | function toJSON(obj) { 19 | var state = {}; 20 | for (var i = 0; i < obj.JSON_PROPERTIES.length; i++) { 21 | state[obj.JSON_PROPERTIES[i]] = obj[obj.JSON_PROPERTIES[i]]; 22 | } 23 | return state; 24 | } 25 | 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LiquidBounce Script API 2 | 3 | LiquidBounce's script API allows the development of custom modules and commands for the client without having to modify its source code. It is based on [GraalJS](https://github.com/oracle/graaljs), an ECMAScript 2023 compliant JavaScript implementation built on [GraalVM](https://www.graalvm.org/). GraalVM's [polyglot functionality](https://www.graalvm.org/latest/reference-manual/polyglot-programming/) enables saemless interoperation between scripts written in JavaScript and the client written in Java and Kotlin. This integration facilitates easy access and utilization of Java's and Minecraft's classes, methods, and fields, making development particularly intuitive for those already familiar with Minecraft modding. 4 | 5 | Documentation can be found [on our website](https://liquidbounce.net/docs/Script%20API/Getting%20Started). 6 | 7 | ## Structure of this repository 8 | 9 | - [/examples](examples): Contains example scripts that may be used as a reference. 10 | 11 | ## License 12 | 13 | All files in this repository are licensed under the [MIT License](LICENSE). 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 CCBlueX 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/translator.js: -------------------------------------------------------------------------------- 1 | // printing bugs out with latin alfabet. because minecraft is wierd and doesnt "allow" it 2 | 3 | const script = registerScript({ 4 | name: "Translator", 5 | version: "2.0.0", 6 | authors: ["Trikaes", "1zuna", "MukjepScarlet"], 7 | }); 8 | 9 | script.registerCommand({ 10 | name: "translate", 11 | aliases: ["tr"], 12 | parameters: [ 13 | { 14 | name: "sourceLanguage", 15 | type: "string", 16 | required: true, 17 | description: "The source language code (en, fr, de, etc...)", 18 | }, 19 | { 20 | name: "targetLanguage", 21 | type: "string", 22 | required: true, 23 | description: "The target language code (en, fr, de, etc...)", 24 | }, 25 | { 26 | name: "text", 27 | type: "string", 28 | required: true, 29 | vararg: true, 30 | description: "The text to translate", 31 | }, 32 | ], 33 | onExecute: async (sourceLanguage, targetLanguage, texts) => { 34 | try { 35 | const text = texts.join(" "); 36 | 37 | const response = await AsyncUtil.request((builder) => { 38 | builder.url(`https://translate.googleapis.com/translate_a/single?client=gtx&sl=${sourceLanguage}&tl=${targetLanguage}&dt=t&q=${encodeURIComponent( 39 | text 40 | )}`); 41 | }); 42 | 43 | if (response.code() === 200) { 44 | const jsonResponse = JSON.parse(response.body().string()); 45 | const translatedText = jsonResponse[0][0][0]; 46 | 47 | Client.displayChatMessage( 48 | `§cTranslation §7(§c${sourceLanguage} §7to §c${targetLanguage}§7): §c${text} §7-> §c${translatedText}` 49 | ); // cool colors 50 | } else { 51 | Client.displayChatMessage( 52 | `§4failed to translate text '${text}'. response code: ${response.code()}` 53 | ); // hope i dont get this 54 | } 55 | } catch (error) { 56 | Client.displayChatMessage(`§4error translating text '${text}': ${error}`); // hope i dont get this 57 | } 58 | }, 59 | }); 60 | -------------------------------------------------------------------------------- /examples/macros.js: -------------------------------------------------------------------------------- 1 | /* 2 | This script allows binding client and server commands as well as chat messages to a key. 3 | 4 | Usage: 5 | .macros add 6 | .macros remove 7 | .macros list 8 | 9 | Examples: 10 | .macros add k .help 11 | .macros add o /gamerules doDaylightCycle false 12 | .macros add p Some chat message 13 | */ 14 | const script = registerScript({ 15 | name: "Macros", 16 | version: "1.0.0", 17 | authors: ["Senk Ju"] 18 | }); 19 | 20 | const Files = Java.type("java.nio.file.Files"); 21 | const Paths = Java.type("java.nio.file.Paths"); 22 | const JString = Java.type("java.lang.String"); 23 | 24 | function printFormattedText(message) { 25 | Client.displayChatMessage(`§8[§9§lMacros§8] §3 ${message}`); 26 | } 27 | 28 | function readMacros() { 29 | try { 30 | const file = new JString(Files.readAllBytes(Paths.get("macros.json"))); 31 | return JSON.parse(file); 32 | } catch (e) { 33 | return {}; 34 | } 35 | } 36 | 37 | function writeMacros() { 38 | Files.write(Paths.get("macros.json"), JSON.stringify(macros).getBytes()); 39 | } 40 | 41 | const clientCommandPrefix = "."; // TODO: Get from client 42 | const macros = readMacros(); 43 | 44 | script.registerModule({ 45 | name: "Macros", 46 | description: "Allows you to bind commands to keys.", 47 | category: "Client" 48 | }, (module) => { 49 | module.on("key", (event) => { 50 | if (event.getAction() !== 1) { 51 | return; 52 | } 53 | 54 | const key = event.getKey().getTranslationKey().split(".").pop(); 55 | 56 | const command = macros[key]; 57 | 58 | if (!command) { 59 | return; 60 | } 61 | 62 | switch (command[0]) { 63 | case clientCommandPrefix: 64 | Client.commandManager.execute(command.substring(1)); 65 | break; 66 | case "/": 67 | NetworkUtil.sendCommand(command.substring(1)); 68 | break; 69 | default: 70 | NetworkUtil.sendChatMessage(command); 71 | } 72 | }); 73 | }); 74 | 75 | 76 | script.registerCommand({ 77 | name: "macroy", 78 | aliases: ["macro"], 79 | hub: true, 80 | subcommands: [ 81 | { 82 | name: "add", 83 | parameters: [ 84 | { 85 | name: "key", 86 | required: true, 87 | }, 88 | { 89 | name: "command", 90 | required: true, 91 | vararg: true 92 | } 93 | ], 94 | onExecute(key, commandParts) { 95 | const command = commandParts.join(" "); 96 | 97 | macros[key] = command; 98 | 99 | writeMacros(); 100 | printFormattedText(`Added macro '${command}' to '${key}'.`); 101 | } 102 | }, 103 | { 104 | name: "remove", 105 | parameters: [ 106 | { 107 | name: "key", 108 | required: true 109 | } 110 | ], 111 | onExecute(key) { 112 | delete macros[key]; 113 | 114 | writeMacros(); 115 | printFormattedText(`Removed macro from '${key}'.`); 116 | } 117 | }, 118 | { 119 | name: "list", 120 | onExecute() { 121 | for (const key in macros) { 122 | printFormattedText(`${key} -> '${macros[key]}'`); 123 | } 124 | } 125 | } 126 | ] 127 | }); -------------------------------------------------------------------------------- /examples/nes_emulator/emu/nes.js: -------------------------------------------------------------------------------- 1 | load("./cpu.js"); 2 | load("./controller.js"); 3 | load("./ppu.js"); 4 | load("./papu.js"); 5 | load("./rom.js"); 6 | 7 | var NES = function (opts) { 8 | this.opts = { 9 | onFrame: function () { }, 10 | onAudioSample: null, 11 | onStatusUpdate: function () { }, 12 | onBatteryRamWrite: function () { }, 13 | 14 | // FIXME: not actually used except for in PAPU 15 | preferredFrameRate: 60, 16 | 17 | emulateSound: true, 18 | sampleRate: 44100 // Sound sample rate in hz 19 | }; 20 | if (typeof opts !== "undefined") { 21 | var key; 22 | for (key in this.opts) { 23 | if (typeof opts[key] !== "undefined") { 24 | this.opts[key] = opts[key]; 25 | } 26 | } 27 | } 28 | 29 | this.frameTime = 1000 / this.opts.preferredFrameRate; 30 | 31 | this.ui = { 32 | writeFrame: this.opts.onFrame, 33 | updateStatus: this.opts.onStatusUpdate 34 | }; 35 | this.cpu = new CPU(this); 36 | this.ppu = new PPU(this); 37 | this.papu = new PAPU(this); 38 | this.mmap = null; // set in loadROM() 39 | this.controllers = { 40 | 1: new Controller(), 41 | 2: new Controller() 42 | }; 43 | 44 | this.ui.updateStatus("Ready to load a ROM."); 45 | 46 | this.frame = this.frame.bind(this); 47 | this.buttonDown = this.buttonDown.bind(this); 48 | this.buttonUp = this.buttonUp.bind(this); 49 | this.zapperMove = this.zapperMove.bind(this); 50 | this.zapperFireDown = this.zapperFireDown.bind(this); 51 | this.zapperFireUp = this.zapperFireUp.bind(this); 52 | }; 53 | 54 | NES.prototype = { 55 | fpsFrameCount: 0, 56 | romData: null, 57 | 58 | // Resets the system 59 | reset: function () { 60 | if (this.mmap !== null) { 61 | this.mmap.reset(); 62 | } 63 | 64 | this.cpu.reset(); 65 | this.ppu.reset(); 66 | this.papu.reset(); 67 | 68 | this.lastFpsTime = null; 69 | this.fpsFrameCount = 0; 70 | }, 71 | 72 | frame: function () { 73 | this.ppu.startFrame(); 74 | var cycles = 0; 75 | var emulateSound = this.opts.emulateSound; 76 | var cpu = this.cpu; 77 | var ppu = this.ppu; 78 | var papu = this.papu; 79 | FRAMELOOP: for (; ;) { 80 | if (cpu.cyclesToHalt === 0) { 81 | // Execute a CPU instruction 82 | cycles = cpu.emulate(); 83 | if (emulateSound) { 84 | papu.clockFrameCounter(cycles); 85 | } 86 | cycles *= 3; 87 | } else { 88 | if (cpu.cyclesToHalt > 8) { 89 | cycles = 24; 90 | if (emulateSound) { 91 | papu.clockFrameCounter(8); 92 | } 93 | cpu.cyclesToHalt -= 8; 94 | } else { 95 | cycles = cpu.cyclesToHalt * 3; 96 | if (emulateSound) { 97 | papu.clockFrameCounter(cpu.cyclesToHalt); 98 | } 99 | cpu.cyclesToHalt = 0; 100 | } 101 | } 102 | 103 | for (; cycles > 0; cycles--) { 104 | if ( 105 | ppu.curX === ppu.spr0HitX && 106 | ppu.f_spVisibility === 1 && 107 | ppu.scanline - 21 === ppu.spr0HitY 108 | ) { 109 | // Set sprite 0 hit flag: 110 | ppu.setStatusFlag(ppu.STATUS_SPRITE0HIT, true); 111 | } 112 | 113 | if (ppu.requestEndFrame) { 114 | ppu.nmiCounter--; 115 | if (ppu.nmiCounter === 0) { 116 | ppu.requestEndFrame = false; 117 | ppu.startVBlank(); 118 | break FRAMELOOP; 119 | } 120 | } 121 | 122 | ppu.curX++; 123 | if (ppu.curX === 341) { 124 | ppu.curX = 0; 125 | ppu.endScanline(); 126 | } 127 | } 128 | } 129 | this.fpsFrameCount++; 130 | }, 131 | 132 | buttonDown: function (controller, button) { 133 | this.controllers[controller].buttonDown(button); 134 | }, 135 | 136 | buttonUp: function (controller, button) { 137 | this.controllers[controller].buttonUp(button); 138 | }, 139 | 140 | zapperMove: function (x, y) { 141 | if (!this.mmap) return; 142 | this.mmap.zapperX = x; 143 | this.mmap.zapperY = y; 144 | }, 145 | 146 | zapperFireDown: function () { 147 | if (!this.mmap) return; 148 | this.mmap.zapperFired = true; 149 | }, 150 | 151 | zapperFireUp: function () { 152 | if (!this.mmap) return; 153 | this.mmap.zapperFired = false; 154 | }, 155 | 156 | getFPS: function () { 157 | var now = +new Date(); 158 | var fps = null; 159 | if (this.lastFpsTime) { 160 | fps = this.fpsFrameCount / ((now - this.lastFpsTime) / 1000); 161 | } 162 | this.fpsFrameCount = 0; 163 | this.lastFpsTime = now; 164 | return fps; 165 | }, 166 | 167 | reloadROM: function () { 168 | if (this.romData !== null) { 169 | this.loadROM(this.romData); 170 | } 171 | }, 172 | 173 | // Loads a ROM file into the CPU and PPU. 174 | // The ROM file is validated first. 175 | loadROM: function (data) { 176 | // Load ROM file: 177 | this.rom = new ROM(this); 178 | this.rom.load(data); 179 | 180 | this.reset(); 181 | this.mmap = this.rom.createMapper(); 182 | this.mmap.loadROM(); 183 | this.ppu.setMirroring(this.rom.getMirroringType()); 184 | this.romData = data; 185 | }, 186 | 187 | setFramerate: function (rate) { 188 | this.opts.preferredFrameRate = rate; 189 | this.frameTime = 1000 / rate; 190 | this.papu.setSampleRate(this.opts.sampleRate, false); 191 | }, 192 | 193 | toJSON: function () { 194 | return { 195 | romData: this.romData, 196 | cpu: this.cpu.toJSON(), 197 | mmap: this.mmap.toJSON(), 198 | ppu: this.ppu.toJSON() 199 | }; 200 | }, 201 | 202 | fromJSON: function (s) { 203 | this.loadROM(s.romData); 204 | this.cpu.fromJSON(s.cpu); 205 | this.mmap.fromJSON(s.mmap); 206 | this.ppu.fromJSON(s.ppu); 207 | } 208 | }; 209 | -------------------------------------------------------------------------------- /examples/nes_emulator/main.js: -------------------------------------------------------------------------------- 1 | const script = registerScript({ 2 | name: "NES Emulator", 3 | version: "1.0.0", 4 | authors: ["Senk Ju"], 5 | }); 6 | 7 | const File = Java.type("java.io.File"); 8 | const Files = Java.type("java.nio.file.Files"); 9 | const RenderShortcutsKt = Java.type("net.ccbluex.liquidbounce.render.RenderShortcutsKt"); 10 | const NativeImageBackedTexture = Java.type("net.minecraft.client.texture.NativeImageBackedTexture"); 11 | const RenderSystem = Java.type("com.mojang.blaze3d.systems.RenderSystem"); 12 | const Vec3d = Java.type("net.minecraft.util.math.Vec3d"); 13 | const FileUtils = Java.type("org.apache.commons.io.FileUtils"); 14 | const StandardCharsets = Java.type("java.nio.charset.StandardCharsets"); 15 | const Thread = Java.type("java.lang.Thread"); 16 | 17 | load("./emu/nes.js"); 18 | load("./emu/controller.js"); 19 | 20 | const ROMS_BASE_PATH = "./LiquidBounce/scripts/nes_emulator/roms"; 21 | const SCREEN_HEIGHT = 240; 22 | const SCREEN_WIDTH = 256; 23 | 24 | function readRom(name) { 25 | return FileUtils.readFileToString(new File(`${ROMS_BASE_PATH}/${name}`), StandardCharsets.ISO_8859_1); 26 | } 27 | 28 | function getInstalledRoms() { 29 | return ["None", ...new File(ROMS_BASE_PATH).listFiles().map(f => f.getName())]; 30 | } 31 | 32 | const installedRoms = getInstalledRoms(); 33 | 34 | const texture = new NativeImageBackedTexture(SCREEN_WIDTH, SCREEN_HEIGHT, false); 35 | 36 | script.registerModule({ 37 | name: "NESEmulator", 38 | description: "Allows you to play NES games in the client.", 39 | category: "Fun", 40 | settings: { 41 | rom: Setting.choose({ 42 | name: "ROM", 43 | choices: installedRoms, 44 | default: installedRoms[0] 45 | }), 46 | region: Setting.choose({ 47 | name: "Region", 48 | choices: ["NTSC", "PAL"], 49 | default: "NTSC" 50 | }), 51 | buttonA: Setting.key({ 52 | name: "ButtonA", 53 | default: "key.keyboard.z" 54 | }), 55 | buttonB: Setting.key({ 56 | name: "ButtonB", 57 | default: "key.keyboard.x" 58 | }), 59 | buttonSelect: Setting.key({ 60 | name: "ButtonSelect", 61 | default: "key.keyboard.c" 62 | }), 63 | buttonStart: Setting.key({ 64 | name: "ButtonStart", 65 | default: "key.keyboard.v" 66 | }), 67 | buttonUp: Setting.key({ 68 | name: "ButtonUp", 69 | default: "key.keyboard.up" 70 | }), 71 | buttonDown: Setting.key({ 72 | name: "ButtonDown", 73 | default: "key.keyboard.down" 74 | }), 75 | buttonLeft: Setting.key({ 76 | name: "ButtonLeft", 77 | default: "key.keyboard.left" 78 | }), 79 | buttonRight: Setting.key({ 80 | name: "ButtonRight", 81 | default: "key.keyboard.right" 82 | }) 83 | } 84 | }, mod => { 85 | const controller = [ 86 | { 87 | setting: mod.settings.buttonA, 88 | emulatorKey: Controller.BUTTON_A 89 | }, 90 | { 91 | setting: mod.settings.buttonB, 92 | emulatorKey: Controller.BUTTON_B 93 | }, 94 | { 95 | setting: mod.settings.buttonSelect, 96 | emulatorKey: Controller.BUTTON_SELECT 97 | }, 98 | { 99 | setting: mod.settings.buttonStart, 100 | emulatorKey: Controller.BUTTON_START 101 | }, 102 | { 103 | setting: mod.settings.buttonUp, 104 | emulatorKey: Controller.BUTTON_UP 105 | }, 106 | { 107 | setting: mod.settings.buttonDown, 108 | emulatorKey: Controller.BUTTON_DOWN 109 | }, 110 | { 111 | setting: mod.settings.buttonLeft, 112 | emulatorKey: Controller.BUTTON_LEFT 113 | }, 114 | { 115 | setting: mod.settings.buttonRight, 116 | emulatorKey: Controller.BUTTON_RIGHT 117 | } 118 | ]; 119 | let nes = null; 120 | let dirty = false; 121 | let runEmulator = false; 122 | 123 | mod.on("enable", () => { 124 | if (mod.settings.rom.value === "None") { 125 | Client.displayChatMessage("Error: Mo ROM file selected. Select a ROM, then re-enable the module."); 126 | return; 127 | } 128 | 129 | let targetFramerate = 60; 130 | if (mod.settings.region.value === "PAL") { 131 | targetFramerate = 50; 132 | } 133 | 134 | runEmulator = true; 135 | nes = new NES({ 136 | onFrame: frameBuffer => { 137 | for (let i = 0; i < frameBuffer.length; i++) { 138 | const x = i % SCREEN_WIDTH; 139 | const y = (i / SCREEN_WIDTH) | 0; 140 | 141 | texture.getImage().setColor(x, y, frameBuffer[i] | (255 << 24)); 142 | } 143 | 144 | dirty = true; 145 | } 146 | }); 147 | 148 | nes.loadROM(readRom(mod.settings.rom.value)); 149 | 150 | UnsafeThread.run(() => { 151 | while (runEmulator) { 152 | nes.frame(); 153 | Thread.sleep((1000 / targetFramerate) | 0); 154 | } 155 | }); 156 | }); 157 | 158 | mod.on("disable", () => { 159 | runEmulator = false; 160 | }); 161 | 162 | mod.on("key", e => { 163 | const key = e.getKey(); 164 | 165 | for (const k of controller) { 166 | if (k.setting.value.getTranslationKey() === key.getTranslationKey()) { 167 | if (e.getAction() === 1 || e.getAction() === 2) { 168 | nes.buttonDown(1, k.emulatorKey); 169 | } else { 170 | nes.buttonUp(1, k.emulatorKey); 171 | } 172 | } 173 | } 174 | }); 175 | 176 | mod.on("overlayRender", e => { 177 | const context = e.getContext(); 178 | 179 | const x = context.getScaledWindowWidth() / 2 - SCREEN_WIDTH / 2; 180 | const y = context.getScaledWindowHeight() / 2 - SCREEN_HEIGHT / 2; 181 | 182 | RenderShortcutsKt.renderEnvironmentForGUI(context.getMatrices(), renderEnvironment => { 183 | texture.bindTexture(); 184 | 185 | if (dirty) { 186 | texture.upload(); 187 | dirty = false; 188 | } 189 | 190 | RenderSystem.bindTexture(texture.getGlId()); 191 | RenderSystem.setShaderTexture(0, texture.getGlId()); 192 | 193 | RenderShortcutsKt.drawTextureQuad(renderEnvironment, new Vec3d(x, y, 0), new Vec3d(x + SCREEN_WIDTH, y + SCREEN_HEIGHT, 0)); 194 | 195 | RenderSystem.setShaderTexture(0, 0); 196 | }); 197 | }); 198 | }); 199 | 200 | -------------------------------------------------------------------------------- /examples/nes_emulator/emu/rom.js: -------------------------------------------------------------------------------- 1 | load("./mappers.js"); 2 | load("./tile.js"); 3 | 4 | var ROM = function (nes) { 5 | this.nes = nes; 6 | 7 | this.mapperName = new Array(92); 8 | 9 | for (var i = 0; i < 92; i++) { 10 | this.mapperName[i] = "Unknown Mapper"; 11 | } 12 | this.mapperName[0] = "Direct Access"; 13 | this.mapperName[1] = "Nintendo MMC1"; 14 | this.mapperName[2] = "UNROM"; 15 | this.mapperName[3] = "CNROM"; 16 | this.mapperName[4] = "Nintendo MMC3"; 17 | this.mapperName[5] = "Nintendo MMC5"; 18 | this.mapperName[6] = "FFE F4xxx"; 19 | this.mapperName[7] = "AOROM"; 20 | this.mapperName[8] = "FFE F3xxx"; 21 | this.mapperName[9] = "Nintendo MMC2"; 22 | this.mapperName[10] = "Nintendo MMC4"; 23 | this.mapperName[11] = "Color Dreams Chip"; 24 | this.mapperName[12] = "FFE F6xxx"; 25 | this.mapperName[15] = "100-in-1 switch"; 26 | this.mapperName[16] = "Bandai chip"; 27 | this.mapperName[17] = "FFE F8xxx"; 28 | this.mapperName[18] = "Jaleco SS8806 chip"; 29 | this.mapperName[19] = "Namcot 106 chip"; 30 | this.mapperName[20] = "Famicom Disk System"; 31 | this.mapperName[21] = "Konami VRC4a"; 32 | this.mapperName[22] = "Konami VRC2a"; 33 | this.mapperName[23] = "Konami VRC2a"; 34 | this.mapperName[24] = "Konami VRC6"; 35 | this.mapperName[25] = "Konami VRC4b"; 36 | this.mapperName[32] = "Irem G-101 chip"; 37 | this.mapperName[33] = "Taito TC0190/TC0350"; 38 | this.mapperName[34] = "32kB ROM switch"; 39 | 40 | this.mapperName[64] = "Tengen RAMBO-1 chip"; 41 | this.mapperName[65] = "Irem H-3001 chip"; 42 | this.mapperName[66] = "GNROM switch"; 43 | this.mapperName[67] = "SunSoft3 chip"; 44 | this.mapperName[68] = "SunSoft4 chip"; 45 | this.mapperName[69] = "SunSoft5 FME-7 chip"; 46 | this.mapperName[71] = "Camerica chip"; 47 | this.mapperName[78] = "Irem 74HC161/32-based"; 48 | this.mapperName[91] = "Pirate HK-SF3 chip"; 49 | }; 50 | 51 | ROM.prototype = { 52 | // Mirroring types: 53 | VERTICAL_MIRRORING: 0, 54 | HORIZONTAL_MIRRORING: 1, 55 | FOURSCREEN_MIRRORING: 2, 56 | SINGLESCREEN_MIRRORING: 3, 57 | SINGLESCREEN_MIRRORING2: 4, 58 | SINGLESCREEN_MIRRORING3: 5, 59 | SINGLESCREEN_MIRRORING4: 6, 60 | CHRROM_MIRRORING: 7, 61 | 62 | header: null, 63 | rom: null, 64 | vrom: null, 65 | vromTile: null, 66 | 67 | romCount: null, 68 | vromCount: null, 69 | mirroring: null, 70 | batteryRam: null, 71 | trainer: null, 72 | fourScreen: null, 73 | mapperType: null, 74 | valid: false, 75 | 76 | load: function (data) { 77 | var i, j, v; 78 | 79 | if (data.indexOf("NES\x1a") === -1) { 80 | throw new Error("Not a valid NES ROM."); 81 | } 82 | this.header = new Array(16); 83 | for (i = 0; i < 16; i++) { 84 | this.header[i] = data.charCodeAt(i) & 0xff; 85 | } 86 | this.romCount = this.header[4]; 87 | this.vromCount = this.header[5] * 2; // Get the number of 4kB banks, not 8kB 88 | this.mirroring = (this.header[6] & 1) !== 0 ? 1 : 0; 89 | this.batteryRam = (this.header[6] & 2) !== 0; 90 | this.trainer = (this.header[6] & 4) !== 0; 91 | this.fourScreen = (this.header[6] & 8) !== 0; 92 | this.mapperType = (this.header[6] >> 4) | (this.header[7] & 0xf0); 93 | /* TODO 94 | if (this.batteryRam) 95 | this.loadBatteryRam();*/ 96 | // Check whether byte 8-15 are zero's: 97 | var foundError = false; 98 | for (i = 8; i < 16; i++) { 99 | if (this.header[i] !== 0) { 100 | foundError = true; 101 | break; 102 | } 103 | } 104 | if (foundError) { 105 | this.mapperType &= 0xf; // Ignore byte 7 106 | } 107 | // Load PRG-ROM banks: 108 | this.rom = new Array(this.romCount); 109 | var offset = 16; 110 | for (i = 0; i < this.romCount; i++) { 111 | this.rom[i] = new Array(16384); 112 | for (j = 0; j < 16384; j++) { 113 | if (offset + j >= data.length) { 114 | break; 115 | } 116 | this.rom[i][j] = data.charCodeAt(offset + j) & 0xff; 117 | } 118 | offset += 16384; 119 | } 120 | // Load CHR-ROM banks: 121 | this.vrom = new Array(this.vromCount); 122 | for (i = 0; i < this.vromCount; i++) { 123 | this.vrom[i] = new Array(4096); 124 | for (j = 0; j < 4096; j++) { 125 | if (offset + j >= data.length) { 126 | break; 127 | } 128 | this.vrom[i][j] = data.charCodeAt(offset + j) & 0xff; 129 | } 130 | offset += 4096; 131 | } 132 | 133 | // Create VROM tiles: 134 | this.vromTile = new Array(this.vromCount); 135 | for (i = 0; i < this.vromCount; i++) { 136 | this.vromTile[i] = new Array(256); 137 | for (j = 0; j < 256; j++) { 138 | this.vromTile[i][j] = new Tile(); 139 | } 140 | } 141 | 142 | // Convert CHR-ROM banks to tiles: 143 | var tileIndex; 144 | var leftOver; 145 | for (v = 0; v < this.vromCount; v++) { 146 | for (i = 0; i < 4096; i++) { 147 | tileIndex = i >> 4; 148 | leftOver = i % 16; 149 | if (leftOver < 8) { 150 | this.vromTile[v][tileIndex].setScanline( 151 | leftOver, 152 | this.vrom[v][i], 153 | this.vrom[v][i + 8] 154 | ); 155 | } else { 156 | this.vromTile[v][tileIndex].setScanline( 157 | leftOver - 8, 158 | this.vrom[v][i - 8], 159 | this.vrom[v][i] 160 | ); 161 | } 162 | } 163 | } 164 | 165 | this.valid = true; 166 | }, 167 | 168 | getMirroringType: function () { 169 | if (this.fourScreen) { 170 | return this.FOURSCREEN_MIRRORING; 171 | } 172 | if (this.mirroring === 0) { 173 | return this.HORIZONTAL_MIRRORING; 174 | } 175 | return this.VERTICAL_MIRRORING; 176 | }, 177 | 178 | getMapperName: function () { 179 | if (this.mapperType >= 0 && this.mapperType < this.mapperName.length) { 180 | return this.mapperName[this.mapperType]; 181 | } 182 | return "Unknown Mapper, " + this.mapperType; 183 | }, 184 | 185 | mapperSupported: function () { 186 | return typeof Mappers[this.mapperType] !== "undefined"; 187 | }, 188 | 189 | createMapper: function () { 190 | if (this.mapperSupported()) { 191 | return new Mappers[this.mapperType](this.nes); 192 | } else { 193 | throw new Error( 194 | "This ROM uses a mapper not supported by JSNES: " + 195 | this.getMapperName() + 196 | "(" + 197 | this.mapperType + 198 | ")" 199 | ); 200 | } 201 | } 202 | }; 203 | -------------------------------------------------------------------------------- /examples/nes_emulator/emu/tile.js: -------------------------------------------------------------------------------- 1 | var Tile = function () { 2 | // Tile data: 3 | this.pix = new Array(64); 4 | 5 | this.fbIndex = null; 6 | this.tIndex = null; 7 | this.x = null; 8 | this.y = null; 9 | this.w = null; 10 | this.h = null; 11 | this.incX = null; 12 | this.incY = null; 13 | this.palIndex = null; 14 | this.tpri = null; 15 | this.c = null; 16 | this.initialized = false; 17 | this.opaque = new Array(8); 18 | }; 19 | 20 | Tile.prototype = { 21 | setBuffer: function (scanline) { 22 | for (this.y = 0; this.y < 8; this.y++) { 23 | this.setScanline(this.y, scanline[this.y], scanline[this.y + 8]); 24 | } 25 | }, 26 | 27 | setScanline: function (sline, b1, b2) { 28 | this.initialized = true; 29 | this.tIndex = sline << 3; 30 | for (this.x = 0; this.x < 8; this.x++) { 31 | this.pix[this.tIndex + this.x] = 32 | ((b1 >> (7 - this.x)) & 1) + (((b2 >> (7 - this.x)) & 1) << 1); 33 | if (this.pix[this.tIndex + this.x] === 0) { 34 | this.opaque[sline] = false; 35 | } 36 | } 37 | }, 38 | 39 | render: function ( 40 | buffer, 41 | srcx1, 42 | srcy1, 43 | srcx2, 44 | srcy2, 45 | dx, 46 | dy, 47 | palAdd, 48 | palette, 49 | flipHorizontal, 50 | flipVertical, 51 | pri, 52 | priTable 53 | ) { 54 | if (dx < -7 || dx >= 256 || dy < -7 || dy >= 240) { 55 | return; 56 | } 57 | 58 | this.w = srcx2 - srcx1; 59 | this.h = srcy2 - srcy1; 60 | 61 | if (dx < 0) { 62 | srcx1 -= dx; 63 | } 64 | if (dx + srcx2 >= 256) { 65 | srcx2 = 256 - dx; 66 | } 67 | 68 | if (dy < 0) { 69 | srcy1 -= dy; 70 | } 71 | if (dy + srcy2 >= 240) { 72 | srcy2 = 240 - dy; 73 | } 74 | 75 | if (!flipHorizontal && !flipVertical) { 76 | this.fbIndex = (dy << 8) + dx; 77 | this.tIndex = 0; 78 | for (this.y = 0; this.y < 8; this.y++) { 79 | for (this.x = 0; this.x < 8; this.x++) { 80 | if ( 81 | this.x >= srcx1 && 82 | this.x < srcx2 && 83 | this.y >= srcy1 && 84 | this.y < srcy2 85 | ) { 86 | this.palIndex = this.pix[this.tIndex]; 87 | this.tpri = priTable[this.fbIndex]; 88 | if (this.palIndex !== 0 && pri <= (this.tpri & 0xff)) { 89 | //console.log("Rendering upright tile to buffer"); 90 | buffer[this.fbIndex] = palette[this.palIndex + palAdd]; 91 | this.tpri = (this.tpri & 0xf00) | pri; 92 | priTable[this.fbIndex] = this.tpri; 93 | } 94 | } 95 | this.fbIndex++; 96 | this.tIndex++; 97 | } 98 | this.fbIndex -= 8; 99 | this.fbIndex += 256; 100 | } 101 | } else if (flipHorizontal && !flipVertical) { 102 | this.fbIndex = (dy << 8) + dx; 103 | this.tIndex = 7; 104 | for (this.y = 0; this.y < 8; this.y++) { 105 | for (this.x = 0; this.x < 8; this.x++) { 106 | if ( 107 | this.x >= srcx1 && 108 | this.x < srcx2 && 109 | this.y >= srcy1 && 110 | this.y < srcy2 111 | ) { 112 | this.palIndex = this.pix[this.tIndex]; 113 | this.tpri = priTable[this.fbIndex]; 114 | if (this.palIndex !== 0 && pri <= (this.tpri & 0xff)) { 115 | buffer[this.fbIndex] = palette[this.palIndex + palAdd]; 116 | this.tpri = (this.tpri & 0xf00) | pri; 117 | priTable[this.fbIndex] = this.tpri; 118 | } 119 | } 120 | this.fbIndex++; 121 | this.tIndex--; 122 | } 123 | this.fbIndex -= 8; 124 | this.fbIndex += 256; 125 | this.tIndex += 16; 126 | } 127 | } else if (flipVertical && !flipHorizontal) { 128 | this.fbIndex = (dy << 8) + dx; 129 | this.tIndex = 56; 130 | for (this.y = 0; this.y < 8; this.y++) { 131 | for (this.x = 0; this.x < 8; this.x++) { 132 | if ( 133 | this.x >= srcx1 && 134 | this.x < srcx2 && 135 | this.y >= srcy1 && 136 | this.y < srcy2 137 | ) { 138 | this.palIndex = this.pix[this.tIndex]; 139 | this.tpri = priTable[this.fbIndex]; 140 | if (this.palIndex !== 0 && pri <= (this.tpri & 0xff)) { 141 | buffer[this.fbIndex] = palette[this.palIndex + palAdd]; 142 | this.tpri = (this.tpri & 0xf00) | pri; 143 | priTable[this.fbIndex] = this.tpri; 144 | } 145 | } 146 | this.fbIndex++; 147 | this.tIndex++; 148 | } 149 | this.fbIndex -= 8; 150 | this.fbIndex += 256; 151 | this.tIndex -= 16; 152 | } 153 | } else { 154 | this.fbIndex = (dy << 8) + dx; 155 | this.tIndex = 63; 156 | for (this.y = 0; this.y < 8; this.y++) { 157 | for (this.x = 0; this.x < 8; this.x++) { 158 | if ( 159 | this.x >= srcx1 && 160 | this.x < srcx2 && 161 | this.y >= srcy1 && 162 | this.y < srcy2 163 | ) { 164 | this.palIndex = this.pix[this.tIndex]; 165 | this.tpri = priTable[this.fbIndex]; 166 | if (this.palIndex !== 0 && pri <= (this.tpri & 0xff)) { 167 | buffer[this.fbIndex] = palette[this.palIndex + palAdd]; 168 | this.tpri = (this.tpri & 0xf00) | pri; 169 | priTable[this.fbIndex] = this.tpri; 170 | } 171 | } 172 | this.fbIndex++; 173 | this.tIndex--; 174 | } 175 | this.fbIndex -= 8; 176 | this.fbIndex += 256; 177 | } 178 | } 179 | }, 180 | 181 | isTransparent: function (x, y) { 182 | return this.pix[(y << 3) + x] === 0; 183 | }, 184 | 185 | toJSON: function () { 186 | return { 187 | opaque: this.opaque, 188 | pix: this.pix 189 | }; 190 | }, 191 | 192 | fromJSON: function (s) { 193 | this.opaque = s.opaque; 194 | this.pix = s.pix; 195 | } 196 | }; -------------------------------------------------------------------------------- /examples/nes_emulator/emu/papu.js: -------------------------------------------------------------------------------- 1 | var CPU_FREQ_NTSC = 1789772.5; //1789772.72727272d; 2 | // var CPU_FREQ_PAL = 1773447.4; 3 | 4 | var PAPU = function (nes) { 5 | this.nes = nes; 6 | 7 | this.square1 = new ChannelSquare(this, true); 8 | this.square2 = new ChannelSquare(this, false); 9 | this.triangle = new ChannelTriangle(this); 10 | this.noise = new ChannelNoise(this); 11 | this.dmc = new ChannelDM(this); 12 | 13 | this.frameIrqCounter = null; 14 | this.frameIrqCounterMax = 4; 15 | this.initCounter = 2048; 16 | this.channelEnableValue = null; 17 | 18 | this.sampleRate = 44100; 19 | 20 | this.lengthLookup = null; 21 | this.dmcFreqLookup = null; 22 | this.noiseWavelengthLookup = null; 23 | this.square_table = null; 24 | this.tnd_table = null; 25 | 26 | this.frameIrqEnabled = false; 27 | this.frameIrqActive = null; 28 | this.frameClockNow = null; 29 | this.startedPlaying = false; 30 | this.recordOutput = false; 31 | this.initingHardware = false; 32 | 33 | this.masterFrameCounter = null; 34 | this.derivedFrameCounter = null; 35 | this.countSequence = null; 36 | this.sampleTimer = null; 37 | this.frameTime = null; 38 | this.sampleTimerMax = null; 39 | this.sampleCount = null; 40 | this.triValue = 0; 41 | 42 | this.smpSquare1 = null; 43 | this.smpSquare2 = null; 44 | this.smpTriangle = null; 45 | this.smpDmc = null; 46 | this.accCount = null; 47 | 48 | // DC removal vars: 49 | this.prevSampleL = 0; 50 | this.prevSampleR = 0; 51 | this.smpAccumL = 0; 52 | this.smpAccumR = 0; 53 | 54 | // DAC range: 55 | this.dacRange = 0; 56 | this.dcValue = 0; 57 | 58 | // Master volume: 59 | this.masterVolume = 256; 60 | 61 | // Stereo positioning: 62 | this.stereoPosLSquare1 = null; 63 | this.stereoPosLSquare2 = null; 64 | this.stereoPosLTriangle = null; 65 | this.stereoPosLNoise = null; 66 | this.stereoPosLDMC = null; 67 | this.stereoPosRSquare1 = null; 68 | this.stereoPosRSquare2 = null; 69 | this.stereoPosRTriangle = null; 70 | this.stereoPosRNoise = null; 71 | this.stereoPosRDMC = null; 72 | 73 | this.extraCycles = null; 74 | 75 | this.maxSample = null; 76 | this.minSample = null; 77 | 78 | // Panning: 79 | this.panning = [80, 170, 100, 150, 128]; 80 | this.setPanning(this.panning); 81 | 82 | // Initialize lookup tables: 83 | this.initLengthLookup(); 84 | this.initDmcFrequencyLookup(); 85 | this.initNoiseWavelengthLookup(); 86 | this.initDACtables(); 87 | 88 | // Init sound registers: 89 | for (var i = 0; i < 0x14; i++) { 90 | if (i === 0x10) { 91 | this.writeReg(0x4010, 0x10); 92 | } else { 93 | this.writeReg(0x4000 + i, 0); 94 | } 95 | } 96 | 97 | this.reset(); 98 | }; 99 | 100 | PAPU.prototype = { 101 | reset: function () { 102 | this.sampleRate = this.nes.opts.sampleRate; 103 | this.sampleTimerMax = Math.floor( 104 | (1024.0 * CPU_FREQ_NTSC * this.nes.opts.preferredFrameRate) / 105 | (this.sampleRate * 60.0) 106 | ); 107 | 108 | this.frameTime = Math.floor( 109 | (14915.0 * this.nes.opts.preferredFrameRate) / 60.0 110 | ); 111 | 112 | this.sampleTimer = 0; 113 | 114 | this.updateChannelEnable(0); 115 | this.masterFrameCounter = 0; 116 | this.derivedFrameCounter = 0; 117 | this.countSequence = 0; 118 | this.sampleCount = 0; 119 | this.initCounter = 2048; 120 | this.frameIrqEnabled = false; 121 | this.initingHardware = false; 122 | 123 | this.resetCounter(); 124 | 125 | this.square1.reset(); 126 | this.square2.reset(); 127 | this.triangle.reset(); 128 | this.noise.reset(); 129 | this.dmc.reset(); 130 | 131 | this.accCount = 0; 132 | this.smpSquare1 = 0; 133 | this.smpSquare2 = 0; 134 | this.smpTriangle = 0; 135 | this.smpDmc = 0; 136 | 137 | this.frameIrqEnabled = false; 138 | this.frameIrqCounterMax = 4; 139 | 140 | this.channelEnableValue = 0xff; 141 | this.startedPlaying = false; 142 | this.prevSampleL = 0; 143 | this.prevSampleR = 0; 144 | this.smpAccumL = 0; 145 | this.smpAccumR = 0; 146 | 147 | this.maxSample = -500000; 148 | this.minSample = 500000; 149 | }, 150 | 151 | // eslint-disable-next-line no-unused-vars 152 | readReg: function (address) { 153 | // Read 0x4015: 154 | var tmp = 0; 155 | tmp |= this.square1.getLengthStatus(); 156 | tmp |= this.square2.getLengthStatus() << 1; 157 | tmp |= this.triangle.getLengthStatus() << 2; 158 | tmp |= this.noise.getLengthStatus() << 3; 159 | tmp |= this.dmc.getLengthStatus() << 4; 160 | tmp |= (this.frameIrqActive && this.frameIrqEnabled ? 1 : 0) << 6; 161 | tmp |= this.dmc.getIrqStatus() << 7; 162 | 163 | this.frameIrqActive = false; 164 | this.dmc.irqGenerated = false; 165 | 166 | return tmp & 0xffff; 167 | }, 168 | 169 | writeReg: function (address, value) { 170 | if (address >= 0x4000 && address < 0x4004) { 171 | // Square Wave 1 Control 172 | this.square1.writeReg(address, value); 173 | // console.log("Square Write"); 174 | } else if (address >= 0x4004 && address < 0x4008) { 175 | // Square 2 Control 176 | this.square2.writeReg(address, value); 177 | } else if (address >= 0x4008 && address < 0x400c) { 178 | // Triangle Control 179 | this.triangle.writeReg(address, value); 180 | } else if (address >= 0x400c && address <= 0x400f) { 181 | // Noise Control 182 | this.noise.writeReg(address, value); 183 | } else if (address === 0x4010) { 184 | // DMC Play mode & DMA frequency 185 | this.dmc.writeReg(address, value); 186 | } else if (address === 0x4011) { 187 | // DMC Delta Counter 188 | this.dmc.writeReg(address, value); 189 | } else if (address === 0x4012) { 190 | // DMC Play code starting address 191 | this.dmc.writeReg(address, value); 192 | } else if (address === 0x4013) { 193 | // DMC Play code length 194 | this.dmc.writeReg(address, value); 195 | } else if (address === 0x4015) { 196 | // Channel enable 197 | this.updateChannelEnable(value); 198 | 199 | if (value !== 0 && this.initCounter > 0) { 200 | // Start hardware initialization 201 | this.initingHardware = true; 202 | } 203 | 204 | // DMC/IRQ Status 205 | this.dmc.writeReg(address, value); 206 | } else if (address === 0x4017) { 207 | // Frame counter control 208 | this.countSequence = (value >> 7) & 1; 209 | this.masterFrameCounter = 0; 210 | this.frameIrqActive = false; 211 | 212 | if (((value >> 6) & 0x1) === 0) { 213 | this.frameIrqEnabled = true; 214 | } else { 215 | this.frameIrqEnabled = false; 216 | } 217 | 218 | if (this.countSequence === 0) { 219 | // NTSC: 220 | this.frameIrqCounterMax = 4; 221 | this.derivedFrameCounter = 4; 222 | } else { 223 | // PAL: 224 | this.frameIrqCounterMax = 5; 225 | this.derivedFrameCounter = 0; 226 | this.frameCounterTick(); 227 | } 228 | } 229 | }, 230 | 231 | resetCounter: function () { 232 | if (this.countSequence === 0) { 233 | this.derivedFrameCounter = 4; 234 | } else { 235 | this.derivedFrameCounter = 0; 236 | } 237 | }, 238 | 239 | // Updates channel enable status. 240 | // This is done on writes to the 241 | // channel enable register (0x4015), 242 | // and when the user enables/disables channels 243 | // in the GUI. 244 | updateChannelEnable: function (value) { 245 | this.channelEnableValue = value & 0xffff; 246 | this.square1.setEnabled((value & 1) !== 0); 247 | this.square2.setEnabled((value & 2) !== 0); 248 | this.triangle.setEnabled((value & 4) !== 0); 249 | this.noise.setEnabled((value & 8) !== 0); 250 | this.dmc.setEnabled((value & 16) !== 0); 251 | }, 252 | 253 | // Clocks the frame counter. It should be clocked at 254 | // twice the cpu speed, so the cycles will be 255 | // divided by 2 for those counters that are 256 | // clocked at cpu speed. 257 | clockFrameCounter: function (nCycles) { 258 | if (this.initCounter > 0) { 259 | if (this.initingHardware) { 260 | this.initCounter -= nCycles; 261 | if (this.initCounter <= 0) { 262 | this.initingHardware = false; 263 | } 264 | return; 265 | } 266 | } 267 | 268 | // Don't process ticks beyond next sampling: 269 | nCycles += this.extraCycles; 270 | var maxCycles = this.sampleTimerMax - this.sampleTimer; 271 | if (nCycles << 10 > maxCycles) { 272 | this.extraCycles = ((nCycles << 10) - maxCycles) >> 10; 273 | nCycles -= this.extraCycles; 274 | } else { 275 | this.extraCycles = 0; 276 | } 277 | 278 | var dmc = this.dmc; 279 | var triangle = this.triangle; 280 | var square1 = this.square1; 281 | var square2 = this.square2; 282 | var noise = this.noise; 283 | 284 | // Clock DMC: 285 | if (dmc.isEnabled) { 286 | dmc.shiftCounter -= nCycles << 3; 287 | while (dmc.shiftCounter <= 0 && dmc.dmaFrequency > 0) { 288 | dmc.shiftCounter += dmc.dmaFrequency; 289 | dmc.clockDmc(); 290 | } 291 | } 292 | 293 | // Clock Triangle channel Prog timer: 294 | if (triangle.progTimerMax > 0) { 295 | triangle.progTimerCount -= nCycles; 296 | while (triangle.progTimerCount <= 0) { 297 | triangle.progTimerCount += triangle.progTimerMax + 1; 298 | if (triangle.linearCounter > 0 && triangle.lengthCounter > 0) { 299 | triangle.triangleCounter++; 300 | triangle.triangleCounter &= 0x1f; 301 | 302 | if (triangle.isEnabled) { 303 | if (triangle.triangleCounter >= 0x10) { 304 | // Normal value. 305 | triangle.sampleValue = triangle.triangleCounter & 0xf; 306 | } else { 307 | // Inverted value. 308 | triangle.sampleValue = 0xf - (triangle.triangleCounter & 0xf); 309 | } 310 | triangle.sampleValue <<= 4; 311 | } 312 | } 313 | } 314 | } 315 | 316 | // Clock Square channel 1 Prog timer: 317 | square1.progTimerCount -= nCycles; 318 | if (square1.progTimerCount <= 0) { 319 | square1.progTimerCount += (square1.progTimerMax + 1) << 1; 320 | 321 | square1.squareCounter++; 322 | square1.squareCounter &= 0x7; 323 | square1.updateSampleValue(); 324 | } 325 | 326 | // Clock Square channel 2 Prog timer: 327 | square2.progTimerCount -= nCycles; 328 | if (square2.progTimerCount <= 0) { 329 | square2.progTimerCount += (square2.progTimerMax + 1) << 1; 330 | 331 | square2.squareCounter++; 332 | square2.squareCounter &= 0x7; 333 | square2.updateSampleValue(); 334 | } 335 | 336 | // Clock noise channel Prog timer: 337 | var acc_c = nCycles; 338 | if (noise.progTimerCount - acc_c > 0) { 339 | // Do all cycles at once: 340 | noise.progTimerCount -= acc_c; 341 | noise.accCount += acc_c; 342 | noise.accValue += acc_c * noise.sampleValue; 343 | } else { 344 | // Slow-step: 345 | while (acc_c-- > 0) { 346 | if (--noise.progTimerCount <= 0 && noise.progTimerMax > 0) { 347 | // Update noise shift register: 348 | noise.shiftReg <<= 1; 349 | noise.tmp = 350 | ((noise.shiftReg << (noise.randomMode === 0 ? 1 : 6)) ^ 351 | noise.shiftReg) & 352 | 0x8000; 353 | if (noise.tmp !== 0) { 354 | // Sample value must be 0. 355 | noise.shiftReg |= 0x01; 356 | noise.randomBit = 0; 357 | noise.sampleValue = 0; 358 | } else { 359 | // Find sample value: 360 | noise.randomBit = 1; 361 | if (noise.isEnabled && noise.lengthCounter > 0) { 362 | noise.sampleValue = noise.masterVolume; 363 | } else { 364 | noise.sampleValue = 0; 365 | } 366 | } 367 | 368 | noise.progTimerCount += noise.progTimerMax; 369 | } 370 | 371 | noise.accValue += noise.sampleValue; 372 | noise.accCount++; 373 | } 374 | } 375 | 376 | // Frame IRQ handling: 377 | if (this.frameIrqEnabled && this.frameIrqActive) { 378 | this.nes.cpu.requestIrq(this.nes.cpu.IRQ_NORMAL); 379 | } 380 | 381 | // Clock frame counter at double CPU speed: 382 | this.masterFrameCounter += nCycles << 1; 383 | if (this.masterFrameCounter >= this.frameTime) { 384 | // 240Hz tick: 385 | this.masterFrameCounter -= this.frameTime; 386 | this.frameCounterTick(); 387 | } 388 | 389 | // Accumulate sample value: 390 | this.accSample(nCycles); 391 | 392 | // Clock sample timer: 393 | this.sampleTimer += nCycles << 10; 394 | if (this.sampleTimer >= this.sampleTimerMax) { 395 | // Sample channels: 396 | this.sample(); 397 | this.sampleTimer -= this.sampleTimerMax; 398 | } 399 | }, 400 | 401 | accSample: function (cycles) { 402 | // Special treatment for triangle channel - need to interpolate. 403 | if (this.triangle.sampleCondition) { 404 | this.triValue = Math.floor( 405 | (this.triangle.progTimerCount << 4) / (this.triangle.progTimerMax + 1) 406 | ); 407 | if (this.triValue > 16) { 408 | this.triValue = 16; 409 | } 410 | if (this.triangle.triangleCounter >= 16) { 411 | this.triValue = 16 - this.triValue; 412 | } 413 | 414 | // Add non-interpolated sample value: 415 | this.triValue += this.triangle.sampleValue; 416 | } 417 | 418 | // Now sample normally: 419 | if (cycles === 2) { 420 | this.smpTriangle += this.triValue << 1; 421 | this.smpDmc += this.dmc.sample << 1; 422 | this.smpSquare1 += this.square1.sampleValue << 1; 423 | this.smpSquare2 += this.square2.sampleValue << 1; 424 | this.accCount += 2; 425 | } else if (cycles === 4) { 426 | this.smpTriangle += this.triValue << 2; 427 | this.smpDmc += this.dmc.sample << 2; 428 | this.smpSquare1 += this.square1.sampleValue << 2; 429 | this.smpSquare2 += this.square2.sampleValue << 2; 430 | this.accCount += 4; 431 | } else { 432 | this.smpTriangle += cycles * this.triValue; 433 | this.smpDmc += cycles * this.dmc.sample; 434 | this.smpSquare1 += cycles * this.square1.sampleValue; 435 | this.smpSquare2 += cycles * this.square2.sampleValue; 436 | this.accCount += cycles; 437 | } 438 | }, 439 | 440 | frameCounterTick: function () { 441 | this.derivedFrameCounter++; 442 | if (this.derivedFrameCounter >= this.frameIrqCounterMax) { 443 | this.derivedFrameCounter = 0; 444 | } 445 | 446 | if (this.derivedFrameCounter === 1 || this.derivedFrameCounter === 3) { 447 | // Clock length & sweep: 448 | this.triangle.clockLengthCounter(); 449 | this.square1.clockLengthCounter(); 450 | this.square2.clockLengthCounter(); 451 | this.noise.clockLengthCounter(); 452 | this.square1.clockSweep(); 453 | this.square2.clockSweep(); 454 | } 455 | 456 | if (this.derivedFrameCounter >= 0 && this.derivedFrameCounter < 4) { 457 | // Clock linear & decay: 458 | this.square1.clockEnvDecay(); 459 | this.square2.clockEnvDecay(); 460 | this.noise.clockEnvDecay(); 461 | this.triangle.clockLinearCounter(); 462 | } 463 | 464 | if (this.derivedFrameCounter === 3 && this.countSequence === 0) { 465 | // Enable IRQ: 466 | this.frameIrqActive = true; 467 | } 468 | 469 | // End of 240Hz tick 470 | }, 471 | 472 | // Samples the channels, mixes the output together, then writes to buffer. 473 | sample: function () { 474 | var sq_index, tnd_index; 475 | 476 | if (this.accCount > 0) { 477 | this.smpSquare1 <<= 4; 478 | this.smpSquare1 = Math.floor(this.smpSquare1 / this.accCount); 479 | 480 | this.smpSquare2 <<= 4; 481 | this.smpSquare2 = Math.floor(this.smpSquare2 / this.accCount); 482 | 483 | this.smpTriangle = Math.floor(this.smpTriangle / this.accCount); 484 | 485 | this.smpDmc <<= 4; 486 | this.smpDmc = Math.floor(this.smpDmc / this.accCount); 487 | 488 | this.accCount = 0; 489 | } else { 490 | this.smpSquare1 = this.square1.sampleValue << 4; 491 | this.smpSquare2 = this.square2.sampleValue << 4; 492 | this.smpTriangle = this.triangle.sampleValue; 493 | this.smpDmc = this.dmc.sample << 4; 494 | } 495 | 496 | var smpNoise = Math.floor((this.noise.accValue << 4) / this.noise.accCount); 497 | this.noise.accValue = smpNoise >> 4; 498 | this.noise.accCount = 1; 499 | 500 | // Stereo sound. 501 | 502 | // Left channel: 503 | sq_index = 504 | (this.smpSquare1 * this.stereoPosLSquare1 + 505 | this.smpSquare2 * this.stereoPosLSquare2) >> 506 | 8; 507 | tnd_index = 508 | (3 * this.smpTriangle * this.stereoPosLTriangle + 509 | (smpNoise << 1) * this.stereoPosLNoise + 510 | this.smpDmc * this.stereoPosLDMC) >> 511 | 8; 512 | if (sq_index >= this.square_table.length) { 513 | sq_index = this.square_table.length - 1; 514 | } 515 | if (tnd_index >= this.tnd_table.length) { 516 | tnd_index = this.tnd_table.length - 1; 517 | } 518 | var sampleValueL = 519 | this.square_table[sq_index] + this.tnd_table[tnd_index] - this.dcValue; 520 | 521 | // Right channel: 522 | sq_index = 523 | (this.smpSquare1 * this.stereoPosRSquare1 + 524 | this.smpSquare2 * this.stereoPosRSquare2) >> 525 | 8; 526 | tnd_index = 527 | (3 * this.smpTriangle * this.stereoPosRTriangle + 528 | (smpNoise << 1) * this.stereoPosRNoise + 529 | this.smpDmc * this.stereoPosRDMC) >> 530 | 8; 531 | if (sq_index >= this.square_table.length) { 532 | sq_index = this.square_table.length - 1; 533 | } 534 | if (tnd_index >= this.tnd_table.length) { 535 | tnd_index = this.tnd_table.length - 1; 536 | } 537 | var sampleValueR = 538 | this.square_table[sq_index] + this.tnd_table[tnd_index] - this.dcValue; 539 | 540 | // Remove DC from left channel: 541 | var smpDiffL = sampleValueL - this.prevSampleL; 542 | this.prevSampleL += smpDiffL; 543 | this.smpAccumL += smpDiffL - (this.smpAccumL >> 10); 544 | sampleValueL = this.smpAccumL; 545 | 546 | // Remove DC from right channel: 547 | var smpDiffR = sampleValueR - this.prevSampleR; 548 | this.prevSampleR += smpDiffR; 549 | this.smpAccumR += smpDiffR - (this.smpAccumR >> 10); 550 | sampleValueR = this.smpAccumR; 551 | 552 | // Write: 553 | if (sampleValueL > this.maxSample) { 554 | this.maxSample = sampleValueL; 555 | } 556 | if (sampleValueL < this.minSample) { 557 | this.minSample = sampleValueL; 558 | } 559 | 560 | if (this.nes.opts.onAudioSample) { 561 | this.nes.opts.onAudioSample(sampleValueL / 32768, sampleValueR / 32768); 562 | } 563 | 564 | // Reset sampled values: 565 | this.smpSquare1 = 0; 566 | this.smpSquare2 = 0; 567 | this.smpTriangle = 0; 568 | this.smpDmc = 0; 569 | }, 570 | 571 | getLengthMax: function (value) { 572 | return this.lengthLookup[value >> 3]; 573 | }, 574 | 575 | getDmcFrequency: function (value) { 576 | if (value >= 0 && value < 0x10) { 577 | return this.dmcFreqLookup[value]; 578 | } 579 | return 0; 580 | }, 581 | 582 | getNoiseWaveLength: function (value) { 583 | if (value >= 0 && value < 0x10) { 584 | return this.noiseWavelengthLookup[value]; 585 | } 586 | return 0; 587 | }, 588 | 589 | setPanning: function (pos) { 590 | for (var i = 0; i < 5; i++) { 591 | this.panning[i] = pos[i]; 592 | } 593 | this.updateStereoPos(); 594 | }, 595 | 596 | setMasterVolume: function (value) { 597 | if (value < 0) { 598 | value = 0; 599 | } 600 | if (value > 256) { 601 | value = 256; 602 | } 603 | this.masterVolume = value; 604 | this.updateStereoPos(); 605 | }, 606 | 607 | updateStereoPos: function () { 608 | this.stereoPosLSquare1 = (this.panning[0] * this.masterVolume) >> 8; 609 | this.stereoPosLSquare2 = (this.panning[1] * this.masterVolume) >> 8; 610 | this.stereoPosLTriangle = (this.panning[2] * this.masterVolume) >> 8; 611 | this.stereoPosLNoise = (this.panning[3] * this.masterVolume) >> 8; 612 | this.stereoPosLDMC = (this.panning[4] * this.masterVolume) >> 8; 613 | 614 | this.stereoPosRSquare1 = this.masterVolume - this.stereoPosLSquare1; 615 | this.stereoPosRSquare2 = this.masterVolume - this.stereoPosLSquare2; 616 | this.stereoPosRTriangle = this.masterVolume - this.stereoPosLTriangle; 617 | this.stereoPosRNoise = this.masterVolume - this.stereoPosLNoise; 618 | this.stereoPosRDMC = this.masterVolume - this.stereoPosLDMC; 619 | }, 620 | 621 | initLengthLookup: function () { 622 | // prettier-ignore 623 | this.lengthLookup = [ 624 | 0x0A, 0xFE, 625 | 0x14, 0x02, 626 | 0x28, 0x04, 627 | 0x50, 0x06, 628 | 0xA0, 0x08, 629 | 0x3C, 0x0A, 630 | 0x0E, 0x0C, 631 | 0x1A, 0x0E, 632 | 0x0C, 0x10, 633 | 0x18, 0x12, 634 | 0x30, 0x14, 635 | 0x60, 0x16, 636 | 0xC0, 0x18, 637 | 0x48, 0x1A, 638 | 0x10, 0x1C, 639 | 0x20, 0x1E 640 | ]; 641 | }, 642 | 643 | initDmcFrequencyLookup: function () { 644 | this.dmcFreqLookup = new Array(16); 645 | 646 | this.dmcFreqLookup[0x0] = 0xd60; 647 | this.dmcFreqLookup[0x1] = 0xbe0; 648 | this.dmcFreqLookup[0x2] = 0xaa0; 649 | this.dmcFreqLookup[0x3] = 0xa00; 650 | this.dmcFreqLookup[0x4] = 0x8f0; 651 | this.dmcFreqLookup[0x5] = 0x7f0; 652 | this.dmcFreqLookup[0x6] = 0x710; 653 | this.dmcFreqLookup[0x7] = 0x6b0; 654 | this.dmcFreqLookup[0x8] = 0x5f0; 655 | this.dmcFreqLookup[0x9] = 0x500; 656 | this.dmcFreqLookup[0xa] = 0x470; 657 | this.dmcFreqLookup[0xb] = 0x400; 658 | this.dmcFreqLookup[0xc] = 0x350; 659 | this.dmcFreqLookup[0xd] = 0x2a0; 660 | this.dmcFreqLookup[0xe] = 0x240; 661 | this.dmcFreqLookup[0xf] = 0x1b0; 662 | //for(int i=0;i<16;i++)dmcFreqLookup[i]/=8; 663 | }, 664 | 665 | initNoiseWavelengthLookup: function () { 666 | this.noiseWavelengthLookup = new Array(16); 667 | 668 | this.noiseWavelengthLookup[0x0] = 0x004; 669 | this.noiseWavelengthLookup[0x1] = 0x008; 670 | this.noiseWavelengthLookup[0x2] = 0x010; 671 | this.noiseWavelengthLookup[0x3] = 0x020; 672 | this.noiseWavelengthLookup[0x4] = 0x040; 673 | this.noiseWavelengthLookup[0x5] = 0x060; 674 | this.noiseWavelengthLookup[0x6] = 0x080; 675 | this.noiseWavelengthLookup[0x7] = 0x0a0; 676 | this.noiseWavelengthLookup[0x8] = 0x0ca; 677 | this.noiseWavelengthLookup[0x9] = 0x0fe; 678 | this.noiseWavelengthLookup[0xa] = 0x17c; 679 | this.noiseWavelengthLookup[0xb] = 0x1fc; 680 | this.noiseWavelengthLookup[0xc] = 0x2fa; 681 | this.noiseWavelengthLookup[0xd] = 0x3f8; 682 | this.noiseWavelengthLookup[0xe] = 0x7f2; 683 | this.noiseWavelengthLookup[0xf] = 0xfe4; 684 | }, 685 | 686 | initDACtables: function () { 687 | var value, ival, i; 688 | var max_sqr = 0; 689 | var max_tnd = 0; 690 | 691 | this.square_table = new Array(32 * 16); 692 | this.tnd_table = new Array(204 * 16); 693 | 694 | for (i = 0; i < 32 * 16; i++) { 695 | value = 95.52 / (8128.0 / (i / 16.0) + 100.0); 696 | value *= 0.98411; 697 | value *= 50000.0; 698 | ival = Math.floor(value); 699 | 700 | this.square_table[i] = ival; 701 | if (ival > max_sqr) { 702 | max_sqr = ival; 703 | } 704 | } 705 | 706 | for (i = 0; i < 204 * 16; i++) { 707 | value = 163.67 / (24329.0 / (i / 16.0) + 100.0); 708 | value *= 0.98411; 709 | value *= 50000.0; 710 | ival = Math.floor(value); 711 | 712 | this.tnd_table[i] = ival; 713 | if (ival > max_tnd) { 714 | max_tnd = ival; 715 | } 716 | } 717 | 718 | this.dacRange = max_sqr + max_tnd; 719 | this.dcValue = this.dacRange / 2; 720 | } 721 | }; 722 | 723 | var ChannelDM = function (papu) { 724 | this.papu = papu; 725 | 726 | this.MODE_NORMAL = 0; 727 | this.MODE_LOOP = 1; 728 | this.MODE_IRQ = 2; 729 | 730 | this.isEnabled = null; 731 | this.hasSample = null; 732 | this.irqGenerated = false; 733 | 734 | this.playMode = null; 735 | this.dmaFrequency = null; 736 | this.dmaCounter = null; 737 | this.deltaCounter = null; 738 | this.playStartAddress = null; 739 | this.playAddress = null; 740 | this.playLength = null; 741 | this.playLengthCounter = null; 742 | this.shiftCounter = null; 743 | this.reg4012 = null; 744 | this.reg4013 = null; 745 | this.sample = null; 746 | this.dacLsb = null; 747 | this.data = null; 748 | 749 | this.reset(); 750 | }; 751 | 752 | ChannelDM.prototype = { 753 | clockDmc: function () { 754 | // Only alter DAC value if the sample buffer has data: 755 | if (this.hasSample) { 756 | if ((this.data & 1) === 0) { 757 | // Decrement delta: 758 | if (this.deltaCounter > 0) { 759 | this.deltaCounter--; 760 | } 761 | } else { 762 | // Increment delta: 763 | if (this.deltaCounter < 63) { 764 | this.deltaCounter++; 765 | } 766 | } 767 | 768 | // Update sample value: 769 | this.sample = this.isEnabled ? (this.deltaCounter << 1) + this.dacLsb : 0; 770 | 771 | // Update shift register: 772 | this.data >>= 1; 773 | } 774 | 775 | this.dmaCounter--; 776 | if (this.dmaCounter <= 0) { 777 | // No more sample bits. 778 | this.hasSample = false; 779 | this.endOfSample(); 780 | this.dmaCounter = 8; 781 | } 782 | 783 | if (this.irqGenerated) { 784 | this.papu.nes.cpu.requestIrq(this.papu.nes.cpu.IRQ_NORMAL); 785 | } 786 | }, 787 | 788 | endOfSample: function () { 789 | if (this.playLengthCounter === 0 && this.playMode === this.MODE_LOOP) { 790 | // Start from beginning of sample: 791 | this.playAddress = this.playStartAddress; 792 | this.playLengthCounter = this.playLength; 793 | } 794 | 795 | if (this.playLengthCounter > 0) { 796 | // Fetch next sample: 797 | this.nextSample(); 798 | 799 | if (this.playLengthCounter === 0) { 800 | // Last byte of sample fetched, generate IRQ: 801 | if (this.playMode === this.MODE_IRQ) { 802 | // Generate IRQ: 803 | this.irqGenerated = true; 804 | } 805 | } 806 | } 807 | }, 808 | 809 | nextSample: function () { 810 | // Fetch byte: 811 | this.data = this.papu.nes.mmap.load(this.playAddress); 812 | this.papu.nes.cpu.haltCycles(4); 813 | 814 | this.playLengthCounter--; 815 | this.playAddress++; 816 | if (this.playAddress > 0xffff) { 817 | this.playAddress = 0x8000; 818 | } 819 | 820 | this.hasSample = true; 821 | }, 822 | 823 | writeReg: function (address, value) { 824 | if (address === 0x4010) { 825 | // Play mode, DMA Frequency 826 | if (value >> 6 === 0) { 827 | this.playMode = this.MODE_NORMAL; 828 | } else if (((value >> 6) & 1) === 1) { 829 | this.playMode = this.MODE_LOOP; 830 | } else if (value >> 6 === 2) { 831 | this.playMode = this.MODE_IRQ; 832 | } 833 | 834 | if ((value & 0x80) === 0) { 835 | this.irqGenerated = false; 836 | } 837 | 838 | this.dmaFrequency = this.papu.getDmcFrequency(value & 0xf); 839 | } else if (address === 0x4011) { 840 | // Delta counter load register: 841 | this.deltaCounter = (value >> 1) & 63; 842 | this.dacLsb = value & 1; 843 | this.sample = (this.deltaCounter << 1) + this.dacLsb; // update sample value 844 | } else if (address === 0x4012) { 845 | // DMA address load register 846 | this.playStartAddress = (value << 6) | 0x0c000; 847 | this.playAddress = this.playStartAddress; 848 | this.reg4012 = value; 849 | } else if (address === 0x4013) { 850 | // Length of play code 851 | this.playLength = (value << 4) + 1; 852 | this.playLengthCounter = this.playLength; 853 | this.reg4013 = value; 854 | } else if (address === 0x4015) { 855 | // DMC/IRQ Status 856 | if (((value >> 4) & 1) === 0) { 857 | // Disable: 858 | this.playLengthCounter = 0; 859 | } else { 860 | // Restart: 861 | this.playAddress = this.playStartAddress; 862 | this.playLengthCounter = this.playLength; 863 | } 864 | this.irqGenerated = false; 865 | } 866 | }, 867 | 868 | setEnabled: function (value) { 869 | if (!this.isEnabled && value) { 870 | this.playLengthCounter = this.playLength; 871 | } 872 | this.isEnabled = value; 873 | }, 874 | 875 | getLengthStatus: function () { 876 | return this.playLengthCounter === 0 || !this.isEnabled ? 0 : 1; 877 | }, 878 | 879 | getIrqStatus: function () { 880 | return this.irqGenerated ? 1 : 0; 881 | }, 882 | 883 | reset: function () { 884 | this.isEnabled = false; 885 | this.irqGenerated = false; 886 | this.playMode = this.MODE_NORMAL; 887 | this.dmaFrequency = 0; 888 | this.dmaCounter = 0; 889 | this.deltaCounter = 0; 890 | this.playStartAddress = 0; 891 | this.playAddress = 0; 892 | this.playLength = 0; 893 | this.playLengthCounter = 0; 894 | this.sample = 0; 895 | this.dacLsb = 0; 896 | this.shiftCounter = 0; 897 | this.reg4012 = 0; 898 | this.reg4013 = 0; 899 | this.data = 0; 900 | } 901 | }; 902 | 903 | var ChannelNoise = function (papu) { 904 | this.papu = papu; 905 | 906 | this.isEnabled = null; 907 | this.envDecayDisable = null; 908 | this.envDecayLoopEnable = null; 909 | this.lengthCounterEnable = null; 910 | this.envReset = null; 911 | this.shiftNow = null; 912 | 913 | this.lengthCounter = null; 914 | this.progTimerCount = null; 915 | this.progTimerMax = null; 916 | this.envDecayRate = null; 917 | this.envDecayCounter = null; 918 | this.envVolume = null; 919 | this.masterVolume = null; 920 | this.shiftReg = 1 << 14; 921 | this.randomBit = null; 922 | this.randomMode = null; 923 | this.sampleValue = null; 924 | this.accValue = 0; 925 | this.accCount = 1; 926 | this.tmp = null; 927 | 928 | this.reset(); 929 | }; 930 | 931 | ChannelNoise.prototype = { 932 | reset: function () { 933 | this.progTimerCount = 0; 934 | this.progTimerMax = 0; 935 | this.isEnabled = false; 936 | this.lengthCounter = 0; 937 | this.lengthCounterEnable = false; 938 | this.envDecayDisable = false; 939 | this.envDecayLoopEnable = false; 940 | this.shiftNow = false; 941 | this.envDecayRate = 0; 942 | this.envDecayCounter = 0; 943 | this.envVolume = 0; 944 | this.masterVolume = 0; 945 | this.shiftReg = 1; 946 | this.randomBit = 0; 947 | this.randomMode = 0; 948 | this.sampleValue = 0; 949 | this.tmp = 0; 950 | }, 951 | 952 | clockLengthCounter: function () { 953 | if (this.lengthCounterEnable && this.lengthCounter > 0) { 954 | this.lengthCounter--; 955 | if (this.lengthCounter === 0) { 956 | this.updateSampleValue(); 957 | } 958 | } 959 | }, 960 | 961 | clockEnvDecay: function () { 962 | if (this.envReset) { 963 | // Reset envelope: 964 | this.envReset = false; 965 | this.envDecayCounter = this.envDecayRate + 1; 966 | this.envVolume = 0xf; 967 | } else if (--this.envDecayCounter <= 0) { 968 | // Normal handling: 969 | this.envDecayCounter = this.envDecayRate + 1; 970 | if (this.envVolume > 0) { 971 | this.envVolume--; 972 | } else { 973 | this.envVolume = this.envDecayLoopEnable ? 0xf : 0; 974 | } 975 | } 976 | if (this.envDecayDisable) { 977 | this.masterVolume = this.envDecayRate; 978 | } else { 979 | this.masterVolume = this.envVolume; 980 | } 981 | this.updateSampleValue(); 982 | }, 983 | 984 | updateSampleValue: function () { 985 | if (this.isEnabled && this.lengthCounter > 0) { 986 | this.sampleValue = this.randomBit * this.masterVolume; 987 | } 988 | }, 989 | 990 | writeReg: function (address, value) { 991 | if (address === 0x400c) { 992 | // Volume/Envelope decay: 993 | this.envDecayDisable = (value & 0x10) !== 0; 994 | this.envDecayRate = value & 0xf; 995 | this.envDecayLoopEnable = (value & 0x20) !== 0; 996 | this.lengthCounterEnable = (value & 0x20) === 0; 997 | if (this.envDecayDisable) { 998 | this.masterVolume = this.envDecayRate; 999 | } else { 1000 | this.masterVolume = this.envVolume; 1001 | } 1002 | } else if (address === 0x400e) { 1003 | // Programmable timer: 1004 | this.progTimerMax = this.papu.getNoiseWaveLength(value & 0xf); 1005 | this.randomMode = value >> 7; 1006 | } else if (address === 0x400f) { 1007 | // Length counter 1008 | this.lengthCounter = this.papu.getLengthMax(value & 248); 1009 | this.envReset = true; 1010 | } 1011 | // Update: 1012 | //updateSampleValue(); 1013 | }, 1014 | 1015 | setEnabled: function (value) { 1016 | this.isEnabled = value; 1017 | if (!value) { 1018 | this.lengthCounter = 0; 1019 | } 1020 | this.updateSampleValue(); 1021 | }, 1022 | 1023 | getLengthStatus: function () { 1024 | return this.lengthCounter === 0 || !this.isEnabled ? 0 : 1; 1025 | } 1026 | }; 1027 | 1028 | var ChannelSquare = function (papu, square1) { 1029 | this.papu = papu; 1030 | 1031 | // prettier-ignore 1032 | this.dutyLookup = [ 1033 | 0, 1, 0, 0, 0, 0, 0, 0, 1034 | 0, 1, 1, 0, 0, 0, 0, 0, 1035 | 0, 1, 1, 1, 1, 0, 0, 0, 1036 | 1, 0, 0, 1, 1, 1, 1, 1 1037 | ]; 1038 | // prettier-ignore 1039 | this.impLookup = [ 1040 | 1, -1, 0, 0, 0, 0, 0, 0, 1041 | 1, 0, -1, 0, 0, 0, 0, 0, 1042 | 1, 0, 0, 0, -1, 0, 0, 0, 1043 | -1, 0, 1, 0, 0, 0, 0, 0 1044 | ]; 1045 | 1046 | this.sqr1 = square1; 1047 | this.isEnabled = null; 1048 | this.lengthCounterEnable = null; 1049 | this.sweepActive = null; 1050 | this.envDecayDisable = null; 1051 | this.envDecayLoopEnable = null; 1052 | this.envReset = null; 1053 | this.sweepCarry = null; 1054 | this.updateSweepPeriod = null; 1055 | 1056 | this.progTimerCount = null; 1057 | this.progTimerMax = null; 1058 | this.lengthCounter = null; 1059 | this.squareCounter = null; 1060 | this.sweepCounter = null; 1061 | this.sweepCounterMax = null; 1062 | this.sweepMode = null; 1063 | this.sweepShiftAmount = null; 1064 | this.envDecayRate = null; 1065 | this.envDecayCounter = null; 1066 | this.envVolume = null; 1067 | this.masterVolume = null; 1068 | this.dutyMode = null; 1069 | this.sweepResult = null; 1070 | this.sampleValue = null; 1071 | this.vol = null; 1072 | 1073 | this.reset(); 1074 | }; 1075 | 1076 | ChannelSquare.prototype = { 1077 | reset: function () { 1078 | this.progTimerCount = 0; 1079 | this.progTimerMax = 0; 1080 | this.lengthCounter = 0; 1081 | this.squareCounter = 0; 1082 | this.sweepCounter = 0; 1083 | this.sweepCounterMax = 0; 1084 | this.sweepMode = 0; 1085 | this.sweepShiftAmount = 0; 1086 | this.envDecayRate = 0; 1087 | this.envDecayCounter = 0; 1088 | this.envVolume = 0; 1089 | this.masterVolume = 0; 1090 | this.dutyMode = 0; 1091 | this.vol = 0; 1092 | 1093 | this.isEnabled = false; 1094 | this.lengthCounterEnable = false; 1095 | this.sweepActive = false; 1096 | this.sweepCarry = false; 1097 | this.envDecayDisable = false; 1098 | this.envDecayLoopEnable = false; 1099 | }, 1100 | 1101 | clockLengthCounter: function () { 1102 | if (this.lengthCounterEnable && this.lengthCounter > 0) { 1103 | this.lengthCounter--; 1104 | if (this.lengthCounter === 0) { 1105 | this.updateSampleValue(); 1106 | } 1107 | } 1108 | }, 1109 | 1110 | clockEnvDecay: function () { 1111 | if (this.envReset) { 1112 | // Reset envelope: 1113 | this.envReset = false; 1114 | this.envDecayCounter = this.envDecayRate + 1; 1115 | this.envVolume = 0xf; 1116 | } else if (--this.envDecayCounter <= 0) { 1117 | // Normal handling: 1118 | this.envDecayCounter = this.envDecayRate + 1; 1119 | if (this.envVolume > 0) { 1120 | this.envVolume--; 1121 | } else { 1122 | this.envVolume = this.envDecayLoopEnable ? 0xf : 0; 1123 | } 1124 | } 1125 | 1126 | if (this.envDecayDisable) { 1127 | this.masterVolume = this.envDecayRate; 1128 | } else { 1129 | this.masterVolume = this.envVolume; 1130 | } 1131 | this.updateSampleValue(); 1132 | }, 1133 | 1134 | clockSweep: function () { 1135 | if (--this.sweepCounter <= 0) { 1136 | this.sweepCounter = this.sweepCounterMax + 1; 1137 | if ( 1138 | this.sweepActive && 1139 | this.sweepShiftAmount > 0 && 1140 | this.progTimerMax > 7 1141 | ) { 1142 | // Calculate result from shifter: 1143 | this.sweepCarry = false; 1144 | if (this.sweepMode === 0) { 1145 | this.progTimerMax += this.progTimerMax >> this.sweepShiftAmount; 1146 | if (this.progTimerMax > 4095) { 1147 | this.progTimerMax = 4095; 1148 | this.sweepCarry = true; 1149 | } 1150 | } else { 1151 | this.progTimerMax = 1152 | this.progTimerMax - 1153 | ((this.progTimerMax >> this.sweepShiftAmount) - 1154 | (this.sqr1 ? 1 : 0)); 1155 | } 1156 | } 1157 | } 1158 | 1159 | if (this.updateSweepPeriod) { 1160 | this.updateSweepPeriod = false; 1161 | this.sweepCounter = this.sweepCounterMax + 1; 1162 | } 1163 | }, 1164 | 1165 | updateSampleValue: function () { 1166 | if (this.isEnabled && this.lengthCounter > 0 && this.progTimerMax > 7) { 1167 | if ( 1168 | this.sweepMode === 0 && 1169 | this.progTimerMax + (this.progTimerMax >> this.sweepShiftAmount) > 4095 1170 | ) { 1171 | //if (this.sweepCarry) { 1172 | this.sampleValue = 0; 1173 | } else { 1174 | this.sampleValue = 1175 | this.masterVolume * 1176 | this.dutyLookup[(this.dutyMode << 3) + this.squareCounter]; 1177 | } 1178 | } else { 1179 | this.sampleValue = 0; 1180 | } 1181 | }, 1182 | 1183 | writeReg: function (address, value) { 1184 | var addrAdd = this.sqr1 ? 0 : 4; 1185 | if (address === 0x4000 + addrAdd) { 1186 | // Volume/Envelope decay: 1187 | this.envDecayDisable = (value & 0x10) !== 0; 1188 | this.envDecayRate = value & 0xf; 1189 | this.envDecayLoopEnable = (value & 0x20) !== 0; 1190 | this.dutyMode = (value >> 6) & 0x3; 1191 | this.lengthCounterEnable = (value & 0x20) === 0; 1192 | if (this.envDecayDisable) { 1193 | this.masterVolume = this.envDecayRate; 1194 | } else { 1195 | this.masterVolume = this.envVolume; 1196 | } 1197 | this.updateSampleValue(); 1198 | } else if (address === 0x4001 + addrAdd) { 1199 | // Sweep: 1200 | this.sweepActive = (value & 0x80) !== 0; 1201 | this.sweepCounterMax = (value >> 4) & 7; 1202 | this.sweepMode = (value >> 3) & 1; 1203 | this.sweepShiftAmount = value & 7; 1204 | this.updateSweepPeriod = true; 1205 | } else if (address === 0x4002 + addrAdd) { 1206 | // Programmable timer: 1207 | this.progTimerMax &= 0x700; 1208 | this.progTimerMax |= value; 1209 | } else if (address === 0x4003 + addrAdd) { 1210 | // Programmable timer, length counter 1211 | this.progTimerMax &= 0xff; 1212 | this.progTimerMax |= (value & 0x7) << 8; 1213 | 1214 | if (this.isEnabled) { 1215 | this.lengthCounter = this.papu.getLengthMax(value & 0xf8); 1216 | } 1217 | 1218 | this.envReset = true; 1219 | } 1220 | }, 1221 | 1222 | setEnabled: function (value) { 1223 | this.isEnabled = value; 1224 | if (!value) { 1225 | this.lengthCounter = 0; 1226 | } 1227 | this.updateSampleValue(); 1228 | }, 1229 | 1230 | getLengthStatus: function () { 1231 | return this.lengthCounter === 0 || !this.isEnabled ? 0 : 1; 1232 | } 1233 | }; 1234 | 1235 | var ChannelTriangle = function (papu) { 1236 | this.papu = papu; 1237 | 1238 | this.isEnabled = null; 1239 | this.sampleCondition = null; 1240 | this.lengthCounterEnable = null; 1241 | this.lcHalt = null; 1242 | this.lcControl = null; 1243 | 1244 | this.progTimerCount = null; 1245 | this.progTimerMax = null; 1246 | this.triangleCounter = null; 1247 | this.lengthCounter = null; 1248 | this.linearCounter = null; 1249 | this.lcLoadValue = null; 1250 | this.sampleValue = null; 1251 | this.tmp = null; 1252 | 1253 | this.reset(); 1254 | }; 1255 | 1256 | ChannelTriangle.prototype = { 1257 | reset: function () { 1258 | this.progTimerCount = 0; 1259 | this.progTimerMax = 0; 1260 | this.triangleCounter = 0; 1261 | this.isEnabled = false; 1262 | this.sampleCondition = false; 1263 | this.lengthCounter = 0; 1264 | this.lengthCounterEnable = false; 1265 | this.linearCounter = 0; 1266 | this.lcLoadValue = 0; 1267 | this.lcHalt = true; 1268 | this.lcControl = false; 1269 | this.tmp = 0; 1270 | this.sampleValue = 0xf; 1271 | }, 1272 | 1273 | clockLengthCounter: function () { 1274 | if (this.lengthCounterEnable && this.lengthCounter > 0) { 1275 | this.lengthCounter--; 1276 | if (this.lengthCounter === 0) { 1277 | this.updateSampleCondition(); 1278 | } 1279 | } 1280 | }, 1281 | 1282 | clockLinearCounter: function () { 1283 | if (this.lcHalt) { 1284 | // Load: 1285 | this.linearCounter = this.lcLoadValue; 1286 | this.updateSampleCondition(); 1287 | } else if (this.linearCounter > 0) { 1288 | // Decrement: 1289 | this.linearCounter--; 1290 | this.updateSampleCondition(); 1291 | } 1292 | if (!this.lcControl) { 1293 | // Clear halt flag: 1294 | this.lcHalt = false; 1295 | } 1296 | }, 1297 | 1298 | getLengthStatus: function () { 1299 | return this.lengthCounter === 0 || !this.isEnabled ? 0 : 1; 1300 | }, 1301 | 1302 | // eslint-disable-next-line no-unused-vars 1303 | readReg: function (address) { 1304 | return 0; 1305 | }, 1306 | 1307 | writeReg: function (address, value) { 1308 | if (address === 0x4008) { 1309 | // New values for linear counter: 1310 | this.lcControl = (value & 0x80) !== 0; 1311 | this.lcLoadValue = value & 0x7f; 1312 | 1313 | // Length counter enable: 1314 | this.lengthCounterEnable = !this.lcControl; 1315 | } else if (address === 0x400a) { 1316 | // Programmable timer: 1317 | this.progTimerMax &= 0x700; 1318 | this.progTimerMax |= value; 1319 | } else if (address === 0x400b) { 1320 | // Programmable timer, length counter 1321 | this.progTimerMax &= 0xff; 1322 | this.progTimerMax |= (value & 0x07) << 8; 1323 | this.lengthCounter = this.papu.getLengthMax(value & 0xf8); 1324 | this.lcHalt = true; 1325 | } 1326 | 1327 | this.updateSampleCondition(); 1328 | }, 1329 | 1330 | clockProgrammableTimer: function (nCycles) { 1331 | if (this.progTimerMax > 0) { 1332 | this.progTimerCount += nCycles; 1333 | while ( 1334 | this.progTimerMax > 0 && 1335 | this.progTimerCount >= this.progTimerMax 1336 | ) { 1337 | this.progTimerCount -= this.progTimerMax; 1338 | if ( 1339 | this.isEnabled && 1340 | this.lengthCounter > 0 && 1341 | this.linearCounter > 0 1342 | ) { 1343 | this.clockTriangleGenerator(); 1344 | } 1345 | } 1346 | } 1347 | }, 1348 | 1349 | clockTriangleGenerator: function () { 1350 | this.triangleCounter++; 1351 | this.triangleCounter &= 0x1f; 1352 | }, 1353 | 1354 | setEnabled: function (value) { 1355 | this.isEnabled = value; 1356 | if (!value) { 1357 | this.lengthCounter = 0; 1358 | } 1359 | this.updateSampleCondition(); 1360 | }, 1361 | 1362 | updateSampleCondition: function () { 1363 | this.sampleCondition = 1364 | this.isEnabled && 1365 | this.progTimerMax > 7 && 1366 | this.linearCounter > 0 && 1367 | this.lengthCounter > 0; 1368 | } 1369 | }; -------------------------------------------------------------------------------- /examples/nes_emulator/emu/mappers.js: -------------------------------------------------------------------------------- 1 | load("./utils.js"); 2 | 3 | var Mappers = {}; 4 | 5 | Mappers[0] = function (nes) { 6 | this.nes = nes; 7 | }; 8 | 9 | Mappers[0].prototype = { 10 | reset: function () { 11 | this.joy1StrobeState = 0; 12 | this.joy2StrobeState = 0; 13 | this.joypadLastWrite = 0; 14 | 15 | this.zapperFired = false; 16 | this.zapperX = null; 17 | this.zapperY = null; 18 | }, 19 | 20 | write: function (address, value) { 21 | if (address < 0x2000) { 22 | // Mirroring of RAM: 23 | this.nes.cpu.mem[address & 0x7ff] = value; 24 | } else if (address > 0x4017) { 25 | this.nes.cpu.mem[address] = value; 26 | if (address >= 0x6000 && address < 0x8000) { 27 | // Write to persistent RAM 28 | this.nes.opts.onBatteryRamWrite(address, value); 29 | } 30 | } else if (address > 0x2007 && address < 0x4000) { 31 | this.regWrite(0x2000 + (address & 0x7), value); 32 | } else { 33 | this.regWrite(address, value); 34 | } 35 | }, 36 | 37 | writelow: function (address, value) { 38 | if (address < 0x2000) { 39 | // Mirroring of RAM: 40 | this.nes.cpu.mem[address & 0x7ff] = value; 41 | } else if (address > 0x4017) { 42 | this.nes.cpu.mem[address] = value; 43 | } else if (address > 0x2007 && address < 0x4000) { 44 | this.regWrite(0x2000 + (address & 0x7), value); 45 | } else { 46 | this.regWrite(address, value); 47 | } 48 | }, 49 | 50 | load: function (address) { 51 | // Wrap around: 52 | address &= 0xffff; 53 | 54 | // Check address range: 55 | if (address > 0x4017) { 56 | // ROM: 57 | return this.nes.cpu.mem[address]; 58 | } else if (address >= 0x2000) { 59 | // I/O Ports. 60 | return this.regLoad(address); 61 | } else { 62 | // RAM (mirrored) 63 | return this.nes.cpu.mem[address & 0x7ff]; 64 | } 65 | }, 66 | 67 | regLoad: function (address) { 68 | switch ( 69 | address >> 12 // use fourth nibble (0xF000) 70 | ) { 71 | case 0: 72 | break; 73 | 74 | case 1: 75 | break; 76 | 77 | case 2: 78 | // Fall through to case 3 79 | case 3: 80 | // PPU Registers 81 | switch (address & 0x7) { 82 | case 0x0: 83 | // 0x2000: 84 | // PPU Control Register 1. 85 | // (the value is stored both 86 | // in main memory and in the 87 | // PPU as flags): 88 | // (not in the real NES) 89 | return this.nes.cpu.mem[0x2000]; 90 | 91 | case 0x1: 92 | // 0x2001: 93 | // PPU Control Register 2. 94 | // (the value is stored both 95 | // in main memory and in the 96 | // PPU as flags): 97 | // (not in the real NES) 98 | return this.nes.cpu.mem[0x2001]; 99 | 100 | case 0x2: 101 | // 0x2002: 102 | // PPU Status Register. 103 | // The value is stored in 104 | // main memory in addition 105 | // to as flags in the PPU. 106 | // (not in the real NES) 107 | return this.nes.ppu.readStatusRegister(); 108 | 109 | case 0x3: 110 | return 0; 111 | 112 | case 0x4: 113 | // 0x2004: 114 | // Sprite Memory read. 115 | return this.nes.ppu.sramLoad(); 116 | case 0x5: 117 | return 0; 118 | 119 | case 0x6: 120 | return 0; 121 | 122 | case 0x7: 123 | // 0x2007: 124 | // VRAM read: 125 | return this.nes.ppu.vramLoad(); 126 | } 127 | break; 128 | case 4: 129 | // Sound+Joypad registers 130 | switch (address - 0x4015) { 131 | case 0: 132 | // 0x4015: 133 | // Sound channel enable, DMC Status 134 | return this.nes.papu.readReg(address); 135 | 136 | case 1: 137 | // 0x4016: 138 | // Joystick 1 + Strobe 139 | return this.joy1Read(); 140 | 141 | case 2: 142 | // 0x4017: 143 | // Joystick 2 + Strobe 144 | // https://wiki.nesdev.com/w/index.php/Zapper 145 | var w; 146 | 147 | if ( 148 | this.zapperX !== null && 149 | this.zapperY !== null && 150 | this.nes.ppu.isPixelWhite(this.zapperX, this.zapperY) 151 | ) { 152 | w = 0; 153 | } else { 154 | w = 0x1 << 3; 155 | } 156 | 157 | if (this.zapperFired) { 158 | w |= 0x1 << 4; 159 | } 160 | return (this.joy2Read() | w) & 0xffff; 161 | } 162 | break; 163 | } 164 | return 0; 165 | }, 166 | 167 | regWrite: function (address, value) { 168 | switch (address) { 169 | case 0x2000: 170 | // PPU Control register 1 171 | this.nes.cpu.mem[address] = value; 172 | this.nes.ppu.updateControlReg1(value); 173 | break; 174 | 175 | case 0x2001: 176 | // PPU Control register 2 177 | this.nes.cpu.mem[address] = value; 178 | this.nes.ppu.updateControlReg2(value); 179 | break; 180 | 181 | case 0x2003: 182 | // Set Sprite RAM address: 183 | this.nes.ppu.writeSRAMAddress(value); 184 | break; 185 | 186 | case 0x2004: 187 | // Write to Sprite RAM: 188 | this.nes.ppu.sramWrite(value); 189 | break; 190 | 191 | case 0x2005: 192 | // Screen Scroll offsets: 193 | this.nes.ppu.scrollWrite(value); 194 | break; 195 | 196 | case 0x2006: 197 | // Set VRAM address: 198 | this.nes.ppu.writeVRAMAddress(value); 199 | break; 200 | 201 | case 0x2007: 202 | // Write to VRAM: 203 | this.nes.ppu.vramWrite(value); 204 | break; 205 | 206 | case 0x4014: 207 | // Sprite Memory DMA Access 208 | this.nes.ppu.sramDMA(value); 209 | break; 210 | 211 | case 0x4015: 212 | // Sound Channel Switch, DMC Status 213 | this.nes.papu.writeReg(address, value); 214 | break; 215 | 216 | case 0x4016: 217 | // Joystick 1 + Strobe 218 | if ((value & 1) === 0 && (this.joypadLastWrite & 1) === 1) { 219 | this.joy1StrobeState = 0; 220 | this.joy2StrobeState = 0; 221 | } 222 | this.joypadLastWrite = value; 223 | break; 224 | 225 | case 0x4017: 226 | // Sound channel frame sequencer: 227 | this.nes.papu.writeReg(address, value); 228 | break; 229 | 230 | default: 231 | // Sound registers 232 | // console.log("write to sound reg"); 233 | if (address >= 0x4000 && address <= 0x4017) { 234 | this.nes.papu.writeReg(address, value); 235 | } 236 | } 237 | }, 238 | 239 | joy1Read: function () { 240 | var ret; 241 | 242 | switch (this.joy1StrobeState) { 243 | case 0: 244 | case 1: 245 | case 2: 246 | case 3: 247 | case 4: 248 | case 5: 249 | case 6: 250 | case 7: 251 | ret = this.nes.controllers[1].state[this.joy1StrobeState]; 252 | break; 253 | case 8: 254 | case 9: 255 | case 10: 256 | case 11: 257 | case 12: 258 | case 13: 259 | case 14: 260 | case 15: 261 | case 16: 262 | case 17: 263 | case 18: 264 | ret = 0; 265 | break; 266 | case 19: 267 | ret = 1; 268 | break; 269 | default: 270 | ret = 0; 271 | } 272 | 273 | this.joy1StrobeState++; 274 | if (this.joy1StrobeState === 24) { 275 | this.joy1StrobeState = 0; 276 | } 277 | 278 | return ret; 279 | }, 280 | 281 | joy2Read: function () { 282 | var ret; 283 | 284 | switch (this.joy2StrobeState) { 285 | case 0: 286 | case 1: 287 | case 2: 288 | case 3: 289 | case 4: 290 | case 5: 291 | case 6: 292 | case 7: 293 | ret = this.nes.controllers[2].state[this.joy2StrobeState]; 294 | break; 295 | case 8: 296 | case 9: 297 | case 10: 298 | case 11: 299 | case 12: 300 | case 13: 301 | case 14: 302 | case 15: 303 | case 16: 304 | case 17: 305 | case 18: 306 | ret = 0; 307 | break; 308 | case 19: 309 | ret = 1; 310 | break; 311 | default: 312 | ret = 0; 313 | } 314 | 315 | this.joy2StrobeState++; 316 | if (this.joy2StrobeState === 24) { 317 | this.joy2StrobeState = 0; 318 | } 319 | 320 | return ret; 321 | }, 322 | 323 | loadROM: function () { 324 | if (!this.nes.rom.valid || this.nes.rom.romCount < 1) { 325 | throw new Error("NoMapper: Invalid ROM! Unable to load."); 326 | } 327 | 328 | // Load ROM into memory: 329 | this.loadPRGROM(); 330 | 331 | // Load CHR-ROM: 332 | this.loadCHRROM(); 333 | 334 | // Load Battery RAM (if present): 335 | this.loadBatteryRam(); 336 | 337 | // Reset IRQ: 338 | //nes.getCpu().doResetInterrupt(); 339 | this.nes.cpu.requestIrq(this.nes.cpu.IRQ_RESET); 340 | }, 341 | 342 | loadPRGROM: function () { 343 | if (this.nes.rom.romCount > 1) { 344 | // Load the two first banks into memory. 345 | this.loadRomBank(0, 0x8000); 346 | this.loadRomBank(1, 0xc000); 347 | } else { 348 | // Load the one bank into both memory locations: 349 | this.loadRomBank(0, 0x8000); 350 | this.loadRomBank(0, 0xc000); 351 | } 352 | }, 353 | 354 | loadCHRROM: function () { 355 | // console.log("Loading CHR ROM.."); 356 | if (this.nes.rom.vromCount > 0) { 357 | if (this.nes.rom.vromCount === 1) { 358 | this.loadVromBank(0, 0x0000); 359 | this.loadVromBank(0, 0x1000); 360 | } else { 361 | this.loadVromBank(0, 0x0000); 362 | this.loadVromBank(1, 0x1000); 363 | } 364 | } else { 365 | //System.out.println("There aren't any CHR-ROM banks.."); 366 | } 367 | }, 368 | 369 | loadBatteryRam: function () { 370 | if (this.nes.rom.batteryRam) { 371 | var ram = this.nes.rom.batteryRam; 372 | if (ram !== null && ram.length === 0x2000) { 373 | // Load Battery RAM into memory: 374 | copyArrayElements(ram, 0, this.nes.cpu.mem, 0x6000, 0x2000); 375 | } 376 | } 377 | }, 378 | 379 | loadRomBank: function (bank, address) { 380 | // Loads a ROM bank into the specified address. 381 | bank %= this.nes.rom.romCount; 382 | //var data = this.nes.rom.rom[bank]; 383 | //cpuMem.write(address,data,data.length); 384 | copyArrayElements( 385 | this.nes.rom.rom[bank], 386 | 0, 387 | this.nes.cpu.mem, 388 | address, 389 | 16384 390 | ); 391 | }, 392 | 393 | loadVromBank: function (bank, address) { 394 | if (this.nes.rom.vromCount === 0) { 395 | return; 396 | } 397 | this.nes.ppu.triggerRendering(); 398 | 399 | copyArrayElements( 400 | this.nes.rom.vrom[bank % this.nes.rom.vromCount], 401 | 0, 402 | this.nes.ppu.vramMem, 403 | address, 404 | 4096 405 | ); 406 | 407 | var vromTile = this.nes.rom.vromTile[bank % this.nes.rom.vromCount]; 408 | copyArrayElements( 409 | vromTile, 410 | 0, 411 | this.nes.ppu.ptTile, 412 | address >> 4, 413 | 256 414 | ); 415 | }, 416 | 417 | load32kRomBank: function (bank, address) { 418 | this.loadRomBank((bank * 2) % this.nes.rom.romCount, address); 419 | this.loadRomBank((bank * 2 + 1) % this.nes.rom.romCount, address + 16384); 420 | }, 421 | 422 | load8kVromBank: function (bank4kStart, address) { 423 | if (this.nes.rom.vromCount === 0) { 424 | return; 425 | } 426 | this.nes.ppu.triggerRendering(); 427 | 428 | this.loadVromBank(bank4kStart % this.nes.rom.vromCount, address); 429 | this.loadVromBank( 430 | (bank4kStart + 1) % this.nes.rom.vromCount, 431 | address + 4096 432 | ); 433 | }, 434 | 435 | load1kVromBank: function (bank1k, address) { 436 | if (this.nes.rom.vromCount === 0) { 437 | return; 438 | } 439 | this.nes.ppu.triggerRendering(); 440 | 441 | var bank4k = Math.floor(bank1k / 4) % this.nes.rom.vromCount; 442 | var bankoffset = (bank1k % 4) * 1024; 443 | copyArrayElements( 444 | this.nes.rom.vrom[bank4k], 445 | bankoffset, 446 | this.nes.ppu.vramMem, 447 | address, 448 | 1024 449 | ); 450 | 451 | // Update tiles: 452 | var vromTile = this.nes.rom.vromTile[bank4k]; 453 | var baseIndex = address >> 4; 454 | for (var i = 0; i < 64; i++) { 455 | this.nes.ppu.ptTile[baseIndex + i] = vromTile[(bank1k % 4 << 6) + i]; 456 | } 457 | }, 458 | 459 | load2kVromBank: function (bank2k, address) { 460 | if (this.nes.rom.vromCount === 0) { 461 | return; 462 | } 463 | this.nes.ppu.triggerRendering(); 464 | 465 | var bank4k = Math.floor(bank2k / 2) % this.nes.rom.vromCount; 466 | var bankoffset = (bank2k % 2) * 2048; 467 | copyArrayElements( 468 | this.nes.rom.vrom[bank4k], 469 | bankoffset, 470 | this.nes.ppu.vramMem, 471 | address, 472 | 2048 473 | ); 474 | 475 | // Update tiles: 476 | var vromTile = this.nes.rom.vromTile[bank4k]; 477 | var baseIndex = address >> 4; 478 | for (var i = 0; i < 128; i++) { 479 | this.nes.ppu.ptTile[baseIndex + i] = vromTile[(bank2k % 2 << 7) + i]; 480 | } 481 | }, 482 | 483 | load8kRomBank: function (bank8k, address) { 484 | var bank16k = Math.floor(bank8k / 2) % this.nes.rom.romCount; 485 | var offset = (bank8k % 2) * 8192; 486 | 487 | //this.nes.cpu.mem.write(address,this.nes.rom.rom[bank16k],offset,8192); 488 | copyArrayElements( 489 | this.nes.rom.rom[bank16k], 490 | offset, 491 | this.nes.cpu.mem, 492 | address, 493 | 8192 494 | ); 495 | }, 496 | 497 | clockIrqCounter: function () { 498 | // Does nothing. This is used by the MMC3 mapper. 499 | }, 500 | 501 | // eslint-disable-next-line no-unused-vars 502 | latchAccess: function (address) { 503 | // Does nothing. This is used by MMC2. 504 | }, 505 | 506 | toJSON: function () { 507 | return { 508 | joy1StrobeState: this.joy1StrobeState, 509 | joy2StrobeState: this.joy2StrobeState, 510 | joypadLastWrite: this.joypadLastWrite 511 | }; 512 | }, 513 | 514 | fromJSON: function (s) { 515 | this.joy1StrobeState = s.joy1StrobeState; 516 | this.joy2StrobeState = s.joy2StrobeState; 517 | this.joypadLastWrite = s.joypadLastWrite; 518 | } 519 | }; 520 | 521 | Mappers[1] = function (nes) { 522 | this.nes = nes; 523 | }; 524 | 525 | Mappers[1].prototype = new Mappers[0](); 526 | 527 | Mappers[1].prototype.reset = function () { 528 | Mappers[0].prototype.reset.apply(this); 529 | 530 | // 5-bit buffer: 531 | this.regBuffer = 0; 532 | this.regBufferCounter = 0; 533 | 534 | // Register 0: 535 | this.mirroring = 0; 536 | this.oneScreenMirroring = 0; 537 | this.prgSwitchingArea = 1; 538 | this.prgSwitchingSize = 1; 539 | this.vromSwitchingSize = 0; 540 | 541 | // Register 1: 542 | this.romSelectionReg0 = 0; 543 | 544 | // Register 2: 545 | this.romSelectionReg1 = 0; 546 | 547 | // Register 3: 548 | this.romBankSelect = 0; 549 | }; 550 | 551 | Mappers[1].prototype.write = function (address, value) { 552 | // Writes to addresses other than MMC registers are handled by NoMapper. 553 | if (address < 0x8000) { 554 | Mappers[0].prototype.write.apply(this, arguments); 555 | return; 556 | } 557 | 558 | // See what should be done with the written value: 559 | if ((value & 128) !== 0) { 560 | // Reset buffering: 561 | this.regBufferCounter = 0; 562 | this.regBuffer = 0; 563 | 564 | // Reset register: 565 | if (this.getRegNumber(address) === 0) { 566 | this.prgSwitchingArea = 1; 567 | this.prgSwitchingSize = 1; 568 | } 569 | } else { 570 | // Continue buffering: 571 | //regBuffer = (regBuffer & (0xFF-(1<> 2) & 1; 611 | 612 | // PRG Switching Size: 613 | this.prgSwitchingSize = (value >> 3) & 1; 614 | 615 | // VROM Switching Size: 616 | this.vromSwitchingSize = (value >> 4) & 1; 617 | 618 | break; 619 | 620 | case 1: 621 | // ROM selection: 622 | this.romSelectionReg0 = (value >> 4) & 1; 623 | 624 | // Check whether the cart has VROM: 625 | if (this.nes.rom.vromCount > 0) { 626 | // Select VROM bank at 0x0000: 627 | if (this.vromSwitchingSize === 0) { 628 | // Swap 8kB VROM: 629 | if (this.romSelectionReg0 === 0) { 630 | this.load8kVromBank(value & 0xf, 0x0000); 631 | } else { 632 | this.load8kVromBank( 633 | Math.floor(this.nes.rom.vromCount / 2) + (value & 0xf), 634 | 0x0000 635 | ); 636 | } 637 | } else { 638 | // Swap 4kB VROM: 639 | if (this.romSelectionReg0 === 0) { 640 | this.loadVromBank(value & 0xf, 0x0000); 641 | } else { 642 | this.loadVromBank( 643 | Math.floor(this.nes.rom.vromCount / 2) + (value & 0xf), 644 | 0x0000 645 | ); 646 | } 647 | } 648 | } 649 | 650 | break; 651 | 652 | case 2: 653 | // ROM selection: 654 | this.romSelectionReg1 = (value >> 4) & 1; 655 | 656 | // Check whether the cart has VROM: 657 | if (this.nes.rom.vromCount > 0) { 658 | // Select VROM bank at 0x1000: 659 | if (this.vromSwitchingSize === 1) { 660 | // Swap 4kB of VROM: 661 | if (this.romSelectionReg1 === 0) { 662 | this.loadVromBank(value & 0xf, 0x1000); 663 | } else { 664 | this.loadVromBank( 665 | Math.floor(this.nes.rom.vromCount / 2) + (value & 0xf), 666 | 0x1000 667 | ); 668 | } 669 | } 670 | } 671 | break; 672 | 673 | default: 674 | // Select ROM bank: 675 | // ------------------------- 676 | tmp = value & 0xf; 677 | var bank; 678 | var baseBank = 0; 679 | 680 | if (this.nes.rom.romCount >= 32) { 681 | // 1024 kB cart 682 | if (this.vromSwitchingSize === 0) { 683 | if (this.romSelectionReg0 === 1) { 684 | baseBank = 16; 685 | } 686 | } else { 687 | baseBank = 688 | (this.romSelectionReg0 | (this.romSelectionReg1 << 1)) << 3; 689 | } 690 | } else if (this.nes.rom.romCount >= 16) { 691 | // 512 kB cart 692 | if (this.romSelectionReg0 === 1) { 693 | baseBank = 8; 694 | } 695 | } 696 | 697 | if (this.prgSwitchingSize === 0) { 698 | // 32kB 699 | bank = baseBank + (value & 0xf); 700 | this.load32kRomBank(bank, 0x8000); 701 | } else { 702 | // 16kB 703 | bank = baseBank * 2 + (value & 0xf); 704 | if (this.prgSwitchingArea === 0) { 705 | this.loadRomBank(bank, 0xc000); 706 | } else { 707 | this.loadRomBank(bank, 0x8000); 708 | } 709 | } 710 | } 711 | }; 712 | 713 | // Returns the register number from the address written to: 714 | Mappers[1].prototype.getRegNumber = function (address) { 715 | if (address >= 0x8000 && address <= 0x9fff) { 716 | return 0; 717 | } else if (address >= 0xa000 && address <= 0xbfff) { 718 | return 1; 719 | } else if (address >= 0xc000 && address <= 0xdfff) { 720 | return 2; 721 | } else { 722 | return 3; 723 | } 724 | }; 725 | 726 | Mappers[1].prototype.loadROM = function () { 727 | if (!this.nes.rom.valid) { 728 | throw new Error("MMC1: Invalid ROM! Unable to load."); 729 | } 730 | 731 | // Load PRG-ROM: 732 | this.loadRomBank(0, 0x8000); // First ROM bank.. 733 | this.loadRomBank(this.nes.rom.romCount - 1, 0xc000); // ..and last ROM bank. 734 | 735 | // Load CHR-ROM: 736 | this.loadCHRROM(); 737 | 738 | // Load Battery RAM (if present): 739 | this.loadBatteryRam(); 740 | 741 | // Do Reset-Interrupt: 742 | this.nes.cpu.requestIrq(this.nes.cpu.IRQ_RESET); 743 | }; 744 | 745 | // eslint-disable-next-line no-unused-vars 746 | Mappers[1].prototype.switchLowHighPrgRom = function (oldSetting) { 747 | // not yet. 748 | }; 749 | 750 | Mappers[1].prototype.switch16to32 = function () { 751 | // not yet. 752 | }; 753 | 754 | Mappers[1].prototype.switch32to16 = function () { 755 | // not yet. 756 | }; 757 | 758 | Mappers[1].prototype.toJSON = function () { 759 | var s = Mappers[0].prototype.toJSON.apply(this); 760 | s.mirroring = this.mirroring; 761 | s.oneScreenMirroring = this.oneScreenMirroring; 762 | s.prgSwitchingArea = this.prgSwitchingArea; 763 | s.prgSwitchingSize = this.prgSwitchingSize; 764 | s.vromSwitchingSize = this.vromSwitchingSize; 765 | s.romSelectionReg0 = this.romSelectionReg0; 766 | s.romSelectionReg1 = this.romSelectionReg1; 767 | s.romBankSelect = this.romBankSelect; 768 | s.regBuffer = this.regBuffer; 769 | s.regBufferCounter = this.regBufferCounter; 770 | return s; 771 | }; 772 | 773 | Mappers[1].prototype.fromJSON = function (s) { 774 | Mappers[0].prototype.fromJSON.apply(this, arguments); 775 | this.mirroring = s.mirroring; 776 | this.oneScreenMirroring = s.oneScreenMirroring; 777 | this.prgSwitchingArea = s.prgSwitchingArea; 778 | this.prgSwitchingSize = s.prgSwitchingSize; 779 | this.vromSwitchingSize = s.vromSwitchingSize; 780 | this.romSelectionReg0 = s.romSelectionReg0; 781 | this.romSelectionReg1 = s.romSelectionReg1; 782 | this.romBankSelect = s.romBankSelect; 783 | this.regBuffer = s.regBuffer; 784 | this.regBufferCounter = s.regBufferCounter; 785 | }; 786 | 787 | Mappers[2] = function (nes) { 788 | this.nes = nes; 789 | }; 790 | 791 | Mappers[2].prototype = new Mappers[0](); 792 | 793 | Mappers[2].prototype.write = function (address, value) { 794 | // Writes to addresses other than MMC registers are handled by NoMapper. 795 | if (address < 0x8000) { 796 | Mappers[0].prototype.write.apply(this, arguments); 797 | return; 798 | } else { 799 | // This is a ROM bank select command. 800 | // Swap in the given ROM bank at 0x8000: 801 | this.loadRomBank(value, 0x8000); 802 | } 803 | }; 804 | 805 | Mappers[2].prototype.loadROM = function () { 806 | if (!this.nes.rom.valid) { 807 | throw new Error("UNROM: Invalid ROM! Unable to load."); 808 | } 809 | 810 | // Load PRG-ROM: 811 | this.loadRomBank(0, 0x8000); 812 | this.loadRomBank(this.nes.rom.romCount - 1, 0xc000); 813 | 814 | // Load CHR-ROM: 815 | this.loadCHRROM(); 816 | 817 | // Do Reset-Interrupt: 818 | this.nes.cpu.requestIrq(this.nes.cpu.IRQ_RESET); 819 | }; 820 | 821 | /** 822 | * Mapper 003 (CNROM) 823 | * 824 | * @constructor 825 | * @example Solomon's Key, Arkanoid, Arkista's Ring, Bump 'n' Jump, Cybernoid 826 | * @description http://wiki.nesdev.com/w/index.php/INES_Mapper_003 827 | */ 828 | Mappers[3] = function (nes) { 829 | this.nes = nes; 830 | }; 831 | 832 | Mappers[3].prototype = new Mappers[0](); 833 | 834 | Mappers[3].prototype.write = function (address, value) { 835 | // Writes to addresses other than MMC registers are handled by NoMapper. 836 | if (address < 0x8000) { 837 | Mappers[0].prototype.write.apply(this, arguments); 838 | return; 839 | } else { 840 | // This is a ROM bank select command. 841 | // Swap in the given ROM bank at 0x8000: 842 | // This is a VROM bank select command. 843 | // Swap in the given VROM bank at 0x0000: 844 | var bank = (value % (this.nes.rom.vromCount / 2)) * 2; 845 | this.loadVromBank(bank, 0x0000); 846 | this.loadVromBank(bank + 1, 0x1000); 847 | this.load8kVromBank(value * 2, 0x0000); 848 | } 849 | }; 850 | 851 | Mappers[4] = function (nes) { 852 | this.nes = nes; 853 | 854 | this.CMD_SEL_2_1K_VROM_0000 = 0; 855 | this.CMD_SEL_2_1K_VROM_0800 = 1; 856 | this.CMD_SEL_1K_VROM_1000 = 2; 857 | this.CMD_SEL_1K_VROM_1400 = 3; 858 | this.CMD_SEL_1K_VROM_1800 = 4; 859 | this.CMD_SEL_1K_VROM_1C00 = 5; 860 | this.CMD_SEL_ROM_PAGE1 = 6; 861 | this.CMD_SEL_ROM_PAGE2 = 7; 862 | 863 | this.command = null; 864 | this.prgAddressSelect = null; 865 | this.chrAddressSelect = null; 866 | this.pageNumber = null; 867 | this.irqCounter = null; 868 | this.irqLatchValue = null; 869 | this.irqEnable = null; 870 | this.prgAddressChanged = false; 871 | }; 872 | 873 | Mappers[4].prototype = new Mappers[0](); 874 | 875 | Mappers[4].prototype.write = function (address, value) { 876 | // Writes to addresses other than MMC registers are handled by NoMapper. 877 | if (address < 0x8000) { 878 | Mappers[0].prototype.write.apply(this, arguments); 879 | return; 880 | } 881 | 882 | switch (address) { 883 | case 0x8000: 884 | // Command/Address Select register 885 | this.command = value & 7; 886 | var tmp = (value >> 6) & 1; 887 | if (tmp !== this.prgAddressSelect) { 888 | this.prgAddressChanged = true; 889 | } 890 | this.prgAddressSelect = tmp; 891 | this.chrAddressSelect = (value >> 7) & 1; 892 | break; 893 | 894 | case 0x8001: 895 | // Page number for command 896 | this.executeCommand(this.command, value); 897 | break; 898 | 899 | case 0xa000: 900 | // Mirroring select 901 | if ((value & 1) !== 0) { 902 | this.nes.ppu.setMirroring(this.nes.rom.HORIZONTAL_MIRRORING); 903 | } else { 904 | this.nes.ppu.setMirroring(this.nes.rom.VERTICAL_MIRRORING); 905 | } 906 | break; 907 | 908 | case 0xa001: 909 | // SaveRAM Toggle 910 | // TODO 911 | //nes.getRom().setSaveState((value&1)!=0); 912 | break; 913 | 914 | case 0xc000: 915 | // IRQ Counter register 916 | this.irqCounter = value; 917 | //nes.ppu.mapperIrqCounter = 0; 918 | break; 919 | 920 | case 0xc001: 921 | // IRQ Latch register 922 | this.irqLatchValue = value; 923 | break; 924 | 925 | case 0xe000: 926 | // IRQ Control Reg 0 (disable) 927 | //irqCounter = irqLatchValue; 928 | this.irqEnable = 0; 929 | break; 930 | 931 | case 0xe001: 932 | // IRQ Control Reg 1 (enable) 933 | this.irqEnable = 1; 934 | break; 935 | 936 | default: 937 | // Not a MMC3 register. 938 | // The game has probably crashed, 939 | // since it tries to write to ROM.. 940 | // IGNORE. 941 | } 942 | }; 943 | 944 | Mappers[4].prototype.executeCommand = function (cmd, arg) { 945 | switch (cmd) { 946 | case this.CMD_SEL_2_1K_VROM_0000: 947 | // Select 2 1KB VROM pages at 0x0000: 948 | if (this.chrAddressSelect === 0) { 949 | this.load1kVromBank(arg, 0x0000); 950 | this.load1kVromBank(arg + 1, 0x0400); 951 | } else { 952 | this.load1kVromBank(arg, 0x1000); 953 | this.load1kVromBank(arg + 1, 0x1400); 954 | } 955 | break; 956 | 957 | case this.CMD_SEL_2_1K_VROM_0800: 958 | // Select 2 1KB VROM pages at 0x0800: 959 | if (this.chrAddressSelect === 0) { 960 | this.load1kVromBank(arg, 0x0800); 961 | this.load1kVromBank(arg + 1, 0x0c00); 962 | } else { 963 | this.load1kVromBank(arg, 0x1800); 964 | this.load1kVromBank(arg + 1, 0x1c00); 965 | } 966 | break; 967 | 968 | case this.CMD_SEL_1K_VROM_1000: 969 | // Select 1K VROM Page at 0x1000: 970 | if (this.chrAddressSelect === 0) { 971 | this.load1kVromBank(arg, 0x1000); 972 | } else { 973 | this.load1kVromBank(arg, 0x0000); 974 | } 975 | break; 976 | 977 | case this.CMD_SEL_1K_VROM_1400: 978 | // Select 1K VROM Page at 0x1400: 979 | if (this.chrAddressSelect === 0) { 980 | this.load1kVromBank(arg, 0x1400); 981 | } else { 982 | this.load1kVromBank(arg, 0x0400); 983 | } 984 | break; 985 | 986 | case this.CMD_SEL_1K_VROM_1800: 987 | // Select 1K VROM Page at 0x1800: 988 | if (this.chrAddressSelect === 0) { 989 | this.load1kVromBank(arg, 0x1800); 990 | } else { 991 | this.load1kVromBank(arg, 0x0800); 992 | } 993 | break; 994 | 995 | case this.CMD_SEL_1K_VROM_1C00: 996 | // Select 1K VROM Page at 0x1C00: 997 | if (this.chrAddressSelect === 0) { 998 | this.load1kVromBank(arg, 0x1c00); 999 | } else { 1000 | this.load1kVromBank(arg, 0x0c00); 1001 | } 1002 | break; 1003 | 1004 | case this.CMD_SEL_ROM_PAGE1: 1005 | if (this.prgAddressChanged) { 1006 | // Load the two hardwired banks: 1007 | if (this.prgAddressSelect === 0) { 1008 | this.load8kRomBank((this.nes.rom.romCount - 1) * 2, 0xc000); 1009 | } else { 1010 | this.load8kRomBank((this.nes.rom.romCount - 1) * 2, 0x8000); 1011 | } 1012 | this.prgAddressChanged = false; 1013 | } 1014 | 1015 | // Select first switchable ROM page: 1016 | if (this.prgAddressSelect === 0) { 1017 | this.load8kRomBank(arg, 0x8000); 1018 | } else { 1019 | this.load8kRomBank(arg, 0xc000); 1020 | } 1021 | break; 1022 | 1023 | case this.CMD_SEL_ROM_PAGE2: 1024 | // Select second switchable ROM page: 1025 | this.load8kRomBank(arg, 0xa000); 1026 | 1027 | // hardwire appropriate bank: 1028 | if (this.prgAddressChanged) { 1029 | // Load the two hardwired banks: 1030 | if (this.prgAddressSelect === 0) { 1031 | this.load8kRomBank((this.nes.rom.romCount - 1) * 2, 0xc000); 1032 | } else { 1033 | this.load8kRomBank((this.nes.rom.romCount - 1) * 2, 0x8000); 1034 | } 1035 | this.prgAddressChanged = false; 1036 | } 1037 | } 1038 | }; 1039 | 1040 | Mappers[4].prototype.loadROM = function () { 1041 | if (!this.nes.rom.valid) { 1042 | throw new Error("MMC3: Invalid ROM! Unable to load."); 1043 | } 1044 | 1045 | // Load hardwired PRG banks (0xC000 and 0xE000): 1046 | this.load8kRomBank((this.nes.rom.romCount - 1) * 2, 0xc000); 1047 | this.load8kRomBank((this.nes.rom.romCount - 1) * 2 + 1, 0xe000); 1048 | 1049 | // Load swappable PRG banks (0x8000 and 0xA000): 1050 | this.load8kRomBank(0, 0x8000); 1051 | this.load8kRomBank(1, 0xa000); 1052 | 1053 | // Load CHR-ROM: 1054 | this.loadCHRROM(); 1055 | 1056 | // Load Battery RAM (if present): 1057 | this.loadBatteryRam(); 1058 | 1059 | // Do Reset-Interrupt: 1060 | this.nes.cpu.requestIrq(this.nes.cpu.IRQ_RESET); 1061 | }; 1062 | 1063 | Mappers[4].prototype.clockIrqCounter = function () { 1064 | if (this.irqEnable === 1) { 1065 | this.irqCounter--; 1066 | if (this.irqCounter < 0) { 1067 | // Trigger IRQ: 1068 | //nes.getCpu().doIrq(); 1069 | this.nes.cpu.requestIrq(this.nes.cpu.IRQ_NORMAL); 1070 | this.irqCounter = this.irqLatchValue; 1071 | } 1072 | } 1073 | }; 1074 | 1075 | Mappers[4].prototype.toJSON = function () { 1076 | var s = Mappers[0].prototype.toJSON.apply(this); 1077 | s.command = this.command; 1078 | s.prgAddressSelect = this.prgAddressSelect; 1079 | s.chrAddressSelect = this.chrAddressSelect; 1080 | s.pageNumber = this.pageNumber; 1081 | s.irqCounter = this.irqCounter; 1082 | s.irqLatchValue = this.irqLatchValue; 1083 | s.irqEnable = this.irqEnable; 1084 | s.prgAddressChanged = this.prgAddressChanged; 1085 | return s; 1086 | }; 1087 | 1088 | Mappers[4].prototype.fromJSON = function (s) { 1089 | Mappers[0].prototype.fromJSON.apply(this, arguments); 1090 | this.command = s.command; 1091 | this.prgAddressSelect = s.prgAddressSelect; 1092 | this.chrAddressSelect = s.chrAddressSelect; 1093 | this.pageNumber = s.pageNumber; 1094 | this.irqCounter = s.irqCounter; 1095 | this.irqLatchValue = s.irqLatchValue; 1096 | this.irqEnable = s.irqEnable; 1097 | this.prgAddressChanged = s.prgAddressChanged; 1098 | }; 1099 | 1100 | /** 1101 | * Mapper005 (MMC5,ExROM) 1102 | * 1103 | * @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 1104 | * @description http://wiki.nesdev.com/w/index.php/INES_Mapper_005 1105 | * @constructor 1106 | */ 1107 | Mappers[5] = function (nes) { 1108 | this.nes = nes; 1109 | }; 1110 | 1111 | Mappers[5].prototype = new Mappers[0](); 1112 | 1113 | Mappers[5].prototype.write = function (address, value) { 1114 | // Writes to addresses other than MMC registers are handled by NoMapper. 1115 | if (address < 0x8000) { 1116 | Mappers[0].prototype.write.apply(this, arguments); 1117 | } else { 1118 | this.load8kVromBank(value, 0x0000); 1119 | } 1120 | }; 1121 | 1122 | Mappers[5].prototype.write = function (address, value) { 1123 | // Writes to addresses other than MMC registers are handled by NoMapper. 1124 | if (address < 0x5000) { 1125 | Mappers[0].prototype.write.apply(this, arguments); 1126 | return; 1127 | } 1128 | 1129 | switch (address) { 1130 | case 0x5100: 1131 | this.prg_size = value & 3; 1132 | break; 1133 | case 0x5101: 1134 | this.chr_size = value & 3; 1135 | break; 1136 | case 0x5102: 1137 | this.sram_we_a = value & 3; 1138 | break; 1139 | case 0x5103: 1140 | this.sram_we_b = value & 3; 1141 | break; 1142 | case 0x5104: 1143 | this.graphic_mode = value & 3; 1144 | break; 1145 | case 0x5105: 1146 | this.nametable_mode = value; 1147 | this.nametable_type[0] = value & 3; 1148 | this.load1kVromBank(value & 3, 0x2000); 1149 | value >>= 2; 1150 | this.nametable_type[1] = value & 3; 1151 | this.load1kVromBank(value & 3, 0x2400); 1152 | value >>= 2; 1153 | this.nametable_type[2] = value & 3; 1154 | this.load1kVromBank(value & 3, 0x2800); 1155 | value >>= 2; 1156 | this.nametable_type[3] = value & 3; 1157 | this.load1kVromBank(value & 3, 0x2c00); 1158 | break; 1159 | case 0x5106: 1160 | this.fill_chr = value; 1161 | break; 1162 | case 0x5107: 1163 | this.fill_pal = value & 3; 1164 | break; 1165 | case 0x5113: 1166 | this.SetBank_SRAM(3, value & 3); 1167 | break; 1168 | case 0x5114: 1169 | case 0x5115: 1170 | case 0x5116: 1171 | case 0x5117: 1172 | this.SetBank_CPU(address, value); 1173 | break; 1174 | case 0x5120: 1175 | case 0x5121: 1176 | case 0x5122: 1177 | case 0x5123: 1178 | case 0x5124: 1179 | case 0x5125: 1180 | case 0x5126: 1181 | case 0x5127: 1182 | this.chr_mode = 0; 1183 | this.chr_page[0][address & 7] = value; 1184 | this.SetBank_PPU(); 1185 | break; 1186 | case 0x5128: 1187 | case 0x5129: 1188 | case 0x512a: 1189 | case 0x512b: 1190 | this.chr_mode = 1; 1191 | this.chr_page[1][(address & 3) + 0] = value; 1192 | this.chr_page[1][(address & 3) + 4] = value; 1193 | this.SetBank_PPU(); 1194 | break; 1195 | case 0x5200: 1196 | this.split_control = value; 1197 | break; 1198 | case 0x5201: 1199 | this.split_scroll = value; 1200 | break; 1201 | case 0x5202: 1202 | this.split_page = value & 0x3f; 1203 | break; 1204 | case 0x5203: 1205 | this.irq_line = value; 1206 | this.nes.cpu.ClearIRQ(); 1207 | break; 1208 | case 0x5204: 1209 | this.irq_enable = value; 1210 | this.nes.cpu.ClearIRQ(); 1211 | break; 1212 | case 0x5205: 1213 | this.mult_a = value; 1214 | break; 1215 | case 0x5206: 1216 | this.mult_b = value; 1217 | break; 1218 | default: 1219 | if (address >= 0x5000 && address <= 0x5015) { 1220 | this.nes.papu.exWrite(address, value); 1221 | } else if (address >= 0x5c00 && address <= 0x5fff) { 1222 | if (this.graphic_mode === 2) { 1223 | // ExRAM 1224 | // vram write 1225 | } else if (this.graphic_mode !== 3) { 1226 | // Split,ExGraphic 1227 | if (this.irq_status & 0x40) { 1228 | // vram write 1229 | } else { 1230 | // vram write 1231 | } 1232 | } 1233 | } else if (address >= 0x6000 && address <= 0x7fff) { 1234 | if (this.sram_we_a === 2 && this.sram_we_b === 1) { 1235 | // additional ram write 1236 | } 1237 | } 1238 | break; 1239 | } 1240 | }; 1241 | 1242 | Mappers[5].prototype.loadROM = function () { 1243 | if (!this.nes.rom.valid) { 1244 | throw new Error("UNROM: Invalid ROM! Unable to load."); 1245 | } 1246 | 1247 | // Load PRG-ROM: 1248 | this.load8kRomBank(this.nes.rom.romCount * 2 - 1, 0x8000); 1249 | this.load8kRomBank(this.nes.rom.romCount * 2 - 1, 0xa000); 1250 | this.load8kRomBank(this.nes.rom.romCount * 2 - 1, 0xc000); 1251 | this.load8kRomBank(this.nes.rom.romCount * 2 - 1, 0xe000); 1252 | 1253 | // Load CHR-ROM: 1254 | this.loadCHRROM(); 1255 | 1256 | // Do Reset-Interrupt: 1257 | this.nes.cpu.requestIrq(this.nes.cpu.IRQ_RESET); 1258 | }; 1259 | 1260 | /** 1261 | * Mapper007 (AxROM) 1262 | * @example Battletoads, Time Lord, Marble Madness 1263 | * @description http://wiki.nesdev.com/w/index.php/INES_Mapper_007 1264 | * @constructor 1265 | */ 1266 | Mappers[7] = function (nes) { 1267 | this.nes = nes; 1268 | }; 1269 | 1270 | Mappers[7].prototype = new Mappers[0](); 1271 | 1272 | Mappers[7].prototype.write = function (address, value) { 1273 | // Writes to addresses other than MMC registers are handled by NoMapper. 1274 | if (address < 0x8000) { 1275 | Mappers[0].prototype.write.apply(this, arguments); 1276 | } else { 1277 | this.load32kRomBank(value & 0x7, 0x8000); 1278 | if (value & 0x10) { 1279 | this.nes.ppu.setMirroring(this.nes.rom.SINGLESCREEN_MIRRORING2); 1280 | } else { 1281 | this.nes.ppu.setMirroring(this.nes.rom.SINGLESCREEN_MIRRORING); 1282 | } 1283 | } 1284 | }; 1285 | 1286 | Mappers[7].prototype.loadROM = function () { 1287 | if (!this.nes.rom.valid) { 1288 | throw new Error("AOROM: Invalid ROM! Unable to load."); 1289 | } 1290 | 1291 | // Load PRG-ROM: 1292 | this.loadPRGROM(); 1293 | 1294 | // Load CHR-ROM: 1295 | this.loadCHRROM(); 1296 | 1297 | // Do Reset-Interrupt: 1298 | this.nes.cpu.requestIrq(this.nes.cpu.IRQ_RESET); 1299 | }; 1300 | 1301 | /** 1302 | * Mapper 011 (Color Dreams) 1303 | * 1304 | * @description http://wiki.nesdev.com/w/index.php/Color_Dreams 1305 | * @example Crystal Mines, Metal Fighter 1306 | * @constructor 1307 | */ 1308 | Mappers[11] = function (nes) { 1309 | this.nes = nes; 1310 | }; 1311 | 1312 | Mappers[11].prototype = new Mappers[0](); 1313 | 1314 | Mappers[11].prototype.write = function (address, value) { 1315 | if (address < 0x8000) { 1316 | Mappers[0].prototype.write.apply(this, arguments); 1317 | return; 1318 | } else { 1319 | // Swap in the given PRG-ROM bank: 1320 | var prgbank1 = ((value & 0xf) * 2) % this.nes.rom.romCount; 1321 | var prgbank2 = ((value & 0xf) * 2 + 1) % this.nes.rom.romCount; 1322 | 1323 | this.loadRomBank(prgbank1, 0x8000); 1324 | this.loadRomBank(prgbank2, 0xc000); 1325 | 1326 | if (this.nes.rom.vromCount > 0) { 1327 | // Swap in the given VROM bank at 0x0000: 1328 | var bank = ((value >> 4) * 2) % this.nes.rom.vromCount; 1329 | this.loadVromBank(bank, 0x0000); 1330 | this.loadVromBank(bank + 1, 0x1000); 1331 | } 1332 | } 1333 | }; 1334 | 1335 | /** 1336 | * Mapper 034 (BNROM, NINA-01) 1337 | * 1338 | * @description http://wiki.nesdev.com/w/index.php/INES_Mapper_034 1339 | * @example Darkseed, Mashou, Mission Impossible 2 1340 | * @constructor 1341 | */ 1342 | Mappers[34] = function (nes) { 1343 | this.nes = nes; 1344 | }; 1345 | 1346 | Mappers[34].prototype = new Mappers[0](); 1347 | 1348 | Mappers[34].prototype.write = function (address, value) { 1349 | if (address < 0x8000) { 1350 | Mappers[0].prototype.write.apply(this, arguments); 1351 | return; 1352 | } else { 1353 | this.load32kRomBank(value, 0x8000); 1354 | } 1355 | }; 1356 | 1357 | /** 1358 | * Mapper 038 1359 | * 1360 | * @description http://wiki.nesdev.com/w/index.php/INES_Mapper_038 1361 | * @example Crime Busters 1362 | * @constructor 1363 | */ 1364 | Mappers[38] = function (nes) { 1365 | this.nes = nes; 1366 | }; 1367 | 1368 | Mappers[38].prototype = new Mappers[0](); 1369 | 1370 | Mappers[38].prototype.write = function (address, value) { 1371 | if (address < 0x7000 || address > 0x7fff) { 1372 | Mappers[0].prototype.write.apply(this, arguments); 1373 | return; 1374 | } else { 1375 | // Swap in the given PRG-ROM bank at 0x8000: 1376 | this.load32kRomBank(value & 3, 0x8000); 1377 | 1378 | // Swap in the given VROM bank at 0x0000: 1379 | this.load8kVromBank(((value >> 2) & 3) * 2, 0x0000); 1380 | } 1381 | }; 1382 | 1383 | /** 1384 | * Mapper 066 (GxROM) 1385 | * 1386 | * @description http://wiki.nesdev.com/w/index.php/INES_Mapper_066 1387 | * @example Doraemon, Dragon Power, Gumshoe, Thunder & Lightning, 1388 | * Super Mario Bros. + Duck Hunt 1389 | * @constructor 1390 | */ 1391 | Mappers[66] = function (nes) { 1392 | this.nes = nes; 1393 | }; 1394 | 1395 | Mappers[66].prototype = new Mappers[0](); 1396 | 1397 | Mappers[66].prototype.write = function (address, value) { 1398 | if (address < 0x8000) { 1399 | Mappers[0].prototype.write.apply(this, arguments); 1400 | return; 1401 | } else { 1402 | // Swap in the given PRG-ROM bank at 0x8000: 1403 | this.load32kRomBank((value >> 4) & 3, 0x8000); 1404 | 1405 | // Swap in the given VROM bank at 0x0000: 1406 | this.load8kVromBank((value & 3) * 2, 0x0000); 1407 | } 1408 | }; 1409 | 1410 | /** 1411 | * Mapper 094 (UN1ROM) 1412 | * 1413 | * @description http://wiki.nesdev.com/w/index.php/INES_Mapper_094 1414 | * @example Senjou no Ookami 1415 | * @constructor 1416 | */ 1417 | Mappers[94] = function (nes) { 1418 | this.nes = nes; 1419 | }; 1420 | 1421 | Mappers[94].prototype = new Mappers[0](); 1422 | 1423 | Mappers[94].prototype.write = function (address, value) { 1424 | // Writes to addresses other than MMC registers are handled by NoMapper. 1425 | if (address < 0x8000) { 1426 | Mappers[0].prototype.write.apply(this, arguments); 1427 | return; 1428 | } else { 1429 | // This is a ROM bank select command. 1430 | // Swap in the given ROM bank at 0x8000: 1431 | this.loadRomBank(value >> 2, 0x8000); 1432 | } 1433 | }; 1434 | 1435 | Mappers[94].prototype.loadROM = function () { 1436 | if (!this.nes.rom.valid) { 1437 | throw new Error("UN1ROM: Invalid ROM! Unable to load."); 1438 | } 1439 | 1440 | // Load PRG-ROM: 1441 | this.loadRomBank(0, 0x8000); 1442 | this.loadRomBank(this.nes.rom.romCount - 1, 0xc000); 1443 | 1444 | // Load CHR-ROM: 1445 | this.loadCHRROM(); 1446 | 1447 | // Do Reset-Interrupt: 1448 | this.nes.cpu.requestIrq(this.nes.cpu.IRQ_RESET); 1449 | }; 1450 | 1451 | /** 1452 | * Mapper 140 1453 | * 1454 | * @description http://wiki.nesdev.com/w/index.php/INES_Mapper_140 1455 | * @example Bio Senshi Dan - Increaser Tono Tatakai 1456 | * @constructor 1457 | */ 1458 | Mappers[140] = function (nes) { 1459 | this.nes = nes; 1460 | }; 1461 | 1462 | Mappers[140].prototype = new Mappers[0](); 1463 | 1464 | Mappers[140].prototype.write = function (address, value) { 1465 | if (address < 0x6000 || address > 0x7fff) { 1466 | Mappers[0].prototype.write.apply(this, arguments); 1467 | return; 1468 | } else { 1469 | // Swap in the given PRG-ROM bank at 0x8000: 1470 | this.load32kRomBank((value >> 4) & 3, 0x8000); 1471 | 1472 | // Swap in the given VROM bank at 0x0000: 1473 | this.load8kVromBank((value & 0xf) * 2, 0x0000); 1474 | } 1475 | }; 1476 | 1477 | /** 1478 | * Mapper 180 1479 | * 1480 | * @description http://wiki.nesdev.com/w/index.php/INES_Mapper_180 1481 | * @example Crazy Climber 1482 | * @constructor 1483 | */ 1484 | Mappers[180] = function (nes) { 1485 | this.nes = nes; 1486 | }; 1487 | 1488 | Mappers[180].prototype = new Mappers[0](); 1489 | 1490 | Mappers[180].prototype.write = function (address, value) { 1491 | // Writes to addresses other than MMC registers are handled by NoMapper. 1492 | if (address < 0x8000) { 1493 | Mappers[0].prototype.write.apply(this, arguments); 1494 | return; 1495 | } else { 1496 | // This is a ROM bank select command. 1497 | // Swap in the given ROM bank at 0xc000: 1498 | this.loadRomBank(value, 0xc000); 1499 | } 1500 | }; 1501 | 1502 | Mappers[180].prototype.loadROM = function () { 1503 | if (!this.nes.rom.valid) { 1504 | throw new Error("Mapper 180: Invalid ROM! Unable to load."); 1505 | } 1506 | 1507 | // Load PRG-ROM: 1508 | this.loadRomBank(0, 0x8000); 1509 | this.loadRomBank(this.nes.rom.romCount - 1, 0xc000); 1510 | 1511 | // Load CHR-ROM: 1512 | this.loadCHRROM(); 1513 | 1514 | // Do Reset-Interrupt: 1515 | this.nes.cpu.requestIrq(this.nes.cpu.IRQ_RESET); 1516 | }; 1517 | -------------------------------------------------------------------------------- /examples/nes_emulator/emu/ppu.js: -------------------------------------------------------------------------------- 1 | load("./tile.js"); 2 | load("./utils.js"); 3 | 4 | var PPU = function (nes) { 5 | this.nes = nes; 6 | 7 | // Keep Chrome happy 8 | this.vramMem = null; 9 | this.spriteMem = null; 10 | this.vramAddress = null; 11 | this.vramTmpAddress = null; 12 | this.vramBufferedReadValue = null; 13 | this.firstWrite = null; 14 | this.sramAddress = null; 15 | this.currentMirroring = null; 16 | this.requestEndFrame = null; 17 | this.nmiOk = null; 18 | this.dummyCycleToggle = null; 19 | this.validTileData = null; 20 | this.nmiCounter = null; 21 | this.scanlineAlreadyRendered = null; 22 | this.f_nmiOnVblank = null; 23 | this.f_spriteSize = null; 24 | this.f_bgPatternTable = null; 25 | this.f_spPatternTable = null; 26 | this.f_addrInc = null; 27 | this.f_nTblAddress = null; 28 | this.f_color = null; 29 | this.f_spVisibility = null; 30 | this.f_bgVisibility = null; 31 | this.f_spClipping = null; 32 | this.f_bgClipping = null; 33 | this.f_dispType = null; 34 | this.cntFV = null; 35 | this.cntV = null; 36 | this.cntH = null; 37 | this.cntVT = null; 38 | this.cntHT = null; 39 | this.regFV = null; 40 | this.regV = null; 41 | this.regH = null; 42 | this.regVT = null; 43 | this.regHT = null; 44 | this.regFH = null; 45 | this.regS = null; 46 | this.curNt = null; 47 | this.attrib = null; 48 | this.buffer = null; 49 | this.bgbuffer = null; 50 | this.pixrendered = null; 51 | 52 | this.validTileData = null; 53 | this.scantile = null; 54 | this.scanline = null; 55 | this.lastRenderedScanline = null; 56 | this.curX = null; 57 | this.sprX = null; 58 | this.sprY = null; 59 | this.sprTile = null; 60 | this.sprCol = null; 61 | this.vertFlip = null; 62 | this.horiFlip = null; 63 | this.bgPriority = null; 64 | this.spr0HitX = null; 65 | this.spr0HitY = null; 66 | this.hitSpr0 = null; 67 | this.sprPalette = null; 68 | this.imgPalette = null; 69 | this.ptTile = null; 70 | this.ntable1 = null; 71 | this.currentMirroring = null; 72 | this.nameTable = null; 73 | this.vramMirrorTable = null; 74 | this.palTable = null; 75 | 76 | // Rendering Options: 77 | this.showSpr0Hit = false; 78 | this.clipToTvSize = true; 79 | 80 | this.reset(); 81 | }; 82 | 83 | PPU.prototype = { 84 | // Status flags: 85 | STATUS_VRAMWRITE: 4, 86 | STATUS_SLSPRITECOUNT: 5, 87 | STATUS_SPRITE0HIT: 6, 88 | STATUS_VBLANK: 7, 89 | 90 | reset: function () { 91 | var i; 92 | 93 | // Memory 94 | this.vramMem = new Array(0x8000); 95 | this.spriteMem = new Array(0x100); 96 | for (i = 0; i < this.vramMem.length; i++) { 97 | this.vramMem[i] = 0; 98 | } 99 | for (i = 0; i < this.spriteMem.length; i++) { 100 | this.spriteMem[i] = 0; 101 | } 102 | 103 | // VRAM I/O: 104 | this.vramAddress = null; 105 | this.vramTmpAddress = null; 106 | this.vramBufferedReadValue = 0; 107 | this.firstWrite = true; // VRAM/Scroll Hi/Lo latch 108 | 109 | // SPR-RAM I/O: 110 | this.sramAddress = 0; // 8-bit only. 111 | 112 | this.currentMirroring = -1; 113 | this.requestEndFrame = false; 114 | this.nmiOk = false; 115 | this.dummyCycleToggle = false; 116 | this.validTileData = false; 117 | this.nmiCounter = 0; 118 | this.scanlineAlreadyRendered = null; 119 | 120 | // Control Flags Register 1: 121 | this.f_nmiOnVblank = 0; // NMI on VBlank. 0=disable, 1=enable 122 | this.f_spriteSize = 0; // Sprite size. 0=8x8, 1=8x16 123 | this.f_bgPatternTable = 0; // Background Pattern Table address. 0=0x0000,1=0x1000 124 | this.f_spPatternTable = 0; // Sprite Pattern Table address. 0=0x0000,1=0x1000 125 | this.f_addrInc = 0; // PPU Address Increment. 0=1,1=32 126 | this.f_nTblAddress = 0; // Name Table Address. 0=0x2000,1=0x2400,2=0x2800,3=0x2C00 127 | 128 | // Control Flags Register 2: 129 | this.f_color = 0; // Background color. 0=black, 1=blue, 2=green, 4=red 130 | this.f_spVisibility = 0; // Sprite visibility. 0=not displayed,1=displayed 131 | this.f_bgVisibility = 0; // Background visibility. 0=Not Displayed,1=displayed 132 | this.f_spClipping = 0; // Sprite clipping. 0=Sprites invisible in left 8-pixel column,1=No clipping 133 | this.f_bgClipping = 0; // Background clipping. 0=BG invisible in left 8-pixel column, 1=No clipping 134 | this.f_dispType = 0; // Display type. 0=color, 1=monochrome 135 | 136 | // Counters: 137 | this.cntFV = 0; 138 | this.cntV = 0; 139 | this.cntH = 0; 140 | this.cntVT = 0; 141 | this.cntHT = 0; 142 | 143 | // Registers: 144 | this.regFV = 0; 145 | this.regV = 0; 146 | this.regH = 0; 147 | this.regVT = 0; 148 | this.regHT = 0; 149 | this.regFH = 0; 150 | this.regS = 0; 151 | 152 | // These are temporary variables used in rendering and sound procedures. 153 | // Their states outside of those procedures can be ignored. 154 | // TODO: the use of this is a bit weird, investigate 155 | this.curNt = null; 156 | 157 | // Variables used when rendering: 158 | this.attrib = new Array(32); 159 | this.buffer = new Array(256 * 240); 160 | this.bgbuffer = new Array(256 * 240); 161 | this.pixrendered = new Array(256 * 240); 162 | 163 | this.validTileData = null; 164 | 165 | this.scantile = new Array(32); 166 | 167 | // Initialize misc vars: 168 | this.scanline = 0; 169 | this.lastRenderedScanline = -1; 170 | this.curX = 0; 171 | 172 | // Sprite data: 173 | this.sprX = new Array(64); // X coordinate 174 | this.sprY = new Array(64); // Y coordinate 175 | this.sprTile = new Array(64); // Tile Index (into pattern table) 176 | this.sprCol = new Array(64); // Upper two bits of color 177 | this.vertFlip = new Array(64); // Vertical Flip 178 | this.horiFlip = new Array(64); // Horizontal Flip 179 | this.bgPriority = new Array(64); // Background priority 180 | this.spr0HitX = 0; // Sprite #0 hit X coordinate 181 | this.spr0HitY = 0; // Sprite #0 hit Y coordinate 182 | this.hitSpr0 = false; 183 | 184 | // Palette data: 185 | this.sprPalette = new Array(16); 186 | this.imgPalette = new Array(16); 187 | 188 | // Create pattern table tile buffers: 189 | this.ptTile = new Array(512); 190 | for (i = 0; i < 512; i++) { 191 | this.ptTile[i] = new Tile(); 192 | } 193 | 194 | // Create nametable buffers: 195 | // Name table data: 196 | this.ntable1 = new Array(4); 197 | this.currentMirroring = -1; 198 | this.nameTable = new Array(4); 199 | for (i = 0; i < 4; i++) { 200 | this.nameTable[i] = new NameTable(32, 32, "Nt" + i); 201 | } 202 | 203 | // Initialize mirroring lookup table: 204 | this.vramMirrorTable = new Array(0x8000); 205 | for (i = 0; i < 0x8000; i++) { 206 | this.vramMirrorTable[i] = i; 207 | } 208 | 209 | this.palTable = new PaletteTable(); 210 | this.palTable.loadNTSCPalette(); 211 | //this.palTable.loadDefaultPalette(); 212 | 213 | this.updateControlReg1(0); 214 | this.updateControlReg2(0); 215 | }, 216 | 217 | // Sets Nametable mirroring. 218 | setMirroring: function (mirroring) { 219 | if (mirroring === this.currentMirroring) { 220 | return; 221 | } 222 | 223 | this.currentMirroring = mirroring; 224 | this.triggerRendering(); 225 | 226 | // Remove mirroring: 227 | if (this.vramMirrorTable === null) { 228 | this.vramMirrorTable = new Array(0x8000); 229 | } 230 | for (var i = 0; i < 0x8000; i++) { 231 | this.vramMirrorTable[i] = i; 232 | } 233 | 234 | // Palette mirroring: 235 | this.defineMirrorRegion(0x3f20, 0x3f00, 0x20); 236 | this.defineMirrorRegion(0x3f40, 0x3f00, 0x20); 237 | this.defineMirrorRegion(0x3f80, 0x3f00, 0x20); 238 | this.defineMirrorRegion(0x3fc0, 0x3f00, 0x20); 239 | 240 | // Additional mirroring: 241 | this.defineMirrorRegion(0x3000, 0x2000, 0xf00); 242 | this.defineMirrorRegion(0x4000, 0x0000, 0x4000); 243 | 244 | if (mirroring === this.nes.rom.HORIZONTAL_MIRRORING) { 245 | // Horizontal mirroring. 246 | 247 | this.ntable1[0] = 0; 248 | this.ntable1[1] = 0; 249 | this.ntable1[2] = 1; 250 | this.ntable1[3] = 1; 251 | 252 | this.defineMirrorRegion(0x2400, 0x2000, 0x400); 253 | this.defineMirrorRegion(0x2c00, 0x2800, 0x400); 254 | } else if (mirroring === this.nes.rom.VERTICAL_MIRRORING) { 255 | // Vertical mirroring. 256 | 257 | this.ntable1[0] = 0; 258 | this.ntable1[1] = 1; 259 | this.ntable1[2] = 0; 260 | this.ntable1[3] = 1; 261 | 262 | this.defineMirrorRegion(0x2800, 0x2000, 0x400); 263 | this.defineMirrorRegion(0x2c00, 0x2400, 0x400); 264 | } else if (mirroring === this.nes.rom.SINGLESCREEN_MIRRORING) { 265 | // Single Screen mirroring 266 | 267 | this.ntable1[0] = 0; 268 | this.ntable1[1] = 0; 269 | this.ntable1[2] = 0; 270 | this.ntable1[3] = 0; 271 | 272 | this.defineMirrorRegion(0x2400, 0x2000, 0x400); 273 | this.defineMirrorRegion(0x2800, 0x2000, 0x400); 274 | this.defineMirrorRegion(0x2c00, 0x2000, 0x400); 275 | } else if (mirroring === this.nes.rom.SINGLESCREEN_MIRRORING2) { 276 | this.ntable1[0] = 1; 277 | this.ntable1[1] = 1; 278 | this.ntable1[2] = 1; 279 | this.ntable1[3] = 1; 280 | 281 | this.defineMirrorRegion(0x2400, 0x2400, 0x400); 282 | this.defineMirrorRegion(0x2800, 0x2400, 0x400); 283 | this.defineMirrorRegion(0x2c00, 0x2400, 0x400); 284 | } else { 285 | // Assume Four-screen mirroring. 286 | 287 | this.ntable1[0] = 0; 288 | this.ntable1[1] = 1; 289 | this.ntable1[2] = 2; 290 | this.ntable1[3] = 3; 291 | } 292 | }, 293 | 294 | // Define a mirrored area in the address lookup table. 295 | // Assumes the regions don't overlap. 296 | // The 'to' region is the region that is physically in memory. 297 | defineMirrorRegion: function (fromStart, toStart, size) { 298 | for (var i = 0; i < size; i++) { 299 | this.vramMirrorTable[fromStart + i] = toStart + i; 300 | } 301 | }, 302 | 303 | startVBlank: function () { 304 | // Do NMI: 305 | this.nes.cpu.requestIrq(this.nes.cpu.IRQ_NMI); 306 | 307 | // Make sure everything is rendered: 308 | if (this.lastRenderedScanline < 239) { 309 | this.renderFramePartially( 310 | this.lastRenderedScanline + 1, 311 | 240 - this.lastRenderedScanline 312 | ); 313 | } 314 | 315 | // End frame: 316 | this.endFrame(); 317 | 318 | // Reset scanline counter: 319 | this.lastRenderedScanline = -1; 320 | }, 321 | 322 | endScanline: function () { 323 | switch (this.scanline) { 324 | case 19: 325 | // Dummy scanline. 326 | // May be variable length: 327 | if (this.dummyCycleToggle) { 328 | // Remove dead cycle at end of scanline, 329 | // for next scanline: 330 | this.curX = 1; 331 | this.dummyCycleToggle = !this.dummyCycleToggle; 332 | } 333 | break; 334 | 335 | case 20: 336 | // Clear VBlank flag: 337 | this.setStatusFlag(this.STATUS_VBLANK, false); 338 | 339 | // Clear Sprite #0 hit flag: 340 | this.setStatusFlag(this.STATUS_SPRITE0HIT, false); 341 | this.hitSpr0 = false; 342 | this.spr0HitX = -1; 343 | this.spr0HitY = -1; 344 | 345 | if (this.f_bgVisibility === 1 || this.f_spVisibility === 1) { 346 | // Update counters: 347 | this.cntFV = this.regFV; 348 | this.cntV = this.regV; 349 | this.cntH = this.regH; 350 | this.cntVT = this.regVT; 351 | this.cntHT = this.regHT; 352 | 353 | if (this.f_bgVisibility === 1) { 354 | // Render dummy scanline: 355 | this.renderBgScanline(false, 0); 356 | } 357 | } 358 | 359 | if (this.f_bgVisibility === 1 && this.f_spVisibility === 1) { 360 | // Check sprite 0 hit for first scanline: 361 | this.checkSprite0(0); 362 | } 363 | 364 | if (this.f_bgVisibility === 1 || this.f_spVisibility === 1) { 365 | // Clock mapper IRQ Counter: 366 | this.nes.mmap.clockIrqCounter(); 367 | } 368 | break; 369 | 370 | case 261: 371 | // Dead scanline, no rendering. 372 | // Set VINT: 373 | this.setStatusFlag(this.STATUS_VBLANK, true); 374 | this.requestEndFrame = true; 375 | this.nmiCounter = 9; 376 | 377 | // Wrap around: 378 | this.scanline = -1; // will be incremented to 0 379 | 380 | break; 381 | 382 | default: 383 | if (this.scanline >= 21 && this.scanline <= 260) { 384 | // Render normally: 385 | if (this.f_bgVisibility === 1) { 386 | if (!this.scanlineAlreadyRendered) { 387 | // update scroll: 388 | this.cntHT = this.regHT; 389 | this.cntH = this.regH; 390 | this.renderBgScanline(true, this.scanline + 1 - 21); 391 | } 392 | this.scanlineAlreadyRendered = false; 393 | 394 | // Check for sprite 0 (next scanline): 395 | if (!this.hitSpr0 && this.f_spVisibility === 1) { 396 | if ( 397 | this.sprX[0] >= -7 && 398 | this.sprX[0] < 256 && 399 | this.sprY[0] + 1 <= this.scanline - 20 && 400 | this.sprY[0] + 1 + (this.f_spriteSize === 0 ? 8 : 16) >= 401 | this.scanline - 20 402 | ) { 403 | if (this.checkSprite0(this.scanline - 20)) { 404 | this.hitSpr0 = true; 405 | } 406 | } 407 | } 408 | } 409 | 410 | if (this.f_bgVisibility === 1 || this.f_spVisibility === 1) { 411 | // Clock mapper IRQ Counter: 412 | this.nes.mmap.clockIrqCounter(); 413 | } 414 | } 415 | } 416 | 417 | this.scanline++; 418 | this.regsToAddress(); 419 | this.cntsToAddress(); 420 | }, 421 | 422 | startFrame: function () { 423 | // Set background color: 424 | var bgColor = 0; 425 | 426 | if (this.f_dispType === 0) { 427 | // Color display. 428 | // f_color determines color emphasis. 429 | // Use first entry of image palette as BG color. 430 | bgColor = this.imgPalette[0]; 431 | } else { 432 | // Monochrome display. 433 | // f_color determines the bg color. 434 | switch (this.f_color) { 435 | case 0: 436 | // Black 437 | bgColor = 0x00000; 438 | break; 439 | case 1: 440 | // Green 441 | bgColor = 0x00ff00; 442 | break; 443 | case 2: 444 | // Blue 445 | bgColor = 0xff0000; 446 | break; 447 | case 3: 448 | // Invalid. Use black. 449 | bgColor = 0x000000; 450 | break; 451 | case 4: 452 | // Red 453 | bgColor = 0x0000ff; 454 | break; 455 | default: 456 | // Invalid. Use black. 457 | bgColor = 0x0; 458 | } 459 | } 460 | 461 | var buffer = this.buffer; 462 | var i; 463 | for (i = 0; i < 256 * 240; i++) { 464 | buffer[i] = bgColor; 465 | } 466 | var pixrendered = this.pixrendered; 467 | for (i = 0; i < pixrendered.length; i++) { 468 | pixrendered[i] = 65; 469 | } 470 | }, 471 | 472 | endFrame: function () { 473 | var i, x, y; 474 | var buffer = this.buffer; 475 | 476 | // Draw spr#0 hit coordinates: 477 | if (this.showSpr0Hit) { 478 | // Spr 0 position: 479 | if ( 480 | this.sprX[0] >= 0 && 481 | this.sprX[0] < 256 && 482 | this.sprY[0] >= 0 && 483 | this.sprY[0] < 240 484 | ) { 485 | for (i = 0; i < 256; i++) { 486 | buffer[(this.sprY[0] << 8) + i] = 0xff5555; 487 | } 488 | for (i = 0; i < 240; i++) { 489 | buffer[(i << 8) + this.sprX[0]] = 0xff5555; 490 | } 491 | } 492 | // Hit position: 493 | if ( 494 | this.spr0HitX >= 0 && 495 | this.spr0HitX < 256 && 496 | this.spr0HitY >= 0 && 497 | this.spr0HitY < 240 498 | ) { 499 | for (i = 0; i < 256; i++) { 500 | buffer[(this.spr0HitY << 8) + i] = 0x55ff55; 501 | } 502 | for (i = 0; i < 240; i++) { 503 | buffer[(i << 8) + this.spr0HitX] = 0x55ff55; 504 | } 505 | } 506 | } 507 | 508 | // This is a bit lazy.. 509 | // if either the sprites or the background should be clipped, 510 | // both are clipped after rendering is finished. 511 | if ( 512 | this.clipToTvSize || 513 | this.f_bgClipping === 0 || 514 | this.f_spClipping === 0 515 | ) { 516 | // Clip left 8-pixels column: 517 | for (y = 0; y < 240; y++) { 518 | for (x = 0; x < 8; x++) { 519 | buffer[(y << 8) + x] = 0; 520 | } 521 | } 522 | } 523 | 524 | if (this.clipToTvSize) { 525 | // Clip right 8-pixels column too: 526 | for (y = 0; y < 240; y++) { 527 | for (x = 0; x < 8; x++) { 528 | buffer[(y << 8) + 255 - x] = 0; 529 | } 530 | } 531 | } 532 | 533 | // Clip top and bottom 8 pixels: 534 | if (this.clipToTvSize) { 535 | for (y = 0; y < 8; y++) { 536 | for (x = 0; x < 256; x++) { 537 | buffer[(y << 8) + x] = 0; 538 | buffer[((239 - y) << 8) + x] = 0; 539 | } 540 | } 541 | } 542 | 543 | this.nes.ui.writeFrame(buffer); 544 | }, 545 | 546 | updateControlReg1: function (value) { 547 | this.triggerRendering(); 548 | 549 | this.f_nmiOnVblank = (value >> 7) & 1; 550 | this.f_spriteSize = (value >> 5) & 1; 551 | this.f_bgPatternTable = (value >> 4) & 1; 552 | this.f_spPatternTable = (value >> 3) & 1; 553 | this.f_addrInc = (value >> 2) & 1; 554 | this.f_nTblAddress = value & 3; 555 | 556 | this.regV = (value >> 1) & 1; 557 | this.regH = value & 1; 558 | this.regS = (value >> 4) & 1; 559 | }, 560 | 561 | updateControlReg2: function (value) { 562 | this.triggerRendering(); 563 | 564 | this.f_color = (value >> 5) & 7; 565 | this.f_spVisibility = (value >> 4) & 1; 566 | this.f_bgVisibility = (value >> 3) & 1; 567 | this.f_spClipping = (value >> 2) & 1; 568 | this.f_bgClipping = (value >> 1) & 1; 569 | this.f_dispType = value & 1; 570 | 571 | if (this.f_dispType === 0) { 572 | this.palTable.setEmphasis(this.f_color); 573 | } 574 | this.updatePalettes(); 575 | }, 576 | 577 | setStatusFlag: function (flag, value) { 578 | var n = 1 << flag; 579 | this.nes.cpu.mem[0x2002] = 580 | (this.nes.cpu.mem[0x2002] & (255 - n)) | (value ? n : 0); 581 | }, 582 | 583 | // CPU Register $2002: 584 | // Read the Status Register. 585 | readStatusRegister: function () { 586 | var tmp = this.nes.cpu.mem[0x2002]; 587 | 588 | // Reset scroll & VRAM Address toggle: 589 | this.firstWrite = true; 590 | 591 | // Clear VBlank flag: 592 | this.setStatusFlag(this.STATUS_VBLANK, false); 593 | 594 | // Fetch status data: 595 | return tmp; 596 | }, 597 | 598 | // CPU Register $2003: 599 | // Write the SPR-RAM address that is used for sramWrite (Register 0x2004 in CPU memory map) 600 | writeSRAMAddress: function (address) { 601 | this.sramAddress = address; 602 | }, 603 | 604 | // CPU Register $2004 (R): 605 | // Read from SPR-RAM (Sprite RAM). 606 | // The address should be set first. 607 | sramLoad: function () { 608 | /*short tmp = sprMem.load(sramAddress); 609 | sramAddress++; // Increment address 610 | sramAddress%=0x100; 611 | return tmp;*/ 612 | return this.spriteMem[this.sramAddress]; 613 | }, 614 | 615 | // CPU Register $2004 (W): 616 | // Write to SPR-RAM (Sprite RAM). 617 | // The address should be set first. 618 | sramWrite: function (value) { 619 | this.spriteMem[this.sramAddress] = value; 620 | this.spriteRamWriteUpdate(this.sramAddress, value); 621 | this.sramAddress++; // Increment address 622 | this.sramAddress %= 0x100; 623 | }, 624 | 625 | // CPU Register $2005: 626 | // Write to scroll registers. 627 | // The first write is the vertical offset, the second is the 628 | // horizontal offset: 629 | scrollWrite: function (value) { 630 | this.triggerRendering(); 631 | 632 | if (this.firstWrite) { 633 | // First write, horizontal scroll: 634 | this.regHT = (value >> 3) & 31; 635 | this.regFH = value & 7; 636 | } else { 637 | // Second write, vertical scroll: 638 | this.regFV = value & 7; 639 | this.regVT = (value >> 3) & 31; 640 | } 641 | this.firstWrite = !this.firstWrite; 642 | }, 643 | 644 | // CPU Register $2006: 645 | // Sets the adress used when reading/writing from/to VRAM. 646 | // The first write sets the high byte, the second the low byte. 647 | writeVRAMAddress: function (address) { 648 | if (this.firstWrite) { 649 | this.regFV = (address >> 4) & 3; 650 | this.regV = (address >> 3) & 1; 651 | this.regH = (address >> 2) & 1; 652 | this.regVT = (this.regVT & 7) | ((address & 3) << 3); 653 | } else { 654 | this.triggerRendering(); 655 | 656 | this.regVT = (this.regVT & 24) | ((address >> 5) & 7); 657 | this.regHT = address & 31; 658 | 659 | this.cntFV = this.regFV; 660 | this.cntV = this.regV; 661 | this.cntH = this.regH; 662 | this.cntVT = this.regVT; 663 | this.cntHT = this.regHT; 664 | 665 | this.checkSprite0(this.scanline - 20); 666 | } 667 | 668 | this.firstWrite = !this.firstWrite; 669 | 670 | // Invoke mapper latch: 671 | this.cntsToAddress(); 672 | if (this.vramAddress < 0x2000) { 673 | this.nes.mmap.latchAccess(this.vramAddress); 674 | } 675 | }, 676 | 677 | // CPU Register $2007(R): 678 | // Read from PPU memory. The address should be set first. 679 | vramLoad: function () { 680 | var tmp; 681 | 682 | this.cntsToAddress(); 683 | this.regsToAddress(); 684 | 685 | // If address is in range 0x0000-0x3EFF, return buffered values: 686 | if (this.vramAddress <= 0x3eff) { 687 | tmp = this.vramBufferedReadValue; 688 | 689 | // Update buffered value: 690 | if (this.vramAddress < 0x2000) { 691 | this.vramBufferedReadValue = this.vramMem[this.vramAddress]; 692 | } else { 693 | this.vramBufferedReadValue = this.mirroredLoad(this.vramAddress); 694 | } 695 | 696 | // Mapper latch access: 697 | if (this.vramAddress < 0x2000) { 698 | this.nes.mmap.latchAccess(this.vramAddress); 699 | } 700 | 701 | // Increment by either 1 or 32, depending on d2 of Control Register 1: 702 | this.vramAddress += this.f_addrInc === 1 ? 32 : 1; 703 | 704 | this.cntsFromAddress(); 705 | this.regsFromAddress(); 706 | 707 | return tmp; // Return the previous buffered value. 708 | } 709 | 710 | // No buffering in this mem range. Read normally. 711 | tmp = this.mirroredLoad(this.vramAddress); 712 | 713 | // Increment by either 1 or 32, depending on d2 of Control Register 1: 714 | this.vramAddress += this.f_addrInc === 1 ? 32 : 1; 715 | 716 | this.cntsFromAddress(); 717 | this.regsFromAddress(); 718 | 719 | return tmp; 720 | }, 721 | 722 | // CPU Register $2007(W): 723 | // Write to PPU memory. The address should be set first. 724 | vramWrite: function (value) { 725 | this.triggerRendering(); 726 | this.cntsToAddress(); 727 | this.regsToAddress(); 728 | 729 | if (this.vramAddress >= 0x2000) { 730 | // Mirroring is used. 731 | this.mirroredWrite(this.vramAddress, value); 732 | } else { 733 | // Write normally. 734 | this.writeMem(this.vramAddress, value); 735 | 736 | // Invoke mapper latch: 737 | this.nes.mmap.latchAccess(this.vramAddress); 738 | } 739 | 740 | // Increment by either 1 or 32, depending on d2 of Control Register 1: 741 | this.vramAddress += this.f_addrInc === 1 ? 32 : 1; 742 | this.regsFromAddress(); 743 | this.cntsFromAddress(); 744 | }, 745 | 746 | // CPU Register $4014: 747 | // Write 256 bytes of main memory 748 | // into Sprite RAM. 749 | sramDMA: function (value) { 750 | var baseAddress = value * 0x100; 751 | var data; 752 | for (var i = this.sramAddress; i < 256; i++) { 753 | data = this.nes.cpu.mem[baseAddress + i]; 754 | this.spriteMem[i] = data; 755 | this.spriteRamWriteUpdate(i, data); 756 | } 757 | 758 | this.nes.cpu.haltCycles(513); 759 | }, 760 | 761 | // Updates the scroll registers from a new VRAM address. 762 | regsFromAddress: function () { 763 | var address = (this.vramTmpAddress >> 8) & 0xff; 764 | this.regFV = (address >> 4) & 7; 765 | this.regV = (address >> 3) & 1; 766 | this.regH = (address >> 2) & 1; 767 | this.regVT = (this.regVT & 7) | ((address & 3) << 3); 768 | 769 | address = this.vramTmpAddress & 0xff; 770 | this.regVT = (this.regVT & 24) | ((address >> 5) & 7); 771 | this.regHT = address & 31; 772 | }, 773 | 774 | // Updates the scroll registers from a new VRAM address. 775 | cntsFromAddress: function () { 776 | var address = (this.vramAddress >> 8) & 0xff; 777 | this.cntFV = (address >> 4) & 3; 778 | this.cntV = (address >> 3) & 1; 779 | this.cntH = (address >> 2) & 1; 780 | this.cntVT = (this.cntVT & 7) | ((address & 3) << 3); 781 | 782 | address = this.vramAddress & 0xff; 783 | this.cntVT = (this.cntVT & 24) | ((address >> 5) & 7); 784 | this.cntHT = address & 31; 785 | }, 786 | 787 | regsToAddress: function () { 788 | var b1 = (this.regFV & 7) << 4; 789 | b1 |= (this.regV & 1) << 3; 790 | b1 |= (this.regH & 1) << 2; 791 | b1 |= (this.regVT >> 3) & 3; 792 | 793 | var b2 = (this.regVT & 7) << 5; 794 | b2 |= this.regHT & 31; 795 | 796 | this.vramTmpAddress = ((b1 << 8) | b2) & 0x7fff; 797 | }, 798 | 799 | cntsToAddress: function () { 800 | var b1 = (this.cntFV & 7) << 4; 801 | b1 |= (this.cntV & 1) << 3; 802 | b1 |= (this.cntH & 1) << 2; 803 | b1 |= (this.cntVT >> 3) & 3; 804 | 805 | var b2 = (this.cntVT & 7) << 5; 806 | b2 |= this.cntHT & 31; 807 | 808 | this.vramAddress = ((b1 << 8) | b2) & 0x7fff; 809 | }, 810 | 811 | incTileCounter: function (count) { 812 | for (var i = count; i !== 0; i--) { 813 | this.cntHT++; 814 | if (this.cntHT === 32) { 815 | this.cntHT = 0; 816 | this.cntVT++; 817 | if (this.cntVT >= 30) { 818 | this.cntH++; 819 | if (this.cntH === 2) { 820 | this.cntH = 0; 821 | this.cntV++; 822 | if (this.cntV === 2) { 823 | this.cntV = 0; 824 | this.cntFV++; 825 | this.cntFV &= 0x7; 826 | } 827 | } 828 | } 829 | } 830 | } 831 | }, 832 | 833 | // Reads from memory, taking into account 834 | // mirroring/mapping of address ranges. 835 | mirroredLoad: function (address) { 836 | return this.vramMem[this.vramMirrorTable[address]]; 837 | }, 838 | 839 | // Writes to memory, taking into account 840 | // mirroring/mapping of address ranges. 841 | mirroredWrite: function (address, value) { 842 | if (address >= 0x3f00 && address < 0x3f20) { 843 | // Palette write mirroring. 844 | if (address === 0x3f00 || address === 0x3f10) { 845 | this.writeMem(0x3f00, value); 846 | this.writeMem(0x3f10, value); 847 | } else if (address === 0x3f04 || address === 0x3f14) { 848 | this.writeMem(0x3f04, value); 849 | this.writeMem(0x3f14, value); 850 | } else if (address === 0x3f08 || address === 0x3f18) { 851 | this.writeMem(0x3f08, value); 852 | this.writeMem(0x3f18, value); 853 | } else if (address === 0x3f0c || address === 0x3f1c) { 854 | this.writeMem(0x3f0c, value); 855 | this.writeMem(0x3f1c, value); 856 | } else { 857 | this.writeMem(address, value); 858 | } 859 | } else { 860 | // Use lookup table for mirrored address: 861 | if (address < this.vramMirrorTable.length) { 862 | this.writeMem(this.vramMirrorTable[address], value); 863 | } else { 864 | throw new Error("Invalid VRAM address: " + address.toString(16)); 865 | } 866 | } 867 | }, 868 | 869 | triggerRendering: function () { 870 | if (this.scanline >= 21 && this.scanline <= 260) { 871 | // Render sprites, and combine: 872 | this.renderFramePartially( 873 | this.lastRenderedScanline + 1, 874 | this.scanline - 21 - this.lastRenderedScanline 875 | ); 876 | 877 | // Set last rendered scanline: 878 | this.lastRenderedScanline = this.scanline - 21; 879 | } 880 | }, 881 | 882 | renderFramePartially: function (startScan, scanCount) { 883 | if (this.f_spVisibility === 1) { 884 | this.renderSpritesPartially(startScan, scanCount, true); 885 | } 886 | 887 | if (this.f_bgVisibility === 1) { 888 | var si = startScan << 8; 889 | var ei = (startScan + scanCount) << 8; 890 | if (ei > 0xf000) { 891 | ei = 0xf000; 892 | } 893 | var buffer = this.buffer; 894 | var bgbuffer = this.bgbuffer; 895 | var pixrendered = this.pixrendered; 896 | for (var destIndex = si; destIndex < ei; destIndex++) { 897 | if (pixrendered[destIndex] > 0xff) { 898 | buffer[destIndex] = bgbuffer[destIndex]; 899 | } 900 | } 901 | } 902 | 903 | if (this.f_spVisibility === 1) { 904 | this.renderSpritesPartially(startScan, scanCount, false); 905 | } 906 | 907 | this.validTileData = false; 908 | }, 909 | 910 | renderBgScanline: function (bgbuffer, scan) { 911 | var baseTile = this.regS === 0 ? 0 : 256; 912 | var destIndex = (scan << 8) - this.regFH; 913 | 914 | this.curNt = this.ntable1[this.cntV + this.cntV + this.cntH]; 915 | 916 | this.cntHT = this.regHT; 917 | this.cntH = this.regH; 918 | this.curNt = this.ntable1[this.cntV + this.cntV + this.cntH]; 919 | 920 | if (scan < 240 && scan - this.cntFV >= 0) { 921 | var tscanoffset = this.cntFV << 3; 922 | var scantile = this.scantile; 923 | var attrib = this.attrib; 924 | var ptTile = this.ptTile; 925 | var nameTable = this.nameTable; 926 | var imgPalette = this.imgPalette; 927 | var pixrendered = this.pixrendered; 928 | var targetBuffer = bgbuffer ? this.bgbuffer : this.buffer; 929 | 930 | var t, tpix, att, col; 931 | 932 | for (var tile = 0; tile < 32; tile++) { 933 | if (scan >= 0) { 934 | // Fetch tile & attrib data: 935 | if (this.validTileData) { 936 | // Get data from array: 937 | t = scantile[tile]; 938 | if (typeof t === "undefined") { 939 | continue; 940 | } 941 | tpix = t.pix; 942 | att = attrib[tile]; 943 | } else { 944 | // Fetch data: 945 | t = 946 | ptTile[ 947 | baseTile + 948 | nameTable[this.curNt].getTileIndex(this.cntHT, this.cntVT) 949 | ]; 950 | if (typeof t === "undefined") { 951 | continue; 952 | } 953 | tpix = t.pix; 954 | att = nameTable[this.curNt].getAttrib(this.cntHT, this.cntVT); 955 | scantile[tile] = t; 956 | attrib[tile] = att; 957 | } 958 | 959 | // Render tile scanline: 960 | var sx = 0; 961 | var x = (tile << 3) - this.regFH; 962 | 963 | if (x > -8) { 964 | if (x < 0) { 965 | destIndex -= x; 966 | sx = -x; 967 | } 968 | if (t.opaque[this.cntFV]) { 969 | for (; sx < 8; sx++) { 970 | targetBuffer[destIndex] = 971 | imgPalette[tpix[tscanoffset + sx] + att]; 972 | pixrendered[destIndex] |= 256; 973 | destIndex++; 974 | } 975 | } else { 976 | for (; sx < 8; sx++) { 977 | col = tpix[tscanoffset + sx]; 978 | if (col !== 0) { 979 | targetBuffer[destIndex] = imgPalette[col + att]; 980 | pixrendered[destIndex] |= 256; 981 | } 982 | destIndex++; 983 | } 984 | } 985 | } 986 | } 987 | 988 | // Increase Horizontal Tile Counter: 989 | if (++this.cntHT === 32) { 990 | this.cntHT = 0; 991 | this.cntH++; 992 | this.cntH %= 2; 993 | this.curNt = this.ntable1[(this.cntV << 1) + this.cntH]; 994 | } 995 | } 996 | 997 | // Tile data for one row should now have been fetched, 998 | // so the data in the array is valid. 999 | this.validTileData = true; 1000 | } 1001 | 1002 | // update vertical scroll: 1003 | this.cntFV++; 1004 | if (this.cntFV === 8) { 1005 | this.cntFV = 0; 1006 | this.cntVT++; 1007 | if (this.cntVT === 30) { 1008 | this.cntVT = 0; 1009 | this.cntV++; 1010 | this.cntV %= 2; 1011 | this.curNt = this.ntable1[(this.cntV << 1) + this.cntH]; 1012 | } else if (this.cntVT === 32) { 1013 | this.cntVT = 0; 1014 | } 1015 | 1016 | // Invalidate fetched data: 1017 | this.validTileData = false; 1018 | } 1019 | }, 1020 | 1021 | renderSpritesPartially: function (startscan, scancount, bgPri) { 1022 | if (this.f_spVisibility === 1) { 1023 | for (var i = 0; i < 64; i++) { 1024 | if ( 1025 | this.bgPriority[i] === bgPri && 1026 | this.sprX[i] >= 0 && 1027 | this.sprX[i] < 256 && 1028 | this.sprY[i] + 8 >= startscan && 1029 | this.sprY[i] < startscan + scancount 1030 | ) { 1031 | // Show sprite. 1032 | if (this.f_spriteSize === 0) { 1033 | // 8x8 sprites 1034 | 1035 | this.srcy1 = 0; 1036 | this.srcy2 = 8; 1037 | 1038 | if (this.sprY[i] < startscan) { 1039 | this.srcy1 = startscan - this.sprY[i] - 1; 1040 | } 1041 | 1042 | if (this.sprY[i] + 8 > startscan + scancount) { 1043 | this.srcy2 = startscan + scancount - this.sprY[i] + 1; 1044 | } 1045 | 1046 | if (this.f_spPatternTable === 0) { 1047 | this.ptTile[this.sprTile[i]].render( 1048 | this.buffer, 1049 | 0, 1050 | this.srcy1, 1051 | 8, 1052 | this.srcy2, 1053 | this.sprX[i], 1054 | this.sprY[i] + 1, 1055 | this.sprCol[i], 1056 | this.sprPalette, 1057 | this.horiFlip[i], 1058 | this.vertFlip[i], 1059 | i, 1060 | this.pixrendered 1061 | ); 1062 | } else { 1063 | this.ptTile[this.sprTile[i] + 256].render( 1064 | this.buffer, 1065 | 0, 1066 | this.srcy1, 1067 | 8, 1068 | this.srcy2, 1069 | this.sprX[i], 1070 | this.sprY[i] + 1, 1071 | this.sprCol[i], 1072 | this.sprPalette, 1073 | this.horiFlip[i], 1074 | this.vertFlip[i], 1075 | i, 1076 | this.pixrendered 1077 | ); 1078 | } 1079 | } else { 1080 | // 8x16 sprites 1081 | var top = this.sprTile[i]; 1082 | if ((top & 1) !== 0) { 1083 | top = this.sprTile[i] - 1 + 256; 1084 | } 1085 | 1086 | var srcy1 = 0; 1087 | var srcy2 = 8; 1088 | 1089 | if (this.sprY[i] < startscan) { 1090 | srcy1 = startscan - this.sprY[i] - 1; 1091 | } 1092 | 1093 | if (this.sprY[i] + 8 > startscan + scancount) { 1094 | srcy2 = startscan + scancount - this.sprY[i]; 1095 | } 1096 | 1097 | this.ptTile[top + (this.vertFlip[i] ? 1 : 0)].render( 1098 | this.buffer, 1099 | 0, 1100 | srcy1, 1101 | 8, 1102 | srcy2, 1103 | this.sprX[i], 1104 | this.sprY[i] + 1, 1105 | this.sprCol[i], 1106 | this.sprPalette, 1107 | this.horiFlip[i], 1108 | this.vertFlip[i], 1109 | i, 1110 | this.pixrendered 1111 | ); 1112 | 1113 | srcy1 = 0; 1114 | srcy2 = 8; 1115 | 1116 | if (this.sprY[i] + 8 < startscan) { 1117 | srcy1 = startscan - (this.sprY[i] + 8 + 1); 1118 | } 1119 | 1120 | if (this.sprY[i] + 16 > startscan + scancount) { 1121 | srcy2 = startscan + scancount - (this.sprY[i] + 8); 1122 | } 1123 | 1124 | this.ptTile[top + (this.vertFlip[i] ? 0 : 1)].render( 1125 | this.buffer, 1126 | 0, 1127 | srcy1, 1128 | 8, 1129 | srcy2, 1130 | this.sprX[i], 1131 | this.sprY[i] + 1 + 8, 1132 | this.sprCol[i], 1133 | this.sprPalette, 1134 | this.horiFlip[i], 1135 | this.vertFlip[i], 1136 | i, 1137 | this.pixrendered 1138 | ); 1139 | } 1140 | } 1141 | } 1142 | } 1143 | }, 1144 | 1145 | checkSprite0: function (scan) { 1146 | this.spr0HitX = -1; 1147 | this.spr0HitY = -1; 1148 | 1149 | var toffset; 1150 | var tIndexAdd = this.f_spPatternTable === 0 ? 0 : 256; 1151 | var x, y, t, i; 1152 | var bufferIndex; 1153 | 1154 | x = this.sprX[0]; 1155 | y = this.sprY[0] + 1; 1156 | 1157 | if (this.f_spriteSize === 0) { 1158 | // 8x8 sprites. 1159 | 1160 | // Check range: 1161 | if (y <= scan && y + 8 > scan && x >= -7 && x < 256) { 1162 | // Sprite is in range. 1163 | // Draw scanline: 1164 | t = this.ptTile[this.sprTile[0] + tIndexAdd]; 1165 | 1166 | if (this.vertFlip[0]) { 1167 | toffset = 7 - (scan - y); 1168 | } else { 1169 | toffset = scan - y; 1170 | } 1171 | toffset *= 8; 1172 | 1173 | bufferIndex = scan * 256 + x; 1174 | if (this.horiFlip[0]) { 1175 | for (i = 7; i >= 0; i--) { 1176 | if (x >= 0 && x < 256) { 1177 | if ( 1178 | bufferIndex >= 0 && 1179 | bufferIndex < 61440 && 1180 | this.pixrendered[bufferIndex] !== 0 1181 | ) { 1182 | if (t.pix[toffset + i] !== 0) { 1183 | this.spr0HitX = bufferIndex % 256; 1184 | this.spr0HitY = scan; 1185 | return true; 1186 | } 1187 | } 1188 | } 1189 | x++; 1190 | bufferIndex++; 1191 | } 1192 | } else { 1193 | for (i = 0; i < 8; i++) { 1194 | if (x >= 0 && x < 256) { 1195 | if ( 1196 | bufferIndex >= 0 && 1197 | bufferIndex < 61440 && 1198 | this.pixrendered[bufferIndex] !== 0 1199 | ) { 1200 | if (t.pix[toffset + i] !== 0) { 1201 | this.spr0HitX = bufferIndex % 256; 1202 | this.spr0HitY = scan; 1203 | return true; 1204 | } 1205 | } 1206 | } 1207 | x++; 1208 | bufferIndex++; 1209 | } 1210 | } 1211 | } 1212 | } else { 1213 | // 8x16 sprites: 1214 | 1215 | // Check range: 1216 | if (y <= scan && y + 16 > scan && x >= -7 && x < 256) { 1217 | // Sprite is in range. 1218 | // Draw scanline: 1219 | 1220 | if (this.vertFlip[0]) { 1221 | toffset = 15 - (scan - y); 1222 | } else { 1223 | toffset = scan - y; 1224 | } 1225 | 1226 | if (toffset < 8) { 1227 | // first half of sprite. 1228 | t = this.ptTile[ 1229 | this.sprTile[0] + 1230 | (this.vertFlip[0] ? 1 : 0) + 1231 | ((this.sprTile[0] & 1) !== 0 ? 255 : 0) 1232 | ]; 1233 | } else { 1234 | // second half of sprite. 1235 | t = this.ptTile[ 1236 | this.sprTile[0] + 1237 | (this.vertFlip[0] ? 0 : 1) + 1238 | ((this.sprTile[0] & 1) !== 0 ? 255 : 0) 1239 | ]; 1240 | if (this.vertFlip[0]) { 1241 | toffset = 15 - toffset; 1242 | } else { 1243 | toffset -= 8; 1244 | } 1245 | } 1246 | toffset *= 8; 1247 | 1248 | bufferIndex = scan * 256 + x; 1249 | if (this.horiFlip[0]) { 1250 | for (i = 7; i >= 0; i--) { 1251 | if (x >= 0 && x < 256) { 1252 | if ( 1253 | bufferIndex >= 0 && 1254 | bufferIndex < 61440 && 1255 | this.pixrendered[bufferIndex] !== 0 1256 | ) { 1257 | if (t.pix[toffset + i] !== 0) { 1258 | this.spr0HitX = bufferIndex % 256; 1259 | this.spr0HitY = scan; 1260 | return true; 1261 | } 1262 | } 1263 | } 1264 | x++; 1265 | bufferIndex++; 1266 | } 1267 | } else { 1268 | for (i = 0; i < 8; i++) { 1269 | if (x >= 0 && x < 256) { 1270 | if ( 1271 | bufferIndex >= 0 && 1272 | bufferIndex < 61440 && 1273 | this.pixrendered[bufferIndex] !== 0 1274 | ) { 1275 | if (t.pix[toffset + i] !== 0) { 1276 | this.spr0HitX = bufferIndex % 256; 1277 | this.spr0HitY = scan; 1278 | return true; 1279 | } 1280 | } 1281 | } 1282 | x++; 1283 | bufferIndex++; 1284 | } 1285 | } 1286 | } 1287 | } 1288 | 1289 | return false; 1290 | }, 1291 | 1292 | // This will write to PPU memory, and 1293 | // update internally buffered data 1294 | // appropriately. 1295 | writeMem: function (address, value) { 1296 | this.vramMem[address] = value; 1297 | 1298 | // Update internally buffered data: 1299 | if (address < 0x2000) { 1300 | this.vramMem[address] = value; 1301 | this.patternWrite(address, value); 1302 | } else if (address >= 0x2000 && address < 0x23c0) { 1303 | this.nameTableWrite(this.ntable1[0], address - 0x2000, value); 1304 | } else if (address >= 0x23c0 && address < 0x2400) { 1305 | this.attribTableWrite(this.ntable1[0], address - 0x23c0, value); 1306 | } else if (address >= 0x2400 && address < 0x27c0) { 1307 | this.nameTableWrite(this.ntable1[1], address - 0x2400, value); 1308 | } else if (address >= 0x27c0 && address < 0x2800) { 1309 | this.attribTableWrite(this.ntable1[1], address - 0x27c0, value); 1310 | } else if (address >= 0x2800 && address < 0x2bc0) { 1311 | this.nameTableWrite(this.ntable1[2], address - 0x2800, value); 1312 | } else if (address >= 0x2bc0 && address < 0x2c00) { 1313 | this.attribTableWrite(this.ntable1[2], address - 0x2bc0, value); 1314 | } else if (address >= 0x2c00 && address < 0x2fc0) { 1315 | this.nameTableWrite(this.ntable1[3], address - 0x2c00, value); 1316 | } else if (address >= 0x2fc0 && address < 0x3000) { 1317 | this.attribTableWrite(this.ntable1[3], address - 0x2fc0, value); 1318 | } else if (address >= 0x3f00 && address < 0x3f20) { 1319 | this.updatePalettes(); 1320 | } 1321 | }, 1322 | 1323 | // Reads data from $3f00 to $f20 1324 | // into the two buffered palettes. 1325 | updatePalettes: function () { 1326 | var i; 1327 | 1328 | for (i = 0; i < 16; i++) { 1329 | if (this.f_dispType === 0) { 1330 | this.imgPalette[i] = this.palTable.getEntry( 1331 | this.vramMem[0x3f00 + i] & 63 1332 | ); 1333 | } else { 1334 | this.imgPalette[i] = this.palTable.getEntry( 1335 | this.vramMem[0x3f00 + i] & 32 1336 | ); 1337 | } 1338 | } 1339 | for (i = 0; i < 16; i++) { 1340 | if (this.f_dispType === 0) { 1341 | this.sprPalette[i] = this.palTable.getEntry( 1342 | this.vramMem[0x3f10 + i] & 63 1343 | ); 1344 | } else { 1345 | this.sprPalette[i] = this.palTable.getEntry( 1346 | this.vramMem[0x3f10 + i] & 32 1347 | ); 1348 | } 1349 | } 1350 | }, 1351 | 1352 | // Updates the internal pattern 1353 | // table buffers with this new byte. 1354 | // In vNES, there is a version of this with 4 arguments which isn't used. 1355 | patternWrite: function (address, value) { 1356 | var tileIndex = Math.floor(address / 16); 1357 | var leftOver = address % 16; 1358 | if (leftOver < 8) { 1359 | this.ptTile[tileIndex].setScanline( 1360 | leftOver, 1361 | value, 1362 | this.vramMem[address + 8] 1363 | ); 1364 | } else { 1365 | this.ptTile[tileIndex].setScanline( 1366 | leftOver - 8, 1367 | this.vramMem[address - 8], 1368 | value 1369 | ); 1370 | } 1371 | }, 1372 | 1373 | // Updates the internal name table buffers 1374 | // with this new byte. 1375 | nameTableWrite: function (index, address, value) { 1376 | this.nameTable[index].tile[address] = value; 1377 | 1378 | // Update Sprite #0 hit: 1379 | //updateSpr0Hit(); 1380 | this.checkSprite0(this.scanline - 20); 1381 | }, 1382 | 1383 | // Updates the internal pattern 1384 | // table buffers with this new attribute 1385 | // table byte. 1386 | attribTableWrite: function (index, address, value) { 1387 | this.nameTable[index].writeAttrib(address, value); 1388 | }, 1389 | 1390 | // Updates the internally buffered sprite 1391 | // data with this new byte of info. 1392 | spriteRamWriteUpdate: function (address, value) { 1393 | var tIndex = Math.floor(address / 4); 1394 | 1395 | if (tIndex === 0) { 1396 | //updateSpr0Hit(); 1397 | this.checkSprite0(this.scanline - 20); 1398 | } 1399 | 1400 | if (address % 4 === 0) { 1401 | // Y coordinate 1402 | this.sprY[tIndex] = value; 1403 | } else if (address % 4 === 1) { 1404 | // Tile index 1405 | this.sprTile[tIndex] = value; 1406 | } else if (address % 4 === 2) { 1407 | // Attributes 1408 | this.vertFlip[tIndex] = (value & 0x80) !== 0; 1409 | this.horiFlip[tIndex] = (value & 0x40) !== 0; 1410 | this.bgPriority[tIndex] = (value & 0x20) !== 0; 1411 | this.sprCol[tIndex] = (value & 3) << 2; 1412 | } else if (address % 4 === 3) { 1413 | // X coordinate 1414 | this.sprX[tIndex] = value; 1415 | } 1416 | }, 1417 | 1418 | doNMI: function () { 1419 | // Set VBlank flag: 1420 | this.setStatusFlag(this.STATUS_VBLANK, true); 1421 | //nes.getCpu().doNonMaskableInterrupt(); 1422 | this.nes.cpu.requestIrq(this.nes.cpu.IRQ_NMI); 1423 | }, 1424 | 1425 | isPixelWhite: function (x, y) { 1426 | this.triggerRendering(); 1427 | return this.nes.ppu.buffer[(y << 8) + x] === 0xffffff; 1428 | }, 1429 | 1430 | JSON_PROPERTIES: [ 1431 | // Memory 1432 | "vramMem", 1433 | "spriteMem", 1434 | // Counters 1435 | "cntFV", 1436 | "cntV", 1437 | "cntH", 1438 | "cntVT", 1439 | "cntHT", 1440 | // Registers 1441 | "regFV", 1442 | "regV", 1443 | "regH", 1444 | "regVT", 1445 | "regHT", 1446 | "regFH", 1447 | "regS", 1448 | // VRAM addr 1449 | "vramAddress", 1450 | "vramTmpAddress", 1451 | // Control/Status registers 1452 | "f_nmiOnVblank", 1453 | "f_spriteSize", 1454 | "f_bgPatternTable", 1455 | "f_spPatternTable", 1456 | "f_addrInc", 1457 | "f_nTblAddress", 1458 | "f_color", 1459 | "f_spVisibility", 1460 | "f_bgVisibility", 1461 | "f_spClipping", 1462 | "f_bgClipping", 1463 | "f_dispType", 1464 | // VRAM I/O 1465 | "vramBufferedReadValue", 1466 | "firstWrite", 1467 | // Mirroring 1468 | "currentMirroring", 1469 | "vramMirrorTable", 1470 | "ntable1", 1471 | // SPR-RAM I/O 1472 | "sramAddress", 1473 | // Sprites. Most sprite data is rebuilt from spriteMem 1474 | "hitSpr0", 1475 | // Palettes 1476 | "sprPalette", 1477 | "imgPalette", 1478 | // Rendering progression 1479 | "curX", 1480 | "scanline", 1481 | "lastRenderedScanline", 1482 | "curNt", 1483 | "scantile", 1484 | // Used during rendering 1485 | "attrib", 1486 | "buffer", 1487 | "bgbuffer", 1488 | "pixrendered", 1489 | // Misc 1490 | "requestEndFrame", 1491 | "nmiOk", 1492 | "dummyCycleToggle", 1493 | "nmiCounter", 1494 | "validTileData", 1495 | "scanlineAlreadyRendered" 1496 | ], 1497 | 1498 | toJSON: function () { 1499 | var i; 1500 | var state = toJSON(this); 1501 | 1502 | state.nameTable = []; 1503 | for (i = 0; i < this.nameTable.length; i++) { 1504 | state.nameTable[i] = this.nameTable[i].toJSON(); 1505 | } 1506 | 1507 | state.ptTile = []; 1508 | for (i = 0; i < this.ptTile.length; i++) { 1509 | state.ptTile[i] = this.ptTile[i].toJSON(); 1510 | } 1511 | 1512 | return state; 1513 | }, 1514 | 1515 | fromJSON: function (state) { 1516 | var i; 1517 | 1518 | fromJSON(this, state); 1519 | 1520 | for (i = 0; i < this.nameTable.length; i++) { 1521 | this.nameTable[i].fromJSON(state.nameTable[i]); 1522 | } 1523 | 1524 | for (i = 0; i < this.ptTile.length; i++) { 1525 | this.ptTile[i].fromJSON(state.ptTile[i]); 1526 | } 1527 | 1528 | // Sprite data: 1529 | for (i = 0; i < this.spriteMem.length; i++) { 1530 | this.spriteRamWriteUpdate(i, this.spriteMem[i]); 1531 | } 1532 | } 1533 | }; 1534 | 1535 | var NameTable = function (width, height, name) { 1536 | this.width = width; 1537 | this.height = height; 1538 | this.name = name; 1539 | 1540 | this.tile = new Array(width * height); 1541 | this.attrib = new Array(width * height); 1542 | for (var i = 0; i < width * height; i++) { 1543 | this.tile[i] = 0; 1544 | this.attrib[i] = 0; 1545 | } 1546 | }; 1547 | 1548 | NameTable.prototype = { 1549 | getTileIndex: function (x, y) { 1550 | return this.tile[y * this.width + x]; 1551 | }, 1552 | 1553 | getAttrib: function (x, y) { 1554 | return this.attrib[y * this.width + x]; 1555 | }, 1556 | 1557 | writeAttrib: function (index, value) { 1558 | var basex = (index % 8) * 4; 1559 | var basey = Math.floor(index / 8) * 4; 1560 | var add; 1561 | var tx, ty; 1562 | var attindex; 1563 | 1564 | for (var sqy = 0; sqy < 2; sqy++) { 1565 | for (var sqx = 0; sqx < 2; sqx++) { 1566 | add = (value >> (2 * (sqy * 2 + sqx))) & 3; 1567 | for (var y = 0; y < 2; y++) { 1568 | for (var x = 0; x < 2; x++) { 1569 | tx = basex + sqx * 2 + x; 1570 | ty = basey + sqy * 2 + y; 1571 | attindex = ty * this.width + tx; 1572 | this.attrib[attindex] = (add << 2) & 12; 1573 | } 1574 | } 1575 | } 1576 | } 1577 | }, 1578 | 1579 | toJSON: function () { 1580 | return { 1581 | tile: this.tile, 1582 | attrib: this.attrib 1583 | }; 1584 | }, 1585 | 1586 | fromJSON: function (s) { 1587 | this.tile = s.tile; 1588 | this.attrib = s.attrib; 1589 | } 1590 | }; 1591 | 1592 | var PaletteTable = function () { 1593 | this.curTable = new Array(64); 1594 | this.emphTable = new Array(8); 1595 | this.currentEmph = -1; 1596 | }; 1597 | 1598 | PaletteTable.prototype = { 1599 | reset: function () { 1600 | this.setEmphasis(0); 1601 | }, 1602 | 1603 | loadNTSCPalette: function () { 1604 | // prettier-ignore 1605 | 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]; 1606 | this.makeTables(); 1607 | this.setEmphasis(0); 1608 | }, 1609 | 1610 | loadPALPalette: function () { 1611 | // prettier-ignore 1612 | 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]; 1613 | this.makeTables(); 1614 | this.setEmphasis(0); 1615 | }, 1616 | 1617 | makeTables: function () { 1618 | var r, g, b, col, i, rFactor, gFactor, bFactor; 1619 | 1620 | // Calculate a table for each possible emphasis setting: 1621 | for (var emph = 0; emph < 8; emph++) { 1622 | // Determine color component factors: 1623 | rFactor = 1.0; 1624 | gFactor = 1.0; 1625 | bFactor = 1.0; 1626 | 1627 | if ((emph & 1) !== 0) { 1628 | rFactor = 0.75; 1629 | bFactor = 0.75; 1630 | } 1631 | if ((emph & 2) !== 0) { 1632 | rFactor = 0.75; 1633 | gFactor = 0.75; 1634 | } 1635 | if ((emph & 4) !== 0) { 1636 | gFactor = 0.75; 1637 | bFactor = 0.75; 1638 | } 1639 | 1640 | this.emphTable[emph] = new Array(64); 1641 | 1642 | // Calculate table: 1643 | for (i = 0; i < 64; i++) { 1644 | col = this.curTable[i]; 1645 | r = Math.floor(this.getRed(col) * rFactor); 1646 | g = Math.floor(this.getGreen(col) * gFactor); 1647 | b = Math.floor(this.getBlue(col) * bFactor); 1648 | this.emphTable[emph][i] = this.getRgb(r, g, b); 1649 | } 1650 | } 1651 | }, 1652 | 1653 | setEmphasis: function (emph) { 1654 | if (emph !== this.currentEmph) { 1655 | this.currentEmph = emph; 1656 | for (var i = 0; i < 64; i++) { 1657 | this.curTable[i] = this.emphTable[emph][i]; 1658 | } 1659 | } 1660 | }, 1661 | 1662 | getEntry: function (yiq) { 1663 | return this.curTable[yiq]; 1664 | }, 1665 | 1666 | getRed: function (rgb) { 1667 | return (rgb >> 16) & 0xff; 1668 | }, 1669 | 1670 | getGreen: function (rgb) { 1671 | return (rgb >> 8) & 0xff; 1672 | }, 1673 | 1674 | getBlue: function (rgb) { 1675 | return rgb & 0xff; 1676 | }, 1677 | 1678 | getRgb: function (r, g, b) { 1679 | return (r << 16) | (g << 8) | b; 1680 | }, 1681 | 1682 | loadDefaultPalette: function () { 1683 | this.curTable[0] = this.getRgb(117, 117, 117); 1684 | this.curTable[1] = this.getRgb(39, 27, 143); 1685 | this.curTable[2] = this.getRgb(0, 0, 171); 1686 | this.curTable[3] = this.getRgb(71, 0, 159); 1687 | this.curTable[4] = this.getRgb(143, 0, 119); 1688 | this.curTable[5] = this.getRgb(171, 0, 19); 1689 | this.curTable[6] = this.getRgb(167, 0, 0); 1690 | this.curTable[7] = this.getRgb(127, 11, 0); 1691 | this.curTable[8] = this.getRgb(67, 47, 0); 1692 | this.curTable[9] = this.getRgb(0, 71, 0); 1693 | this.curTable[10] = this.getRgb(0, 81, 0); 1694 | this.curTable[11] = this.getRgb(0, 63, 23); 1695 | this.curTable[12] = this.getRgb(27, 63, 95); 1696 | this.curTable[13] = this.getRgb(0, 0, 0); 1697 | this.curTable[14] = this.getRgb(0, 0, 0); 1698 | this.curTable[15] = this.getRgb(0, 0, 0); 1699 | this.curTable[16] = this.getRgb(188, 188, 188); 1700 | this.curTable[17] = this.getRgb(0, 115, 239); 1701 | this.curTable[18] = this.getRgb(35, 59, 239); 1702 | this.curTable[19] = this.getRgb(131, 0, 243); 1703 | this.curTable[20] = this.getRgb(191, 0, 191); 1704 | this.curTable[21] = this.getRgb(231, 0, 91); 1705 | this.curTable[22] = this.getRgb(219, 43, 0); 1706 | this.curTable[23] = this.getRgb(203, 79, 15); 1707 | this.curTable[24] = this.getRgb(139, 115, 0); 1708 | this.curTable[25] = this.getRgb(0, 151, 0); 1709 | this.curTable[26] = this.getRgb(0, 171, 0); 1710 | this.curTable[27] = this.getRgb(0, 147, 59); 1711 | this.curTable[28] = this.getRgb(0, 131, 139); 1712 | this.curTable[29] = this.getRgb(0, 0, 0); 1713 | this.curTable[30] = this.getRgb(0, 0, 0); 1714 | this.curTable[31] = this.getRgb(0, 0, 0); 1715 | this.curTable[32] = this.getRgb(255, 255, 255); 1716 | this.curTable[33] = this.getRgb(63, 191, 255); 1717 | this.curTable[34] = this.getRgb(95, 151, 255); 1718 | this.curTable[35] = this.getRgb(167, 139, 253); 1719 | this.curTable[36] = this.getRgb(247, 123, 255); 1720 | this.curTable[37] = this.getRgb(255, 119, 183); 1721 | this.curTable[38] = this.getRgb(255, 119, 99); 1722 | this.curTable[39] = this.getRgb(255, 155, 59); 1723 | this.curTable[40] = this.getRgb(243, 191, 63); 1724 | this.curTable[41] = this.getRgb(131, 211, 19); 1725 | this.curTable[42] = this.getRgb(79, 223, 75); 1726 | this.curTable[43] = this.getRgb(88, 248, 152); 1727 | this.curTable[44] = this.getRgb(0, 235, 219); 1728 | this.curTable[45] = this.getRgb(0, 0, 0); 1729 | this.curTable[46] = this.getRgb(0, 0, 0); 1730 | this.curTable[47] = this.getRgb(0, 0, 0); 1731 | this.curTable[48] = this.getRgb(255, 255, 255); 1732 | this.curTable[49] = this.getRgb(171, 231, 255); 1733 | this.curTable[50] = this.getRgb(199, 215, 255); 1734 | this.curTable[51] = this.getRgb(215, 203, 255); 1735 | this.curTable[52] = this.getRgb(255, 199, 255); 1736 | this.curTable[53] = this.getRgb(255, 199, 219); 1737 | this.curTable[54] = this.getRgb(255, 191, 179); 1738 | this.curTable[55] = this.getRgb(255, 219, 171); 1739 | this.curTable[56] = this.getRgb(255, 231, 163); 1740 | this.curTable[57] = this.getRgb(227, 255, 163); 1741 | this.curTable[58] = this.getRgb(171, 243, 191); 1742 | this.curTable[59] = this.getRgb(179, 255, 207); 1743 | this.curTable[60] = this.getRgb(159, 255, 243); 1744 | this.curTable[61] = this.getRgb(0, 0, 0); 1745 | this.curTable[62] = this.getRgb(0, 0, 0); 1746 | this.curTable[63] = this.getRgb(0, 0, 0); 1747 | 1748 | this.makeTables(); 1749 | this.setEmphasis(0); 1750 | } 1751 | }; 1752 | --------------------------------------------------------------------------------