Audio almost working; mic is doing the right thing
Signed-off-by: James Ketrenos <james_eikona@ketrenos.com>
This commit is contained in:
parent
4909647e75
commit
f6b2ada2ee
@ -6,3 +6,39 @@ body {
|
|||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Table {
|
||||||
|
display: flex;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
min-height: 100%;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between; /* left-justify 'board', right-justify 'game' */
|
||||||
|
background-image: url("./assets/tabletop.png");
|
||||||
|
}
|
||||||
|
|
||||||
|
.Table .Sidebar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 30rem;
|
||||||
|
max-width: 30rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Table .Board {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
margin: 1rem;
|
||||||
|
border: 1px solid purple;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Table .Sidebar .Chat {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -1,31 +1,201 @@
|
|||||||
/* Polyfills for IE */
|
import React, { useCallback, useState,
|
||||||
import 'react-app-polyfill/ie11';
|
useReducer, useContext, useEffect,
|
||||||
import 'core-js/features/array/find';
|
useRef } from "react";
|
||||||
import 'core-js/features/array/includes';
|
|
||||||
import 'core-js/features/number/is-nan';
|
|
||||||
|
|
||||||
/* App starts here */
|
|
||||||
import React from "react";
|
|
||||||
import {
|
import {
|
||||||
BrowserRouter as Router,
|
BrowserRouter as Router,
|
||||||
Route,
|
Route,
|
||||||
Routes
|
Routes,
|
||||||
|
useParams
|
||||||
} from "react-router-dom";
|
} from "react-router-dom";
|
||||||
|
import { base, gamesPath } from './Common.js';
|
||||||
|
import history from "./history.js";
|
||||||
|
|
||||||
//import 'typeface-roboto';
|
import { GlobalContext } from "./GlobalContext.js";
|
||||||
|
import { PingPong } from "./PingPong.js";
|
||||||
|
import { PlayerList } from "./PlayerList.js";
|
||||||
|
import { PlayerName } from "./PlayerName.js";
|
||||||
|
import { Chat } from "./Chat.js";
|
||||||
|
import { MediaAgent, MediaContext } from "./MediaControl.js";
|
||||||
|
|
||||||
//import history from "./history.js";
|
|
||||||
import Table from "./Table.js";
|
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
|
|
||||||
let base = process.env.PUBLIC_URL;
|
const Table = () => {
|
||||||
|
const params = useParams();
|
||||||
|
const global = useContext(GlobalContext);
|
||||||
|
const [ gameId, setGameId ] = useState(params.gameId ? params.gameId : undefined);
|
||||||
|
const [ ws, setWs ] = useState(global.ws);
|
||||||
|
const [ name, setName ] = useState(global.name);
|
||||||
|
const [ error, setError ] = useState(undefined);
|
||||||
|
const [ peers, setPeers ] = useState({});
|
||||||
|
|
||||||
function App() {
|
useEffect(() => {
|
||||||
|
console.log(peers);
|
||||||
|
}, [peers]);
|
||||||
|
|
||||||
|
const onWsOpen = (event) => {
|
||||||
|
console.log(`ws: open`);
|
||||||
|
setError("");
|
||||||
|
|
||||||
|
/* We do not set the socket as bound until the 'open' message
|
||||||
|
* comes through */
|
||||||
|
setWs(event.target);
|
||||||
|
event.target.send(JSON.stringify({ type: 'game-update' }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onWsMessage = (event) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
switch (data.type) {
|
||||||
|
case 'game-update':
|
||||||
|
if ('name' in data.update && data.update.name !== name) {
|
||||||
|
console.log(`Updating name to ${data.update.name}`);
|
||||||
|
setName(data.update.name);
|
||||||
|
}
|
||||||
|
if ('id' in data.update && data.update.id !== gameId) {
|
||||||
|
console.log(`Updating id to ${data.update.id}`);
|
||||||
|
setGameId(data.update.id);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onWsError = (event) => {
|
||||||
|
console.log(`ws: error`, event);
|
||||||
|
const error = `Connection to Ketr Ketran game server failed! ` +
|
||||||
|
`Try refreshing in a few seconds.`;
|
||||||
|
console.error(error);
|
||||||
|
setError(error);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onWsClose = (event) => {
|
||||||
|
console.log(`ws: close`);
|
||||||
|
setWs(undefined);
|
||||||
|
global.ws = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* callback refs are used to provide correct state reference
|
||||||
|
* in the callback handlers, while also preventing rebinding
|
||||||
|
* of event handlers on every render */
|
||||||
|
const refWsOpen = useRef(onWsOpen);
|
||||||
|
useEffect(() => { refWsOpen.current = onWsOpen; });
|
||||||
|
const refWsMessage = useRef(onWsMessage);
|
||||||
|
useEffect(() => { refWsMessage.current = onWsMessage; });
|
||||||
|
const refWsClose = useRef(onWsClose);
|
||||||
|
useEffect(() => { refWsClose.current = onWsClose; });
|
||||||
|
const refWsError = useRef(onWsError);
|
||||||
|
useEffect(() => { refWsError.current = onWsError; });
|
||||||
|
|
||||||
|
/* This effect is responsible for triggering a new game load if a
|
||||||
|
* game id is not provided in the URL. If the game is provided
|
||||||
|
* in the URL, the backend will create a new game if necessary
|
||||||
|
* during the WebSocket connection sequence.
|
||||||
|
*
|
||||||
|
* This should be the only HTTP request made from the game.
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (gameId) {
|
||||||
|
console.log(`Game in use ${gameId}`)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Requesting new game.");
|
||||||
|
|
||||||
|
window.fetch(`${base}/api/v1/games/`, {
|
||||||
|
method: 'POST',
|
||||||
|
cache: 'no-cache',
|
||||||
|
credentials: 'same-origin',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
}).then((res) => {
|
||||||
|
if (res.status >= 400) {
|
||||||
|
const error = `Unable to connect to Ketr Ketran game server!` +
|
||||||
|
`Try refreshing in a few seconds.`;
|
||||||
|
console.error(error);
|
||||||
|
setError(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
}).then((update) => {
|
||||||
|
if (update.id !== gameId) {
|
||||||
|
console.log(`New game started: ${update.id}`);
|
||||||
|
history.push(`${gamesPath}/${update.id}`);
|
||||||
|
setGameId(update.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [ gameId, setGameId ]);
|
||||||
|
|
||||||
|
/* Once a game id is known, create the sole WebSocket connection
|
||||||
|
* to the backend. This WebSocket is then shared with any component
|
||||||
|
* that performs game state updates. Those components should
|
||||||
|
* bind to the 'message:game-update' WebSocket event and parse
|
||||||
|
* their update information from those messages
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (!gameId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let socket = ws;
|
||||||
|
if (!ws) {
|
||||||
|
let loc = window.location, new_uri;
|
||||||
|
if (loc.protocol === "https:") {
|
||||||
|
new_uri = "wss";
|
||||||
|
} else {
|
||||||
|
new_uri = "ws";
|
||||||
|
}
|
||||||
|
new_uri = `${new_uri}://${loc.host}${base}/api/v1/games/ws/${gameId}`;
|
||||||
|
console.log(`Attempting WebSocket connection to ${new_uri}`);
|
||||||
|
socket = new WebSocket(new_uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('table - bind');
|
||||||
|
const cbOpen = e => refWsOpen.current(e);
|
||||||
|
const cbMessage = e => refWsMessage.current(e);
|
||||||
|
const cbClose = e => refWsClose.current(e);
|
||||||
|
const cbError = e => refWsError.current(e);
|
||||||
|
|
||||||
|
socket.addEventListener('open', cbOpen);
|
||||||
|
socket.addEventListener('close', cbClose);
|
||||||
|
socket.addEventListener('error', cbError);
|
||||||
|
socket.addEventListener('message', cbMessage);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (socket) {
|
||||||
|
console.log('table - unbind');
|
||||||
|
socket.removeEventListener('open', cbOpen);
|
||||||
|
socket.removeEventListener('close', cbClose);
|
||||||
|
socket.removeEventListener('error', cbError);
|
||||||
|
socket.removeEventListener('message', cbMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [ setWs, gameId, ws, refWsOpen, refWsMessage, refWsClose, refWsError ]);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <div>{ error }</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <GlobalContext.Provider value={{ ws, name, gameId, peers, setPeers }}>
|
||||||
|
<MediaAgent/>
|
||||||
|
<PingPong/>
|
||||||
|
<div className="Table">
|
||||||
|
<div className="Board">board goes here</div>
|
||||||
|
<div className="Sidebar">
|
||||||
|
{ name === "" && <PlayerName/> }
|
||||||
|
{ name !== "" && <PlayerList/> }
|
||||||
|
{ name !== "" && <Chat/> }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</GlobalContext.Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
console.log(`Base: ${base}`);
|
console.log(`Base: ${base}`);
|
||||||
return (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route exact element={<Table/>} path={`${base}/games/:id`}/>
|
<Route exact element={<Table/>} path={`${base}/games/:gameId`}/>
|
||||||
<Route exact element={<Table/>} path={`${base}`}/>
|
<Route exact element={<Table/>} path={`${base}`}/>
|
||||||
</Routes>
|
</Routes>
|
||||||
</Router>
|
</Router>
|
||||||
|
@ -2,19 +2,21 @@
|
|||||||
.Chat {
|
.Chat {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex-grow: 1;
|
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ChatList {
|
.ChatList {
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
flex-grow: 1;
|
||||||
/* for Firefox */
|
/* for Firefox */
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
flex-grow: 1;
|
|
||||||
flex-shrink: 1;
|
|
||||||
overflow: auto;
|
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ChatList .System {
|
.ChatList .System {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect, useContext, useRef } from "react";
|
||||||
import "./Chat.css";
|
import "./Chat.css";
|
||||||
import PlayerColor from './PlayerColor.js';
|
import PlayerColor from './PlayerColor.js';
|
||||||
import Paper from '@material-ui/core/Paper';
|
import Paper from '@material-ui/core/Paper';
|
||||||
@ -7,18 +7,63 @@ import ListItem from '@material-ui/core/ListItem';
|
|||||||
import ListItemText from '@material-ui/core/ListItemText';
|
import ListItemText from '@material-ui/core/ListItemText';
|
||||||
import Moment from 'react-moment';
|
import Moment from 'react-moment';
|
||||||
import TextField from '@material-ui/core/TextField';
|
import TextField from '@material-ui/core/TextField';
|
||||||
|
import 'moment-timezone';
|
||||||
|
|
||||||
import Resource from './Resource.js';
|
import Resource from './Resource.js';
|
||||||
import Dice from './Dice.js';
|
import Dice from './Dice.js';
|
||||||
|
import { GlobalContext } from "./GlobalContext.js";
|
||||||
|
|
||||||
const Chat = ({ table, game }) => {
|
const Chat = () => {
|
||||||
const [lastTop, setLastTop] = useState(0),
|
const [lastTop, setLastTop] = useState(0);
|
||||||
[autoScroll, setAutoscroll] = useState(true),
|
const [autoScroll, setAutoscroll] = useState(true);
|
||||||
[latest, setLatest] = useState(''),
|
const [latest, setLatest] = useState('');
|
||||||
[scrollTime, setScrollTime] = useState(0);
|
const [scrollTime, setScrollTime] = useState(0);
|
||||||
|
const [chat, setChat] = useState([]);
|
||||||
|
const [startTime, setStartTime] = useState(0);
|
||||||
|
|
||||||
const chatInput = (event) => {
|
const global = useContext(GlobalContext);
|
||||||
|
|
||||||
|
const onWsMessage = (event) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
switch (data.type) {
|
||||||
|
case 'game-update':
|
||||||
|
console.log(`chat - game update`);
|
||||||
|
if (data.update.chat) {
|
||||||
|
console.log(`chat - game update - ${data.update.chat.length} lines`);
|
||||||
|
setChat(data.update.chat);
|
||||||
|
}
|
||||||
|
if (data.update.startTime && data.update.startTime !== startTime) {
|
||||||
|
setStartTime(data.update.startTime);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
const refWsMessage = useRef(onWsMessage);
|
||||||
|
|
||||||
|
useEffect(() => { refWsMessage.current = onWsMessage; });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!global.ws) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cbMessage = e => refWsMessage.current(e);
|
||||||
|
global.ws.addEventListener('message', cbMessage);
|
||||||
|
return () => {
|
||||||
|
global.ws.removeEventListener('message', cbMessage);
|
||||||
|
}
|
||||||
|
}, [global.ws, refWsMessage]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!global.ws) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
global.ws.send(JSON.stringify({
|
||||||
|
type: 'get',
|
||||||
|
fields: ['chat', 'startTime' ]
|
||||||
|
}));
|
||||||
|
}, [global.ws]);
|
||||||
|
|
||||||
const chatKeyPress = (event) => {
|
const chatKeyPress = (event) => {
|
||||||
if (event.key === "Enter") {
|
if (event.key === "Enter") {
|
||||||
@ -26,8 +71,7 @@ const Chat = ({ table, game }) => {
|
|||||||
setAutoscroll(true);
|
setAutoscroll(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
table.sendChat(event.target.value);
|
global.ws.send(JSON.stringify({ type: 'chat', message: event.target.value }));
|
||||||
|
|
||||||
event.target.value = "";
|
event.target.value = "";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -75,12 +119,7 @@ const Chat = ({ table, game }) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
//const timeDelta = game.timestamp - Date.now();
|
const messages = chat.map((item, index) => {
|
||||||
if (!game.id) {
|
|
||||||
console.log("Why no game id?");
|
|
||||||
}
|
|
||||||
|
|
||||||
const messages = game && game.chat.map((item, index) => {
|
|
||||||
const punctuation = item.message.match(/(\.+$)/);
|
const punctuation = item.message.match(/(\.+$)/);
|
||||||
let period;
|
let period;
|
||||||
if (punctuation) {
|
if (punctuation) {
|
||||||
@ -130,27 +169,26 @@ const Chat = ({ table, game }) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (game.chat &&
|
if (chat.length && chat[chat.length - 1].date !== latest) {
|
||||||
game.chat.length &&
|
setLatest(chat[chat.length - 1].date);
|
||||||
game.chat[game.chat.length - 1].date !== latest) {
|
|
||||||
setLatest(game.chat[game.chat.length - 1].date);
|
|
||||||
setAutoscroll(true);
|
setAutoscroll(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = game ? game.name : "Why no game?";
|
|
||||||
const elapsed = game ? (game.timestamp - game.startTime) : undefined;
|
|
||||||
return (
|
return (
|
||||||
<Paper className="Chat">
|
<Paper className="Chat">
|
||||||
<List className="ChatList" id="ChatList" onScroll={chatScroll}>
|
<List className="ChatList" id="ChatList" onScroll={chatScroll}>
|
||||||
{ messages }
|
{ messages }
|
||||||
</List>
|
</List>
|
||||||
<TextField className="ChatInput"
|
<TextField className="ChatInput"
|
||||||
disabled={!name}
|
disabled={!global.name}
|
||||||
onChange={chatInput}
|
|
||||||
onKeyPress={chatKeyPress}
|
onKeyPress={chatKeyPress}
|
||||||
label={elapsed && <Moment tz={"Etc/GMT"} format="h:mm:ss" durationFromNow interval={1000} date={game.startTime}></Moment>} variant="outlined"/>
|
label={startTime !== 0 && <Moment tz={"Etc/GMT"}
|
||||||
|
format="h:mm:ss"
|
||||||
|
durationFromNow interval={1000}
|
||||||
|
date={startTime}/>}
|
||||||
|
variant="outlined"/>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Chat;
|
export { Chat };
|
12
client/src/GlobalContext.js
Normal file
12
client/src/GlobalContext.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { createContext } from "react";
|
||||||
|
|
||||||
|
const global = {
|
||||||
|
gameId: undefined,
|
||||||
|
ws: undefined,
|
||||||
|
name: "",
|
||||||
|
chat: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const GlobalContext = createContext(global);
|
||||||
|
|
||||||
|
export { GlobalContext, global };
|
@ -1,10 +1,10 @@
|
|||||||
.MediaAgent {
|
.MediaAgent {
|
||||||
display: none;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: flex;
|
display: flex;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 50000;
|
z-index: 50000;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.MediaControl {
|
.MediaControl {
|
||||||
@ -19,3 +19,7 @@
|
|||||||
.MediaControl > div {
|
.MediaControl > div {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.MediaAgent .Local {
|
||||||
|
border: 3px solid red;
|
||||||
|
}
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
import React, { useState, useEffect, useRef, useCallback,
|
import React, { useState, useEffect, useRef, useCallback,
|
||||||
useContext, createContext } from "react";
|
useContext } from "react";
|
||||||
import "./MediaControl.css";
|
import "./MediaControl.css";
|
||||||
|
|
||||||
import VolumeOff from '@mui/icons-material/VolumeOff';
|
import VolumeOff from '@mui/icons-material/VolumeOff';
|
||||||
import VolumeUp from '@mui/icons-material/VolumeUp';
|
import VolumeUp from '@mui/icons-material/VolumeUp';
|
||||||
import MicOff from '@mui/icons-material/MicOff';
|
import MicOff from '@mui/icons-material/MicOff';
|
||||||
import Mic from '@mui/icons-material/Mic';
|
import Mic from '@mui/icons-material/Mic';
|
||||||
|
import { GlobalContext } from "./GlobalContext.js";
|
||||||
const MediaContext = createContext();
|
|
||||||
|
|
||||||
/* Proxy object so we can pass in srcObject to <audio> */
|
/* Proxy object so we can pass in srcObject to <audio> */
|
||||||
const Audio = ({ srcObject, ...props }) => {
|
const Audio = ({ srcObject, paused, muted, ...props }) => {
|
||||||
const refAudio = useRef(null);
|
const refAudio = useRef(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!refAudio.current) {
|
if (!refAudio.current) {
|
||||||
@ -20,41 +19,213 @@ const Audio = ({ srcObject, ...props }) => {
|
|||||||
refAudio.current.srcObject = srcObject;
|
refAudio.current.srcObject = srcObject;
|
||||||
return () => {
|
return () => {
|
||||||
console.log('<audio> unbind');
|
console.log('<audio> unbind');
|
||||||
refAudio.current.srcObject = undefined;
|
if (refAudio.current) {
|
||||||
|
refAudio.current.srcObject = undefined;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [srcObject]);
|
}, [srcObject, paused, muted]);
|
||||||
return <audio ref={refAudio} {...props} />;
|
return <audio ref={refAudio} {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MediaAgent = ({ name, ws }) => {
|
const MediaAgent = () => {
|
||||||
|
const { name, ws, peers, setPeers } = useContext(GlobalContext);
|
||||||
|
|
||||||
const [stream, setStream] = useState(undefined);
|
const [stream, setStream] = useState(undefined);
|
||||||
const { peers, setPeers } = useContext(MediaContext);
|
|
||||||
|
|
||||||
const onClose = useCallback((event) => {
|
const onTrack = useCallback((event) => {
|
||||||
console.log(`${name} Disconnected from signaling server`);
|
const connection = event.target;
|
||||||
/* Tear down all of our peer connections and remove all the
|
|
||||||
* media divs when we disconnect */
|
console.log("ontrack", event);
|
||||||
for (let peer_id in peers) {
|
let isLocal = true;
|
||||||
peers[peer_id].close();
|
for (let key in peers) {
|
||||||
|
if (peers[key].connection === connection) {
|
||||||
|
isLocal = false;
|
||||||
|
Object.assign(peers[key].attributes, {
|
||||||
|
srcObject: event.streams[0]
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let id in peers) {
|
if (isLocal) {
|
||||||
delete peers[id];
|
throw new Error('Should not be local!');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`MediaAgent - ontrack - remote`, peers);
|
||||||
setPeers(Object.assign({}, peers));
|
setPeers(Object.assign({}, peers));
|
||||||
}, [peers, setPeers, name]);
|
}, [peers, setPeers]);
|
||||||
|
const refOnTrack = useRef(onTrack);
|
||||||
|
|
||||||
const onOpen = (event) => {
|
const onWsMessage = useCallback((event) => {
|
||||||
console.log(`MediaAgent - WebSocket open request. Attempting to create local media.`)
|
|
||||||
setup_local_media().then(() => {
|
|
||||||
/* once the user has given us access to their
|
|
||||||
* microphone/camcorder, join the channel and start peering up */
|
|
||||||
join_chat(ws);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onMessage = (event) => {
|
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
|
console.log(`MediaAgent - onWsMessage`, peers);
|
||||||
|
|
||||||
|
const addPeer = (config) => {
|
||||||
|
console.log('Signaling server said to add peer:', config);
|
||||||
|
|
||||||
|
if (!stream) {
|
||||||
|
console.log(`No local media stream`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const peer_id = config.peer_id;
|
||||||
|
if (peer_id in peers) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const connection = new RTCPeerConnection({
|
||||||
|
/*configuration: {
|
||||||
|
offerToReceiveAudio: true,
|
||||||
|
offerToReceiveVideo: false
|
||||||
|
},*/
|
||||||
|
iceServers: [ { urls: "stun:stun.l.google.com:19302" } ]
|
||||||
|
}, {
|
||||||
|
/* this will no longer be needed by chrome
|
||||||
|
* eventually (supposedly), but is necessary
|
||||||
|
* for now to get firefox to talk to chrome */
|
||||||
|
optional: [{DtlsSrtpKeyAgreement: true}]
|
||||||
|
});
|
||||||
|
|
||||||
|
peers[peer_id] = {
|
||||||
|
connection,
|
||||||
|
attributes: {
|
||||||
|
local: false,
|
||||||
|
muted: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
console.log(`MediaAgent - addPeer - remote`, peers);
|
||||||
|
setPeers(Object.assign({}, peers));
|
||||||
|
|
||||||
|
connection.onicecandidate = (event) => {
|
||||||
|
if (!event.candidate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'relayICECandidate',
|
||||||
|
config: {
|
||||||
|
peer_id: peer_id,
|
||||||
|
ice_candidate: {
|
||||||
|
sdpMLineIndex: event.candidate.sdpMLineIndex,
|
||||||
|
candidate: event.candidate.candidate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
connection.ontrack = e => refOnTrack.current(e);;
|
||||||
|
|
||||||
|
/* Add our local stream */
|
||||||
|
connection.addStream(stream);
|
||||||
|
|
||||||
|
/* Only one side of the peer connection should create the
|
||||||
|
* offer, the signaling server picks one to be the offerer.
|
||||||
|
* The other user will get a 'sessionDescription' event and will
|
||||||
|
* create an offer, then send back an answer 'sessionDescription'
|
||||||
|
* to us
|
||||||
|
*/
|
||||||
|
if (config.should_create_offer) {
|
||||||
|
console.log("MediaAgent - Creating RTC offer to ", peer_id);
|
||||||
|
return connection.createOffer()
|
||||||
|
.then((local_description) => {
|
||||||
|
console.log("Local offer description is: ", local_description);
|
||||||
|
return connection.setLocalDescription(local_description)
|
||||||
|
.then(() => {
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'relaySessionDescription',
|
||||||
|
config: {
|
||||||
|
'peer_id': peer_id,
|
||||||
|
'session_description': local_description
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
console.log("Offer setLocalDescription succeeded");
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Offer setLocalDescription failed!");
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log("Error sending offer: ", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionDescription = ({ peer_id, session_description }) => {
|
||||||
|
/**
|
||||||
|
* Peers exchange session descriptions which contains information
|
||||||
|
* about their audio / video settings and that sort of stuff. First
|
||||||
|
* the 'offerer' sends a description to the 'answerer' (with type
|
||||||
|
* "offer"), then the answerer sends one back (with type "answer").
|
||||||
|
*/
|
||||||
|
// console.log('Remote description received: ', peer_id, session_description);
|
||||||
|
const peer = peers[peer_id];
|
||||||
|
if (!peer) {
|
||||||
|
console.error(`No peer for ${peer_id}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// console.log(session_description);
|
||||||
|
const { connection } = peer;
|
||||||
|
const desc = new RTCSessionDescription(session_description);
|
||||||
|
return connection.setRemoteDescription(desc, () => {
|
||||||
|
console.log("setRemoteDescription succeeded");
|
||||||
|
if (session_description.type === "offer") {
|
||||||
|
console.log("Creating answer");
|
||||||
|
connection.createAnswer((local_description) => {
|
||||||
|
console.log("Answer description is: ", local_description);
|
||||||
|
connection.setLocalDescription(local_description, () => {
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'relaySessionDescription',
|
||||||
|
config: {
|
||||||
|
peer_id,
|
||||||
|
session_description: local_description
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
console.log("Answer setLocalDescription succeeded");
|
||||||
|
}, () => {
|
||||||
|
console.error("Answer setLocalDescription failed!");
|
||||||
|
});
|
||||||
|
}, (error) => {
|
||||||
|
// console.log("Error creating answer: ", error);
|
||||||
|
console.error(peer);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, (error) => {
|
||||||
|
console.log("setRemoteDescription error: ", error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// console.log("Description Object: ", desc);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removePeer = ({peer_id}) => {
|
||||||
|
console.log('Signaling server said to remove peer:', peer_id);
|
||||||
|
if (peer_id in peers) {
|
||||||
|
if (peers[peer_id].connection) {
|
||||||
|
peers[peer_id].connnection.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete peers[peer_id];
|
||||||
|
console.log(`MediaAgent - removePeer - remote or local?`, peers);
|
||||||
|
setPeers(Object.assign({}, peers));
|
||||||
|
};
|
||||||
|
|
||||||
|
const iceCandidate = ({ peer_id, ice_candidate }) => {
|
||||||
|
/**
|
||||||
|
* The offerer will send a number of ICE Candidate blobs to the
|
||||||
|
* answerer so they can begin trying to find the best path to one
|
||||||
|
* another on the net.
|
||||||
|
*/
|
||||||
|
const peer = peers[peer_id];
|
||||||
|
if (!peer) {
|
||||||
|
console.error(`No peer for ${peer_id}`, peers);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
peer.connection.addIceCandidate(new RTCIceCandidate(ice_candidate))
|
||||||
|
.then(() => {
|
||||||
|
console.log(`Successfully added Ice Candidate for ${peer_id}`);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error, peer, ice_candidate);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
case 'addPeer': addPeer(data.data); break;
|
case 'addPeer': addPeer(data.data); break;
|
||||||
case 'removePeer': removePeer(data.data); break;
|
case 'removePeer': removePeer(data.data); break;
|
||||||
@ -62,310 +233,199 @@ const MediaAgent = ({ name, ws }) => {
|
|||||||
case 'sessionDescription': sessionDescription(data.data); break;
|
case 'sessionDescription': sessionDescription(data.data); break;
|
||||||
default: break;
|
default: break;
|
||||||
}
|
}
|
||||||
};
|
}, [ peers, setPeers, stream, ws, refOnTrack ]);
|
||||||
|
const refWsMessage = useRef(onWsMessage);
|
||||||
|
|
||||||
const onError = (event) => {
|
const onWsClose = (event) => {
|
||||||
console.error(`${name} WebSocket error`);
|
console.log(`${name} Disconnected from signaling server`);
|
||||||
};
|
/* Tear down all of our peer connections and remove all the
|
||||||
|
* media divs when we disconnect */
|
||||||
const setup_local_media = useCallback(async () => {
|
for (let peer_id in peers) {
|
||||||
if (stream !== undefined) { /* ie, if we've already been initialized */
|
if (peers[peer_id].local) {
|
||||||
console.log(stream);
|
continue;
|
||||||
return;
|
}
|
||||||
|
peers[peer_id].connection.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ask user for permission to use the computers microphone and/or camera,
|
for (let id in peers) {
|
||||||
* attach it to an <audio> or <video> tag if they give us access. */
|
delete peers[id];
|
||||||
console.log("Requesting access to local audio / video inputs");
|
}
|
||||||
|
console.log(`MediaAgent - close - local or remote?`, peers);
|
||||||
|
setPeers(Object.assign({}, peers));
|
||||||
|
}
|
||||||
|
const refWsClose = useRef(onWsClose);
|
||||||
|
|
||||||
navigator.getUserMedia = (navigator.getUserMedia ||
|
useEffect(() => {
|
||||||
navigator.webkitGetUserMedia ||
|
refWsMessage.current = onWsMessage;
|
||||||
navigator.mozGetUserMedia ||
|
refWsClose.current = onWsClose;
|
||||||
navigator.msGetUserMedia);
|
refOnTrack.current = onTrack;
|
||||||
|
});
|
||||||
|
|
||||||
return await navigator.mediaDevices.getUserMedia({audio: true, video: false})//, "video": true})
|
useEffect(() => {
|
||||||
.then((media) => { /* user accepted access to a/v */
|
if (!ws) {
|
||||||
console.log("Access granted to audio/video");
|
return;
|
||||||
setStream(media);
|
}
|
||||||
})
|
const cbMessage = e => refWsMessage.current(e);
|
||||||
.catch((error) => { /* user denied access to a/v */
|
ws.addEventListener('message', cbMessage);
|
||||||
console.error(error);
|
const cbClose = e => refWsClose.current(e);
|
||||||
console.log("Access denied for audio/video");
|
ws.addEventListener('close', cbClose);
|
||||||
window.alert("You chose not to provide access to the microphone!" +
|
return () => {
|
||||||
"Ketran will have no audio :(");
|
ws.removeEventListener('message', cbMessage);
|
||||||
});
|
ws.removeEventListener('close', cbClose);
|
||||||
}, [ setStream, stream ]);
|
}
|
||||||
|
}, [ws, refWsMessage, refWsClose ]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let update = false;
|
||||||
|
if (!(name in peers)) {
|
||||||
|
update = true;
|
||||||
|
peers[name] = {
|
||||||
|
local: true,
|
||||||
|
attributes: {
|
||||||
|
local: true,
|
||||||
|
muted: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let key in peers) {
|
||||||
|
if (peers[key].local && key !== name) {
|
||||||
|
delete peers[key];
|
||||||
|
update = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (update) {
|
||||||
|
console.log(`MediaAgent - Adding local`, peers);
|
||||||
|
setPeers(Object.assign({}, peers));
|
||||||
|
}
|
||||||
|
}, [peers, name, setPeers]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ws) {
|
if (!ws) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('MediaAgent bind');
|
const setup_local_media = async () => {
|
||||||
|
/* Ask user for permission to use the computers microphone and/or camera,
|
||||||
|
* attach it to an <audio> or <video> tag if they give us access. */
|
||||||
|
console.log("Requesting access to local audio / video inputs");
|
||||||
|
|
||||||
console.log('Media Agent peers: ', peers);
|
navigator.getUserMedia = (navigator.getUserMedia ||
|
||||||
|
navigator.webkitGetUserMedia ||
|
||||||
|
navigator.mozGetUserMedia ||
|
||||||
|
navigator.msGetUserMedia);
|
||||||
|
|
||||||
ws.addEventListener('message', onMessage);
|
return await navigator.mediaDevices.getUserMedia({audio: true, video: false})//, "video": true})
|
||||||
ws.addEventListener('error', onError);
|
.then((media) => { /* user accepted access to a/v */
|
||||||
ws.addEventListener('close', onClose);
|
console.log("Access granted to audio/video");
|
||||||
ws.addEventListener('open', onOpen);
|
setStream(media);
|
||||||
|
})
|
||||||
return () => {
|
.catch((error) => { /* user denied access to a/v */
|
||||||
console.log('MediaAgent unbind');
|
console.error(error);
|
||||||
|
console.log("Access denied for audio/video");
|
||||||
ws.removeEventListener('message', onMessage);
|
window.alert("You chose not to provide access to the microphone!" +
|
||||||
ws.removeEventListener('error', onError);
|
"Ketran will have no audio :(");
|
||||||
ws.removeEventListener('close', onClose);
|
});
|
||||||
ws.removeEventListener('open', onOpen);
|
|
||||||
};
|
};
|
||||||
});
|
|
||||||
|
const join_chat = () => {
|
||||||
|
ws.send(JSON.stringify({ type: 'join' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`MediaAgent - WebSocket open request. Attempting to create local media.`)
|
||||||
|
setup_local_media().then(() => {
|
||||||
|
/* once the user has given us access to their
|
||||||
|
* microphone/camcorder, join the channel and start peering up */
|
||||||
|
join_chat();
|
||||||
|
});
|
||||||
|
}, [ws, setStream]);
|
||||||
|
|
||||||
if (!ws) {
|
if (!ws) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`MediaAgent`);
|
console.log(`MediaAgent`, peers);
|
||||||
const addPeer = (config) => {
|
|
||||||
console.log('Signaling server said to add peer:', config);
|
|
||||||
|
|
||||||
if (!stream) {
|
|
||||||
console.log(`No local media stream`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const peer_id = config.peer_id;
|
|
||||||
if (peer_id in peers) {
|
|
||||||
/* This could happen if the user joins multiple channels where the
|
|
||||||
* other peer is also in. */
|
|
||||||
//console.log("Already connected to peer ", peer_id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const peer_connection = new RTCPeerConnection({
|
|
||||||
/*configuration: {
|
|
||||||
offerToReceiveAudio: true,
|
|
||||||
offerToReceiveVideo: false
|
|
||||||
},*/
|
|
||||||
iceServers: [ { urls: "stun:stun.l.google.com:19302" } ]
|
|
||||||
}, {
|
|
||||||
/* this will no longer be needed by chrome
|
|
||||||
* eventually (supposedly), but is necessary
|
|
||||||
* for now to get firefox to talk to chrome */
|
|
||||||
optional: [{DtlsSrtpKeyAgreement: true}]
|
|
||||||
});
|
|
||||||
|
|
||||||
peers[peer_id] = peer_connection;
|
|
||||||
setPeers(Object.assign({}, peers));
|
|
||||||
|
|
||||||
peer_connection.onicecandidate = (event) => {
|
|
||||||
if (!event.candidate) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ws.send(JSON.stringify({
|
|
||||||
type: 'relayICECandidate',
|
|
||||||
config: {
|
|
||||||
peer_id: peer_id,
|
|
||||||
ice_candidate: {
|
|
||||||
sdpMLineIndex: event.candidate.sdpMLineIndex,
|
|
||||||
candidate: event.candidate.candidate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
peer_connection.ontrack = (event) => {
|
|
||||||
console.log("ontrack", event);
|
|
||||||
peer_connection.attributes = {
|
|
||||||
autoPlay: 'autoplay',
|
|
||||||
muted: true,
|
|
||||||
controls: true
|
|
||||||
};
|
|
||||||
|
|
||||||
if (window.URL) {
|
|
||||||
peer_connection.attributes.srcObject = event.streams[0];
|
|
||||||
} else {
|
|
||||||
peer_connection.attributes.src = event.streams[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
setPeers(Object.assign({}, peers));
|
|
||||||
};
|
|
||||||
|
|
||||||
/* Add our local stream */
|
|
||||||
peer_connection.addStream(stream);
|
|
||||||
|
|
||||||
/* Only one side of the peer connection should create the
|
|
||||||
* offer, the signaling server picks one to be the offerer.
|
|
||||||
* The other user will get a 'sessionDescription' event and will
|
|
||||||
* create an offer, then send back an answer 'sessionDescription'
|
|
||||||
* to us
|
|
||||||
*/
|
|
||||||
if (config.should_create_offer) {
|
|
||||||
console.log("MediaAgent - Creating RTC offer to ", peer_id);
|
|
||||||
return peer_connection.createOffer()
|
|
||||||
.then((local_description) => {
|
|
||||||
console.log("Local offer description is: ", local_description);
|
|
||||||
return peer_connection.setLocalDescription(local_description)
|
|
||||||
.then(() => {
|
|
||||||
ws.send(JSON.stringify({
|
|
||||||
type: 'relaySessionDescription',
|
|
||||||
config: {
|
|
||||||
'peer_id': peer_id,
|
|
||||||
'session_description': local_description
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
console.log("Offer setLocalDescription succeeded");
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Offer setLocalDescription failed!");
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.log("Error sending offer: ", error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const sessionDescription = ({ peer_id, session_description }) => {
|
|
||||||
/**
|
|
||||||
* Peers exchange session descriptions which contains information
|
|
||||||
* about their audio / video settings and that sort of stuff. First
|
|
||||||
* the 'offerer' sends a description to the 'answerer' (with type
|
|
||||||
* "offer"), then the answerer sends one back (with type "answer").
|
|
||||||
*/
|
|
||||||
// console.log('Remote description received: ', peer_id, session_description);
|
|
||||||
const peer = peers[peer_id];
|
|
||||||
if (!peer) {
|
|
||||||
console.error(`No peer for ${peer_id}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// console.log(session_description);
|
|
||||||
|
|
||||||
const desc = new RTCSessionDescription(session_description);
|
|
||||||
return peer.setRemoteDescription(desc, () => {
|
|
||||||
console.log("setRemoteDescription succeeded");
|
|
||||||
if (session_description.type === "offer") {
|
|
||||||
console.log("Creating answer");
|
|
||||||
peer.createAnswer((local_description) => {
|
|
||||||
console.log("Answer description is: ", local_description);
|
|
||||||
peer.setLocalDescription(local_description, () => {
|
|
||||||
ws.send(JSON.stringify({
|
|
||||||
type: 'relaySessionDescription',
|
|
||||||
config: {
|
|
||||||
peer_id,
|
|
||||||
session_description: local_description
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
console.log("Answer setLocalDescription succeeded");
|
|
||||||
}, () => {
|
|
||||||
console.error("Answer setLocalDescription failed!");
|
|
||||||
});
|
|
||||||
}, (error) => {
|
|
||||||
// console.log("Error creating answer: ", error);
|
|
||||||
console.error(peer);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, (error) => {
|
|
||||||
console.log("setRemoteDescription error: ", error);
|
|
||||||
});
|
|
||||||
|
|
||||||
// console.log("Description Object: ", desc);
|
|
||||||
};
|
|
||||||
|
|
||||||
const removePeer = ({peer_id}) => {
|
|
||||||
console.log('Signaling server said to remove peer:', peer_id);
|
|
||||||
if (peer_id in peers) {
|
|
||||||
peers[peer_id].close();
|
|
||||||
}
|
|
||||||
|
|
||||||
delete peers[peer_id];
|
|
||||||
setPeers(Object.assign({}, peers));
|
|
||||||
};
|
|
||||||
|
|
||||||
const iceCandidate = ({ peer_id, ice_candidate }) => {
|
|
||||||
/**
|
|
||||||
* The offerer will send a number of ICE Candidate blobs to the
|
|
||||||
* answerer so they can begin trying to find the best path to one
|
|
||||||
* another on the net.
|
|
||||||
*/
|
|
||||||
const peer = peers[peer_id];
|
|
||||||
if (!peer) {
|
|
||||||
console.error(`No peer for ${peer_id}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
peer.addIceCandidate(new RTCIceCandidate(ice_candidate))
|
|
||||||
.then(() => {
|
|
||||||
console.log(`Successfully added Ice Candidate for ${peer_id}`);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(error, peer, ice_candidate);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const join_chat = (ws) => {
|
|
||||||
ws.send(JSON.stringify({ type: 'join' }));
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
const part_chat_channel = (channel) => {
|
|
||||||
ws.send(JSON.stringify({ type: 'part' }));
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
const audioPeers = [];
|
const audioPeers = [];
|
||||||
for (let id in peers) {
|
for (let id in peers) {
|
||||||
const peer = peers[id];
|
const peer = peers[id];
|
||||||
if (peer.attributes) {
|
if (peer.local) {
|
||||||
|
peer.attributes.srcObject = stream;
|
||||||
|
if (peer.attributes.srcObject) {
|
||||||
|
peer.attributes.srcObject.getAudioTracks().forEach((track) => {
|
||||||
|
track.enabled = !peer.attributes.muted;
|
||||||
|
});
|
||||||
|
}
|
||||||
audioPeers.push(
|
audioPeers.push(
|
||||||
<Audio
|
<Audio
|
||||||
|
className={peer.local ? 'Local' : 'Remote'}
|
||||||
key={`Peer-${id}`}
|
key={`Peer-${id}`}
|
||||||
className="Remote"
|
autoPlay='autoplay'
|
||||||
|
controls
|
||||||
|
{...peer.attributes}/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
audioPeers.push(
|
||||||
|
<Audio
|
||||||
|
className={peer.local ? 'Local' : 'Remote'}
|
||||||
|
key={`Peer-${id}`}
|
||||||
|
autoPlay='autoplay'
|
||||||
|
controls
|
||||||
|
muted={peer.attributes.muted}
|
||||||
{...peer.attributes}/>
|
{...peer.attributes}/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className="MediaAgent">
|
return <div className="MediaAgent">
|
||||||
<Audio className="local" srcObject={stream} autoPlay="autoplay" muted={true} controls/>
|
|
||||||
{ audioPeers }
|
{ audioPeers }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MediaControl = ({isSelf, peer}) => {
|
const MediaControl = ({isSelf, peer}) => {
|
||||||
const [mute, setMute] = useState(false);
|
const { peers, setPeers } = useContext(GlobalContext);
|
||||||
const [mic, setMic] = useState(true);
|
const [control, setControl] = useState(undefined);
|
||||||
const [noAudio, setNoAudio] = useState(false);
|
|
||||||
const peers = useContext(MediaContext);
|
|
||||||
/*
|
|
||||||
if (game.players[color].status !== 'Active') {
|
|
||||||
return <div className="MediaControl"/>;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
const toggleMic = (event) => {
|
useEffect(() => {
|
||||||
if (peer in peers) {
|
// console.log(peer, peers);
|
||||||
console.log('found color in peers!');
|
setControl(peers[peer]);
|
||||||
}
|
}, [peer, peers, setControl]);
|
||||||
setMic(!mic);
|
|
||||||
event.stopPropagation();
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleMute = (event) => {
|
const toggleMute = (event) => {
|
||||||
if (peer in peers) {
|
if (control) {
|
||||||
console.log('found color in peers!');
|
control.muted = !control.muted;
|
||||||
}
|
}
|
||||||
setMute(!mute);
|
const update = Object.assign({}, peers);
|
||||||
|
if (isSelf) {
|
||||||
|
update[peer].attributes.muted = control.muted;
|
||||||
|
} else {
|
||||||
|
update[peer].attributes.muted = control.muted;
|
||||||
|
}
|
||||||
|
console.log(`MediaControl - toggleMute`, update);
|
||||||
|
setPeers(update);
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
|
|
||||||
return <MediaContext.Provider value={peers}>
|
if (!control) {
|
||||||
<div className="MediaControl">
|
return <div className="MediaControl">
|
||||||
{ isSelf && <div onClick={toggleMic}>
|
{ isSelf && <MicOff color={'disabled'}/> }
|
||||||
{ mic && <MicOff color={noAudio ? 'disabled' : 'primary'}/> }
|
{ !isSelf && <VolumeOff color={'disabled'}/> }
|
||||||
{ !mic && <Mic color={noAudio ? 'disabled' : 'primary'}/> }
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="MediaControl">
|
||||||
|
{ isSelf && <div onClick={toggleMute}>
|
||||||
|
{ control.muted && <MicOff color={'primary'}/> }
|
||||||
|
{ !control.muted && <Mic color={'primary'}/> }
|
||||||
</div> }
|
</div> }
|
||||||
<div onClick={toggleMute}>
|
{ !isSelf && <div onClick={toggleMute}>
|
||||||
{ mute && <VolumeOff color={noAudio ? 'disabled' : 'primary'}/> }
|
{ control.muted && <VolumeOff color={'primary'}/> }
|
||||||
{ !mute && <VolumeUp color={noAudio ? 'disabled' : 'primary'}/> }
|
{ !control.muted && <VolumeUp color={'primary'}/> }
|
||||||
</div>
|
</div> }
|
||||||
</div>
|
</div>;
|
||||||
</MediaContext.Provider>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export { MediaControl, MediaAgent, MediaContext };
|
export { MediaControl, MediaAgent };
|
||||||
|
7
client/src/PingPong.css
Normal file
7
client/src/PingPong.css
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
.PingPong {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
41
client/src/PingPong.js
Normal file
41
client/src/PingPong.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
|
||||||
|
import React, { useState, useContext, useEffect, useRef } from "react";
|
||||||
|
import { GlobalContext} from "./GlobalContext.js";
|
||||||
|
import "./PingPong.css";
|
||||||
|
|
||||||
|
const PingPong = () => {
|
||||||
|
const [ count, setCount ] = useState(0);
|
||||||
|
const global = useContext(GlobalContext);
|
||||||
|
|
||||||
|
const onWsMessage = (event) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
switch (data.type) {
|
||||||
|
case 'ping':
|
||||||
|
global.ws.send(JSON.stringify({ type: 'pong', timestamp: data.ping }));
|
||||||
|
setCount(count + 1);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const refWsMessage = useRef(onWsMessage);
|
||||||
|
|
||||||
|
useEffect(() => { refWsMessage.current = onWsMessage; });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!global.ws) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cbMessage = e => refWsMessage.current(e);
|
||||||
|
global.ws.addEventListener('message', cbMessage);
|
||||||
|
return () => {
|
||||||
|
global.ws.removeEventListener('message', cbMessage);
|
||||||
|
}
|
||||||
|
}, [global.ws, refWsMessage]);
|
||||||
|
|
||||||
|
return <div className="PingPong">
|
||||||
|
Game {global.gameId}: {global.name} {global.ws ? 'has socket' : 'no socket' } { count } pings
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { PingPong };
|
81
client/src/PlayerList.css
Normal file
81
client/src/PlayerList.css
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
.PlayerList {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
padding: 0.5em;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.PlayerList .PlayerSelector .PlayerColor {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.PlayerList .PlayerSelector {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.PlayerList .PlayerSelector.MuiList-padding {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.PlayerList .PlayerSelector .MuiTypography-body1 {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
/* white-space: nowrap;*/
|
||||||
|
}
|
||||||
|
|
||||||
|
.PlayerList .PlayerSelector .MuiTypography-body2 {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.PlayerList .PlayerEntry {
|
||||||
|
border: 1px solid rgba(0,0,0,0);
|
||||||
|
border-radius: 0.5em;
|
||||||
|
min-width: 10em;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.PlayerList .PlayerSelector .PlayerEntry {
|
||||||
|
flex: 1 1 0px;
|
||||||
|
align-items: center;
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: row;
|
||||||
|
min-width: 10em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.PlayerList .PlayerEntry[data-selectable=true]:hover {
|
||||||
|
border-color: rgba(0,0,0,0.5);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.PlayerList .PlayerEntry[data-selected=true] {
|
||||||
|
background-color: rgba(255, 255, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.PlayerList .PlayerEntry > *:last-child {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.PlayerList .Players .PlayerToggle {
|
||||||
|
min-width: 5em;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.PlayerList .PlayerName {
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.PlayerList .Players > * {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.PlayerList .Players .nameInput {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
100
client/src/PlayerList.js
Normal file
100
client/src/PlayerList.js
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import React, { useState, useEffect, useContext, useRef } from "react";
|
||||||
|
import "./PlayerList.css";
|
||||||
|
import PlayerColor from './PlayerColor.js';
|
||||||
|
import Paper from '@material-ui/core/Paper';
|
||||||
|
import List from '@material-ui/core/List';
|
||||||
|
|
||||||
|
import { MediaControl } from "./MediaControl.js";
|
||||||
|
import { GlobalContext } from "./GlobalContext.js";
|
||||||
|
|
||||||
|
const PlayerList = () => {
|
||||||
|
const { ws, name } = useContext(GlobalContext);
|
||||||
|
const [players, setPlayers] = useState({});
|
||||||
|
const [state, setState] = useState('lobby');
|
||||||
|
const [color, setColor] = useState(undefined);
|
||||||
|
|
||||||
|
const onWsMessage = (event) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
switch (data.type) {
|
||||||
|
case 'game-update':
|
||||||
|
if (data.update.players) {
|
||||||
|
for (let key in data.update.players) {
|
||||||
|
if (data.update.players[key].name === name) {
|
||||||
|
setColor(key);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setPlayers(data.update.players);
|
||||||
|
}
|
||||||
|
if (data.update.state) {
|
||||||
|
setState(data.update.state);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const refWsMessage = useRef(onWsMessage);
|
||||||
|
|
||||||
|
useEffect(() => { refWsMessage.current = onWsMessage; });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ws) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cbMessage = e => refWsMessage.current(e);
|
||||||
|
ws.addEventListener('message', cbMessage);
|
||||||
|
return () => {
|
||||||
|
ws.removeEventListener('message', cbMessage);
|
||||||
|
}
|
||||||
|
}, [ws, refWsMessage]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ws) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'get',
|
||||||
|
fields: [ 'state', 'players' ]
|
||||||
|
}));
|
||||||
|
}, [ws]);
|
||||||
|
|
||||||
|
const toggleSelected = (key) => {
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'set',
|
||||||
|
field: 'color',
|
||||||
|
value: color === key ? "" : key
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const playerElements = [];
|
||||||
|
|
||||||
|
const inLobby = state === 'lobby';
|
||||||
|
for (let key in players) {
|
||||||
|
const item = players[key];
|
||||||
|
const name = item.name;
|
||||||
|
const selectable = inLobby && (item.status === 'Not active' || color === key);
|
||||||
|
playerElements.push(
|
||||||
|
<div
|
||||||
|
data-selectable={selectable}
|
||||||
|
data-selected={key === color}
|
||||||
|
className="PlayerEntry"
|
||||||
|
onClick={() => { inLobby && selectable && toggleSelected(key) }}
|
||||||
|
key={`player-${key}`}>
|
||||||
|
<PlayerColor color={key}/>{name ? name : 'Available' }
|
||||||
|
{ name && <MediaControl peer={name} isSelf={key === color}/> }
|
||||||
|
{ !name && <div></div> }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper className="PlayerList">
|
||||||
|
<List className="PlayerSelector">
|
||||||
|
{ playerElements }
|
||||||
|
</List>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { PlayerList };
|
17
client/src/PlayerName.css
Normal file
17
client/src/PlayerName.css
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
.PlayerName {
|
||||||
|
padding: 0.5em;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.PlayerName > .nameInput {
|
||||||
|
margin-right: 1em;
|
||||||
|
flex: 1;
|
||||||
|
max-width: 30em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.PlayerName > Button {
|
||||||
|
background: lightblue;
|
||||||
|
}
|
82
client/src/PlayerName.js
Normal file
82
client/src/PlayerName.js
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import React, { useState, useEffect, useContext, useRef } from "react";
|
||||||
|
import "./PlayerName.css";
|
||||||
|
import Paper from '@material-ui/core/Paper';
|
||||||
|
import TextField from '@material-ui/core/TextField';
|
||||||
|
import Button from '@material-ui/core/Button';
|
||||||
|
|
||||||
|
import { GlobalContext } from "./GlobalContext.js";
|
||||||
|
|
||||||
|
const PlayerName = () => {
|
||||||
|
const global = useContext(GlobalContext);
|
||||||
|
const [name, setName] = useState(global.name ? global.name : "");
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
const onWsMessage = (event) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
switch (data.type) {
|
||||||
|
case 'game-update':
|
||||||
|
if ('name' in data.update && data.update.name !== name) {
|
||||||
|
setName(data.update.name);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'player-name':
|
||||||
|
if ('error' in data) {
|
||||||
|
setError(data.error);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const refWsMessage = useRef(onWsMessage);
|
||||||
|
|
||||||
|
useEffect(() => { refWsMessage.current = onWsMessage; });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!global.ws) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cbMessage = e => refWsMessage.current(e);
|
||||||
|
global.ws.addEventListener('message', cbMessage);
|
||||||
|
return () => {
|
||||||
|
global.ws.removeEventListener('message', cbMessage);
|
||||||
|
}
|
||||||
|
}, [global.ws, refWsMessage]);
|
||||||
|
|
||||||
|
const sendName = () => {
|
||||||
|
if (name !== global.name && name !== "") {
|
||||||
|
if (error) {
|
||||||
|
setError("");
|
||||||
|
}
|
||||||
|
global.ws.send(JSON.stringify({ type: 'player-name', name }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nameChange = (event) => {
|
||||||
|
setName(event.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const nameKeyPress = (event) => {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
sendName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper className="PlayerName">
|
||||||
|
{ error !== "" && <div>Error: {error}</div> }
|
||||||
|
<TextField className="nameInput"
|
||||||
|
onChange={nameChange}
|
||||||
|
onKeyPress={nameKeyPress}
|
||||||
|
label="Enter your name"
|
||||||
|
variant="outlined"
|
||||||
|
value={name}
|
||||||
|
/>
|
||||||
|
<Button onClick={() => sendName()}>Set</Button>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { PlayerName };
|
@ -9,6 +9,13 @@
|
|||||||
background-image: url("./assets/tabletop.png");
|
background-image: url("./assets/tabletop.png");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Table .Chat {
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 30rem;
|
||||||
|
}
|
||||||
|
|
||||||
.Loading {
|
.Loading {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 1em;
|
top: 1em;
|
||||||
@ -230,96 +237,6 @@
|
|||||||
.Game.lobby {
|
.Game.lobby {
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Game
|
|
||||||
* Message
|
|
||||||
* Players
|
|
||||||
* Chat
|
|
||||||
* Action
|
|
||||||
*/
|
|
||||||
.Players {
|
|
||||||
flex: 1 0;
|
|
||||||
overflow: hidden;
|
|
||||||
padding: 0.5em;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.PlayerSelector .PlayerColor {
|
|
||||||
width: 1em;
|
|
||||||
height: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.PlayerSelector {
|
|
||||||
display: inline-flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.PlayerSelector.MuiList-padding {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.PlayerSelector .MuiTypography-body1 {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
/* white-space: nowrap;*/
|
|
||||||
}
|
|
||||||
|
|
||||||
.PlayerSelector .MuiTypography-body2 {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Players .PlayerEntry {
|
|
||||||
border: 1px solid rgba(0,0,0,0);
|
|
||||||
border-radius: 0.5em;
|
|
||||||
min-width: 10em;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.PlayerSelector .PlayerEntry {
|
|
||||||
flex: 1 1 0px;
|
|
||||||
align-items: center;
|
|
||||||
display: inline-flex;
|
|
||||||
flex-direction: row;
|
|
||||||
min-width: 10em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Players .PlayerEntry[data-selectable=true]:hover {
|
|
||||||
border-color: rgba(0,0,0,0.5);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Players .PlayerEntry[data-selected=true] {
|
|
||||||
background-color: rgba(255, 255, 0, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.Players .PlayerEntry > *:last-child {
|
|
||||||
display: flex;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.Players .PlayerToggle {
|
|
||||||
min-width: 5em;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.PlayerName {
|
|
||||||
padding: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Players > * {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Players .nameInput {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.Development {
|
.Development {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@ -380,7 +297,9 @@ button {
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.PlayerName {
|
.PlayerName {
|
||||||
|
padding: 0.5em;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -466,42 +466,52 @@ const getPlayer = (game, color) => {
|
|||||||
return game.players[color];
|
return game.players[color];
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSession = (game, session) => {
|
const getSession = (game, reqSession) => {
|
||||||
if (!game.sessions) {
|
if (!game.sessions) {
|
||||||
game.sessions = {};
|
game.sessions = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!session.player_id) {
|
if (!reqSession.player_id) {
|
||||||
session.player_id = crypto.randomBytes(16).toString('hex');
|
reqSession.player_id = crypto.randomBytes(16).toString('hex');
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = session.player_id;
|
const id = reqSession.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)) {
|
if (!(id in game.sessions)) {
|
||||||
game.sessions[id] = {
|
game.sessions[id] = {
|
||||||
name: undefined,
|
name: undefined,
|
||||||
color: undefined,
|
color: undefined,
|
||||||
player: undefined
|
player: undefined,
|
||||||
|
lastActive: Date.now()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Expire old unused sessions */
|
/* Expire old unused sessions */
|
||||||
for (let id in game.sessions) {
|
for (let _id in game.sessions) {
|
||||||
const tmp = game.sessions[id];
|
const _session = game.sessions[_id];
|
||||||
if (tmp.color || tmp.name || tmp.player) {
|
if (_session.color || _session.name || _session.player) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (tmp.player_id === session.player_id) {
|
if (_id === id) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
/* 10 minutes */
|
/* 10 minutes */
|
||||||
if (tmp.lastActive && tmp.lastActive < Date.now() - 10 * 60 * 1000) {
|
const age = Date.now() - _session.lastActive;
|
||||||
console.log(`Expiring old session ${id}`);
|
if (age > 10 * 60 * 1000) {
|
||||||
delete game.sessions[id];
|
console.log(`Expiring old session ${_id}: ${age/(60 * 1000)} minutes`);
|
||||||
|
delete game.sessions[_id];
|
||||||
|
if (_id in game.sessions) {
|
||||||
|
console.log('delete DID NOT WORK!');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
session.lastActive = Date.now();
|
||||||
|
if (session.player) {
|
||||||
|
session.player.lastActive = session.lastActive;
|
||||||
|
}
|
||||||
|
|
||||||
return game.sessions[id];
|
return game.sessions[id];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -514,6 +524,8 @@ const loadGame = async (id) => {
|
|||||||
return games[id];
|
return games[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`Loading game from disk`);
|
||||||
|
|
||||||
let game = await readFile(`games/${id}`)
|
let game = await readFile(`games/${id}`)
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
return;
|
return;
|
||||||
@ -796,9 +808,11 @@ const setPlayerName = (game, session, name) => {
|
|||||||
if (tmp.name && tmp.name.toLowerCase() === name.toLowerCase()) {
|
if (tmp.name && tmp.name.toLowerCase() === name.toLowerCase()) {
|
||||||
if (!tmp.player || (Date.now() - tmp.player.lastActive) > 60000) {
|
if (!tmp.player || (Date.now() - tmp.player.lastActive) > 60000) {
|
||||||
rejoin = true;
|
rejoin = true;
|
||||||
Object.assign(session, tmp);
|
/* Update the session object from tmp, but retain websocket
|
||||||
|
* from active session */
|
||||||
|
Object.assign(session, tmp, { ws: session.ws });
|
||||||
console.log(`${name} has been reallocated to a new session.`);
|
console.log(`${name} has been reallocated to a new session.`);
|
||||||
console.log({ old: game.sessions[key], new: session });
|
// console.log({ old: game.sessions[key], new: session });
|
||||||
delete game.sessions[key];
|
delete game.sessions[key];
|
||||||
} else {
|
} else {
|
||||||
return `${name} is already taken and has been active in the last minute.`;
|
return `${name} is already taken and has been active in the last minute.`;
|
||||||
@ -917,6 +931,8 @@ const setPlayerColor = (game, session, color) => {
|
|||||||
session.player.status = `Active`;
|
session.player.status = `Active`;
|
||||||
session.player.lastActive = Date.now();
|
session.player.lastActive = Date.now();
|
||||||
session.color = color;
|
session.color = color;
|
||||||
|
game.players[color].name = session.name;
|
||||||
|
|
||||||
addChatMessage(game, session, `${session.name} has chosen to play as ${colorToWord(color)}.`);
|
addChatMessage(game, session, `${session.name} has chosen to play as ${colorToWord(color)}.`);
|
||||||
|
|
||||||
const afterActive = getActiveCount(game);
|
const afterActive = getActiveCount(game);
|
||||||
@ -937,10 +953,19 @@ const addActivity = (game, session, message) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const addChatMessage = (game, session, message) => {
|
const addChatMessage = (game, session, message) => {
|
||||||
|
let now = Date.now();
|
||||||
|
let lastTime = 0;
|
||||||
|
if (game.chat.length) {
|
||||||
|
lastTime = game.chat[game.chat.length - 1].date;
|
||||||
|
}
|
||||||
|
if (now === lastTime) {
|
||||||
|
now++;
|
||||||
|
}
|
||||||
|
|
||||||
game.chat.push({
|
game.chat.push({
|
||||||
from: session ? session.name : undefined,
|
from: session ? session.name : undefined,
|
||||||
color: session ? session.color : undefined,
|
color: session ? session.color : undefined,
|
||||||
date: Date.now(),
|
date: now,
|
||||||
message: message
|
message: message
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -2752,7 +2777,7 @@ const join = (peers, session, id) => {
|
|||||||
for (let peer in peers) {
|
for (let peer in peers) {
|
||||||
peers[peer].send(JSON.stringify({
|
peers[peer].send(JSON.stringify({
|
||||||
type: 'addPeer',
|
type: 'addPeer',
|
||||||
data: { 'peer_id': peer, 'should_create_offer': false }
|
data: { 'peer_id': session.name, 'should_create_offer': false }
|
||||||
}));
|
}));
|
||||||
|
|
||||||
ws.send(JSON.stringify({
|
ws.send(JSON.stringify({
|
||||||
@ -2765,13 +2790,17 @@ const join = (peers, session, id) => {
|
|||||||
peers[session.name] = ws;
|
peers[session.name] = ws;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getName = (session) => {
|
||||||
|
return session.name ? session.name : "Unnamed";
|
||||||
|
}
|
||||||
|
|
||||||
const part = (peers, session, id) => {
|
const part = (peers, session, id) => {
|
||||||
const ws = session.ws;
|
const ws = session.ws;
|
||||||
|
|
||||||
console.log(`${id}:${session.name} - Audio part.`);
|
console.log(`${id}:${getName(session)} - Audio part.`);
|
||||||
|
|
||||||
if (!(session.name in peers)) {
|
if (!(session.name in peers)) {
|
||||||
console.log(`${id}:${session.name} - Does not exist in game audio.`);
|
console.log(`${id}:${getName(session)} - Does not exist in game audio.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2791,11 +2820,48 @@ const part = (peers, session, id) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const saveGame = async (game) => {
|
||||||
|
/* Shallow copy game, filling its sessions with a shallow copy of sessions so we can then
|
||||||
|
* delete the player field from them */
|
||||||
|
const reducedGame = Object.assign({}, game, { sessions: {} }),
|
||||||
|
reducedSessions = [];
|
||||||
|
|
||||||
|
for (let id in game.sessions) {
|
||||||
|
const reduced = Object.assign({}, game.sessions[id]);
|
||||||
|
if (reduced.player) {
|
||||||
|
delete reduced.player;
|
||||||
|
}
|
||||||
|
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 */
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
router.ws("/ws/:id", async (ws, req) => {
|
router.ws("/ws/:id", async (ws, req) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
const gameId = id;
|
||||||
ws.id = req.session.player_id;
|
ws.id = req.session.player_id;
|
||||||
|
|
||||||
|
console.log(`${gameId} - New connection from client.`);
|
||||||
if (!(id in audio)) {
|
if (!(id in audio)) {
|
||||||
audio[id] = {}; /* List of peer sockets using session.name as index. */
|
audio[id] = {}; /* List of peer sockets using session.name as index. */
|
||||||
console.log(`${id} - New Game Audio`);
|
console.log(`${id} - New Game Audio`);
|
||||||
@ -2807,7 +2873,7 @@ router.ws("/ws/:id", async (ws, req) => {
|
|||||||
* we may miss the first messages from clients */
|
* we may miss the first messages from clients */
|
||||||
ws.on('error', async (event) => {
|
ws.on('error', async (event) => {
|
||||||
console.error(`WebSocket error: `, event.message);
|
console.error(`WebSocket error: `, event.message);
|
||||||
const game = await loadGame(id);
|
const game = await loadGame(gameId);
|
||||||
if (game) {
|
if (game) {
|
||||||
const session = getSession(game, req.session);
|
const session = getSession(game, req.session);
|
||||||
if (session && session.ws) {
|
if (session && session.ws) {
|
||||||
@ -2819,14 +2885,14 @@ router.ws("/ws/:id", async (ws, req) => {
|
|||||||
|
|
||||||
ws.on('open', async (event) => {
|
ws.on('open', async (event) => {
|
||||||
console.log(`WebSocket open: `, event.message);
|
console.log(`WebSocket open: `, event.message);
|
||||||
const game = await loadGame(id);
|
const game = await loadGame(gameId);
|
||||||
if (game) {
|
if (game) {
|
||||||
resetDisconnectCheck(game, req);
|
resetDisconnectCheck(game, req);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.on('close', async (event) => {
|
ws.on('close', async (event) => {
|
||||||
const game = await loadGame(id);
|
const game = await loadGame(gameId);
|
||||||
if (game) {
|
if (game) {
|
||||||
const session = getSession(game, req.session);
|
const session = getSession(game, req.session);
|
||||||
if (session && session.ws) {
|
if (session && session.ws) {
|
||||||
@ -2836,7 +2902,7 @@ router.ws("/ws/:id", async (ws, req) => {
|
|||||||
}
|
}
|
||||||
session.ws.close();
|
session.ws.close();
|
||||||
session.ws = undefined;
|
session.ws = undefined;
|
||||||
console.log(`WebSocket closed for ${session.name}`);
|
console.log(`WebSocket closed for ${getName(session)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2844,8 +2910,14 @@ router.ws("/ws/:id", async (ws, req) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ws.on('message', async (message) => {
|
ws.on('message', async (message) => {
|
||||||
const data = JSON.parse(message);
|
let data;
|
||||||
const game = await loadGame(id);
|
try {
|
||||||
|
data = JSON.parse(message);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error, message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const game = await loadGame(gameId);
|
||||||
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.`);
|
||||||
return;
|
return;
|
||||||
@ -2860,6 +2932,8 @@ router.ws("/ws/:id", async (ws, req) => {
|
|||||||
session.ws = ws;
|
session.ws = ws;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let error = '', update;
|
||||||
|
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
case 'join':
|
case 'join':
|
||||||
join(audio[id], session, id);
|
join(audio[id], session, id);
|
||||||
@ -2876,14 +2950,16 @@ router.ws("/ws/:id", async (ws, req) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { peer_id, ice_candidate } = data.config;
|
const { peer_id, ice_candidate } = data.config;
|
||||||
console.log(`${id} - relayICECandidate ${session.name} to ${peer_id}`,
|
console.log(`${id} - relayICECandidate ${getName(session)} to ${peer_id}`,
|
||||||
ice_candidate);
|
ice_candidate);
|
||||||
|
|
||||||
|
message = JSON.stringify({
|
||||||
|
type: 'iceCandidate',
|
||||||
|
data: {'peer_id': getName(session), 'ice_candidate': ice_candidate }
|
||||||
|
});
|
||||||
|
|
||||||
if (peer_id in audio[id]) {
|
if (peer_id in audio[id]) {
|
||||||
audio[id][peer_id].send(JSON.stringify({
|
audio[id][peer_id].send(message);
|
||||||
type: 'iceCandidate',
|
|
||||||
data: {'peer_id': session.name, 'ice_candidate': ice_candidate }
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
} break;
|
} break;
|
||||||
|
|
||||||
@ -2893,37 +2969,164 @@ router.ws("/ws/:id", async (ws, req) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { peer_id, session_description } = data.config;
|
const { peer_id, session_description } = data.config;
|
||||||
console.log(`${id} - relaySessionDescription ${session.name} to ${peer_id}`,
|
console.log(`${id} - relaySessionDescription ${getName(session)} to ${peer_id}`,
|
||||||
session_description);
|
session_description);
|
||||||
|
message = JSON.stringify({
|
||||||
|
type: 'sessionDescription',
|
||||||
|
data: {'peer_id': getName(session), 'session_description': session_description }
|
||||||
|
});
|
||||||
if (peer_id in audio[id]) {
|
if (peer_id in audio[id]) {
|
||||||
audio[id][peer_id].send(JSON.stringify({
|
audio[id][peer_id].send(message);
|
||||||
type: 'sessionDescription',
|
|
||||||
data: {'peer_id': session.name, 'session_description': session_description }
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
} break;
|
} break;
|
||||||
|
|
||||||
case 'pong':
|
case 'pong':
|
||||||
resetDisconnectCheck(game, req);
|
resetDisconnectCheck(game, req);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'game-update':
|
case 'game-update':
|
||||||
console.log(`Player ${session.name ? session.name : 'Unnamed'} requested a game update.`);
|
console.log(`Player ${getName(session)} requested a game update.`);
|
||||||
resetDisconnectCheck(game, req);
|
message = JSON.stringify({
|
||||||
sendGame(req, undefined, game, undefined, ws);
|
type: 'game-update',
|
||||||
break;
|
update: filterGameForPlayer(game, session)
|
||||||
|
});
|
||||||
|
session.ws.send(message);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'player-name':
|
||||||
|
console.log(`${id}:${getName(session)} - setPlayerName - ${data.name}`)
|
||||||
|
error = setPlayerName(game, session, data.name);
|
||||||
|
if (error) {
|
||||||
|
session.ws.send(JSON.stringify({ error }));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
update = {};
|
||||||
|
|
||||||
|
session.name = data.name;
|
||||||
|
update.name = session.name
|
||||||
|
if (session.color && session.color in game.players) {
|
||||||
|
game.players[session.color].name = session.name;
|
||||||
|
update.players = game.players;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let key in game.sessions) {
|
||||||
|
const _session = game.sessions[key];
|
||||||
|
if (!_session.ws) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_session.ws.send(JSON.stringify({
|
||||||
|
type: 'game-update',
|
||||||
|
update: filterGameForPlayer(game, _session)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
console.log('TODO: support only change update. fire update to all players in game');
|
||||||
|
await saveGame(game);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'set':
|
||||||
|
console.log(`${id}:${getName(session)} - ${data.type}`);
|
||||||
|
update = {};
|
||||||
|
switch (data.field) {
|
||||||
|
case 'color':
|
||||||
|
error = setPlayerColor(game, session, data.value);
|
||||||
|
if (error) {
|
||||||
|
session.ws.send(JSON.stringify({ error }));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
for (let key in game.sessions) {
|
||||||
|
const _session = game.sessions[key];
|
||||||
|
if (!_session.ws) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_session.ws.send(JSON.stringify({
|
||||||
|
type: 'game-update',
|
||||||
|
update: filterGameForPlayer(game, _session)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
console.log('TODO: support only change update. fire update to all players in game');
|
||||||
|
await saveGame(game);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.warn(`WARNING: Requested SET unsupported field: ${field}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'get':
|
||||||
|
console.log(`${id}:${getName(session)} - ${data.type}`);
|
||||||
|
update = {};
|
||||||
|
data.fields.forEach((field) => {
|
||||||
|
switch (field) {
|
||||||
|
case 'chat':
|
||||||
|
case 'startTime':
|
||||||
|
case 'state':
|
||||||
|
update[field] = game[field];
|
||||||
|
break;
|
||||||
|
case 'players':
|
||||||
|
update[field] = game[field];
|
||||||
|
for (let color in game.players) {
|
||||||
|
if (game.players[color].status !== 'Active') {
|
||||||
|
// continue;
|
||||||
|
}
|
||||||
|
update.players[color] = game.players[color];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.warn(`WARNING: Requested GET unsupported field: ${field}`);
|
||||||
|
if (field in game) {
|
||||||
|
update[field] = game.field;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
message = JSON.stringify({
|
||||||
|
type: 'game-update',
|
||||||
|
update
|
||||||
|
});
|
||||||
|
session.ws.send(message);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'chat':
|
||||||
|
console.log(`${id}:${session.id} - ${data.type} - ${data.message}`)
|
||||||
|
|
||||||
|
/* Update the chat array */
|
||||||
|
addChatMessage(game, session, `${session.name}: ${data.message}`);
|
||||||
|
|
||||||
|
/* Send the update to all players */
|
||||||
|
message = JSON.stringify({
|
||||||
|
type: 'game-update',
|
||||||
|
update: {
|
||||||
|
chat: game.chat
|
||||||
|
}
|
||||||
|
});
|
||||||
|
for (let key in game.sessions) {
|
||||||
|
const _session = game.sessions[key];
|
||||||
|
if (!_session.ws) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_session.ws.send(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Save the current game state to disk */
|
||||||
|
await saveGame(game);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/* This will result in the node tick moving forward; if we haven't already
|
/* 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
|
* setup the event handlers, a 'message' could come through prior to this
|
||||||
* completing */
|
* completing */
|
||||||
const game = await loadGame(id);
|
const game = await loadGame(gameId);
|
||||||
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.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = getSession(game, req.session);
|
const session = getSession(game, req.session);
|
||||||
|
if (!session) {
|
||||||
|
console.error(`Session should never be empty after getSession`,
|
||||||
|
game, req.session);
|
||||||
|
}
|
||||||
|
|
||||||
resetDisconnectCheck(game, req);
|
resetDisconnectCheck(game, req);
|
||||||
|
|
||||||
@ -2941,20 +3144,6 @@ router.ws("/ws/:id", async (ws, req) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get("/:id", async (req, res/*, next*/) => {
|
|
||||||
const { id } = req.params;
|
|
||||||
// console.log("GET games/" + id);
|
|
||||||
|
|
||||||
let game = await loadGame(id);
|
|
||||||
if (game) {
|
|
||||||
return sendGame(req, res, game)
|
|
||||||
}
|
|
||||||
|
|
||||||
game = createGame(id);
|
|
||||||
|
|
||||||
return sendGame(req, res, game);
|
|
||||||
});
|
|
||||||
|
|
||||||
const debugChat = (game, preamble) => {
|
const debugChat = (game, preamble) => {
|
||||||
preamble = `Degug ${preamble.trim()}`;
|
preamble = `Degug ${preamble.trim()}`;
|
||||||
|
|
||||||
@ -3026,7 +3215,7 @@ const sendGameToSession = (session, reducedSessions, game, reducedGame, error, r
|
|||||||
if (!session.ws) {
|
if (!session.ws) {
|
||||||
console.error(`No WebSocket connection to ${session.name}`);
|
console.error(`No WebSocket connection to ${session.name}`);
|
||||||
} else {
|
} else {
|
||||||
console.log(`Sending update to ${session.name}`);
|
console.log(`Sending update to ${session.id}:${session.name ? session.name : 'Unnamed'}`);
|
||||||
session.ws.send(JSON.stringify({
|
session.ws.send(JSON.stringify({
|
||||||
type: 'game-update',
|
type: 'game-update',
|
||||||
update: playerGame
|
update: playerGame
|
||||||
@ -3064,15 +3253,6 @@ const sendGame = async (req, res, game, error, wsUpdate) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure chat messages have a unique date: stamp as it is used as the index key */
|
|
||||||
let lastTime = 0;
|
|
||||||
if (game.chat) game.chat.forEach((message) => {
|
|
||||||
if (message.date <= lastTime) {
|
|
||||||
message.date = lastTime + 1;
|
|
||||||
}
|
|
||||||
lastTime = message.date;
|
|
||||||
});
|
|
||||||
|
|
||||||
/* Calculate points and determine if there is a winner */
|
/* Calculate points and determine if there is a winner */
|
||||||
for (let key in game.players) {
|
for (let key in game.players) {
|
||||||
const player = game.players[key];
|
const player = game.players[key];
|
||||||
@ -3177,6 +3357,110 @@ const sendGame = async (req, res, game, error, wsUpdate) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const filterGameForPlayer = (game, session) => {
|
||||||
|
const active = getActiveCount(game);
|
||||||
|
|
||||||
|
game.active = active;
|
||||||
|
|
||||||
|
/* Calculate points and determine if there is a winner */
|
||||||
|
for (let key in game.players) {
|
||||||
|
const player = game.players[key];
|
||||||
|
if (player.status === 'Not active') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
player.points = 0;
|
||||||
|
if (key === game.longestRoad) {
|
||||||
|
player.points += 2;
|
||||||
|
}
|
||||||
|
if (key === game.largestArmy) {
|
||||||
|
player.points += 2;
|
||||||
|
}
|
||||||
|
player.points += MAX_SETTLEMENTS - player.settlements;
|
||||||
|
player.points += 2 * (MAX_CITIES - player.cities);
|
||||||
|
|
||||||
|
player.unplayed = 0;
|
||||||
|
player.potential = 0;
|
||||||
|
player.development.forEach(card => {
|
||||||
|
if (card.type === 'vp') {
|
||||||
|
if (card.played) {
|
||||||
|
player.points++;
|
||||||
|
} else {
|
||||||
|
player.potential++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!card.played) {
|
||||||
|
player.unplayed++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('TODO: Move game win state to card play section');
|
||||||
|
if (!game.winner && (player.points >= 10 && session.color === key)) {
|
||||||
|
game.winner = key;
|
||||||
|
game.state = 'winner';
|
||||||
|
delete game.turn.roll;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* If the game isn't in a win state, do not share development card information
|
||||||
|
* with other players */
|
||||||
|
if (game.state !== 'winner') {
|
||||||
|
for (let key in game.players) {
|
||||||
|
const player = game.players[key];
|
||||||
|
if (player.status === 'Not active') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
delete player.potential;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Shallow copy game, filling its sessions with a shallow copy of
|
||||||
|
* sessions so we can then delete the player field from them */
|
||||||
|
const reducedGame = Object.assign({}, game, { sessions: {} }),
|
||||||
|
reducedSessions = [];
|
||||||
|
|
||||||
|
for (let id in game.sessions) {
|
||||||
|
const reduced = Object.assign({}, game.sessions[id]);
|
||||||
|
if (reduced.player) {
|
||||||
|
delete reduced.player;
|
||||||
|
}
|
||||||
|
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 */
|
||||||
|
reducedSessions.push(reduced);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
return Object.assign(reducedGame, {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
status: session.error ? session.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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const robberSteal = (game, color, type) => {
|
const robberSteal = (game, color, type) => {
|
||||||
if (!game.stolen) {
|
if (!game.stolen) {
|
||||||
game.stolen = {};
|
game.stolen = {};
|
||||||
@ -3330,18 +3614,13 @@ const createGame = (id) => {
|
|||||||
return game;
|
return game;
|
||||||
};
|
};
|
||||||
|
|
||||||
router.post("/:id?", (req, res/*, next*/) => {
|
router.post("/", (req, res/*, next*/) => {
|
||||||
console.log("POST games/");
|
console.log("POST games/");
|
||||||
const { id } = req.params;
|
const game = createGame();
|
||||||
if (id && id in games) {
|
|
||||||
const error = `Can not create new game for ${id} -- it already exists.`
|
|
||||||
console.error(error);
|
|
||||||
return res.status(400).send(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const game = createGame(id);
|
saveGame(game);
|
||||||
|
|
||||||
return sendGame(req, res, game);
|
return res.status(200).send(filterGameForPlayer(game, session));
|
||||||
});
|
});
|
||||||
|
|
||||||
const setBeginnerGame = (game) => {
|
const setBeginnerGame = (game) => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user