PageRenderTime 56ms CodeModel.GetById 2ms app.highlight 47ms RepoModel.GetById 1ms app.codeStats 1ms

/src/server/server.js

https://bitbucket.org/nbabcock/nodescape
JavaScript | 540 lines | 383 code | 65 blank | 92 comment | 66 complexity | 38df47e716466fb0bca08d34aebfbe60 MD5 | raw file
  1const
  2    gamestate_cache = 'gamestate.json',
  3    client_cache = 'client_cache.json';
  4
  5const WS = require('ws'),
  6    https = require('https'),
  7    fs = require('fs'),
  8    _ = require('lodash'),
  9    APIConnector = require('./api-connector'),
 10    env = require('./../client/js/environment'),
 11    _game = require('./../client/js/game'),
 12    Game = _game.Game,
 13    Node = _game.Node,
 14    Edge = _game.Edge,
 15    Bubble = _game.Bubble;
 16
 17class Server{
 18    constructor(port=8081){
 19        this.port = port;
 20        console.log(`Starting server (${env})`);
 21        //this.initGame();
 22        this.initWebsockets();
 23        this.APIConnector = new APIConnector();
 24        this.disconnectedClients = [];
 25        this.clients = {};
 26    }
 27
 28    generateUUID() {
 29        return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
 30            var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
 31            return v.toString(16);
 32        });
 33    }
 34
 35    initGame(){
 36        console.log("Initializing server game instance");
 37        this.game = new Game();
 38
 39        if(!this.load())
 40            this.game.procgen();
 41
 42        this.startGameLoop();
 43    }
 44
 45    startGameLoop(){
 46        const CLIENT_TIMEOUT = 5 * 1000;        
 47        this.gameloop = setInterval(() => {
 48            try {
 49                this.game.update.bind(this.game)();
 50                if(this.game.spawn_cooldown <= 1){
 51                    this.checkClients();
 52                    this.wss.clients.forEach(this.sendLightGamestate, this);
 53                    // this.wss.clients.forEach(client => {
 54                    //     if(new Date().getTime() > client.lastupdate + CLIENT_TIMEOUT){
 55                    //         console.log(`Client ${client.username} lost connection to the server.`);
 56                    //         client.close();
 57                    //     }
 58                    // });
 59                    // console.log("Disconnected clients:", this.disconnectedClients.length);
 60                    // console.log("Players:", Object.keys(this.game.players).length);
 61                    // this.checkDisconnectedClients();
 62                    this.save();
 63                }
 64            } catch (e) {
 65                console.error(e);
 66
 67            }
 68        }, this.game.config.tick_rate);
 69    }
 70
 71    checkDisconnectedClients(){
 72        const RECONNECT_TIME = 1 * 60 * 1000;
 73        for(var i = 0; i < this.disconnectedClients.length; i++){
 74            if(new Date().getTime() > this.disconnectedClients[i].time + RECONNECT_TIME){
 75                let mClient = this.disconnectedClients[i];
 76                console.log(`Reconnection window closed for player ${mClient.username}`);
 77                if(this.game.players[mClient.username] && !this.game.players[mClient.username].permanent){
 78                    console.log(`Removing non-permanent player ${mClient.username}`);
 79                    this.game.removePlayer(mClient.username);
 80                }
 81                this.disconnectedClients.splice(i, 1);
 82                --i;
 83            }
 84        }
 85    }
 86
 87    checkClients(){
 88        const
 89            CONNECTION_TIMEOUT = 5 * 1000,
 90            RECONNECT_TIMEOUT = 1 * 60 * 1000;
 91        for(var uuid in this.clients){
 92            let client = this.clients[uuid];
 93            // console.log(`Time is ${new Date().getTime()} vs timeout of ${client.lastUpdate + CONNECTION_TIMEOUT}`);
 94            if(client.ws && client.ws.readyState !== WS.CLOSED && new Date().getTime() > client.lastUpdate + CONNECTION_TIMEOUT){
 95                client.lastUpdate = new Date().getTime();
 96                client.ws.terminate();
 97                console.log(`Client connection timed out (${uuid}); reconnection window starting`);
 98            } else if ((!client.ws || client.ws.readyState !== WS.OPEN) && new Date().getTime() > client.lastUpdate + RECONNECT_TIMEOUT) {
 99                console.log(`Reconnection window closed for client (${uuid})`);
100                if(client.username && this.game.players[client.username] && !this.game.players[client.username].permanent){
101                    console.log(`Removing non-permanent player ${client.username}`);
102                    this.game.removePlayer(client.username);
103                }
104                if(client.ws)
105                    client.ws.terminate();
106                delete this.clients[uuid];
107            }
108        }
109    }
110
111    initWebsockets(){
112        console.log("Initializing websockets");
113
114        // Environment
115        let options;
116        if(env === 'production'){
117            options = {
118                cert: fs.readFileSync('cert/fullchain.pem'),
119                key: fs.readFileSync('cert/privkey.pem')
120            }
121        } else {
122            options = {
123                cert: fs.readFileSync('cert/cert-local.pem'),
124                key: fs.readFileSync('cert/key-local.pem')
125            }
126        }
127
128        let server = this.server = new https.createServer(options);
129        let wss = this.wss = new WS.Server({ server });
130        server.listen(this.port);
131        
132        wss.on('open', ()=>console.log(`Websocket server running on port ${this.port}`));
133
134    
135        wss.on('connection', ws => {
136            ws.uuid = this.generateUUID();
137            this.clients[ws.uuid] = {
138                uuid: ws.uuid,
139                ws,
140                lastUpdate: new Date().getTime()
141            };
142            console.log(`Client connected (${ws.uuid})`);
143            console.log(`- total clients: ${Object.keys(this.clients).length}`);
144            ws.on('message', data => {
145                this.clients[ws.uuid].lastUpdate = new Date().getTime();
146                try {
147                    return this.handleClientMsg(data, ws);
148                } catch (e) {
149                    console.error(e);
150                }
151            });
152            ws.on('close', () => {
153                // if(!ws.username)
154                //     return;
155                console.log(`Client ${ws.uuid} disconnected`);
156                // if(this.game.players[ws.username] && !this.game.players[ws.username].permanent){
157                //     console.log(`Removing non-permanent player ${ws.username}`);
158                //     this.game.removePlayer(ws.username);
159                // }
160
161                // console.log(`Client ${ws.username} disconnected (start of reconnection window)`);
162                // this.disconnectedClients.push({username:ws.username, time: new Date().getTime()});
163                // TODO pongs with timeout to detect broken connections
164            });
165            this.send(ws, {msgtype: 'connect', uuid: ws.uuid});
166            this.sendFullGamestate(ws);
167        });
168    }
169
170    sendFullGamestate(ws){
171        // console.log("Sending full gamestate...");
172        this.send(ws, this.game);
173    }
174
175    sendLightGamestate(ws){
176        let viewport = this.clients[ws.uuid].viewport;
177        if(!viewport)
178            return this.sendFullGamestate(ws);
179
180        let gamestate = {
181            spawn_cooldown: this.game.spawn_cooldown,
182            players: this.game.players, // TODO could optimize this array too
183            nodes: {} // send as obj instead of array since it's gonna be sparse
184        };
185
186        let padding = this.game.config.max_edge;
187        for(var i = 0; i < this.game.nodes.length; i++){
188            let node = this.game.nodes[i];
189            if(node.x < viewport.left - padding || node.x > viewport.right + padding || node.y < viewport.top - padding || node.y > viewport.bottom + padding)
190                continue;
191            gamestate.nodes[i] = node;
192        }
193
194        this.send(ws, gamestate);
195
196        // console.log("Sending light gamestate...");
197        // console.error("Not yet implemented");
198        // ws.send(this.serialize(this.game));
199    }
200
201    getSpawn(){
202        const SPAWN_POSSIBILITIES = 5;
203        let center = {x: this.game.config.width / 2, y: this.game.config.height / 2};
204        let centerNodes = [];
205        this.game.nodes.filter(node => node.isSource && node.owner === 'server').forEach(node => {
206            let dist = this.game.distance(center, node);
207            for(var i = 0; i < centerNodes.length; i++){
208                if(centerNodes[i].dist > dist){
209                    centerNodes.splice(i, 0, {node:node, dist:dist});
210                    if(centerNodes.length > SPAWN_POSSIBILITIES)
211                        centerNodes.splice(SPAWN_POSSIBILITIES, centerNodes.length - SPAWN_POSSIBILITIES);
212                    break;
213                }
214            }
215        });
216
217        // No available spawns!
218        if(centerNodes.length === 0)
219            return false;
220
221        return chance.pickone(centerNodes);
222    }
223
224    validateUsername(username){
225        // Username already taken
226        if(this.game.players[username])
227            return `Username taken`;
228
229        // Username too short
230        if(username.length < 1)
231            return `Username too short`;
232
233        // Username too long
234        if(username.length > 32)
235            return `Username too long`;
236
237        return true;
238    }
239
240    handleClientMsg(data, ws){
241        //console.log("Received", data);
242        let msg = this.deserialize(data);
243        // console.log(msg);
244
245        let handlers = {};
246
247        handlers.playerconnect = msg => {
248            this.game.players[msg.username] = { color: msg.color };
249            this.clients[ws.uuid].username = msg.username;
250        };
251
252        handlers.spawnplayer = msg => {
253            let valid = this.validateUsername(msg.username);
254            if(valid !== true) {
255                console.error(valid);
256                this.send(ws, {msgtype: 'spawn_failed', error: valid});
257                return;
258            }
259            
260            // Validation passed; get spawnpoint
261            let spawn = this.game.getSpawn();
262            if(!spawn){
263                let error = `Cannot get spawn for username ${msg.username}; server full`;
264                console.error(error);
265                this.send(ws, {msgtype: 'spawn_failed', error});
266                return;
267            }
268
269            // Successful spawn
270            console.log(`Player spawned with username ${msg.username}`);
271            this.game.players[msg.username] = { color: msg.color };
272            spawn.owner = msg.username;
273            this.clients[ws.uuid].username = msg.username;
274            this.send(ws, {msgtype: 'spawn_success', username: msg.username, spawn:spawn.id, color:msg.color});
275        };
276
277        handlers.viewport = msg => {
278            this.clients[ws.uuid].viewport = msg;
279            //ws.viewport = msg;
280            this.clients[ws.uuid].lastupdate = new Date().getTime();
281            //console.log("Nodes in viewport this frame:", this.game.nodes.filter(node => node.x >= ws.viewport.left && node.x <= ws.viewport.right && node.y <= ws.viewport.bottom && node.y >= ws.viewport.top).length);
282        };
283
284        handlers.registerPermanent = msg => {
285            console.log("Registering a player as permanent");
286            this.APIConnector.auth0RegisterPlayer(msg.id_token, this.clients[ws.uuid].username)
287                .then(() => this.APIConnector.stripeExecutePayment(msg.stripe_token))
288                .then(() => {
289                    this.game.players[this.clients[ws.uuid].username].permanent = true;
290                    this.send(ws, {msgtype: 'register_success'});
291                })
292                .catch(err => {
293                    console.error(err);
294                    this.send(ws, {msgtype: 'register_failed'});
295                });
296        }
297
298        handlers.login = msg => {
299            console.log("Logging in player");
300            this.APIConnector.auth0Login(msg.id_token)
301                .then(player_name => {
302                    this.clients[ws.uuid].username = player_name;
303
304
305                    let origin = null,
306                        respawned = false;
307                    // TODO check for existence of player instance in game list of players
308                    if(!this.game.players[player_name]){
309                        this.game.players[player_name] = {color:0x0}; // TODO get random color
310                        origin = this.game.getSpawn();
311                        origin.owner = player_name;
312                        // TODO alert user that they have been respawned?
313                    }
314
315                    // TODO find and return id of a node owned by the player (or maybe their largest node)
316                    if(origin === null){
317                        let network = this.game.nodes.filter(node => node.owner === player_name);
318                        if(network.length > 0)
319                            origin = network.sort((x, y) => x.value - y.value)[0];
320                    }
321
322                    // TODO if no such node exists, spawn new one
323                    if(origin === null){
324                        origin = this.game.getSpawn();
325                        origin.owner = player_name;
326                    }
327
328                    this.clients[ws.uuid].username = player_name;
329                    this.send(ws, {msgtype: 'login_success', username:player_name, origin:origin.id, respawned, color:this.game.players[player_name].color});
330                })
331                .catch(err => {
332                    console.error(err);
333                    this.send(ws, {msgtype: 'login_failed'});
334                })
335        }
336
337        handlers.changeColor = msg => {
338            console.log(`Changing color for user ${this.clients[ws.uuid].username} (color=${msg.color.toString(16)})`);
339
340            // Failed
341            if(!this.clients[ws.uuid].username){
342                let error='User not spawned yet';
343                console.error(error);
344                return this.send(ws, {
345                    msgtype:'changeColor_failed',
346                    error
347                });
348            }
349
350            // Success
351            this.game.players[this.clients[ws.uuid].username].color = msg.color;
352            this.send(ws, {
353                msgtype:'changeColor_success',
354                color:msg.color
355            });
356        };
357
358        /*
359        handlers.changeName = msg => {
360            console.log(`Changing username for player ${this.clients[ws.uuid].username} to ${msg.username}`);
361
362            // Not spawned yet
363            if(!this.clients[ws.uuid].username){
364                let error='User not spawned yet';
365                console.error(error);
366                return this.send(ws, {
367                    msgtype:'changeName_failed',
368                    error,
369                    username:this.clients[ws.uuid].username
370                });
371            }
372
373            // Validation
374            let valid = true;
375            if(msg.username !== this.clients[ws.uuid].username)
376                valid = this.validateUsername(msg.username);
377            if(valid !== true){
378                console.error(valid);
379                return this.send(ws, {msgtype: 'changeName_failed', error: valid, username: this.clients[ws.uuid].username});
380            }
381
382            if(this.game.changeName(this.clients[ws.uuid].username, msg.username)){
383                this.clients[ws.uuid].username = msg.username;
384                this.send(ws, {
385                    msgtype:'changeName_success',
386                    username:msg.username
387                });
388            } else
389                return this.send(ws, {msgtype: 'changeName_failed', error: 'Unknown error', username: this.clients[ws.uuid].username});
390            
391        };
392        */
393
394        handlers.createEdge = msg => this.game.createEdge(this.clients[ws.uuid].username, msg.from, msg.to);
395        handlers.removeEdge = msg => this.game.removeEdge(this.clients[ws.uuid].username, msg.from, msg.to);
396
397        handlers.reconnect = msg => {
398            let client = null;
399            for(var uuid in this.clients){
400                if(uuid === msg.uuid){
401                    client = this.clients[uuid];
402                    break;
403                }
404            }
405            if(client === null){
406                console.error(`Refusing reconnection request from unrecognized client ${msg.uuid}`);
407                this.send(ws, {msgtype:'reconnect_failed', error: 'Client instance expired or does not exist.', uuid: ws.uuid});
408                return;
409            }
410            if(client.ws && client.ws.readyState !== WS.CLOSED){
411                console.error(`Refusing reconnection request from client who is already connected`);
412                this.send(ws, {msgtype:'reconnect_failed', error: 'Refusing reconnection request from client who is already connected.', uuid: ws.uuid});
413                return;
414                // console.log(`Closing old websocket for client ${client.uuid}`);
415                // client.ws.terminate();
416            }
417            console.log(`Accepted reconnection for client ${client.uuid}`);
418            client.ws = ws;
419            delete this.clients[ws.uuid];
420            ws.uuid = client.uuid;
421            let player = this.game.players[client.username];
422            this.send(ws, {
423                msgtype: 'reconnect_success',
424                uuid: ws.uuid,
425                username: client.username,
426                color: player ? this.game.players[client.username].color : null,
427                permanent: player ? this.game.players[client.username].permanent : null
428            });
429            client.lastUpdate = new Date().getTime();
430        }
431
432        handlers.ping = () => {};
433
434        if(handlers[msg.msgtype] == undefined){
435            console.error(`Unrecognized client msgtype ${msg.msgtype}`);
436            return;
437        }
438        handlers[msg.msgtype](msg);
439    }
440
441    deserialize(data){
442        // TODO: msgpack
443        return JSON.parse(data);
444    }
445
446    serialize(data, replacer){
447        // TODO: msgpack
448        return JSON.stringify(data, replacer);
449    }
450
451    send(ws, obj){
452        if(!ws){
453            console.error("Could not send object; no socket specified", obj);
454            return false;
455        } else if (!obj) {
456            console.error("Server.send called with only one argument (did you forget to pass the websocket instance as the first parameter?)");
457            return false;
458        } else if (ws.readyState !== WS.OPEN){
459            return false;
460        }
461        ws.send(this.serialize(obj));
462    }
463
464    save(){
465        fs.writeFileSync(client_cache, this.serialize(this.clients, (key, val) => key === 'ws' ? undefined : val));
466        fs.writeFileSync(gamestate_cache, this.serialize(this.game));
467        return true;
468    }
469
470    load(){
471        console.log("Loading server state from disk...");
472        if(fs.existsSync(client_cache) && fs.statSync(client_cache).size > 2){
473            this.clients = this.deserialize(fs.readFileSync(client_cache));
474            console.log(`- Client cache loaded (${Object.keys(this.clients).length} clients)`);
475        } else 
476            console.log("- No client cache found.");
477
478        if(fs.existsSync(gamestate_cache) && fs.statSync(client_cache).size > 2){
479            if(!this.game) this.game = new Game();
480            let savedGame = this.deserialize(fs.readFileSync(gamestate_cache));
481            _.merge(this.game, savedGame);
482
483            // // Remove non-permanent players
484            // for (var player in this.game.players) {
485            //     if (this.game.players.hasOwnProperty(player) && !this.game.players[player].permanent) {
486            //         this.game.removePlayer(player);
487            //         console.log(`- Removed non-permanent player ${player}`);
488            //     }
489            // }
490
491            console.log("- Gamestate loaded.");
492            return true;
493        }
494        console.log("- No saved gamestate found.");
495        return false;
496    }
497
498    loadTest(){
499        let game = new Game();
500        game.config.width = 1000;
501        game.config.height = 1000;
502        console.log("Procedurally generating map...");
503        let start = new Date().getTime();
504        game.procgen();
505        console.log(`Procgen took ${new Date().getTime() - start}ms`);
506        console.log("Width:", game.config.width);
507        console.log("Height:", game.config.height);
508        console.log("Nodes:", game.nodes.length);
509    
510        // Create maximum possible edges: from every node to every other node possible
511        console.log("Generating all possible edges");
512        game.nodes.forEach(node => {
513            node.owner = "excalo";
514            game.nodes.forEach(otherNode => {
515                game.createEdge("excalo", node.id, otherNode.id);
516            });
517        });
518        let numEdges = game.countEdges();
519        console.log("Edges:", numEdges);
520        console.log("Avg edges/node", numEdges / game.nodes.length);
521
522        // Game.update
523        for(var i = 0; i < 60 * 4; i++){
524            console.log(`Update #${i}`);
525            let start = new Date().getTime(); // Super rigorous production-quality benchmarking
526            game.update(); // Sample size of one because fuck the stats, who cares
527            let end = new Date().getTime();
528            console.log(`- Update took ${end - start}ms`);
529            console.log(`- Bubbles: ${game.countBubbles()}`);
530        }
531    
532        process.exit();
533        // server.game = game;
534        // server.startGameLoop();
535    }
536}
537
538let server = new Server();
539server.initGame();
540//server.loadTest();