From 0e91755c72fe2281987be00e938b611384687922 Mon Sep 17 00:00:00 2001 From: James Ketrenos Date: Mon, 28 Feb 2022 17:34:17 -0800 Subject: [PATCH] WebSocket seems to be working Signed-off-by: James Ketrenos --- client/src/Board.css | 5 +++ client/src/Board.js | 18 +++++++-- client/src/Table.js | 53 +++++++++++++++++++------ server/app.js | 8 ++-- server/routes/games.js | 87 ++++++++++++++++++++++++++++++++---------- 5 files changed, 130 insertions(+), 41 deletions(-) diff --git a/client/src/Board.css b/client/src/Board.css index c9cf84b..85b3eba 100644 --- a/client/src/Board.css +++ b/client/src/Board.css @@ -150,6 +150,11 @@ clip-path: polygon(25% 0%,75% 0%,100% 50%,75% 100%,25% 100%,0% 50%); } +div[disabled] .Option { + cursor: pointer; + pointer-events: none; +} + .Option { cursor: pointer; pointer-events: all; diff --git a/client/src/Board.js b/client/src/Board.js index b1394de..9a0488a 100644 --- a/client/src/Board.js +++ b/client/src/Board.js @@ -502,6 +502,18 @@ const Board = ({ table, game }) => { return (game && game.turn && Array.isArray(game.turn.actions) && game.turn.actions.indexOf(action) !== -1); }; + const canRoad = (canAction('place-road') + && game.turn.color === game.color + && (game.state === 'initial-placement' || game.state === 'normal')); + + const canCorner = ((canAction('place-settlement') || canAction('place-city')) + && game.turn.color === game.color + && (game.state === 'initial-placement' || game.state === 'normal')); + + const canPip = (canAction('place-robber') + && game.turn.color === game.color + && (game.state === 'initial-placement' || game.state === 'normal')); + return (
@@ -512,13 +524,13 @@ const Board = ({ table, game }) => {
{ tiles }
-
+
{ pips }
-
+
{ corners }
-
+
{ roads }
} diff --git a/client/src/Table.js b/client/src/Table.js index 1137e60..100270e 100755 --- a/client/src/Table.js +++ b/client/src/Table.js @@ -403,7 +403,7 @@ const Action = ({ table }) => { } - { game.state === 'normal' && <> + { !inLobby && <> @@ -430,7 +430,8 @@ const PlayerName = ({table}) => { if (name !== table.game.name) { table.setPlayerName(name); } else { - table.setState({ pickName: false, error: "" }); + table.setError(""); + table.setState({ pickName: false }); } } @@ -604,10 +605,10 @@ class Table extends React.Component { const error = (game.status !== 'success') ? game.status : undefined; this.updateGame(game); this.updateMessage(); - this.setState({ error: error }); + this.setError(error); }).catch((error) => { console.error(error); - this.setState({error: error.message}); + this.setError(error.message); }).then(() => { this.setState({ loading: this.state.loading - 1 }); }); @@ -640,7 +641,7 @@ class Table extends React.Component { shuffleTable() { return this.sendAction('shuffle') .then(() => { - this.setState({ error: "Table shuffled!" }); + this.setError("Table shuffled!"); }); } @@ -676,6 +677,19 @@ class Table extends React.Component { return this.sendAction('roll'); } + setError(error) { + if (!error) { + return; + } + if (this.errorTimeout) { + clearTimeout(this.errorTimeout); + } + setTimeout(() => this.setState({error: undefined}), 3000); + if (this.state.error !== error) { + this.setState({ error }); + } + } + setGameState(state) { if (this.loadTimer) { window.clearTimeout(this.loadTimer); @@ -702,9 +716,9 @@ class Table extends React.Component { this.updateMessage(); }).catch((error) => { console.error(error); - this.setState({error: error.message}); + this.setError(error.message); }).then(() => { - this.setState({ loading: this.state.loading + 1 }); + this.setState({ loading: this.state.loading - 1 }); return this.game.state; }); } @@ -865,7 +879,7 @@ class Table extends React.Component { try { data = JSON.parse(event.data); } catch (error) { - this.setState({ error }); + this.setError(error); return; } let update; @@ -875,7 +889,16 @@ class Table extends React.Component { const error = (update.status !== 'success') ? update.status : undefined; this.updateGame(update); this.updateMessage(); - this.setState({ error }); + this.setError(error); + break; + case 'ping': + if (this.keepAlive) { + clearTimeout(this.keepAlive); + } + this.keepAlive = setTimeout(() => { + console.error(`No server ping for 5 seconds!`); + }, 5000); + this.ws.send(JSON.stringify({ type: 'pong', timestamp: data.ping })); break; default: console.log(`Unknown event type: ${data.type}`); @@ -925,7 +948,7 @@ class Table extends React.Component { error = `Unable to find game ${this.id}. Starting new game.` console.log(error); - this.setState({ error: error }); + this.setError(error); params.url = `${base}/api/v1/games/${this.id}`; params.method = "POST"; @@ -951,7 +974,7 @@ class Table extends React.Component { this.setState({ error: "" }); }).catch((error) => { console.error(error); - this.setState({error: error.message}); + this.setError(error.message); }).then(() => { this.setState({ loading: this.state.loading - 1 }); }); @@ -961,10 +984,16 @@ class Table extends React.Component { if (this.loadTimer) { clearTimeout(this.loadTimer); } + if (this.keepAlive) { + clearTimeout(this.keepAlive); + } if (this.updateSizeTimer) { clearTimeout(this.updateSizeTimer); this.updateSizeTimer = 0; } + if (this.errorTimeout) { + clearTimeout(this.errorTimeout); + } } cardClicked(card) { @@ -1098,7 +1127,7 @@ class Table extends React.Component { (!game.player || !game.player.mustDiscard) && } - { this.state.error &&
{this.state.error}
} + { this.state.error && this.setState({ error: undefined })} className="Error">
{this.state.error}
}
); diff --git a/server/app.js b/server/app.js index b3a1f96..bfda87b 100755 --- a/server/app.js +++ b/server/app.js @@ -115,7 +115,6 @@ app.use(basePath, function(req, res, next) { /* Everything below here requires a successful authentication */ app.use(basePath, express.static(frontendPath, { index: false })); -app.set('ws', ws); app.use(`${basePath}api/v1/games`, require("./routes/games")); /* Declare the "catch all" index route last; the final route is a 404 dynamic router */ @@ -127,10 +126,9 @@ app.use(basePath, index); app.set("port", serverConfig.port); process.on('SIGINT', () => { - server.close(() => { - console.log("Gracefully shutting down from SIGINT (Ctrl-C)"); - process.exit(1); - }); + console.log("Gracefully shutting down from SIGINT (Ctrl-C) in 2 seconds"); + setTimeout(() => process.exit(-1), 2000); + server.close(() => process.exit(1)); }); require("./db/games").then(function(db) { diff --git a/server/routes/games.js b/server/routes/games.js index 7bd8dc6..a9832b1 100755 --- a/server/routes/games.js +++ b/server/routes/games.js @@ -8,6 +8,7 @@ const express = require("express"), accessSync = fs.accessSync, randomWords = require("random-words"); +const session = require("express-session"); const layout = require('./layout.js'); const MAX_SETTLEMENTS = 5; @@ -2489,20 +2490,59 @@ router.put("/:id/:action/:value?", async (req, res) => { return sendGame(req, res, game, error); }) +const ping = (session) => { + session.ping = Date.now(); + session.ws.send(JSON.stringify({ type: 'ping', ping: session.ping })); + if (session.keepAlive) { + clearTimeout(session.keepAlive); + } + session.keepAlive = setTimeout(() => { ping(session); }, 2500); +} + router.ws("/ws/:id", (ws, req) => { const { id } = req.params; - ws.on('message', (msg) => { - console.log(msg); - }); - if (id in games) { - const game = games[id]; - const session = getSession(game, req.session); - if (session) { - console.log(`WebSocket connected for ${session.name}`); - session.ws = ws; - } + console.log(`WebSocket connect from game ${id}`); + + let game; + if (!(id in games)) { + game = createGame(id); + } else { + game = games[id]; } + + const session = getSession(game, req.session); + if (session) { + console.log(`WebSocket connected for ${session.name ? session.name : "Unnamed"}`); + session.ws = ws; + if (session.keepAlive) { + clearTimeout(session.keepAlive); + } + session.keepAlive = setTimeout(() => { ping(session); }, 2500); + } else { + console.log(`No session found for WebSocket with id ${id}`); + } + + ws.on('error', (event) => { + console.error(`WebSocket error: `, event.message); + }); + + ws.on('open', (event) => { + console.log(`WebSocket open: `, event.message); + }); + + ws.on('message', (message) => { + try { + const data = JSON.parse(message); + switch (data.type) { + case 'pong': + console.log(`Latency for ${session.name ? session.name : 'Unammed'} is ${Date.now() - data.timestamp}`); + break; + } + } catch (error) { + console.error(error); + } + }); }); router.get("/:id", async (req, res/*, next*/) => { @@ -2658,6 +2698,9 @@ const sendGame = async (req, res, game, error) => { if (reduced.ws) { delete reduced.ws; } + if (reduced.keepAlive) { + delete reduced.keepAlive; + } reducedGame.sessions[id] = reduced; /* Do not send session-id as those are secrets */ @@ -2704,17 +2747,19 @@ const sendGame = async (req, res, game, error) => { }); if (useWS) { - if (!target.ws) { - console.error(`No WebSocket connection to ${target.name}`); - } else { - console.log(`Sending update to ${target.name}`); - target.ws.send(JSON.stringify({ - type: 'game-update', - update: playerGame - })); + if (!error) { + if (!target.ws) { + console.error(`No WebSocket connection to ${target.name}`); + } else { + console.log(`Sending update to ${target.name}`); + target.ws.send(JSON.stringify({ + type: 'game-update', + update: playerGame + })); + } } } else { - console.log(`Returning update to ${target.name}`); + console.log(`Returning update to ${target.name ? target.name : 'Unnamed'}`); res.status(200).send(playerGame); } } @@ -2726,7 +2771,8 @@ const resetGame = (game) => { Object.assign(game, { startTime: Date.now(), state: 'lobby', - turn: 0, + turns: 0, + turn: {}, sheep: 19, ore: 19, wool: 19, @@ -2738,7 +2784,6 @@ const resetGame = (game) => { }, developmentCards: [], chat: [], - turn: {}, pipOrder: game.pipOrder, borderOrder: game.borderOrder, tileOrder: game.tileOrder,