PageRenderTime 47ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/src/server/server.js

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