├── .gitignore ├── Games ├── mario.js └── zelda.js ├── LICENSE ├── Models └── Game.js ├── README.md ├── romdom.js ├── romdom.lua └── utils.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | 30 | *.swp 31 | -------------------------------------------------------------------------------- /Games/mario.js: -------------------------------------------------------------------------------- 1 | var Game = require('../Models/Game'), 2 | attributes = { 3 | // RAM address for currently playing music. 4 | musicAddress: '00FB', 5 | 6 | // Hex values for possible in-game music tracks. 7 | tracks: ['01', '02', '04', '07'], 8 | 9 | // RAM address for the game's currently used color palette. 10 | paletteAddress: '0773', 11 | 12 | // Hex values for possible color palette changes. 13 | palettes: ['00', '01', '02', '03', '04'], 14 | 15 | // RAM address for the game's current PPU color mode. 16 | ppuModeAddress: '0779', 17 | 18 | // Objects to represent arbitrary in-game sprites. 19 | sprites: [ 20 | { 21 | "address": '0754', 22 | } 23 | ], 24 | 25 | // RAM address for horizontal scrolling. 26 | horizontalAddress: '0778', 27 | 28 | // RAM address for level advancement. 29 | advanceAddress: '0770', 30 | 31 | // RAM address for effective PPU alterations. 32 | ppuAddress: '0200' 33 | }, 34 | Mario = new Game(attributes); 35 | 36 | module.exports = Mario; 37 | -------------------------------------------------------------------------------- /Games/zelda.js: -------------------------------------------------------------------------------- 1 | var Game = require('../Models/Game'), 2 | attributes = { 3 | // RAM address for currently playing music. 4 | musicAddress: '0600', 5 | 6 | // Hex values for possible in-game music tracks. 7 | tracks: ['01', '10', '20', '40', '80'], 8 | 9 | // RAM address for the game's currently used color palette. 10 | paletteAddress: '', 11 | 12 | // Hex values for possible color palette changes. 13 | palettes: [], 14 | 15 | // RAM address for the game's current PPU color mode. 16 | ppuModeAddress: '', 17 | 18 | // Objects to represent arbitrary in-game sprites. 19 | sprites: [ 20 | { 21 | "address": '', 22 | } 23 | ], 24 | 25 | // RAM address for horizontal scrolling. 26 | horizontalAddress: '', 27 | 28 | // RAM address for level advancement. 29 | advanceAddress: '', 30 | 31 | // RAM address for effective PPU alterations. 32 | ppuAddress: '' 33 | }, 34 | Zelda = new Game(attributes); 35 | 36 | module.exports = Zelda; 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Sam Agnew 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 | 23 | -------------------------------------------------------------------------------- /Models/Game.js: -------------------------------------------------------------------------------- 1 | var utils = require('../utils'), 2 | Game = function (options) { 3 | var game = { 4 | sound: { 5 | musicAddress: options.musicAddress, 6 | tracks: options.tracks, 7 | }, 8 | visual: { 9 | paletteAddress: options.paletteAddress, 10 | palettes: options.palettes, 11 | ppuModeAddress: options.ppuModeAddress 12 | }, 13 | sprites: options.sprites, 14 | scroll: { 15 | horizontalAddress: options.horizontalAddress 16 | }, 17 | level: { 18 | advanceAddress: options.advanceAddress 19 | }, 20 | ppu: { 21 | ppuAddress: options.ppuAddress 22 | } 23 | }; 24 | 25 | game.sound.changeTrack = function(track) { 26 | utils.writeRamByte(this.musicAddress, this.tracks[track]); 27 | }; 28 | 29 | game.visual.changePPUMode = function(value) { 30 | utils.writeRamByte(this.ppuModeAddress, value); 31 | }; 32 | 33 | game.visual.changePalette = function(value) { 34 | utils.writeRamByte(this.paletteAddress, this.palettes[value]); 35 | } 36 | 37 | game.scroll.horizontalOffset = function(offset) { 38 | utils.writeRamByte(this.horizontalAddress, offset); 39 | }; 40 | 41 | game.level.advance = function() { 42 | utils.writeRamByte(this.advanceAddress, '02'); 43 | }; 44 | 45 | game.ppu.write = function(value) { 46 | utils.writeRamByte(this.ppuAddress, value); 47 | }; 48 | 49 | game.onSpriteChange = function(sprite, callback){ 50 | utils.memoryListener(sprite.address, callback); 51 | }; 52 | 53 | return game; 54 | }; 55 | 56 | module.exports = Game; 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ROMDOM 2 | A JavaScript NES hacking library for programmatically editing games in real time. ROMDOM allows you to manipulate NES games with JavaScript similar to how you can interact with a web page through the DOM. 3 | 4 | This library allows you to use Node.js to interface with most emulators that include Lua scripting functionality. It was designed to interact with NES games in mind, but most old games act fundamentally the same way. It's intended us is to run with the FCEUX NES emulator, but many emulators have the same Lua API, so this also works with other emulators such as VisualBoy Advance and SNES9x. 5 | 6 | ## Setup 7 | 8 | First you will need to download a proper emulator with Lua scripting included. We recommend [FCEUX](http://www.fceux.com/web/download.html) as it comes with a hex editor and many debugging capabilities. 9 | 10 | Note: Most of these emulators only include the necessary ROM hacking tools in their Windows versions, much to the dismay of many in the developer world. The windows versions of these emulators can be run effectively in Wine if you are a Mac or Linux user, and that is not too difficult to set up(open the .exe file with Wine, and let the magic happen). 11 | 12 | Once you have your emulator set up: 13 | 14 | 1. Open a new Lua script(in FCEUX this is done by clicking file -> Lua -> New Lua script window), and run "romdom.lua" found in the base directory of this repository. This allows the emulator to listen for commands sent from your Node environment. 15 | 2. Require romdom.js in your JavaScript code, which will allow you to access our API to interact with the emulator. 16 | ```javascript 17 | // If you are in the same directory. 18 | var rd = require('./romdom'); 19 | 20 | // Creates an object representing the current game that is being emulated. 21 | // This allows you to access game-specific functions. 22 | var game = rd.getCurrentGame(); 23 | ``` 24 | 25 | ## How it works 26 | 27 | Hacking older games is usually done through hex editing. This involves manipulating the memory of the game by changing hexadecimal values in specific memory locations corresponding to changes you wish to make. Unfortunately, this is pretty unintuitive, tedious and hard to learn for most people. 28 | 29 | The idea behind ROMDOM is to abstract away this aspect of NES hacking in order to allow the developer to focus on making changes to the game immediately, and without having to know the underlying memory map of the game(although this is never a bad thing to learn). We do this by creating JavaScript objects to represent specific games, and mapping important memory addresses, that represent features common to most old games(such as where graphical data is stored, music changes, etc) to variables in the object. This would allow someone with little knowledge of a specific game to still write scripts, and have fun hacking on it. 30 | 31 | This only works with games that we have already mapped out, which you can see in the Games/ directory in this repository. If you are interested in non game-specific functionality, then you can use ROMDOM's utility functions for things such as adding event listeners to memory addresses. 32 | 33 | ## Usage 34 | 35 | Coming soon. We are very early on in this project, and our API is constantly changing. If you are interested in learning exactly what functionality we provide, you can take a look at our source code in romdom.js, utils.js and Models/Game.js to see what functions are available(with comments). If you want to see an example of how games are laid out, you can check out Games/mario.js. 36 | -------------------------------------------------------------------------------- /romdom.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | utils = require('./utils'), 3 | 4 | // I know this is dirty, but I will use _.extend later instead. 5 | romdom = utils; 6 | 7 | // Returns an object corresponding to the current game being emulated. 8 | romdom.setCurrentGame = function() { 9 | // This is terrible 10 | var val = romdom.readRomByteSync('0764'); 11 | if(val === '5'){ 12 | return require('./Games/mario'); 13 | }else{ 14 | return require('./Games/zelda'); 15 | } 16 | }; 17 | 18 | module.exports = romdom; 19 | -------------------------------------------------------------------------------- /romdom.lua: -------------------------------------------------------------------------------- 1 | function write_file (filename, text) 2 | output = io.open(filename, "w"); 3 | io.output(output); 4 | io.write(text); 5 | io.close(output); 6 | end; 7 | 8 | function read_file (filename) 9 | input = io.open(filename, "r"); 10 | io.input(input); 11 | input_content = io.read(); 12 | io.close(input); 13 | 14 | return input_content; 15 | end; 16 | 17 | function write_ram_byte (address, value) 18 | if(address ~= nil and value ~= nil) then 19 | memory.writebyte(address, value); 20 | end; 21 | end; 22 | 23 | function read_ram_byte (address) 24 | if(address ~= nil) then 25 | local value = tonumber(memory.readbyte(address), 16); 26 | write_file("value.txt", value); 27 | end; 28 | end; 29 | 30 | function read_rom_byte (address) 31 | if(address ~= nil) then 32 | local value = tonumber(rom.readbyte(address), 16); 33 | write_file("value.txt", value); 34 | end; 35 | end; 36 | 37 | function handle_input () 38 | local action = read_file("action.txt"); 39 | local addr_string = read_file("address.txt"); 40 | local value_str = read_file("value.txt"); 41 | local print_text = read_file("print.txt"); 42 | 43 | emu.message(print_text); 44 | 45 | if addr_string ~= nil then 46 | address = tonumber(addr_string, 16); 47 | end 48 | 49 | if addr_string ~= nil then 50 | value = tonumber(value_str, 16); 51 | end 52 | 53 | if action == "writeRamByte" then 54 | write_ram_byte(address, value); 55 | elseif action == "readRamByte" then 56 | read_ram_byte(address); 57 | elseif action == "readRomByte" then 58 | read_rom_byte(address); 59 | end; 60 | 61 | write_file("action.txt", "null"); 62 | end; 63 | 64 | while (true) do 65 | handle_input() 66 | emu.frameadvance(); 67 | end; 68 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | utils = {}; 3 | 4 | utils.readRamByte = function(address, callback) { 5 | fs.writeFile('address.txt', address, function(err){ 6 | if(err) { console.log(err); return callback(err); } 7 | fs.writeFile('action.txt', 'readRamByte', function(err){ 8 | if(err) { console.log(err); return callback(err); } 9 | setTimeout(function() { 10 | fs.readFile('value.txt', { 'encoding': 'utf8' }, function(err, data){ 11 | if(err) { console.log(err); return callback(err); } 12 | callback(null, data); 13 | }); 14 | }, 500); 15 | }); 16 | }); 17 | }; 18 | 19 | // Sorry NYTM 20 | utils.readRomByte = function(address, callback) { 21 | fs.writeFile('address.txt', address, function(err){ 22 | if(err) { console.log(err); return callback(err); } 23 | fs.writeFile('action.txt', 'readRomByte', function(err){ 24 | if(err) { console.log(err); return callback(err); } 25 | setTimeout(function() { 26 | fs.readFile('value.txt', { 'encoding': 'utf8' }, function(err, data){ 27 | if(err) { console.log(err); return callback(err); } 28 | callback(null, data); 29 | }); 30 | }, 500); 31 | }); 32 | }); 33 | }; 34 | 35 | // Sorry NYTM 36 | utils.readRomByteSync = function(address) { 37 | var date = new Date(); 38 | var curDate = null; 39 | 40 | fs.writeFileSync('address.txt', address); 41 | fs.writeFileSync('action.txt', 'readRomByte'); 42 | 43 | // This is the worst code I've ever written. 44 | do { curDate = new Date(); } 45 | while(curDate-date < 500); 46 | 47 | return fs.readFileSync('value.txt', { 'encoding': 'utf8' }); 48 | }; 49 | 50 | utils.writeRamByte = function(address, value) { 51 | fs.writeFile('address.txt', address, function(err){ 52 | if(err) throw err; 53 | }); 54 | 55 | fs.writeFile('value.txt', value, function(err){ 56 | if(err) throw err; 57 | }); 58 | 59 | fs.writeFile('action.txt', 'writeRamByte', function(err){ 60 | if(err) throw err; 61 | }); 62 | }; 63 | 64 | utils.memoryListener = function(address, callback) { 65 | utils.readRamByte(address, function(err, initial) { 66 | if(err) { return callback(err); } 67 | var interval = setInterval(function() { 68 | utils.readRamByte(address, function(err, current) { 69 | if(err) { return callback(err); } 70 | if(initial !== current) { 71 | clearTimeout(interval); 72 | callback(null, current); 73 | } 74 | }); 75 | }, 1000); 76 | }); 77 | }; 78 | 79 | utils.printToScreen = function(text) { 80 | fs.writeFile('print.txt', text, function(err){ 81 | if(err) throw err; 82 | }); 83 | }; 84 | 85 | module.exports = utils; 86 | --------------------------------------------------------------------------------