Refactored session management to allow for decoupled network and game routes
This commit is contained in:
parent
25aa24199b
commit
fc76e3690a
@ -8,7 +8,7 @@ import blobToBuffer from "./blobToBuffer";
|
||||
// http://viblast.com/blog/2015/2/5/webrtc-data-channel-message-size/
|
||||
const MAX_BUFFER_SIZE = 16000;
|
||||
|
||||
class Peer extends SimplePeer {
|
||||
class Connection extends SimplePeer {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.currentChunks = {};
|
||||
@ -126,4 +126,4 @@ class Peer extends SimplePeer {
|
||||
}
|
||||
}
|
||||
|
||||
export default Peer;
|
||||
export default Connection;
|
232
src/helpers/Session.js
Normal file
232
src/helpers/Session.js
Normal file
@ -0,0 +1,232 @@
|
||||
import io from "socket.io-client";
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
import Connection from "./Connection";
|
||||
|
||||
/**
|
||||
* @typedef {object} SessionPeer
|
||||
* @property {string} id - The socket id of the peer
|
||||
* @property {Connection} connection - The actual peer connection
|
||||
* @property {boolean} initiator - Is this peer the initiator of the connection
|
||||
* @property {boolean} sync - Should this connection sync other connections
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* Handles connections to multiple peers
|
||||
*
|
||||
* Events:
|
||||
* - connect: A party member has connected
|
||||
* - data
|
||||
* - trackAdded
|
||||
* - trackRemoved
|
||||
* - disconnect: A party member has disconnected
|
||||
* - error
|
||||
* - authenticationSuccess
|
||||
* - authenticationError
|
||||
* - connected: You have connected
|
||||
* - disconnected: You have disconnected
|
||||
*/
|
||||
class Session extends EventEmitter {
|
||||
/**
|
||||
* The socket io connection
|
||||
*
|
||||
* @type {SocketIOClient.Socket}
|
||||
*/
|
||||
socket;
|
||||
|
||||
/**
|
||||
* A mapping of socket ids to session peers
|
||||
*
|
||||
* @type {Object.<string, SessionPeer>}
|
||||
*/
|
||||
peers;
|
||||
|
||||
get id() {
|
||||
return this.socket.id;
|
||||
}
|
||||
|
||||
_iceServers;
|
||||
|
||||
// Store party id and password for reconnect
|
||||
_partyId;
|
||||
_password;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.socket = io(process.env.REACT_APP_BROKER_URL);
|
||||
|
||||
this.socket.on(
|
||||
"party member joined",
|
||||
this._handlePartyMemberJoined.bind(this)
|
||||
);
|
||||
this.socket.on("party member left", this._handlePartyMemberLeft.bind(this));
|
||||
this.socket.on("joined party", this._handleJoinedParty.bind(this));
|
||||
this.socket.on("signal", this._handleSignal.bind(this));
|
||||
this.socket.on("auth error", this._handleAuthError.bind(this));
|
||||
this.socket.on("disconnect", this._handleSocketDisconnect.bind(this));
|
||||
this.socket.on("reconnect", this._handleSocketReconnect.bind(this));
|
||||
|
||||
this.peers = {};
|
||||
|
||||
// Signal connected peers of a closure on refresh
|
||||
window.addEventListener("beforeunload", this._handleUnload.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Send data to all connected peers
|
||||
*
|
||||
* @param {string} id - the id of the event to send
|
||||
* @param {object} data
|
||||
* @param {string} channel
|
||||
*/
|
||||
send(id, data, channel) {
|
||||
for (let peer of Object.values(this.peers)) {
|
||||
peer.connection.send({ id, data }, channel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Join a party
|
||||
*
|
||||
* @param {string} partyId - the id of the party to join
|
||||
* @param {string} password - the password of the party
|
||||
*/
|
||||
async joinParty(partyId, password) {
|
||||
this._partyId = partyId;
|
||||
this._password = password;
|
||||
try {
|
||||
const response = await fetch(process.env.REACT_APP_ICE_SERVERS_URL);
|
||||
const data = await response.json();
|
||||
this._iceServers = data.iceServers;
|
||||
this.socket.emit("join party", partyId, password);
|
||||
} catch (e) {
|
||||
console.error("Unable to join party:", e.message);
|
||||
this.emit("disconnected");
|
||||
}
|
||||
}
|
||||
|
||||
_addPeer(id, initiator, sync) {
|
||||
try {
|
||||
const connection = new Connection({
|
||||
initiator,
|
||||
trickle: true,
|
||||
config: { iceServers: this._iceServers },
|
||||
});
|
||||
if (initiator) {
|
||||
connection.createDataChannel("map", { iceServers: this._iceServers });
|
||||
connection.createDataChannel("token", { iceServers: this._iceServers });
|
||||
}
|
||||
const peer = { id, connection, initiator, sync };
|
||||
|
||||
function sendPeer(id, data) {
|
||||
peer.connection.send({ id, data });
|
||||
}
|
||||
|
||||
function handleSignal(signal) {
|
||||
this.socket.emit("signal", JSON.stringify({ to: peer.id, signal }));
|
||||
}
|
||||
|
||||
function handleConnect() {
|
||||
this.emit("connect", { peer, reply: sendPeer });
|
||||
if (peer.sync) {
|
||||
peer.connection.send({ id: "sync" });
|
||||
}
|
||||
}
|
||||
|
||||
function handleDataComplete(data) {
|
||||
if (data.id === "close") {
|
||||
// Close connection when signaled to close
|
||||
peer.connection.destroy();
|
||||
}
|
||||
this.emit("data", {
|
||||
peer,
|
||||
id: data.id,
|
||||
data: data.data,
|
||||
reply: sendPeer,
|
||||
});
|
||||
}
|
||||
|
||||
function handleDataProgress({ id, count, total }) {
|
||||
this.emit("dataProgress", { peer, id, count, total, reply: sendPeer });
|
||||
}
|
||||
|
||||
function handleTrack(track, stream) {
|
||||
this.emit("trackAdded", { peer, track, stream });
|
||||
track.addEventListener("mute", () => {
|
||||
this.emit("trackRemoved", { peer, track, stream });
|
||||
});
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
peer.connection.destroy();
|
||||
this.emit("disconnect", { peer });
|
||||
}
|
||||
|
||||
function handleError(error) {
|
||||
this.emit("error", { peer, error });
|
||||
}
|
||||
|
||||
peer.connection.on("signal", handleSignal.bind(this));
|
||||
peer.connection.on("connect", handleConnect.bind(this));
|
||||
peer.connection.on("dataComplete", handleDataComplete.bind(this));
|
||||
peer.connection.on("dataProgress", handleDataProgress.bind(this));
|
||||
peer.connection.on("track", handleTrack.bind(this));
|
||||
peer.connection.on("close", handleClose.bind(this));
|
||||
peer.connection.on("error", handleError.bind(this));
|
||||
|
||||
this.peers[id] = peer;
|
||||
} catch (error) {
|
||||
this.emit("error", { error });
|
||||
}
|
||||
}
|
||||
|
||||
_handlePartyMemberJoined(id) {
|
||||
this._addPeer(id, false, false);
|
||||
}
|
||||
|
||||
_handlePartyMemberLeft(id) {
|
||||
if (id in this.peers) {
|
||||
this.peers[id].connection.destroy();
|
||||
delete this.peers[id];
|
||||
}
|
||||
}
|
||||
|
||||
_handleJoinedParty(otherIds) {
|
||||
for (let [index, id] of otherIds.entries()) {
|
||||
// Send a sync request to the first member of the party
|
||||
const sync = index === 0;
|
||||
this._addPeer(id, true, sync);
|
||||
}
|
||||
this.emit("authenticationSuccess");
|
||||
this.emit("connected");
|
||||
}
|
||||
|
||||
_handleSignal(data) {
|
||||
const { from, signal } = JSON.parse(data);
|
||||
if (from in this.peers) {
|
||||
this.peers[from].connection.signal(signal);
|
||||
}
|
||||
}
|
||||
|
||||
_handleAuthError() {
|
||||
this.emit("authenticationError");
|
||||
}
|
||||
|
||||
_handleUnload() {
|
||||
for (let peer of Object.values(this.peers)) {
|
||||
peer.connection.send({ id: "close" });
|
||||
}
|
||||
}
|
||||
|
||||
_handleSocketDisconnect() {
|
||||
this.emit("disconnected");
|
||||
}
|
||||
|
||||
_handleSocketReconnect() {
|
||||
this.emit("connected");
|
||||
this.joinParty(this._partyId, this._password);
|
||||
}
|
||||
}
|
||||
|
||||
export default Session;
|
@ -1,239 +0,0 @@
|
||||
import { useEffect, useState, useContext, useCallback } from "react";
|
||||
import io from "socket.io-client";
|
||||
|
||||
import { omit } from "../helpers/shared";
|
||||
import Peer from "../helpers/Peer";
|
||||
|
||||
import AuthContext from "../contexts/AuthContext";
|
||||
|
||||
const socket = io(process.env.REACT_APP_BROKER_URL);
|
||||
|
||||
function useSession(
|
||||
partyId,
|
||||
onPeerConnected,
|
||||
onPeerDisconnected,
|
||||
onPeerData,
|
||||
onPeerDataProgress,
|
||||
onPeerTrackAdded,
|
||||
onPeerTrackRemoved,
|
||||
onPeerError
|
||||
) {
|
||||
const { password, setAuthenticationStatus } = useContext(AuthContext);
|
||||
const [iceServers, setIceServers] = useState([]);
|
||||
const [connected, setConnected] = useState(false);
|
||||
|
||||
const joinParty = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetch(process.env.REACT_APP_ICE_SERVERS_URL);
|
||||
const data = await response.json();
|
||||
setIceServers(data.iceServers);
|
||||
socket.emit("join party", partyId, password);
|
||||
} catch (e) {
|
||||
console.error("Unable to join party:", e.message);
|
||||
setConnected(false);
|
||||
}
|
||||
}, [partyId, password]);
|
||||
|
||||
useEffect(() => {
|
||||
joinParty();
|
||||
}, [partyId, password, joinParty]);
|
||||
|
||||
const [peers, setPeers] = useState({});
|
||||
|
||||
// Signal connected peers of a closure on refresh
|
||||
useEffect(() => {
|
||||
function handleUnload() {
|
||||
for (let peer of Object.values(peers)) {
|
||||
peer.connection.send({ id: "close" });
|
||||
}
|
||||
}
|
||||
window.addEventListener("beforeunload", handleUnload);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("beforeunload", handleUnload);
|
||||
};
|
||||
}, [peers]);
|
||||
|
||||
// Setup event listeners for peers
|
||||
useEffect(() => {
|
||||
let peerEvents = [];
|
||||
for (let peer of Object.values(peers)) {
|
||||
function handleSignal(signal) {
|
||||
socket.emit("signal", JSON.stringify({ to: peer.id, signal }));
|
||||
}
|
||||
|
||||
function handleConnect() {
|
||||
onPeerConnected && onPeerConnected(peer);
|
||||
if (peer.sync) {
|
||||
peer.connection.send({ id: "sync" });
|
||||
}
|
||||
}
|
||||
|
||||
function handleDataComplete(data) {
|
||||
if (data.id === "close") {
|
||||
// Close connection when signaled to close
|
||||
peer.connection.destroy();
|
||||
}
|
||||
onPeerData && onPeerData({ peer, data });
|
||||
}
|
||||
|
||||
function handleDataProgress({ id, count, total }) {
|
||||
onPeerDataProgress && onPeerDataProgress({ id, count, total });
|
||||
}
|
||||
|
||||
function handleTrack(track, stream) {
|
||||
onPeerTrackAdded && onPeerTrackAdded({ peer, track, stream });
|
||||
track.addEventListener("mute", () => {
|
||||
onPeerTrackRemoved && onPeerTrackRemoved({ peer, track, stream });
|
||||
});
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
onPeerDisconnected && onPeerDisconnected(peer);
|
||||
peer.connection.destroy();
|
||||
setPeers((prevPeers) => omit(prevPeers, [peer.id]));
|
||||
}
|
||||
|
||||
function handleError(error) {
|
||||
onPeerError && onPeerError({ peer, error });
|
||||
}
|
||||
|
||||
peer.connection.on("signal", handleSignal);
|
||||
peer.connection.on("connect", handleConnect);
|
||||
peer.connection.on("dataComplete", handleDataComplete);
|
||||
peer.connection.on("dataProgress", handleDataProgress);
|
||||
peer.connection.on("track", handleTrack);
|
||||
peer.connection.on("close", handleClose);
|
||||
peer.connection.on("error", handleError);
|
||||
// Save events for cleanup
|
||||
peerEvents.push({
|
||||
peer,
|
||||
handleSignal,
|
||||
handleConnect,
|
||||
handleDataComplete,
|
||||
handleDataProgress,
|
||||
handleTrack,
|
||||
handleClose,
|
||||
handleError,
|
||||
});
|
||||
}
|
||||
|
||||
// Cleanup events
|
||||
return () => {
|
||||
for (let {
|
||||
peer,
|
||||
handleSignal,
|
||||
handleConnect,
|
||||
handleDataComplete,
|
||||
handleDataProgress,
|
||||
handleTrack,
|
||||
handleClose,
|
||||
handleError,
|
||||
} of peerEvents) {
|
||||
peer.connection.off("signal", handleSignal);
|
||||
peer.connection.off("connect", handleConnect);
|
||||
peer.connection.off("dataComplete", handleDataComplete);
|
||||
peer.connection.off("dataProgress", handleDataProgress);
|
||||
peer.connection.off("track", handleTrack);
|
||||
peer.connection.off("close", handleClose);
|
||||
peer.connection.off("error", handleError);
|
||||
}
|
||||
};
|
||||
}, [
|
||||
peers,
|
||||
onPeerConnected,
|
||||
onPeerDisconnected,
|
||||
onPeerData,
|
||||
onPeerDataProgress,
|
||||
onPeerTrackAdded,
|
||||
onPeerTrackRemoved,
|
||||
onPeerError,
|
||||
]);
|
||||
|
||||
// Setup event listeners for the socket
|
||||
useEffect(() => {
|
||||
function addPeer(id, initiator, sync) {
|
||||
try {
|
||||
const connection = new Peer({
|
||||
initiator,
|
||||
trickle: true,
|
||||
config: { iceServers },
|
||||
});
|
||||
if (initiator) {
|
||||
connection.createDataChannel("map", { iceServers });
|
||||
connection.createDataChannel("token", { iceServers });
|
||||
}
|
||||
setPeers((prevPeers) => ({
|
||||
...prevPeers,
|
||||
[id]: { id, connection, initiator, sync },
|
||||
}));
|
||||
} catch (error) {
|
||||
onPeerError && onPeerError({ error });
|
||||
}
|
||||
}
|
||||
|
||||
function handlePartyMemberJoined(id) {
|
||||
addPeer(id, false, false);
|
||||
}
|
||||
|
||||
function handlePartyMemberLeft(id) {
|
||||
if (id in peers) {
|
||||
peers[id].connection.destroy();
|
||||
setPeers((prevPeers) => omit(prevPeers, [id]));
|
||||
}
|
||||
}
|
||||
|
||||
function handleJoinedParty(otherIds) {
|
||||
for (let [index, id] of otherIds.entries()) {
|
||||
// Send a sync request to the first member of the party
|
||||
const sync = index === 0;
|
||||
addPeer(id, true, sync);
|
||||
}
|
||||
setAuthenticationStatus("authenticated");
|
||||
setConnected(true);
|
||||
}
|
||||
|
||||
function handleSignal(data) {
|
||||
const { from, signal } = JSON.parse(data);
|
||||
if (from in peers) {
|
||||
peers[from].connection.signal(signal);
|
||||
}
|
||||
}
|
||||
|
||||
function handleAuthError() {
|
||||
setAuthenticationStatus("unauthenticated");
|
||||
}
|
||||
|
||||
function handleSocketDisconnect() {
|
||||
setConnected(false);
|
||||
}
|
||||
|
||||
function handleSocketReconnect() {
|
||||
setConnected(true);
|
||||
joinParty();
|
||||
}
|
||||
|
||||
socket.on("disconnect", handleSocketDisconnect);
|
||||
socket.on("reconnect", handleSocketReconnect);
|
||||
|
||||
socket.on("party member joined", handlePartyMemberJoined);
|
||||
socket.on("party member left", handlePartyMemberLeft);
|
||||
socket.on("joined party", handleJoinedParty);
|
||||
socket.on("signal", handleSignal);
|
||||
socket.on("auth error", handleAuthError);
|
||||
return () => {
|
||||
socket.off("disconnect", handleSocketDisconnect);
|
||||
socket.off("reconnect", handleSocketReconnect);
|
||||
|
||||
socket.off("party member joined", handlePartyMemberJoined);
|
||||
socket.off("party member left", handlePartyMemberLeft);
|
||||
socket.off("joined party", handleJoinedParty);
|
||||
socket.off("signal", handleSignal);
|
||||
socket.off("auth error", handleAuthError);
|
||||
};
|
||||
}, [peers, setAuthenticationStatus, iceServers, joinParty, onPeerError]);
|
||||
|
||||
return { peers, socket, connected };
|
||||
}
|
||||
|
||||
export default useSession;
|
413
src/network/NetworkedMapAndTokens.js
Normal file
413
src/network/NetworkedMapAndTokens.js
Normal file
@ -0,0 +1,413 @@
|
||||
import React, { useState, useContext, useEffect, useCallback } from "react";
|
||||
|
||||
import TokenDataContext from "../contexts/TokenDataContext";
|
||||
import MapDataContext from "../contexts/MapDataContext";
|
||||
import MapLoadingContext from "../contexts/MapLoadingContext";
|
||||
import AuthContext from "../contexts/AuthContext";
|
||||
import DatabaseContext from "../contexts/DatabaseContext";
|
||||
|
||||
import { omit } from "../helpers/shared";
|
||||
import useDebounce from "../helpers/useDebounce";
|
||||
// Load session for auto complete
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import Session from "../helpers/Session";
|
||||
|
||||
import Map from "../components/map/Map";
|
||||
import Tokens from "../components/token/Tokens";
|
||||
|
||||
/**
|
||||
* @typedef {object} NetworkedMapProps
|
||||
* @property {Session} session
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {NetworkedMapProps} props
|
||||
*/
|
||||
function NetworkedMapAndTokens({ session }) {
|
||||
const { userId } = useContext(AuthContext);
|
||||
const { assetLoadStart, assetLoadFinish, assetProgressUpdate } = useContext(
|
||||
MapLoadingContext
|
||||
);
|
||||
|
||||
const { putToken, getToken } = useContext(TokenDataContext);
|
||||
const { putMap, getMap, updateMap } = useContext(MapDataContext);
|
||||
|
||||
const [currentMap, setCurrentMap] = useState(null);
|
||||
const [currentMapState, setCurrentMapState] = useState(null);
|
||||
|
||||
/**
|
||||
* Map state
|
||||
*/
|
||||
|
||||
const { database } = useContext(DatabaseContext);
|
||||
// Sync the map state to the database after 500ms of inactivity
|
||||
const debouncedMapState = useDebounce(currentMapState, 500);
|
||||
useEffect(() => {
|
||||
if (
|
||||
debouncedMapState &&
|
||||
debouncedMapState.mapId &&
|
||||
currentMap &&
|
||||
currentMap.owner === userId &&
|
||||
database
|
||||
) {
|
||||
// Update the database directly to avoid re-renders
|
||||
database
|
||||
.table("states")
|
||||
.update(debouncedMapState.mapId, debouncedMapState);
|
||||
}
|
||||
}, [currentMap, debouncedMapState, userId, database]);
|
||||
|
||||
function handleMapChange(newMap, newMapState) {
|
||||
setCurrentMapState(newMapState);
|
||||
setCurrentMap(newMap);
|
||||
session.send("map", null, "map");
|
||||
session.send("mapState", newMapState, "map");
|
||||
session.send("map", getMapDataToSend(newMap), "map");
|
||||
const tokensToSend = getMapTokensToSend(newMapState);
|
||||
for (let token of tokensToSend) {
|
||||
session.send("token", token);
|
||||
}
|
||||
}
|
||||
|
||||
function getMapDataToSend(mapData) {
|
||||
// Omit file from map change, receiver will request the file if
|
||||
// they have an outdated version
|
||||
if (mapData.type === "file") {
|
||||
const { file, resolutions, ...rest } = mapData;
|
||||
return rest;
|
||||
} else {
|
||||
return mapData;
|
||||
}
|
||||
}
|
||||
|
||||
function handleMapStateChange(newMapState) {
|
||||
setCurrentMapState(newMapState);
|
||||
session.send("mapState", newMapState);
|
||||
}
|
||||
|
||||
function addMapDrawActions(actions, indexKey, actionsKey) {
|
||||
setCurrentMapState((prevMapState) => {
|
||||
const newActions = [
|
||||
...prevMapState[actionsKey].slice(0, prevMapState[indexKey] + 1),
|
||||
...actions,
|
||||
];
|
||||
const newIndex = newActions.length - 1;
|
||||
return {
|
||||
...prevMapState,
|
||||
[actionsKey]: newActions,
|
||||
[indexKey]: newIndex,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function updateDrawActionIndex(change, indexKey, actionsKey) {
|
||||
const newIndex = Math.min(
|
||||
Math.max(currentMapState[indexKey] + change, -1),
|
||||
currentMapState[actionsKey].length - 1
|
||||
);
|
||||
|
||||
setCurrentMapState((prevMapState) => ({
|
||||
...prevMapState,
|
||||
[indexKey]: newIndex,
|
||||
}));
|
||||
return newIndex;
|
||||
}
|
||||
|
||||
function handleMapDraw(action) {
|
||||
addMapDrawActions([action], "mapDrawActionIndex", "mapDrawActions");
|
||||
session.send("mapDraw", [action]);
|
||||
}
|
||||
|
||||
function handleMapDrawUndo() {
|
||||
const index = updateDrawActionIndex(
|
||||
-1,
|
||||
"mapDrawActionIndex",
|
||||
"mapDrawActions"
|
||||
);
|
||||
session.send("mapDrawIndex", index);
|
||||
}
|
||||
|
||||
function handleMapDrawRedo() {
|
||||
const index = updateDrawActionIndex(
|
||||
1,
|
||||
"mapDrawActionIndex",
|
||||
"mapDrawActions"
|
||||
);
|
||||
session.send("mapDrawIndex", index);
|
||||
}
|
||||
|
||||
function handleFogDraw(action) {
|
||||
addMapDrawActions([action], "fogDrawActionIndex", "fogDrawActions");
|
||||
session.send("mapFog", [action]);
|
||||
}
|
||||
|
||||
function handleFogDrawUndo() {
|
||||
const index = updateDrawActionIndex(
|
||||
-1,
|
||||
"fogDrawActionIndex",
|
||||
"fogDrawActions"
|
||||
);
|
||||
session.send("mapFogIndex", index);
|
||||
}
|
||||
|
||||
function handleFogDrawRedo() {
|
||||
const index = updateDrawActionIndex(
|
||||
1,
|
||||
"fogDrawActionIndex",
|
||||
"fogDrawActions"
|
||||
);
|
||||
session.send("mapFogIndex", index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Token state
|
||||
*/
|
||||
|
||||
// Get all tokens from a token state
|
||||
const getMapTokensToSend = useCallback(
|
||||
(state) => {
|
||||
let sentTokens = {};
|
||||
const tokens = [];
|
||||
for (let tokenState of Object.values(state.tokens)) {
|
||||
const token = getToken(tokenState.tokenId);
|
||||
if (
|
||||
token &&
|
||||
token.type === "file" &&
|
||||
!(tokenState.tokenId in sentTokens)
|
||||
) {
|
||||
sentTokens[tokenState.tokenId] = true;
|
||||
// Omit file from token peer will request file if needed
|
||||
const { file, ...rest } = token;
|
||||
tokens.add(rest);
|
||||
}
|
||||
}
|
||||
return tokens;
|
||||
},
|
||||
[getToken]
|
||||
);
|
||||
|
||||
async function handleMapTokenStateCreate(tokenState) {
|
||||
// If file type token send the token to the other peers
|
||||
const token = getToken(tokenState.tokenId);
|
||||
if (token && token.type === "file") {
|
||||
const { file, ...rest } = token;
|
||||
session.send("token", rest);
|
||||
}
|
||||
handleMapTokenStateChange({ [tokenState.id]: tokenState });
|
||||
}
|
||||
|
||||
function handleMapTokenStateChange(change) {
|
||||
if (currentMapState === null) {
|
||||
return;
|
||||
}
|
||||
setCurrentMapState((prevMapState) => ({
|
||||
...prevMapState,
|
||||
tokens: {
|
||||
...prevMapState.tokens,
|
||||
...change,
|
||||
},
|
||||
}));
|
||||
session.send("tokenStateEdit", change);
|
||||
}
|
||||
|
||||
function handleMapTokenStateRemove(tokenState) {
|
||||
setCurrentMapState((prevMapState) => {
|
||||
const { [tokenState.id]: old, ...rest } = prevMapState.tokens;
|
||||
return { ...prevMapState, tokens: rest };
|
||||
});
|
||||
session.send("tokenStateRemove", { [tokenState.id]: tokenState });
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
async function handlePeerData({ id, data, reply }) {
|
||||
if (id === "sync") {
|
||||
if (currentMapState) {
|
||||
reply("mapState", currentMapState);
|
||||
const tokensToSend = getMapTokensToSend(currentMapState);
|
||||
for (let token of tokensToSend) {
|
||||
reply("token", token);
|
||||
}
|
||||
}
|
||||
if (currentMap) {
|
||||
reply("map", getMapDataToSend(currentMap));
|
||||
}
|
||||
}
|
||||
if (id === "map") {
|
||||
const newMap = data;
|
||||
if (newMap && newMap.type === "file") {
|
||||
const cachedMap = getMap(newMap.id);
|
||||
if (cachedMap && cachedMap.lastModified === newMap.lastModified) {
|
||||
setCurrentMap(cachedMap);
|
||||
} else {
|
||||
await putMap(newMap);
|
||||
reply("mapRequest", newMap.id, "map");
|
||||
}
|
||||
} else {
|
||||
setCurrentMap(newMap);
|
||||
}
|
||||
}
|
||||
if (id === "mapRequest") {
|
||||
const map = getMap(data);
|
||||
function replyWithFile(file) {
|
||||
reply("mapResponse", { id: map.id, file }, "map");
|
||||
}
|
||||
|
||||
switch (map.quality) {
|
||||
case "low":
|
||||
replyWithFile(map.resolutions.low.file);
|
||||
break;
|
||||
case "medium":
|
||||
replyWithFile(map.resolutions.low.file);
|
||||
replyWithFile(map.resolutions.medium.file);
|
||||
break;
|
||||
case "high":
|
||||
replyWithFile(map.resolutions.medium.file);
|
||||
replyWithFile(map.resolutions.high.file);
|
||||
break;
|
||||
case "ultra":
|
||||
replyWithFile(map.resolutions.medium.file);
|
||||
replyWithFile(map.resolutions.ultra.file);
|
||||
break;
|
||||
case "original":
|
||||
replyWithFile(map.resolutions.medium.file);
|
||||
replyWithFile(map.file);
|
||||
break;
|
||||
default:
|
||||
replyWithFile(map.file);
|
||||
}
|
||||
}
|
||||
if (id === "mapResponse") {
|
||||
let update = { file: data.file };
|
||||
const map = getMap(data.id);
|
||||
updateMap(map.id, update).then(() => {
|
||||
setCurrentMap({ ...map, ...update });
|
||||
});
|
||||
}
|
||||
if (id === "mapState") {
|
||||
setCurrentMapState(data);
|
||||
}
|
||||
if (id === "token") {
|
||||
const newToken = data;
|
||||
if (newToken && newToken.type === "file") {
|
||||
const cachedToken = getToken(newToken.id);
|
||||
if (
|
||||
!cachedToken ||
|
||||
cachedToken.lastModified !== newToken.lastModified
|
||||
) {
|
||||
reply("tokenRequest", newToken.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (id === "tokenRequest") {
|
||||
const token = getToken(data);
|
||||
reply("tokenResponse", token);
|
||||
}
|
||||
if (id === "tokenResponse") {
|
||||
const newToken = data;
|
||||
if (newToken && newToken.type === "file") {
|
||||
putToken(newToken);
|
||||
}
|
||||
}
|
||||
if (id === "tokenStateEdit") {
|
||||
setCurrentMapState((prevMapState) => ({
|
||||
...prevMapState,
|
||||
tokens: { ...prevMapState.tokens, ...data },
|
||||
}));
|
||||
}
|
||||
if (id === "tokenStateRemove") {
|
||||
setCurrentMapState((prevMapState) => ({
|
||||
...prevMapState,
|
||||
tokens: omit(prevMapState.tokens, Object.keys(data)),
|
||||
}));
|
||||
}
|
||||
if (id === "mapDraw") {
|
||||
addMapDrawActions(data, "mapDrawActionIndex", "mapDrawActions");
|
||||
}
|
||||
if (id === "mapDrawIndex") {
|
||||
setCurrentMapState((prevMapState) => ({
|
||||
...prevMapState,
|
||||
mapDrawActionIndex: data,
|
||||
}));
|
||||
}
|
||||
if (id === "mapFog") {
|
||||
addMapDrawActions(data, "fogDrawActionIndex", "fogDrawActions");
|
||||
}
|
||||
if (id === "mapFogIndex") {
|
||||
setCurrentMapState((prevMapState) => ({
|
||||
...prevMapState,
|
||||
fogDrawActionIndex: data,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
function handlePeerDataProgress({ id, total, count }) {
|
||||
if (count === 1) {
|
||||
assetLoadStart();
|
||||
}
|
||||
if (total === count) {
|
||||
assetLoadFinish();
|
||||
}
|
||||
assetProgressUpdate({ id, total, count });
|
||||
}
|
||||
|
||||
session.on("data", handlePeerData);
|
||||
session.on("dataProgress", handlePeerDataProgress);
|
||||
|
||||
return () => {
|
||||
session.off("data", handlePeerData);
|
||||
session.off("dataProgress", handlePeerDataProgress);
|
||||
};
|
||||
});
|
||||
|
||||
const canEditMapDrawing =
|
||||
currentMap !== null &&
|
||||
currentMapState !== null &&
|
||||
(currentMapState.editFlags.includes("drawing") ||
|
||||
currentMap.owner === userId);
|
||||
|
||||
const canEditFogDrawing =
|
||||
currentMap !== null &&
|
||||
currentMapState !== null &&
|
||||
(currentMapState.editFlags.includes("fog") || currentMap.owner === userId);
|
||||
|
||||
const disabledMapTokens = {};
|
||||
// If we have a map and state and have the token permission disabled
|
||||
// and are not the map owner
|
||||
if (
|
||||
currentMapState !== null &&
|
||||
currentMap !== null &&
|
||||
!currentMapState.editFlags.includes("tokens") &&
|
||||
currentMap.owner !== userId
|
||||
) {
|
||||
for (let token of Object.values(currentMapState.tokens)) {
|
||||
if (token.owner !== userId) {
|
||||
disabledMapTokens[token.id] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Map
|
||||
map={currentMap}
|
||||
mapState={currentMapState}
|
||||
onMapTokenStateChange={handleMapTokenStateChange}
|
||||
onMapTokenStateRemove={handleMapTokenStateRemove}
|
||||
onMapChange={handleMapChange}
|
||||
onMapStateChange={handleMapStateChange}
|
||||
onMapDraw={handleMapDraw}
|
||||
onMapDrawUndo={handleMapDrawUndo}
|
||||
onMapDrawRedo={handleMapDrawRedo}
|
||||
onFogDraw={handleFogDraw}
|
||||
onFogDrawUndo={handleFogDrawUndo}
|
||||
onFogDrawRedo={handleFogDrawRedo}
|
||||
allowMapDrawing={canEditMapDrawing}
|
||||
allowFogDrawing={canEditFogDrawing}
|
||||
disabledTokens={disabledMapTokens}
|
||||
/>
|
||||
<Tokens onMapTokenStateCreate={handleMapTokenStateCreate} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default NetworkedMapAndTokens;
|
146
src/network/NetworkedParty.js
Normal file
146
src/network/NetworkedParty.js
Normal file
@ -0,0 +1,146 @@
|
||||
import React, { useContext, useState, useEffect, useCallback } from "react";
|
||||
|
||||
// Load session for auto complete
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import Session from "../helpers/Session";
|
||||
import { isStreamStopped, omit } from "../helpers/shared";
|
||||
|
||||
import AuthContext from "../contexts/AuthContext";
|
||||
|
||||
import Party from "../components/party/Party";
|
||||
|
||||
/**
|
||||
* @typedef {object} NetworkedPartyProps
|
||||
* @property {string} gameId
|
||||
* @property {Session} session
|
||||
*/
|
||||
|
||||
/**
|
||||
* @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({});
|
||||
|
||||
function handleNicknameChange(nickname) {
|
||||
setNickname(nickname);
|
||||
session.send("nickname", { [session.id]: nickname });
|
||||
}
|
||||
|
||||
function handleStreamStart(localStream) {
|
||||
setStream(localStream);
|
||||
const tracks = localStream.getTracks();
|
||||
for (let track of tracks) {
|
||||
// Only add the audio track of the stream to the remote peer
|
||||
if (track.kind === "audio") {
|
||||
for (let peer of Object.values(session.peers)) {
|
||||
peer.connection.addTrack(track, localStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleStreamEnd = useCallback(
|
||||
(localStream) => {
|
||||
setStream(null);
|
||||
const tracks = localStream.getTracks();
|
||||
for (let track of tracks) {
|
||||
track.stop();
|
||||
// Only sending audio so only remove the audio track
|
||||
if (track.kind === "audio") {
|
||||
for (let peer of Object.values(session.peers)) {
|
||||
peer.connection.removeTrack(track, localStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[session]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
function handlePeerConnect({ peer, reply }) {
|
||||
reply("nickname", { [session.id]: nickname });
|
||||
if (stream) {
|
||||
peer.connection.addStream(stream);
|
||||
}
|
||||
}
|
||||
|
||||
function handlePeerDisconnect({ peer }) {
|
||||
setPartyNicknames((prevNicknames) => omit(prevNicknames, [peer.id]));
|
||||
}
|
||||
|
||||
function handlePeerData({ id, data }) {
|
||||
if (id === "nickname") {
|
||||
setPartyNicknames((prevNicknames) => ({
|
||||
...prevNicknames,
|
||||
...data,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
function handlePeerTrackAdded({ peer, stream: remoteStream }) {
|
||||
setPartyStreams((prevStreams) => ({
|
||||
...prevStreams,
|
||||
[peer.id]: remoteStream,
|
||||
}));
|
||||
}
|
||||
|
||||
function handlePeerTrackRemoved({ peer, stream: remoteStream }) {
|
||||
if (isStreamStopped(remoteStream)) {
|
||||
setPartyStreams((prevStreams) => omit(prevStreams, [peer.id]));
|
||||
} else {
|
||||
setPartyStreams((prevStreams) => ({
|
||||
...prevStreams,
|
||||
[peer.id]: remoteStream,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
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]);
|
||||
|
||||
useEffect(() => {
|
||||
if (stream) {
|
||||
const tracks = stream.getTracks();
|
||||
// Detect when someone has ended the screen sharing
|
||||
// by looking at the streams video track onended
|
||||
// the audio track doesn't seem to trigger this event
|
||||
for (let track of tracks) {
|
||||
if (track.kind === "video") {
|
||||
track.onended = function () {
|
||||
handleStreamEnd(stream);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [stream, handleStreamEnd]);
|
||||
|
||||
return (
|
||||
<Party
|
||||
gameId={gameId}
|
||||
onNicknameChange={handleNicknameChange}
|
||||
onStreamStart={handleStreamStart}
|
||||
onStreamEnd={handleStreamEnd}
|
||||
nickname={nickname}
|
||||
partyNicknames={partyNicknames}
|
||||
stream={stream}
|
||||
partyStreams={partyStreams}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default NetworkedParty;
|
@ -1,20 +1,7 @@
|
||||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
useCallback,
|
||||
useContext,
|
||||
useRef,
|
||||
} from "react";
|
||||
import React, { useState, useEffect, useContext, useRef } from "react";
|
||||
import { Flex, Box, Text } from "theme-ui";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
import { omit, isStreamStopped } from "../helpers/shared";
|
||||
import useSession from "../helpers/useSession";
|
||||
import useDebounce from "../helpers/useDebounce";
|
||||
|
||||
import Party from "../components/party/Party";
|
||||
import Tokens from "../components/token/Tokens";
|
||||
import Map from "../components/map/Map";
|
||||
import Banner from "../components/Banner";
|
||||
import LoadingOverlay from "../components/LoadingOverlay";
|
||||
import Link from "../components/Link";
|
||||
@ -22,520 +9,80 @@ import Link from "../components/Link";
|
||||
import AuthModal from "../modals/AuthModal";
|
||||
|
||||
import AuthContext from "../contexts/AuthContext";
|
||||
import DatabaseContext from "../contexts/DatabaseContext";
|
||||
import TokenDataContext from "../contexts/TokenDataContext";
|
||||
import MapDataContext from "../contexts/MapDataContext";
|
||||
import MapLoadingContext from "../contexts/MapLoadingContext";
|
||||
import { MapStageProvider } from "../contexts/MapStageContext";
|
||||
|
||||
import NetworkedMapAndTokens from "../network/NetworkedMapAndTokens";
|
||||
import NetworkedParty from "../network/NetworkedParty";
|
||||
|
||||
import Session from "../helpers/Session";
|
||||
|
||||
function Game() {
|
||||
const { id: gameId } = useParams();
|
||||
const { authenticationStatus, userId, nickname, setNickname } = useContext(
|
||||
AuthContext
|
||||
);
|
||||
const { assetLoadStart, assetLoadFinish, assetProgressUpdate } = useContext(
|
||||
MapLoadingContext
|
||||
);
|
||||
const {
|
||||
authenticationStatus,
|
||||
password,
|
||||
setAuthenticationStatus,
|
||||
} = useContext(AuthContext);
|
||||
const [session] = useState(new Session());
|
||||
|
||||
const { peers, socket, connected } = useSession(
|
||||
gameId,
|
||||
handlePeerConnected,
|
||||
handlePeerDisconnected,
|
||||
handlePeerData,
|
||||
handlePeerDataProgress,
|
||||
handlePeerTrackAdded,
|
||||
handlePeerTrackRemoved,
|
||||
handlePeerError
|
||||
);
|
||||
|
||||
const { putToken, getToken } = useContext(TokenDataContext);
|
||||
const { putMap, getMap, updateMap } = useContext(MapDataContext);
|
||||
|
||||
/**
|
||||
* Map state
|
||||
*/
|
||||
|
||||
const [currentMap, setCurrentMap] = useState(null);
|
||||
const [currentMapState, setCurrentMapState] = useState(null);
|
||||
|
||||
const canEditMapDrawing =
|
||||
currentMap !== null &&
|
||||
currentMapState !== null &&
|
||||
(currentMapState.editFlags.includes("drawing") ||
|
||||
currentMap.owner === userId);
|
||||
|
||||
const canEditFogDrawing =
|
||||
currentMap !== null &&
|
||||
currentMapState !== null &&
|
||||
(currentMapState.editFlags.includes("fog") || currentMap.owner === userId);
|
||||
|
||||
const disabledMapTokens = {};
|
||||
// If we have a map and state and have the token permission disabled
|
||||
// and are not the map owner
|
||||
if (
|
||||
currentMapState !== null &&
|
||||
currentMap !== null &&
|
||||
!currentMapState.editFlags.includes("tokens") &&
|
||||
currentMap.owner !== userId
|
||||
) {
|
||||
for (let token of Object.values(currentMapState.tokens)) {
|
||||
if (token.owner !== userId) {
|
||||
disabledMapTokens[token.id] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { database } = useContext(DatabaseContext);
|
||||
// Sync the map state to the database after 500ms of inactivity
|
||||
const debouncedMapState = useDebounce(currentMapState, 500);
|
||||
// Handle authentication status
|
||||
useEffect(() => {
|
||||
if (
|
||||
debouncedMapState &&
|
||||
debouncedMapState.mapId &&
|
||||
currentMap &&
|
||||
currentMap.owner === userId &&
|
||||
database
|
||||
) {
|
||||
// Update the database directly to avoid re-renders
|
||||
database
|
||||
.table("states")
|
||||
.update(debouncedMapState.mapId, debouncedMapState);
|
||||
function handleAuthSuccess() {
|
||||
setAuthenticationStatus("authenticated");
|
||||
}
|
||||
}, [currentMap, debouncedMapState, userId, database]);
|
||||
function handleAuthError() {
|
||||
setAuthenticationStatus("unauthenticated");
|
||||
}
|
||||
session.on("authenticationSuccess", handleAuthSuccess);
|
||||
session.on("authenticationError", handleAuthError);
|
||||
|
||||
function handleMapChange(newMap, newMapState) {
|
||||
setCurrentMapState(newMapState);
|
||||
setCurrentMap(newMap);
|
||||
for (let peer of Object.values(peers)) {
|
||||
// Clear the map so the new map state isn't shown on an old map
|
||||
peer.connection.send({ id: "map", data: null }, "map");
|
||||
peer.connection.send({ id: "mapState", data: newMapState });
|
||||
sendMapDataToPeer(peer, newMap);
|
||||
sendTokensToPeer(peer, newMapState);
|
||||
}
|
||||
}
|
||||
|
||||
function sendMapDataToPeer(peer, mapData) {
|
||||
// Omit file from map change, receiver will request the file if
|
||||
// they have an outdated version
|
||||
if (mapData.type === "file") {
|
||||
const { file, resolutions, ...rest } = mapData;
|
||||
peer.connection.send({ id: "map", data: { ...rest } }, "map");
|
||||
} else {
|
||||
peer.connection.send({ id: "map", data: mapData }, "map");
|
||||
}
|
||||
}
|
||||
|
||||
function handleMapStateChange(newMapState) {
|
||||
setCurrentMapState(newMapState);
|
||||
for (let peer of Object.values(peers)) {
|
||||
peer.connection.send({ id: "mapState", data: newMapState });
|
||||
}
|
||||
}
|
||||
|
||||
function addMapDrawActions(actions, indexKey, actionsKey) {
|
||||
setCurrentMapState((prevMapState) => {
|
||||
const newActions = [
|
||||
...prevMapState[actionsKey].slice(0, prevMapState[indexKey] + 1),
|
||||
...actions,
|
||||
];
|
||||
const newIndex = newActions.length - 1;
|
||||
return {
|
||||
...prevMapState,
|
||||
[actionsKey]: newActions,
|
||||
[indexKey]: newIndex,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function updateDrawActionIndex(change, indexKey, actionsKey) {
|
||||
const newIndex = Math.min(
|
||||
Math.max(currentMapState[indexKey] + change, -1),
|
||||
currentMapState[actionsKey].length - 1
|
||||
);
|
||||
|
||||
setCurrentMapState((prevMapState) => ({
|
||||
...prevMapState,
|
||||
[indexKey]: newIndex,
|
||||
}));
|
||||
return newIndex;
|
||||
}
|
||||
|
||||
function handleMapDraw(action) {
|
||||
addMapDrawActions([action], "mapDrawActionIndex", "mapDrawActions");
|
||||
for (let peer of Object.values(peers)) {
|
||||
peer.connection.send({ id: "mapDraw", data: [action] });
|
||||
}
|
||||
}
|
||||
|
||||
function handleMapDrawUndo() {
|
||||
const index = updateDrawActionIndex(
|
||||
-1,
|
||||
"mapDrawActionIndex",
|
||||
"mapDrawActions"
|
||||
);
|
||||
for (let peer of Object.values(peers)) {
|
||||
peer.connection.send({ id: "mapDrawIndex", data: index });
|
||||
}
|
||||
}
|
||||
|
||||
function handleMapDrawRedo() {
|
||||
const index = updateDrawActionIndex(
|
||||
1,
|
||||
"mapDrawActionIndex",
|
||||
"mapDrawActions"
|
||||
);
|
||||
for (let peer of Object.values(peers)) {
|
||||
peer.connection.send({ id: "mapDrawIndex", data: index });
|
||||
}
|
||||
}
|
||||
|
||||
function handleFogDraw(action) {
|
||||
addMapDrawActions([action], "fogDrawActionIndex", "fogDrawActions");
|
||||
for (let peer of Object.values(peers)) {
|
||||
peer.connection.send({ id: "mapFog", data: [action] });
|
||||
}
|
||||
}
|
||||
|
||||
function handleFogDrawUndo() {
|
||||
const index = updateDrawActionIndex(
|
||||
-1,
|
||||
"fogDrawActionIndex",
|
||||
"fogDrawActions"
|
||||
);
|
||||
for (let peer of Object.values(peers)) {
|
||||
peer.connection.send({ id: "mapFogIndex", data: index });
|
||||
}
|
||||
}
|
||||
|
||||
function handleFogDrawRedo() {
|
||||
const index = updateDrawActionIndex(
|
||||
1,
|
||||
"fogDrawActionIndex",
|
||||
"fogDrawActions"
|
||||
);
|
||||
for (let peer of Object.values(peers)) {
|
||||
peer.connection.send({ id: "mapFogIndex", data: index });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Token state
|
||||
*/
|
||||
|
||||
// Get all tokens from a token state and send it to a peer
|
||||
function sendTokensToPeer(peer, state) {
|
||||
let sentTokens = {};
|
||||
for (let tokenState of Object.values(state.tokens)) {
|
||||
const token = getToken(tokenState.tokenId);
|
||||
if (
|
||||
token &&
|
||||
token.type === "file" &&
|
||||
!(tokenState.tokenId in sentTokens)
|
||||
) {
|
||||
sentTokens[tokenState.tokenId] = true;
|
||||
// Omit file from token peer will request file if needed
|
||||
const { file, ...rest } = token;
|
||||
peer.connection.send({ id: "token", data: rest });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleMapTokenStateCreate(tokenState) {
|
||||
// If file type token send the token to the other peers
|
||||
const token = getToken(tokenState.tokenId);
|
||||
if (token && token.type === "file") {
|
||||
const { file, ...rest } = token;
|
||||
for (let peer of Object.values(peers)) {
|
||||
peer.connection.send({ id: "token", data: rest });
|
||||
}
|
||||
}
|
||||
handleMapTokenStateChange({ [tokenState.id]: tokenState });
|
||||
}
|
||||
|
||||
function handleMapTokenStateChange(change) {
|
||||
if (currentMapState === null) {
|
||||
return;
|
||||
}
|
||||
setCurrentMapState((prevMapState) => ({
|
||||
...prevMapState,
|
||||
tokens: {
|
||||
...prevMapState.tokens,
|
||||
...change,
|
||||
},
|
||||
}));
|
||||
for (let peer of Object.values(peers)) {
|
||||
peer.connection.send({ id: "tokenStateEdit", data: change });
|
||||
}
|
||||
}
|
||||
|
||||
function handleMapTokenStateRemove(tokenState) {
|
||||
setCurrentMapState((prevMapState) => {
|
||||
const { [tokenState.id]: old, ...rest } = prevMapState.tokens;
|
||||
return { ...prevMapState, tokens: rest };
|
||||
});
|
||||
for (let peer of Object.values(peers)) {
|
||||
const data = { [tokenState.id]: tokenState };
|
||||
peer.connection.send({ id: "tokenStateRemove", data });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Party state
|
||||
*/
|
||||
|
||||
const [partyNicknames, setPartyNicknames] = useState({});
|
||||
|
||||
function handleNicknameChange(nickname) {
|
||||
setNickname(nickname);
|
||||
for (let peer of Object.values(peers)) {
|
||||
const data = { [socket.id]: nickname };
|
||||
peer.connection.send({ id: "nickname", data });
|
||||
}
|
||||
}
|
||||
|
||||
const [stream, setStream] = useState(null);
|
||||
const [partyStreams, setPartyStreams] = useState({});
|
||||
function handlePeerConnected(peer) {
|
||||
peer.connection.send({ id: "nickname", data: { [socket.id]: nickname } });
|
||||
if (stream) {
|
||||
peer.connection.addStream(stream);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Peer handlers
|
||||
*/
|
||||
|
||||
function handlePeerData({ data, peer }) {
|
||||
if (data.id === "sync") {
|
||||
if (currentMapState) {
|
||||
peer.connection.send({ id: "mapState", data: currentMapState });
|
||||
sendTokensToPeer(peer, currentMapState);
|
||||
}
|
||||
if (currentMap) {
|
||||
sendMapDataToPeer(peer, currentMap);
|
||||
}
|
||||
}
|
||||
if (data.id === "map") {
|
||||
const newMap = data.data;
|
||||
// If is a file map check cache and request the full file if outdated
|
||||
if (newMap && newMap.type === "file") {
|
||||
const cachedMap = getMap(newMap.id);
|
||||
if (cachedMap && cachedMap.lastModified === newMap.lastModified) {
|
||||
setCurrentMap(cachedMap);
|
||||
} else {
|
||||
putMap(newMap).then(() => {
|
||||
peer.connection.send({ id: "mapRequest", data: newMap.id }, "map");
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setCurrentMap(newMap);
|
||||
}
|
||||
}
|
||||
// Send full map data including file
|
||||
if (data.id === "mapRequest") {
|
||||
const map = getMap(data.data);
|
||||
|
||||
function respond(file) {
|
||||
peer.connection.send(
|
||||
{
|
||||
id: "mapResponse",
|
||||
data: { id: map.id, file },
|
||||
},
|
||||
"map"
|
||||
);
|
||||
}
|
||||
|
||||
switch (map.quality) {
|
||||
case "low":
|
||||
respond(map.resolutions.low.file);
|
||||
break;
|
||||
case "medium":
|
||||
respond(map.resolutions.low.file);
|
||||
respond(map.resolutions.medium.file);
|
||||
break;
|
||||
case "high":
|
||||
respond(map.resolutions.medium.file);
|
||||
respond(map.resolutions.high.file);
|
||||
break;
|
||||
case "ultra":
|
||||
respond(map.resolutions.medium.file);
|
||||
respond(map.resolutions.ultra.file);
|
||||
break;
|
||||
case "original":
|
||||
respond(map.resolutions.medium.file);
|
||||
respond(map.file);
|
||||
break;
|
||||
default:
|
||||
respond(map.file);
|
||||
}
|
||||
}
|
||||
// A new map response with a file attached
|
||||
if (data.id === "mapResponse") {
|
||||
let update = { file: data.data.file };
|
||||
const map = getMap(data.data.id);
|
||||
updateMap(map.id, update).then(() => {
|
||||
setCurrentMap({ ...map, ...update });
|
||||
});
|
||||
}
|
||||
if (data.id === "mapState") {
|
||||
setCurrentMapState(data.data);
|
||||
}
|
||||
if (data.id === "token") {
|
||||
const newToken = data.data;
|
||||
if (newToken && newToken.type === "file") {
|
||||
const cachedToken = getToken(newToken.id);
|
||||
if (
|
||||
!cachedToken ||
|
||||
cachedToken.lastModified !== newToken.lastModified
|
||||
) {
|
||||
peer.connection.send({
|
||||
id: "tokenRequest",
|
||||
data: newToken.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (data.id === "tokenRequest") {
|
||||
const token = getToken(data.data);
|
||||
peer.connection.send({ id: "tokenResponse", data: token });
|
||||
}
|
||||
if (data.id === "tokenResponse") {
|
||||
const newToken = data.data;
|
||||
if (newToken && newToken.type === "file") {
|
||||
putToken(newToken);
|
||||
}
|
||||
}
|
||||
if (data.id === "tokenStateEdit") {
|
||||
setCurrentMapState((prevMapState) => ({
|
||||
...prevMapState,
|
||||
tokens: { ...prevMapState.tokens, ...data.data },
|
||||
}));
|
||||
}
|
||||
if (data.id === "tokenStateRemove") {
|
||||
setCurrentMapState((prevMapState) => ({
|
||||
...prevMapState,
|
||||
tokens: omit(prevMapState.tokens, Object.keys(data.data)),
|
||||
}));
|
||||
}
|
||||
if (data.id === "nickname") {
|
||||
setPartyNicknames((prevNicknames) => ({
|
||||
...prevNicknames,
|
||||
...data.data,
|
||||
}));
|
||||
}
|
||||
if (data.id === "mapDraw") {
|
||||
addMapDrawActions(data.data, "mapDrawActionIndex", "mapDrawActions");
|
||||
}
|
||||
if (data.id === "mapDrawIndex") {
|
||||
setCurrentMapState((prevMapState) => ({
|
||||
...prevMapState,
|
||||
mapDrawActionIndex: data.data,
|
||||
}));
|
||||
}
|
||||
if (data.id === "mapFog") {
|
||||
addMapDrawActions(data.data, "fogDrawActionIndex", "fogDrawActions");
|
||||
}
|
||||
if (data.id === "mapFogIndex") {
|
||||
setCurrentMapState((prevMapState) => ({
|
||||
...prevMapState,
|
||||
fogDrawActionIndex: data.data,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
function handlePeerDataProgress({ id, total, count }) {
|
||||
if (count === 1) {
|
||||
assetLoadStart();
|
||||
}
|
||||
if (total === count) {
|
||||
assetLoadFinish();
|
||||
}
|
||||
assetProgressUpdate({ id, total, count });
|
||||
}
|
||||
|
||||
function handlePeerDisconnected(peer) {
|
||||
setPartyNicknames((prevNicknames) => omit(prevNicknames, [peer.id]));
|
||||
}
|
||||
return () => {
|
||||
session.off("authenticationSuccess", handleAuthSuccess);
|
||||
session.off("authenticationError", handleAuthError);
|
||||
};
|
||||
}, [setAuthenticationStatus, session]);
|
||||
|
||||
// Handle session errors
|
||||
const [peerError, setPeerError] = useState(null);
|
||||
function handlePeerError({ error, peer }) {
|
||||
console.error(error.code);
|
||||
if (error.code === "ERR_WEBRTC_SUPPORT") {
|
||||
setPeerError("WebRTC not supported.");
|
||||
} else if (error.code === "ERR_CREATE_OFFER") {
|
||||
setPeerError("Unable to connect to party.");
|
||||
}
|
||||
}
|
||||
|
||||
function handlePeerTrackAdded({ peer, stream: remoteStream }) {
|
||||
setPartyStreams((prevStreams) => ({
|
||||
...prevStreams,
|
||||
[peer.id]: remoteStream,
|
||||
}));
|
||||
}
|
||||
|
||||
function handlePeerTrackRemoved({ peer, stream: remoteStream }) {
|
||||
if (isStreamStopped(remoteStream)) {
|
||||
setPartyStreams((prevStreams) => omit(prevStreams, [peer.id]));
|
||||
} else {
|
||||
setPartyStreams((prevStreams) => ({
|
||||
...prevStreams,
|
||||
[peer.id]: remoteStream,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream handler
|
||||
*/
|
||||
|
||||
function handleStreamStart(localStream) {
|
||||
setStream(localStream);
|
||||
const tracks = localStream.getTracks();
|
||||
for (let track of tracks) {
|
||||
// Only add the audio track of the stream to the remote peer
|
||||
if (track.kind === "audio") {
|
||||
for (let peer of Object.values(peers)) {
|
||||
peer.connection.addTrack(track, localStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleStreamEnd = useCallback(
|
||||
(localStream) => {
|
||||
setStream(null);
|
||||
const tracks = localStream.getTracks();
|
||||
for (let track of tracks) {
|
||||
track.stop();
|
||||
// Only sending audio so only remove the audio track
|
||||
if (track.kind === "audio") {
|
||||
for (let peer of Object.values(peers)) {
|
||||
peer.connection.removeTrack(track, localStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[peers]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (stream) {
|
||||
const tracks = stream.getTracks();
|
||||
// Detect when someone has ended the screen sharing
|
||||
// by looking at the streams video track onended
|
||||
// the audio track doesn't seem to trigger this event
|
||||
for (let track of tracks) {
|
||||
if (track.kind === "video") {
|
||||
track.onended = function () {
|
||||
handleStreamEnd(stream);
|
||||
};
|
||||
}
|
||||
function handlePeerError({ error }) {
|
||||
console.error(error.code);
|
||||
if (error.code === "ERR_WEBRTC_SUPPORT") {
|
||||
setPeerError("WebRTC not supported.");
|
||||
} else if (error.code === "ERR_CREATE_OFFER") {
|
||||
setPeerError("Unable to connect to party.");
|
||||
}
|
||||
}
|
||||
}, [stream, peers, handleStreamEnd]);
|
||||
session.on("error", handlePeerError);
|
||||
return () => {
|
||||
session.off("error", handlePeerError);
|
||||
};
|
||||
}, [session]);
|
||||
|
||||
// Handle connection
|
||||
const [connected, setConnected] = useState(false);
|
||||
useEffect(() => {
|
||||
function handleConnected() {
|
||||
setConnected(true);
|
||||
}
|
||||
|
||||
function handleDisconnected() {
|
||||
setConnected(false);
|
||||
}
|
||||
|
||||
session.on("connected", handleConnected);
|
||||
session.on("disconnected", handleDisconnected);
|
||||
|
||||
return () => {
|
||||
session.off("connected", handleConnected);
|
||||
session.off("disconnected", handleDisconnected);
|
||||
};
|
||||
}, [session]);
|
||||
|
||||
// Join game
|
||||
useEffect(() => {
|
||||
session.joinParty(gameId, password);
|
||||
}, [gameId, password, session]);
|
||||
|
||||
// A ref to the Konva stage
|
||||
// the ref will be assigned in the MapInteraction component
|
||||
@ -551,34 +98,8 @@ function Game() {
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<Party
|
||||
nickname={nickname}
|
||||
partyNicknames={partyNicknames}
|
||||
gameId={gameId}
|
||||
onNicknameChange={handleNicknameChange}
|
||||
stream={stream}
|
||||
partyStreams={partyStreams}
|
||||
onStreamStart={handleStreamStart}
|
||||
onStreamEnd={handleStreamEnd}
|
||||
/>
|
||||
<Map
|
||||
map={currentMap}
|
||||
mapState={currentMapState}
|
||||
onMapTokenStateChange={handleMapTokenStateChange}
|
||||
onMapTokenStateRemove={handleMapTokenStateRemove}
|
||||
onMapChange={handleMapChange}
|
||||
onMapStateChange={handleMapStateChange}
|
||||
onMapDraw={handleMapDraw}
|
||||
onMapDrawUndo={handleMapDrawUndo}
|
||||
onMapDrawRedo={handleMapDrawRedo}
|
||||
onFogDraw={handleFogDraw}
|
||||
onFogDrawUndo={handleFogDrawUndo}
|
||||
onFogDrawRedo={handleFogDrawRedo}
|
||||
allowMapDrawing={canEditMapDrawing}
|
||||
allowFogDrawing={canEditFogDrawing}
|
||||
disabledTokens={disabledMapTokens}
|
||||
/>
|
||||
<Tokens onMapTokenStateCreate={handleMapTokenStateCreate} />
|
||||
<NetworkedParty session={session} gameId={gameId} />
|
||||
<NetworkedMapAndTokens session={session} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Banner isOpen={!!peerError} onRequestClose={() => setPeerError(null)}>
|
||||
|
Loading…
Reference in New Issue
Block a user