Improved WebSocket handshake
Signed-off-by: James Ketrenos <james_eikona@ketrenos.com>
This commit is contained in:
parent
f023ecd41d
commit
e3ccc122dd
@ -1,8 +1,6 @@
|
|||||||
import React, { useState, useCallback, useEffect } from "react";
|
import React, { useState } from "react";
|
||||||
import "./Activities.css";
|
import "./Activities.css";
|
||||||
import Paper from '@material-ui/core/Paper';
|
|
||||||
import Resource from './Resource.js';
|
|
||||||
import { getPlayerName } from './Common.js';
|
|
||||||
import PlayerColor from './PlayerColor.js';
|
import PlayerColor from './PlayerColor.js';
|
||||||
import Dice from './Dice.js';
|
import Dice from './Dice.js';
|
||||||
|
|
||||||
@ -65,8 +63,9 @@ const Activities = ({ table }) => {
|
|||||||
normalPlay = (game.state === 'initial-placement' || game.state === 'normal'),
|
normalPlay = (game.state === 'initial-placement' || game.state === 'normal'),
|
||||||
mustDiscard = game.player ? parseInt(game.player.mustDiscard ? game.player.mustDiscard : 0) : 0,
|
mustDiscard = game.player ? parseInt(game.player.mustDiscard ? game.player.mustDiscard : 0) : 0,
|
||||||
mustPlaceRobber = (game.turn && !game.turn.placedRobber && game.turn.robberInAction),
|
mustPlaceRobber = (game.turn && !game.turn.placedRobber && game.turn.robberInAction),
|
||||||
isInitialPlacement = (game.state == 'initial-placement'),
|
isInitialPlacement = (game.state === 'initial-placement'),
|
||||||
placeRoad = isInitialPlacement && game.turn && game.turn.actions.indexOf('place-road') !== -1;
|
placeRoad = isInitialPlacement && game.turn && game.turn.actions && game.turn.actions.indexOf('place-road') !== -1,
|
||||||
|
mustStealResource = game.turn && game.turn.actions && game.turn.actions.indexOf('steal-resource') !== -1;
|
||||||
|
|
||||||
const list = game.activities
|
const list = game.activities
|
||||||
.filter(activity => game.timestamp - activity.date < 11000)
|
.filter(activity => game.timestamp - activity.date < 11000)
|
||||||
@ -74,24 +73,31 @@ const Activities = ({ table }) => {
|
|||||||
return <Activity key={activity.date} activity={activity}/>;
|
return <Activity key={activity.date} activity={activity}/>;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let who;
|
||||||
|
if (isTurn) {
|
||||||
|
who = 'You';
|
||||||
|
} else {
|
||||||
|
who = <><PlayerColor color={table.game.turn.color}/> {table.game.turn.name}</>
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className="Activities">
|
<div className="Activities">
|
||||||
{ list }
|
{ list }
|
||||||
{ !isTurn && normalPlay && game.player && mustDiscard === 0 && mustPlaceRobber &&
|
{ normalPlay && mustDiscard === 0 && mustPlaceRobber &&
|
||||||
<div className="Requirement"><PlayerColor color={table.game.turn.color}/> {table.game.turn.name} must move the Robber.</div>
|
<div className="Requirement">{who} must move the Robber.</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
{ isTurn && normalPlay && game.player && mustDiscard === 0 && mustPlaceRobber &&
|
{ isInitialPlacement &&
|
||||||
<div className="Requirement">You must move the Robber.</div>
|
<div className="Requirement">{who} must place a {placeRoad ? 'road' : 'settlement'}.</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
{ normalPlay && game.player && mustDiscard !== 0 &&
|
{ mustStealResource &&
|
||||||
|
<div className="Requirement">{who} must select a player to steal from.</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{ normalPlay && mustDiscard !== 0 &&
|
||||||
<div className="Requirement">You must discard <b>{mustDiscard}</b> cards.</div>
|
<div className="Requirement">You must discard <b>{mustDiscard}</b> cards.</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
{ isTurn && isInitialPlacement &&
|
|
||||||
<div className="Requirement">You must place a {placeRoad ? 'road' : 'settlement'}.</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{ !isTurn && normalPlay &&
|
{ !isTurn && normalPlay &&
|
||||||
<div>It is <PlayerColor color={table.game.turn.color}/> {table.game.turn.name}'s turn.</div>
|
<div>It is <PlayerColor color={table.game.turn.color}/> {table.game.turn.name}'s turn.</div>
|
||||||
|
@ -738,7 +738,7 @@ class Table extends React.Component {
|
|||||||
if (isDead) {
|
if (isDead) {
|
||||||
console.log(`Short circuiting keep-alive`);
|
console.log(`Short circuiting keep-alive`);
|
||||||
} else {
|
} else {
|
||||||
console.log(`Resetting keep-alive`);
|
console.log(`${this.game.name} Resetting keep-alive: ${(Date.now() - this.game.startTime) / 1000}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.keepAlive) {
|
if (this.keepAlive) {
|
||||||
@ -749,7 +749,7 @@ class Table extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.keepAlive = setTimeout(() => {
|
this.keepAlive = setTimeout(() => {
|
||||||
console.error(`No server ping after 10 seconds (or connection closed by server)!`);
|
console.log(`${this.game.name} No ping after 10 seconds: ${(Date.now() - this.game.startTime) / 1000}`);
|
||||||
this.setState({ noNetwork: true });
|
this.setState({ noNetwork: true });
|
||||||
if (this.ws) {
|
if (this.ws) {
|
||||||
this.ws.close();
|
this.ws.close();
|
||||||
@ -782,12 +782,13 @@ class Table extends React.Component {
|
|||||||
|
|
||||||
this.ws = new WebSocket(new_uri);
|
this.ws = new WebSocket(new_uri);
|
||||||
|
|
||||||
this.ws.onopen = (event) => {
|
this.ws.addEventListener('open', (event) => {
|
||||||
console.log(`WebSocket open:`, event);
|
console.log(`${this.game.name} WebSocket open: Sending game-update request: ${(Date.now() - this.game.startTime) / 1000}`);
|
||||||
|
this.ws.send(JSON.stringify({ type: 'game-update' }));
|
||||||
this.resetKeepAlive();
|
this.resetKeepAlive();
|
||||||
};
|
});
|
||||||
|
|
||||||
this.ws.onmessage = (event) => {
|
this.ws.addEventListener('message', (event) => {
|
||||||
this.resetKeepAlive();
|
this.resetKeepAlive();
|
||||||
|
|
||||||
let data;
|
let data;
|
||||||
@ -813,19 +814,19 @@ class Table extends React.Component {
|
|||||||
console.log(`Unknown event type: ${data.type}`);
|
console.log(`Unknown event type: ${data.type}`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
this.ws.onerror = (event) => {
|
this.ws.addEventListener('error', (event) => {
|
||||||
this.setState({ error: event.message });
|
this.setState({ error: event.message });
|
||||||
console.error(`WebSocket error:`, event);
|
console.error(`${this.game.name} WebSocket error: ${(Date.now() - this.game.startTime) / 1000}`);
|
||||||
this.resetKeepAlive(true);
|
this.resetKeepAlive(true);
|
||||||
};
|
});
|
||||||
|
|
||||||
this.ws.onclose = (event) => {
|
this.ws.addEventListener('close', (event) => {
|
||||||
console.error(`WebSocket close:`, event);
|
console.log(`${this.game.name} WebSocket close: ${(Date.now() - this.game.startTime) / 1000}`);
|
||||||
this.setState({ error: event.message });
|
this.setState({ error: event.message });
|
||||||
this.resetKeepAlive(true);
|
this.resetKeepAlive(true);
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -445,8 +445,7 @@ const getSession = (game, session) => {
|
|||||||
|
|
||||||
const id = session.player_id;
|
const id = session.player_id;
|
||||||
|
|
||||||
/* If this session is not yet in the game,
|
/* If this session is not yet in the game, add it and set the player's name */
|
||||||
* add it and set the player's name */
|
|
||||||
if (!(id in game.sessions)) {
|
if (!(id in game.sessions)) {
|
||||||
game.sessions[id] = {
|
game.sessions[id] = {
|
||||||
name: undefined,
|
name: undefined,
|
||||||
@ -455,6 +454,22 @@ const getSession = (game, session) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Expire old unused sessions */
|
||||||
|
for (let id in game.sessions) {
|
||||||
|
const tmp = game.sessions[id];
|
||||||
|
if (tmp.color || tmp.name || tmp.player) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (tmp.player_id === session.player_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
/* 10 minutes */
|
||||||
|
if (tmp.lastActive && tmp.lastActive < Date.now() - 10 * 60 * 1000) {
|
||||||
|
console.log(`Expiring old session ${id}`);
|
||||||
|
delete game.sessions[id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return game.sessions[id];
|
return game.sessions[id];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1762,7 +1777,6 @@ router.put("/:id/:action/:value?", async (req, res) => {
|
|||||||
if (colors.length) {
|
if (colors.length) {
|
||||||
game.turn.actions = [ 'steal-resource' ],
|
game.turn.actions = [ 'steal-resource' ],
|
||||||
game.turn.limits = { players: colors };
|
game.turn.limits = { players: colors };
|
||||||
addChatMessage(game, session, `${session.name} must select player to steal resource from.`);
|
|
||||||
} else {
|
} else {
|
||||||
game.turn.actions = [];
|
game.turn.actions = [];
|
||||||
game.turn.robberInAction = false;
|
game.turn.robberInAction = false;
|
||||||
@ -1844,6 +1858,7 @@ router.put("/:id/:action/:value?", async (req, res) => {
|
|||||||
|
|
||||||
debugChat(game, 'Before development purchase');
|
debugChat(game, 'Before development purchase');
|
||||||
addActivity(game, session, `${session.name} purchased a development card.`);
|
addActivity(game, session, `${session.name} purchased a development card.`);
|
||||||
|
addChatMessage(game, session, `${session.name} spent 1 stone, 1 wheat, and 1 sheep to purchase a development card.`)
|
||||||
player.stone--;
|
player.stone--;
|
||||||
player.wheat--;
|
player.wheat--;
|
||||||
player.sheep--;
|
player.sheep--;
|
||||||
@ -2105,6 +2120,7 @@ router.put("/:id/:action/:value?", async (req, res) => {
|
|||||||
player.settlements--;
|
player.settlements--;
|
||||||
|
|
||||||
if (!game.turn.free) {
|
if (!game.turn.free) {
|
||||||
|
addChatMessage(game, session, `${session.name} spent 1 brick, 1 wood, 1 sheep, and 1 wheat to purchase a settlement.`)
|
||||||
player.brick--;
|
player.brick--;
|
||||||
player.wood--;
|
player.wood--;
|
||||||
player.wheat--;
|
player.wheat--;
|
||||||
@ -2263,6 +2279,7 @@ router.put("/:id/:action/:value?", async (req, res) => {
|
|||||||
player.cities--;
|
player.cities--;
|
||||||
player.settlements++;
|
player.settlements++;
|
||||||
if (!game.turn.free) {
|
if (!game.turn.free) {
|
||||||
|
addChatMessage(game, session, `${session.name} spent 2 wheat, and 1 stone to upgrade to a city.`)
|
||||||
player.wheat -= 2;
|
player.wheat -= 2;
|
||||||
player.stone -= 3;
|
player.stone -= 3;
|
||||||
}
|
}
|
||||||
@ -2351,6 +2368,7 @@ router.put("/:id/:action/:value?", async (req, res) => {
|
|||||||
|
|
||||||
player.roads--;
|
player.roads--;
|
||||||
if (!game.turn.free) {
|
if (!game.turn.free) {
|
||||||
|
addChatMessage(game, session, `${session.name} spent 1 brick and 1 wood to purchase a road.`)
|
||||||
player.brick--;
|
player.brick--;
|
||||||
player.wood--;
|
player.wood--;
|
||||||
}
|
}
|
||||||
@ -2521,8 +2539,44 @@ const ping = (session) => {
|
|||||||
router.ws("/ws/:id", async (ws, req) => {
|
router.ws("/ws/:id", async (ws, req) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
||||||
console.log(`WebSocket connect from game ${id}`);
|
/* Setup WebSocket event handlers prior to performing any async calls or
|
||||||
|
* we may miss the first messages from clients */
|
||||||
|
ws.on('error', (event) => {
|
||||||
|
console.error(`WebSocket error: `, event.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('open', (event) => {
|
||||||
|
console.log(`WebSocket open: `, event.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('message', async (message) => {
|
||||||
|
/* Ensure the session is loaded prior to the first 'message'
|
||||||
|
* being processed */
|
||||||
|
const game = await loadGame(id);
|
||||||
|
if (!game) {
|
||||||
|
console.error(`Unable to load/create new game for WS request.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const session = getSession(game, req.session);
|
||||||
|
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;
|
||||||
|
case 'game-update':
|
||||||
|
console.log(`Player ${session.name ? session.name : 'Unnamed'} requested a game update.`);
|
||||||
|
sendGame(req, undefined, game, undefined, ws);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* This will result in the node tick moving forward; if we haven't already
|
||||||
|
* setup the event handlers, a 'message' could come through prior to this
|
||||||
|
* completing */
|
||||||
const game = await loadGame(id);
|
const game = await loadGame(id);
|
||||||
if (!game) {
|
if (!game) {
|
||||||
console.error(`Unable to load/create new game for WS request.`);
|
console.error(`Unable to load/create new game for WS request.`);
|
||||||
@ -2531,6 +2585,8 @@ router.ws("/ws/:id", async (ws, req) => {
|
|||||||
|
|
||||||
const session = getSession(game, req.session);
|
const session = getSession(game, req.session);
|
||||||
|
|
||||||
|
console.log(`WebSocket connect from game ${id}:${session.name ? session.name : "Unnamed"}`);
|
||||||
|
|
||||||
if (session) {
|
if (session) {
|
||||||
console.log(`WebSocket connected for ${session.name ? session.name : "Unnamed"}`);
|
console.log(`WebSocket connected for ${session.name ? session.name : "Unnamed"}`);
|
||||||
session.ws = ws;
|
session.ws = ws;
|
||||||
@ -2541,27 +2597,6 @@ router.ws("/ws/:id", async (ws, req) => {
|
|||||||
} else {
|
} else {
|
||||||
console.log(`No session found for WebSocket with id ${id}`);
|
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*/) => {
|
router.get("/:id", async (req, res/*, next*/) => {
|
||||||
@ -2619,7 +2654,50 @@ const getActiveCount = (game) => {
|
|||||||
return active;
|
return active;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendGame = async (req, res, game, error) => {
|
const sendGameToSession = (session, reducedSessions, game, reducedGame, error, res) => {
|
||||||
|
const player = session.player ? session.player : undefined;
|
||||||
|
|
||||||
|
if (player) {
|
||||||
|
player.haveResources = player.wheat > 0 ||
|
||||||
|
player.brick > 0 ||
|
||||||
|
player.sheep > 0 ||
|
||||||
|
player.stone > 0 ||
|
||||||
|
player.wood > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Strip out data that should not be shared with players */
|
||||||
|
delete reducedGame.developmentCards;
|
||||||
|
|
||||||
|
const playerGame = Object.assign({}, reducedGame, {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
status: error ? error : "success",
|
||||||
|
name: session.name,
|
||||||
|
color: session.color,
|
||||||
|
order: (session.color in game.players) ? game.players[session.color].order : 0,
|
||||||
|
player: player,
|
||||||
|
sessions: reducedSessions,
|
||||||
|
layout: layout
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
if (!error) {
|
||||||
|
if (!session.ws) {
|
||||||
|
console.error(`No WebSocket connection to ${session.name}`);
|
||||||
|
} else {
|
||||||
|
console.log(`Sending update to ${session.name}`);
|
||||||
|
session.ws.send(JSON.stringify({
|
||||||
|
type: 'game-update',
|
||||||
|
update: playerGame
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`Returning update to ${session.name ? session.name : 'Unnamed'}`);
|
||||||
|
res.status(200).send(playerGame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendGame = async (req, res, game, error, wsUpdate) => {
|
||||||
const active = getActiveCount(game);
|
const active = getActiveCount(game);
|
||||||
|
|
||||||
/* Enforce game limit of >= 2 players */
|
/* Enforce game limit of >= 2 players */
|
||||||
@ -2726,6 +2804,7 @@ const sendGame = async (req, res, game, error) => {
|
|||||||
reducedSessions.push(reduced);
|
reducedSessions.push(reduced);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!wsUpdate) {
|
||||||
/* Save per turn while debugging... */
|
/* Save per turn while debugging... */
|
||||||
await writeFile(`games/${game.id}.${game.turns}`, JSON.stringify(reducedGame, null, 2))
|
await writeFile(`games/${game.id}.${game.turns}`, JSON.stringify(reducedGame, null, 2))
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -2737,49 +2816,21 @@ const sendGame = async (req, res, game, error) => {
|
|||||||
console.error(`Unable to write to games/${game.id}`);
|
console.error(`Unable to write to games/${game.id}`);
|
||||||
console.error(error);
|
console.error(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
for (let id in game.sessions) {
|
|
||||||
const target = game.sessions[id],
|
|
||||||
useWS = target !== session,
|
|
||||||
player = target.player ? target.player : undefined;
|
|
||||||
|
|
||||||
if (player) {
|
|
||||||
player.haveResources = player.wheat > 0 ||
|
|
||||||
player.brick > 0 ||
|
|
||||||
player.sheep > 0 ||
|
|
||||||
player.stone > 0 ||
|
|
||||||
player.wood > 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Strip out data that should not be shared with players */
|
if (wsUpdate) {
|
||||||
delete reducedGame.developmentCards;
|
/* This is a one-shot request from a client to send the game-update over WebSocket */
|
||||||
|
sendGameToSession(session, reducedSessions, game, reducedGame);
|
||||||
const playerGame = Object.assign({}, reducedGame, {
|
} else {
|
||||||
timestamp: Date.now(),
|
for (let id in game.sessions) {
|
||||||
status: error ? error : "success",
|
const target = game.sessions[id], useWS = target !== session;
|
||||||
name: target.name,
|
|
||||||
color: target.color,
|
|
||||||
order: (target.color in game.players) ? game.players[target.color].order : 0,
|
|
||||||
player: player,
|
|
||||||
sessions: reducedSessions,
|
|
||||||
layout: layout
|
|
||||||
});
|
|
||||||
|
|
||||||
if (useWS) {
|
if (useWS) {
|
||||||
if (!error) {
|
if (!error) {
|
||||||
if (!target.ws) {
|
sendGameToSession(target, reducedSessions, game, reducedGame);
|
||||||
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 {
|
} else {
|
||||||
console.log(`Returning update to ${target.name ? target.name : 'Unnamed'}`);
|
sendGameToSession(target, reducedSessions, game, reducedGame, error, res);
|
||||||
res.status(200).send(playerGame);
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user