├── .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 | 
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 |
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 = $("