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 Paper from '@material-ui/core/Paper';
|
||||
import Resource from './Resource.js';
|
||||
import { getPlayerName } from './Common.js';
|
||||
|
||||
import PlayerColor from './PlayerColor.js';
|
||||
import Dice from './Dice.js';
|
||||
|
||||
@ -65,8 +63,9 @@ const Activities = ({ table }) => {
|
||||
normalPlay = (game.state === 'initial-placement' || game.state === 'normal'),
|
||||
mustDiscard = game.player ? parseInt(game.player.mustDiscard ? game.player.mustDiscard : 0) : 0,
|
||||
mustPlaceRobber = (game.turn && !game.turn.placedRobber && game.turn.robberInAction),
|
||||
isInitialPlacement = (game.state == 'initial-placement'),
|
||||
placeRoad = isInitialPlacement && game.turn && game.turn.actions.indexOf('place-road') !== -1;
|
||||
isInitialPlacement = (game.state === 'initial-placement'),
|
||||
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
|
||||
.filter(activity => game.timestamp - activity.date < 11000)
|
||||
@ -74,24 +73,31 @@ const Activities = ({ table }) => {
|
||||
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 (
|
||||
<div className="Activities">
|
||||
{ list }
|
||||
{ !isTurn && normalPlay && game.player && mustDiscard === 0 && mustPlaceRobber &&
|
||||
<div className="Requirement"><PlayerColor color={table.game.turn.color}/> {table.game.turn.name} must move the Robber.</div>
|
||||
{ normalPlay && mustDiscard === 0 && mustPlaceRobber &&
|
||||
<div className="Requirement">{who} must move the Robber.</div>
|
||||
}
|
||||
|
||||
{ isTurn && normalPlay && game.player && mustDiscard === 0 && mustPlaceRobber &&
|
||||
<div className="Requirement">You must move the Robber.</div>
|
||||
{ isInitialPlacement &&
|
||||
<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>
|
||||
}
|
||||
|
||||
{ isTurn && isInitialPlacement &&
|
||||
<div className="Requirement">You must place a {placeRoad ? 'road' : 'settlement'}.</div>
|
||||
}
|
||||
|
||||
{ !isTurn && normalPlay &&
|
||||
<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) {
|
||||
console.log(`Short circuiting keep-alive`);
|
||||
} else {
|
||||
console.log(`Resetting keep-alive`);
|
||||
console.log(`${this.game.name} Resetting keep-alive: ${(Date.now() - this.game.startTime) / 1000}`);
|
||||
}
|
||||
|
||||
if (this.keepAlive) {
|
||||
@ -749,7 +749,7 @@ class Table extends React.Component {
|
||||
}
|
||||
|
||||
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 });
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
@ -782,12 +782,13 @@ class Table extends React.Component {
|
||||
|
||||
this.ws = new WebSocket(new_uri);
|
||||
|
||||
this.ws.onopen = (event) => {
|
||||
console.log(`WebSocket open:`, event);
|
||||
this.ws.addEventListener('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.ws.onmessage = (event) => {
|
||||
this.ws.addEventListener('message', (event) => {
|
||||
this.resetKeepAlive();
|
||||
|
||||
let data;
|
||||
@ -813,19 +814,19 @@ class Table extends React.Component {
|
||||
console.log(`Unknown event type: ${data.type}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.ws.onerror = (event) => {
|
||||
this.ws.addEventListener('error', (event) => {
|
||||
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.ws.onclose = (event) => {
|
||||
console.error(`WebSocket close:`, event);
|
||||
this.ws.addEventListener('close', (event) => {
|
||||
console.log(`${this.game.name} WebSocket close: ${(Date.now() - this.game.startTime) / 1000}`);
|
||||
this.setState({ error: event.message });
|
||||
this.resetKeepAlive(true);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -445,8 +445,7 @@ const getSession = (game, session) => {
|
||||
|
||||
const id = session.player_id;
|
||||
|
||||
/* If this session is not yet in the game,
|
||||
* add it and set the player's name */
|
||||
/* If this session is not yet in the game, add it and set the player's name */
|
||||
if (!(id in game.sessions)) {
|
||||
game.sessions[id] = {
|
||||
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];
|
||||
};
|
||||
|
||||
@ -1762,7 +1777,6 @@ router.put("/:id/:action/:value?", async (req, res) => {
|
||||
if (colors.length) {
|
||||
game.turn.actions = [ 'steal-resource' ],
|
||||
game.turn.limits = { players: colors };
|
||||
addChatMessage(game, session, `${session.name} must select player to steal resource from.`);
|
||||
} else {
|
||||
game.turn.actions = [];
|
||||
game.turn.robberInAction = false;
|
||||
@ -1844,6 +1858,7 @@ router.put("/:id/:action/:value?", async (req, res) => {
|
||||
|
||||
debugChat(game, 'Before development purchase');
|
||||
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.wheat--;
|
||||
player.sheep--;
|
||||
@ -2105,6 +2120,7 @@ router.put("/:id/:action/:value?", async (req, res) => {
|
||||
player.settlements--;
|
||||
|
||||
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.wood--;
|
||||
player.wheat--;
|
||||
@ -2263,6 +2279,7 @@ router.put("/:id/:action/:value?", async (req, res) => {
|
||||
player.cities--;
|
||||
player.settlements++;
|
||||
if (!game.turn.free) {
|
||||
addChatMessage(game, session, `${session.name} spent 2 wheat, and 1 stone to upgrade to a city.`)
|
||||
player.wheat -= 2;
|
||||
player.stone -= 3;
|
||||
}
|
||||
@ -2351,6 +2368,7 @@ router.put("/:id/:action/:value?", async (req, res) => {
|
||||
|
||||
player.roads--;
|
||||
if (!game.turn.free) {
|
||||
addChatMessage(game, session, `${session.name} spent 1 brick and 1 wood to purchase a road.`)
|
||||
player.brick--;
|
||||
player.wood--;
|
||||
}
|
||||
@ -2515,14 +2533,50 @@ const ping = (session) => {
|
||||
if (session.keepAlive) {
|
||||
clearTimeout(session.keepAlive);
|
||||
}
|
||||
session.keepAlive = setTimeout(() => { ping(session); }, 2500);
|
||||
session.keepAlive = setTimeout(() => { ping(session); }, 2500);
|
||||
}
|
||||
|
||||
router.ws("/ws/:id", async (ws, req) => {
|
||||
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);
|
||||
if (!game) {
|
||||
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);
|
||||
|
||||
console.log(`WebSocket connect from game ${id}:${session.name ? session.name : "Unnamed"}`);
|
||||
|
||||
if (session) {
|
||||
console.log(`WebSocket connected for ${session.name ? session.name : "Unnamed"}`);
|
||||
session.ws = ws;
|
||||
@ -2541,27 +2597,6 @@ router.ws("/ws/:id", async (ws, req) => {
|
||||
} 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*/) => {
|
||||
@ -2619,7 +2654,50 @@ const getActiveCount = (game) => {
|
||||
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);
|
||||
|
||||
/* Enforce game limit of >= 2 players */
|
||||
@ -2726,60 +2804,33 @@ const sendGame = async (req, res, game, error) => {
|
||||
reducedSessions.push(reduced);
|
||||
}
|
||||
|
||||
/* Save per turn while debugging... */
|
||||
await writeFile(`games/${game.id}.${game.turns}`, JSON.stringify(reducedGame, null, 2))
|
||||
.catch((error) => {
|
||||
console.error(`Unable to write to games/${game.id}`);
|
||||
console.error(error);
|
||||
});
|
||||
await writeFile(`games/${game.id}`, JSON.stringify(reducedGame, null, 2))
|
||||
.catch((error) => {
|
||||
console.error(`Unable to write to games/${game.id}`);
|
||||
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 */
|
||||
delete reducedGame.developmentCards;
|
||||
|
||||
const playerGame = Object.assign({}, reducedGame, {
|
||||
timestamp: Date.now(),
|
||||
status: error ? error : "success",
|
||||
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 (!wsUpdate) {
|
||||
/* Save per turn while debugging... */
|
||||
await writeFile(`games/${game.id}.${game.turns}`, JSON.stringify(reducedGame, null, 2))
|
||||
.catch((error) => {
|
||||
console.error(`Unable to write to games/${game.id}`);
|
||||
console.error(error);
|
||||
});
|
||||
await writeFile(`games/${game.id}`, JSON.stringify(reducedGame, null, 2))
|
||||
.catch((error) => {
|
||||
console.error(`Unable to write to games/${game.id}`);
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
if (useWS) {
|
||||
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
|
||||
}));
|
||||
if (wsUpdate) {
|
||||
/* This is a one-shot request from a client to send the game-update over WebSocket */
|
||||
sendGameToSession(session, reducedSessions, game, reducedGame);
|
||||
} else {
|
||||
for (let id in game.sessions) {
|
||||
const target = game.sessions[id], useWS = target !== session;
|
||||
if (useWS) {
|
||||
if (!error) {
|
||||
sendGameToSession(target, reducedSessions, game, reducedGame);
|
||||
}
|
||||
} else {
|
||||
sendGameToSession(target, reducedSessions, game, reducedGame, error, res);
|
||||
}
|
||||
} else {
|
||||
console.log(`Returning update to ${target.name ? target.name : 'Unnamed'}`);
|
||||
res.status(200).send(playerGame);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user