├── .gitignore ├── Readme.md ├── index.js ├── package.json └── redis.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # weplay 2 | 3 | [![](https://i.cloudup.com/H13p4ll2gu.png)](https://weplay.io) 4 | 5 | ## How to install 6 | 7 | Install with 8 | 9 | ```bash 10 | $ npm install 11 | ``` 12 | 13 | Then run it with the following ENV vars: 14 | 15 | - `WEPLAY_PORT` - pointing to the port you want to listen on (`3001`) 16 | - `WEPLAY_REDIS` - redis uri (`localhost:6379`) 17 | - `WEPLAY_SERVER_UID` - unique persistent identifier for this server's 18 | instance. Used for keeping track of # of clients in redis 19 | (defaults to `WEPLAY_PORT`) 20 | - `WEPLAY_IP_THROTTLE` - the least amount of time in ms that need to 21 | pass between moves from the same IP address (`100`) 22 | 23 | ```bash 24 | $ node index 25 | ``` 26 | 27 | This will set up the IO server for weplay. It's necessary that you also 28 | launch the other services: 29 | 30 | - [weplay-web](https://github.com/guille/weplay-web) serves the HTML 31 | pages and static assets to run the game. It also serves initial state 32 | from Redis with the page for optimal performance. 33 | - [weplay-emulator](https://github.com/guille/weplay-emulator) runs an 34 | emulator and broadcasts the image data from it with 35 | [socket.io-emitter](https://github.com/learnboost/socket.io-emitter) to 36 | the IO instance(s) that users are connected to. 37 | - [weplay-presence](https://github.com/guille/weplay-presence) notifies 38 | all the IO instance(s) of the aggregate number of online users. 39 | 40 | ## FAQ 41 | 42 | ### How does this all work? 43 | 44 | The [weplay-emulator](https://github.com/guille/weplay-emulator) service 45 | runs a JavaScript-based 46 | [gameboy color emulator](http://github.com/guille/gameboy) 47 | that gets painted to an instance of 48 | [node-canvas](http://github.com/learnboost/node-canvas). 49 | 50 | Upon each draw an event is emitted and the PNG buffer is piped through 51 | Redis to all the IO instances of weplay (this project). 52 | 53 | With Socket.IO 1.0 binary support, we can seamlessly transfer the image 54 | data contained in the `Buffer` to all the connected clients, in addition 55 | to all the JSON datastructures to make chat and commands work. 56 | 57 | This makes weplay a 100% JavaScript project! 58 | 59 | ### What are the error handling scenarios? 60 | 61 | - In the event of a crash of a `weplay` IO node, clients can be rerouted 62 | to another instance and reconnect automatically thanks to 63 | [socket.io-client](https://github.com/learnboost/socket.io-client). 64 | - Events that are broadcasted to other users (such as chat messages and 65 | "x moved y" events) get persisted upon broadcast 66 | into a Redis capped list, which means that upon reconnection users will 67 | get the latest events despite having been routed to a new server. 68 | - The connection count will be eventually consistent and correct thanks 69 | to the work of the `weplay-presence` service that aggregates the 70 | connection counts of all servers. 71 | - In the event of a crash of `weplay-emulator`, the next time it boots up 72 | it restores the virtual machine state that gets persisted by default 73 | every 60 seconds (for performance reasons). 74 | 75 | ## Support 76 | 77 | If you have ideas or contributions, join `#weplay` on Freenode. 78 | 79 | ## License 80 | 81 | MIT 82 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | var sio = require('socket.io'); 3 | var browserify = require('browserify-middleware'); 4 | var forwarded = require('forwarded-for'); 5 | var debug = require('debug'); 6 | 7 | process.title = 'weplay-io'; 8 | 9 | var port = process.env.WEPLAY_PORT || 3001; 10 | var io = module.exports = sio(port); 11 | console.log('listening on *:' + port); 12 | 13 | var throttle = process.env.WEPLAY_IP_THROTTLE || 100; 14 | 15 | // redis socket.io adapter 16 | var uri = process.env.WEPLAY_REDIS || 'localhost:6379'; 17 | io.adapter(require('socket.io-redis')(uri)); 18 | 19 | // redis queries instance 20 | var redis = require('./redis')(); 21 | 22 | var keys = { 23 | right: 0, 24 | left: 1, 25 | up: 2, 26 | down: 3, 27 | a: 4, 28 | b: 5, 29 | select: 6, 30 | start: 7 31 | }; 32 | 33 | var uid = process.env.WEPLAY_SERVER_UID || port; 34 | debug('server uid %s', uid); 35 | 36 | io.total = 0; 37 | io.on('connection', function(socket){ 38 | var req = socket.request; 39 | var ip = forwarded(req, req.headers); 40 | debug('client ip %s', ip); 41 | 42 | // keep track of connected clients 43 | updateCount(++io.total); 44 | socket.on('disconnect', function(){ 45 | updateCount(--io.total); 46 | }); 47 | 48 | // send events log so far 49 | redis.lrange('weplay:log', 0, 20, function(err, log){ 50 | if (!Array.isArray(log)) return; 51 | log.reverse().forEach(function(data){ 52 | data = data.toString(); 53 | socket.emit.apply(socket, JSON.parse(data)); 54 | }); 55 | }); 56 | 57 | // broadcast moves, throttling them first 58 | socket.on('move', function(key){ 59 | if (null == keys[key]) return; 60 | redis.get('weplay:move-last:' + ip, function(err, last){ 61 | if (last) { 62 | last = last.toString(); 63 | if (Date.now() - last < throttle) { 64 | return; 65 | } 66 | } 67 | redis.set('weplay:move-last:' + ip, Date.now()); 68 | redis.publish('weplay:move', keys[key]); 69 | socket.emit('move', key, socket.nick); 70 | broadcast(socket, 'move', key, socket.nick); 71 | }); 72 | }); 73 | 74 | // send chat mesages 75 | socket.on('message', function(msg){ 76 | broadcast(socket, 'message', msg, socket.nick); 77 | }); 78 | 79 | // broadcast user joining 80 | socket.on('join', function(nick){ 81 | if (socket.nick) return; 82 | socket.nick = nick; 83 | socket.emit('joined'); 84 | broadcast(socket, 'join', nick); 85 | }); 86 | }); 87 | 88 | // sends connections count to everyone 89 | // by aggregating all servers 90 | function updateCount(total){ 91 | redis.hset('weplay:connections', uid, total); 92 | } 93 | 94 | // broadcast events and persist them to redis 95 | 96 | function broadcast(socket/*, …*/){ 97 | var args = Array.prototype.slice.call(arguments, 1); 98 | redis.lpush('weplay:log', JSON.stringify(args)); 99 | redis.ltrim('weplay:log', 0, 20); 100 | socket.broadcast.emit.apply(socket, args); 101 | } 102 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "weplay", 3 | "version": "0.1.0", 4 | "description": "collaborative gameboy emulation powered by socket.io – weplay.io", 5 | "dependencies": { 6 | "blob": "0.0.1", 7 | "debug": "0.7.4", 8 | "forwarded-for": "0.0.0", 9 | "redis": "0.10.1", 10 | "socket.io": "1.3.7", 11 | "socket.io-redis": "1.0.0" 12 | }, 13 | "devDependencies": { 14 | "browserify": "3.31.2", 15 | "browserify-middleware": "2.3.0" 16 | }, 17 | "scripts": { 18 | "start": "node index.js" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /redis.js: -------------------------------------------------------------------------------- 1 | 2 | var redis = require('redis'); 3 | var uri = process.env.WEPLAY_REDIS_URI || 'localhost:6379'; 4 | var pieces = uri.split(':'); 5 | 6 | module.exports = function(){ 7 | return redis.createClient(pieces[1], pieces[0], { return_buffers: true }); 8 | }; 9 | --------------------------------------------------------------------------------