Moved party to use socket networking

This commit is contained in:
Mitchell McCaffrey 2020-12-08 15:58:01 +11:00
parent b7da0cffa7
commit b40f78042f
6 changed files with 244 additions and 278 deletions

View File

@ -1,6 +1,7 @@
import React from "react";
import React, { useContext, useEffect } from "react";
import { Flex, Box, Text } from "theme-ui";
import SimpleBar from "simplebar-react";
import { useToasts } from "react-toast-notifications";
import AddPartyMemberButton from "./AddPartyMemberButton";
import Nickname from "./Nickname";
@ -13,26 +14,76 @@ import DiceTrayButton from "./DiceTrayButton";
import useSetting from "../../helpers/useSetting";
function Party({
nickname,
partyNicknames,
gameId,
onNicknameChange,
stream,
partyStreams,
onStreamStart,
onStreamEnd,
timer,
partyTimers,
onTimerStart,
onTimerStop,
shareDice,
onShareDiceChage,
diceRolls,
onDiceRollsChange,
partyDiceRolls,
}) {
import PlayerContext from "../../contexts/PlayerContext";
function Party({ gameId, stream, partyStreams, onStreamStart, onStreamEnd }) {
const { playerState, setPlayerState, partyState } = useContext(PlayerContext);
const [fullScreen] = useSetting("map.fullScreen");
const [shareDice, setShareDice] = useSetting("dice.shareDice");
const { addToast } = useToasts();
function handleTimerStart(newTimer) {
setPlayerState((prevState) => ({ ...prevState, timer: newTimer }));
}
function handleTimerStop() {
setPlayerState((prevState) => ({ ...prevState, timer: null }));
}
useEffect(() => {
let prevTime = performance.now();
let request = requestAnimationFrame(update);
let counter = 0;
function update(time) {
request = requestAnimationFrame(update);
const deltaTime = time - prevTime;
prevTime = time;
if (playerState.timer) {
counter += deltaTime;
// Update timer every second
if (counter > 1000) {
const newTimer = {
...playerState.timer,
current: playerState.timer.current - counter,
};
if (newTimer.current < 0) {
setPlayerState((prevState) => ({ ...prevState, timer: null }));
} else {
setPlayerState((prevState) => ({ ...prevState, timer: newTimer }));
}
counter = 0;
}
}
}
return () => {
cancelAnimationFrame(request);
};
}, [playerState, setPlayerState]);
function handleNicknameChange(newNickname) {
setPlayerState((prevState) => ({ ...prevState, nickname: newNickname }));
}
function handleDiceRollsChange(newDiceRolls) {
setPlayerState(
(prevState) => ({
...prevState,
dice: { share: shareDice, rolls: newDiceRolls },
}),
shareDice
);
}
function handleShareDiceChange(newShareDice) {
setShareDice(newShareDice);
setPlayerState((prevState) => ({
...prevState,
dice: { ...prevState.dice, share: newShareDice },
}));
}
return (
<Box
@ -74,31 +125,33 @@ function Party({
}}
>
<Nickname
nickname={`${nickname} (you)`}
diceRolls={shareDice && diceRolls}
nickname={`${playerState.nickname} (you)`}
diceRolls={shareDice && playerState.dice.rolls}
/>
{Object.entries(partyNicknames).map(([id, partyNickname]) => (
{Object.entries(partyState).map(([id, { nickname, dice }]) => (
<Nickname
nickname={partyNickname}
nickname={nickname}
key={id}
stream={partyStreams[id]}
diceRolls={partyDiceRolls[id]}
/>
))}
{timer && <Timer timer={timer} index={0} />}
{Object.entries(partyTimers).map(([id, partyTimer], index) => (
<Timer
timer={partyTimer}
key={id}
// Put party timers above your timer if there is one
index={timer ? index + 1 : index}
diceRolls={dice.share && dice.rolls}
/>
))}
{playerState.timer && <Timer timer={playerState.timer} index={0} />}
{Object.entries(partyState)
.filter(([_, { timer }]) => timer)
.map(([id, { timer }], index) => (
<Timer
timer={timer}
key={id}
// Put party timers above your timer if there is one
index={playerState.timer ? index + 1 : index}
/>
))}
</SimpleBar>
<Flex sx={{ flexDirection: "column" }}>
<ChangeNicknameButton
nickname={nickname}
onChange={onNicknameChange}
nickname={playerState.nickname}
onChange={handleNicknameChange}
/>
<AddPartyMemberButton gameId={gameId} />
<StartStreamButton
@ -107,18 +160,18 @@ function Party({
stream={stream}
/>
<StartTimerButton
onTimerStart={onTimerStart}
onTimerStop={onTimerStop}
timer={timer}
onTimerStart={handleTimerStart}
onTimerStop={handleTimerStop}
timer={playerState.timer}
/>
<SettingsButton />
</Flex>
</Box>
<DiceTrayButton
shareDice={shareDice}
onShareDiceChage={onShareDiceChage}
diceRolls={diceRolls}
onDiceRollsChange={onDiceRollsChange}
onShareDiceChage={handleShareDiceChange}
diceRolls={(playerState.dice && playerState.dice.rolls) || []}
onDiceRollsChange={handleDiceRollsChange}
/>
</Box>
);

View File

@ -3,7 +3,6 @@ import shortid from "shortid";
import DatabaseContext from "./DatabaseContext";
import { getRandomMonster } from "../helpers/monsters";
import FakeStorage from "../helpers/FakeStorage";
const AuthContext = React.createContext();
@ -48,39 +47,8 @@ export function AuthProvider({ children }) {
loadUserId();
}, [database, databaseStatus]);
const [nickname, setNickname] = useState("");
useEffect(() => {
if (!database || databaseStatus === "loading") {
return;
}
async function loadNickname() {
const storedNickname = await database.table("user").get("nickname");
if (storedNickname) {
setNickname(storedNickname.value);
} else {
const name = getRandomMonster();
setNickname(name);
database.table("user").add({ key: "nickname", value: name });
}
}
loadNickname();
}, [database, databaseStatus]);
useEffect(() => {
if (
nickname !== undefined &&
database !== undefined &&
databaseStatus !== "loading"
) {
database.table("user").update("nickname", { value: nickname });
}
}, [nickname, database, databaseStatus]);
const value = {
userId,
nickname,
setNickname,
password,
setPassword,
authenticationStatus,

View File

@ -0,0 +1,96 @@
import React, { useState, useEffect, useContext } from "react";
import useNetworkedState from "../helpers/useNetworkedState";
import DatabaseContext from "./DatabaseContext";
import { getRandomMonster } from "../helpers/monsters";
const PlayerContext = React.createContext();
export function PlayerProvider({ session, children }) {
const { database, databaseStatus } = useContext(DatabaseContext);
const [playerState, setPlayerState] = useNetworkedState(
{
nickname: "",
timer: null,
dice: { share: false, rolls: [] },
pointer: {},
},
session,
"player_state"
);
const [partyState, setPartyState] = useState({});
useEffect(() => {
if (!database || databaseStatus === "loading") {
return;
}
async function loadNickname() {
const storedNickname = await database.table("user").get("nickname");
if (storedNickname !== undefined) {
setPlayerState((prevState) => ({
...prevState,
nickname: storedNickname.value,
}));
} else {
const name = getRandomMonster();
setPlayerState((prevState) => ({ ...prevState, nickname: name }));
database.table("user").add({ key: "nickname", value: name });
}
}
loadNickname();
}, [database, databaseStatus]);
useEffect(() => {
if (
playerState.nickname &&
database !== undefined &&
databaseStatus !== "loading"
) {
database
.table("user")
.update("nickname", { value: playerState.nickname });
}
}, [playerState, database, databaseStatus]);
useEffect(() => {
function handleConnected() {
// Set the player state to trigger a sync
setPlayerState(playerState);
}
function handleSocketPartyState(partyState) {
if (partyState) {
const { [session.id]: _, ...otherMembersState } = partyState;
setPartyState(otherMembersState);
} else {
setPartyState({});
}
}
if (session.socket) {
session.on("connected", handleConnected);
session.socket.on("party_state", handleSocketPartyState);
}
return () => {
if (session.socket) {
session.off("connected", handleConnected);
session.socket.off("party_state", handleSocketPartyState);
}
};
});
const value = {
playerState,
setPlayerState,
partyState,
};
return (
<PlayerContext.Provider value={value}>{children}</PlayerContext.Provider>
);
}
export default PlayerContext;

View File

@ -1,22 +1,22 @@
import { useEffect, useState, useRef } from "react";
import { useEffect, useState } from "react";
function useNetworkedState(defaultState, session, eventName) {
const [state, _setState] = useState(defaultState);
// Used to control whether the state needs to be sent to the socket
const dirtyRef = useRef(false);
const [dirty, setDirty] = useState(false);
// Update dirty at the same time as state
function setState(update, sync = true) {
dirtyRef.current = sync;
_setState(update);
setDirty(sync);
}
useEffect(() => {
if (dirtyRef.current) {
if (session.socket && dirty) {
session.socket.emit(eventName, state);
dirtyRef.current = false;
setDirty(false);
}
}, [state, eventName]);
}, [session.socket, dirty, eventName, state]);
return [state, setState];
}

View File

@ -1,13 +1,9 @@
import React, { useContext, useState, useEffect, useCallback } from "react";
import { useToasts } from "react-toast-notifications";
// Load session for auto complete
// eslint-disable-next-line no-unused-vars
import Session from "./Session";
import { isStreamStopped, omit, fromEntries } from "../helpers/shared";
import AuthContext from "../contexts/AuthContext";
import useSetting from "../helpers/useSetting";
import { isStreamStopped, omit } from "../helpers/shared";
import Party from "../components/party/Party";
@ -21,23 +17,8 @@ import Party from "../components/party/Party";
* @param {NetworkedPartyProps} props
*/
function NetworkedParty({ gameId, session }) {
const { nickname, setNickname } = useContext(AuthContext);
const [partyNicknames, setPartyNicknames] = useState({});
const [stream, setStream] = useState(null);
const [partyStreams, setPartyStreams] = useState({});
const [timer, setTimer] = useState(null);
const [partyTimers, setPartyTimers] = useState({});
const [diceRolls, setDiceRolls] = useState([]);
const [partyDiceRolls, setPartyDiceRolls] = useState({});
const { addToast } = useToasts();
const [shareDice, setShareDice] = useSetting("dice.shareDice");
function handleNicknameChange(newNickname) {
setNickname(newNickname);
session.send("nickname", { [session.id]: newNickname });
}
function handleStreamStart(localStream) {
setStream(localStream);
@ -69,121 +50,11 @@ function NetworkedParty({ gameId, session }) {
[session]
);
function handleTimerStart(newTimer) {
setTimer(newTimer);
session.send("timer", { [session.id]: newTimer });
}
function handleTimerStop() {
setTimer(null);
session.send("timer", { [session.id]: null });
}
useEffect(() => {
let prevTime = performance.now();
let request = requestAnimationFrame(update);
let counter = 0;
function update(time) {
request = requestAnimationFrame(update);
const deltaTime = time - prevTime;
prevTime = time;
if (timer) {
counter += deltaTime;
// Update timer every second
if (counter > 1000) {
const newTimer = {
...timer,
current: timer.current - counter,
};
if (newTimer.current < 0) {
setTimer(null);
session.send("timer", { [session.id]: null });
} else {
setTimer(newTimer);
session.send("timer", { [session.id]: newTimer });
}
counter = 0;
}
}
}
return () => {
cancelAnimationFrame(request);
};
}, [timer, session]);
function handleDiceRollsChange(newDiceRolls) {
setDiceRolls(newDiceRolls);
if (shareDice) {
session.send("dice", { [session.id]: newDiceRolls });
}
}
function handleShareDiceChange(newShareDice) {
setShareDice(newShareDice);
if (newShareDice) {
session.send("dice", { [session.id]: diceRolls });
} else {
session.send("dice", { [session.id]: null });
}
}
useEffect(() => {
function handlePeerConnect({ peer, reply }) {
reply("nickname", { [session.id]: nickname });
if (stream) {
peer.connection.addStream(stream);
}
if (timer) {
reply("timer", { [session.id]: timer });
}
if (shareDice) {
reply("dice", { [session.id]: diceRolls });
}
}
function handlePeerDisconnect({ peer }) {
if (partyNicknames[peer.id]) {
addToast(`${partyNicknames[peer.id]} left the party`);
}
setPartyNicknames((prevNicknames) => omit(prevNicknames, [peer.id]));
setPartyTimers((prevTimers) => omit(prevTimers, [peer.id]));
}
function handlePeerData({ id, data, peer }) {
if (id === "nickname") {
if (!peer.initiator) {
for (let peerId in data) {
if (!(peerId in partyNicknames)) {
addToast(`${data[peerId]} joined the party`);
}
}
}
setPartyNicknames((prevNicknames) => ({
...prevNicknames,
...data,
}));
}
if (id === "timer") {
setPartyTimers((prevTimers) => {
const newTimers = { ...prevTimers, ...data };
// filter out timers that are null
const filtered = Object.entries(newTimers).filter(
([, value]) => value !== null
);
return fromEntries(filtered);
});
}
if (id === "dice") {
setPartyDiceRolls((prevDiceRolls) => {
const newRolls = { ...prevDiceRolls, ...data };
// filter out dice rolls that are null
const filtered = Object.entries(newRolls).filter(
([, value]) => value !== null
);
return fromEntries(filtered);
});
}
}
function handlePeerTrackAdded({ peer, stream: remoteStream }) {
@ -205,28 +76,15 @@ function NetworkedParty({ gameId, session }) {
}
session.on("connect", handlePeerConnect);
session.on("disconnect", handlePeerDisconnect);
session.on("data", handlePeerData);
session.on("trackAdded", handlePeerTrackAdded);
session.on("trackRemoved", handlePeerTrackRemoved);
return () => {
session.off("connect", handlePeerConnect);
session.off("disconnect", handlePeerDisconnect);
session.off("data", handlePeerData);
session.off("trackAdded", handlePeerTrackAdded);
session.off("trackRemoved", handlePeerTrackRemoved);
};
}, [
session,
nickname,
stream,
timer,
shareDice,
diceRolls,
partyNicknames,
addToast,
]);
});
useEffect(() => {
if (stream) {
@ -248,22 +106,10 @@ function NetworkedParty({ gameId, session }) {
<>
<Party
gameId={gameId}
onNicknameChange={handleNicknameChange}
onStreamStart={handleStreamStart}
onStreamEnd={handleStreamEnd}
nickname={nickname}
partyNicknames={partyNicknames}
stream={stream}
partyStreams={partyStreams}
timer={timer}
partyTimers={partyTimers}
onTimerStart={handleTimerStart}
onTimerStop={handleTimerStop}
shareDice={shareDice}
onShareDiceChage={handleShareDiceChange}
diceRolls={diceRolls}
onDiceRollsChange={handleDiceRollsChange}
partyDiceRolls={partyDiceRolls}
/>
</>
);

View File

@ -12,6 +12,7 @@ import AuthModal from "../modals/AuthModal";
import AuthContext from "../contexts/AuthContext";
import { MapStageProvider } from "../contexts/MapStageContext";
import DatabaseContext from "../contexts/DatabaseContext";
import { PlayerProvider } from "../contexts/PlayerContext";
import NetworkedMapAndTokens from "../network/NetworkedMapAndTokens";
import NetworkedParty from "../network/NetworkedParty";
@ -102,49 +103,51 @@ function Game() {
const mapStageRef = useRef();
return (
<MapStageProvider value={mapStageRef}>
<Flex sx={{ flexDirection: "column", height: "100%" }}>
<Flex
sx={{
justifyContent: "space-between",
flexGrow: 1,
height: "100%",
}}
>
<NetworkedParty session={session} gameId={gameId} />
<NetworkedMapAndTokens session={session} />
<PlayerProvider session={session}>
<MapStageProvider value={mapStageRef}>
<Flex sx={{ flexDirection: "column", height: "100%" }}>
<Flex
sx={{
justifyContent: "space-between",
flexGrow: 1,
height: "100%",
}}
>
<NetworkedParty session={session} gameId={gameId} />
<NetworkedMapAndTokens session={session} />
</Flex>
</Flex>
</Flex>
<Banner isOpen={!!peerError} onRequestClose={() => setPeerError(null)}>
<Box p={1}>
<Text as="p" variant="body2">
{peerError} See <Link to="/faq#connection">FAQ</Link> for more
information.
</Text>
</Box>
</Banner>
<Banner isOpen={offline} onRequestClose={() => {}} allowClose={false}>
<Box p={1}>
<Text as="p" variant="body2">
Unable to connect to game, refresh to reconnect.
</Text>
</Box>
</Banner>
<Banner
isOpen={!connected && authenticationStatus === "authenticated"}
onRequestClose={() => {}}
allowClose={false}
>
<Box p={1}>
<Text as="p" variant="body2">
Disconnected. Attempting to reconnect...
</Text>
</Box>
</Banner>
<AuthModal isOpen={authenticationStatus === "unauthenticated"} />
{authenticationStatus === "unknown" && !offline && <LoadingOverlay />}
<MapLoadingOverlay />
</MapStageProvider>
<Banner isOpen={!!peerError} onRequestClose={() => setPeerError(null)}>
<Box p={1}>
<Text as="p" variant="body2">
{peerError} See <Link to="/faq#connection">FAQ</Link> for more
information.
</Text>
</Box>
</Banner>
<Banner isOpen={offline} onRequestClose={() => {}} allowClose={false}>
<Box p={1}>
<Text as="p" variant="body2">
Unable to connect to game, refresh to reconnect.
</Text>
</Box>
</Banner>
<Banner
isOpen={!connected && authenticationStatus === "authenticated"}
onRequestClose={() => {}}
allowClose={false}
>
<Box p={1}>
<Text as="p" variant="body2">
Disconnected. Attempting to reconnect...
</Text>
</Box>
</Banner>
<AuthModal isOpen={authenticationStatus === "unauthenticated"} />
{authenticationStatus === "unknown" && !offline && <LoadingOverlay />}
<MapLoadingOverlay />
</MapStageProvider>
</PlayerProvider>
);
}