├── .gitignore ├── .gitmodules ├── README.md ├── demo.gif ├── go.mod ├── go.sum ├── index.html ├── public ├── controller.svg ├── game.js ├── mobilecheck.js └── nes.js └── server.go /.gitignore: -------------------------------------------------------------------------------- 1 | greasyphone 2 | roms/ -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "jsnes"] 2 | path = jsnes 3 | url = https://github.com/bfirsh/jsnes 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # greasyphone 2 | 3 | > Play NES using smartphones as a joypads. 4 | 5 | ![demo](https://cdn.rawgit.com/olahol/greasyphone/master/demo.gif "Demo using devtools instead of a mobile cause I couldn't capture anything else.") 6 | 7 | ## Usage 8 | 9 | You will need to have your own ROMs in the `roms/` directory with the extensions `.nes`. 10 | 11 | $ git clone --recursive https://github.com/olahol/greasyphone 12 | $ go get 13 | $ go build 14 | $ ./greasyphone ./roms 15 | $ $BROWSER http://localhost:5000 16 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olahol/greasyphone/c047b0dd185e78733e03fb6d8722172815a1c3e5/demo.gif -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/olahol/greasyphone 2 | 3 | go 1.19 4 | 5 | require github.com/olahol/melody v1.1.1 6 | 7 | require github.com/gorilla/websocket v1.5.0 // indirect 8 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= 3 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 4 | github.com/olahol/melody v1.1.1 h1:amgBhR7pDY0rA0JHWprgLF0LnVztognAwEQgf/WYLVM= 5 | github.com/olahol/melody v1.1.1/go.mod h1:GgkTl6Y7yWj/HtfD48Q5vLKPVoZOH+Qqgfa7CvJgJM4= 6 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 7 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 8 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 9 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | GREASY PHONE 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 81 | 82 | 83 | 84 | 85 |
86 |
87 | 90 |
91 |
92 |
93 | 94 |
95 | 96 |
97 | 98 |

99 | 100 | 101 | 102 | 103 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /public/controller.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Layer 1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | START 20 | SELECT 21 | B 22 | A 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | PLAYER 0 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /public/game.js: -------------------------------------------------------------------------------- 1 | (function (ctx) { 2 | "use strict"; 3 | 4 | var Game = ctx.Game = function () { 5 | this.ws = null; 6 | this.whoami = null; 7 | this.rom = null; 8 | this.status = []; 9 | this.nes = new NES(); 10 | this.$emulator = $("#emulator"); 11 | this.$controller = $("#controller"); 12 | this.$selector = $("#selector"); 13 | this.$status = $("#status"); 14 | this.$error = $("#error"); 15 | this.cmd = this.cmd.bind(this); 16 | this.$selector.change(function (e, val) { 17 | this.handleSelect($(e.target).val()); 18 | }.bind(this)); 19 | }; 20 | 21 | Game.prototype.connect = function () { 22 | var url = "ws://" + window.location.host + "/ws"; 23 | this.ws = new WebSocket(url); 24 | this.ws.onopen = this.handleOpen.bind(this); 25 | this.ws.onmessage = this.handleMessage.bind(this); 26 | }; 27 | 28 | Game.prototype.type = function () { 29 | if (mobilecheck() && "ontouchstart" in window) { 30 | return "player"; 31 | } 32 | 33 | return "screen"; 34 | }; 35 | 36 | Game.prototype.cmd = function (cmd, data) { 37 | this.ws.send(JSON.stringify({cmd: cmd, data: data})); 38 | }; 39 | 40 | Game.prototype.handleOpen = function () { 41 | this.cmd(this.type(), ""); 42 | }; 43 | 44 | Game.prototype.handleMessage = function (msg) { 45 | try { 46 | var json = JSON.parse(msg.data); 47 | switch (json.cmd) { 48 | case "status": 49 | this.handleStatus(json.data); 50 | this.updateStatus(); 51 | break; 52 | case "whoami": 53 | this.handleWhoami(json.data); 54 | this.updateStatus(); 55 | break; 56 | case "join": 57 | this.handleJoin(json.data); 58 | this.updateStatus(); 59 | break; 60 | case "part": 61 | this.handlePart(json.data); 62 | this.updateStatus(); 63 | break; 64 | case "player1": 65 | this.handlePlayer(1, json.data); 66 | break; 67 | case "player2": 68 | this.handlePlayer(2, json.data); 69 | break; 70 | default: 71 | console.error("unknown cmd " + json.cmd); 72 | break; 73 | } 74 | } catch (e) { 75 | console.error(msg, e); 76 | } 77 | }; 78 | 79 | Game.prototype.handleStatus = function (data) { 80 | this.status = data.split(",").filter(function (p) { 81 | return p !== ""; 82 | }); 83 | 84 | if (this.ready()) { 85 | this.startPlaying(); 86 | } 87 | }; 88 | 89 | Game.prototype.handleWhoami = function (whoami) { 90 | this.whoami = whoami; 91 | 92 | if (this.whoami === "notscreen") { 93 | return this.showError("there is already a screen connected"); 94 | } 95 | 96 | if (this.whoami === "notplayer") { 97 | return this.showError("there are already two players connected"); 98 | } 99 | 100 | this.status.push(whoami); 101 | 102 | if (this.isScreen()) { 103 | this.showEmulator(); 104 | this.getROMs(); 105 | } 106 | 107 | if (this.isPlayer()) { 108 | this.showController(); 109 | } 110 | 111 | if (this.ready()) { 112 | this.startPlaying(); 113 | } 114 | }; 115 | 116 | Game.prototype.handleJoin = function (data) { 117 | this.status.push(data); 118 | 119 | if (this.ready()) { 120 | this.startPlaying(); 121 | } 122 | }; 123 | 124 | Game.prototype.handlePart = function (data) { 125 | this.status = this.status.filter(function (p) { 126 | return p !== data; 127 | }); 128 | 129 | if (this.isScreen() && this.nes.jsnes !== null && !this.ready()) { 130 | this.stopPlaying(); 131 | } 132 | }; 133 | 134 | Game.prototype.handlePlayer = function (player, data) { 135 | var key = data.split(" "); 136 | this.nes.input(player, key[0], key[1]); 137 | }; 138 | 139 | Game.prototype.handleSelect = function (rom) { 140 | if (rom === "none") { 141 | this.rom = null; 142 | this.stopPlaying(); 143 | return; 144 | } 145 | 146 | this.setROM(rom); 147 | }; 148 | 149 | Game.prototype.showEmulator = function () { 150 | this.nes.create(); 151 | this.nes.clear(); 152 | this.$emulator.append(this.nes.screen); 153 | this.$emulator.show(); 154 | }; 155 | 156 | Game.prototype.showController = function () { 157 | this.$controller.show(); 158 | 159 | var pad = document.getElementById("pad"); 160 | 161 | pad.addEventListener("load", function () { 162 | var $doc = $(pad.getSVGDocument()); 163 | 164 | var playerText = this.whoami === "player1" ? "PLAYER 1" : "PLAYER 2"; 165 | $doc.find("#player").text(playerText); 166 | 167 | var registerKey = function (button, cb) { 168 | var $button = $doc.find("#" + button); 169 | 170 | $button.bind("touchstart", function (e) { 171 | cb("keydown", button); 172 | }); 173 | 174 | $button.bind("touchend", function (e) { 175 | cb("keyup", button); 176 | }); 177 | }; 178 | 179 | registerKey("left", this.cmd); 180 | registerKey("right", this.cmd); 181 | registerKey("up", this.cmd); 182 | registerKey("down", this.cmd); 183 | registerKey("start", this.cmd); 184 | registerKey("select", this.cmd); 185 | registerKey("a", this.cmd); 186 | registerKey("b", this.cmd); 187 | 188 | registerKey("upleft", function (state) { 189 | this.cmd(state, "up"); 190 | this.cmd(state, "left"); 191 | }.bind(this)); 192 | 193 | registerKey("upright", function (state) { 194 | this.cmd(state, "up"); 195 | this.cmd(state, "right"); 196 | }.bind(this)); 197 | 198 | registerKey("downleft", function (state) { 199 | this.cmd(state, "down"); 200 | this.cmd(state, "left"); 201 | }.bind(this)); 202 | 203 | registerKey("downright", function (state) { 204 | this.cmd(state, "down"); 205 | this.cmd(state, "right"); 206 | }.bind(this)); 207 | }.bind(this), false); 208 | }; 209 | 210 | Game.prototype.showError = function (msg) { 211 | this.$error.show(); 212 | this.$error.text(msg); 213 | }; 214 | 215 | Game.prototype.updateStatus = function () { 216 | if (!this.isScreen()) { 217 | return; 218 | } 219 | 220 | var connected = this.status.map(function (p) { 221 | return p.toUpperCase(); 222 | }).join(", "); 223 | 224 | this.$status.text("CONNECTED: " + connected); 225 | }; 226 | 227 | Game.prototype.getROMs = function () { 228 | var $sel = this.$selector; 229 | $.get("/romlist", function (data) { 230 | var roms = data.split(","); 231 | 232 | roms.forEach(function (rom) { 233 | var $option = $("