diff --git a/package.json b/package.json
index c23d9bf..d5721d9 100644
--- a/package.json
+++ b/package.json
@@ -7,15 +7,18 @@
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
+ "blob-to-buffer": "^1.2.8",
"gh-pages": "^2.2.0",
"hookrouter": "^1.2.3",
"interactjs": "^1.9.7",
- "peerjs": "^1.2.0",
+ "js-binarypack": "^0.0.9",
"react": "^16.13.0",
"react-dom": "^16.13.0",
"react-modal": "^3.11.2",
"react-scripts": "3.4.0",
"shortid": "^2.2.15",
+ "simple-peer": "^9.6.2",
+ "socket.io-client": "^2.3.0",
"theme-ui": "^0.3.1"
},
"scripts": {
diff --git a/src/App.js b/src/App.js
index bc41f8d..ace5305 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,7 +1,6 @@
import React from "react";
import { useRoutes } from "hookrouter";
import { ThemeProvider } from "theme-ui";
-import { GameProvider } from "./contexts/GameContext";
import theme from "./theme.js";
import Home from "./routes/Home";
@@ -10,17 +9,13 @@ import Join from "./routes/Join";
const routes = {
"/": () => ,
- "/game": () => ,
- "/join": () =>
+ "/game/:id": ({ id }) => ,
+ "/join": () => ,
};
function App() {
const route = useRoutes(routes);
- return (
-
- {route}
-
- );
+ return {route};
}
export default App;
diff --git a/src/components/AddPartyMemberButton.js b/src/components/AddPartyMemberButton.js
index 1696b59..6f1c69a 100644
--- a/src/components/AddPartyMemberButton.js
+++ b/src/components/AddPartyMemberButton.js
@@ -3,7 +3,7 @@ import { IconButton, Flex, Box, Label, Text } from "theme-ui";
import Modal from "./Modal";
-function AddPartyMemberButton({ peerId }) {
+function AddPartyMemberButton({ gameId }) {
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
function openModal() {
setIsAddModalOpen(true);
@@ -32,7 +32,7 @@ function AddPartyMemberButton({ peerId }) {
- {peerId}
+ {gameId}
diff --git a/src/components/Party.js b/src/components/Party.js
index 8dcf335..7b09886 100644
--- a/src/components/Party.js
+++ b/src/components/Party.js
@@ -3,7 +3,7 @@ import { Flex, Box, Text } from "theme-ui";
import AddPartyMemberButton from "./AddPartyMemberButton";
-function Party({ nicknames, peerId, onNicknameChange }) {
+function Party({ nickname, partyNicknames, gameId, onNicknameChange }) {
return (
@@ -28,17 +28,20 @@ function Party({ nicknames, peerId, onNicknameChange }) {
- {Object.entries(nicknames).map(([id, nickname]) => (
+
+ {nickname || ""} (you)
+
+ {Object.entries(partyNicknames).map(([id, partyNickname]) => (
- {nickname} {id === peerId ? "(you)" : ""}
+ {partyNickname}
))}
-
+
);
diff --git a/src/contexts/GameContext.js b/src/contexts/GameContext.js
deleted file mode 100644
index 2579bb0..0000000
--- a/src/contexts/GameContext.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import React, { useState } from "react";
-
-const GameContext = React.createContext();
-
-export function GameProvider({ children }) {
- const [gameId, setGameId] = useState(null);
- const value = { gameId, setGameId };
- return {children};
-}
-
-export default GameContext;
diff --git a/src/helpers/Peer.js b/src/helpers/Peer.js
new file mode 100644
index 0000000..b3aa6e0
--- /dev/null
+++ b/src/helpers/Peer.js
@@ -0,0 +1,97 @@
+import SimplePeer from "simple-peer";
+import BinaryPack from "js-binarypack";
+import toBuffer from "blob-to-buffer";
+import shortid from "shortid";
+
+// Limit buffer size to 16kb to avoid issues with chrome packet size
+// http://viblast.com/blog/2015/2/5/webrtc-data-channel-message-size/
+const MAX_BUFFER_SIZE = 16000;
+
+class Peer extends SimplePeer {
+ constructor(props) {
+ super(props);
+ this.currentChunks = {};
+
+ this.on("data", (packed) => {
+ const unpacked = BinaryPack.unpack(packed);
+ // If the special property __chunked is set and true
+ // The data is a partial chunk of the a larger file
+ // So wait until all chunks are collected and assembled
+ // before emitting the dataComplete event
+ if (unpacked.__chunked) {
+ let chunk = this.currentChunks[unpacked.id] || {
+ data: [],
+ count: 0,
+ total: unpacked.total,
+ };
+ chunk.data[unpacked.index] = unpacked.data;
+ chunk.count++;
+ this.currentChunks[unpacked.id] = chunk;
+
+ // All chunks have been loaded
+ if (chunk.count === chunk.total) {
+ const merged = BinaryPack.unpack(Buffer.concat(chunk.data));
+ this.emit("dataComplete", merged);
+ delete this.currentChunks[unpacked.id];
+ }
+ } else {
+ this.emit("dataComplete", unpacked);
+ }
+ });
+ }
+
+ sendPackedData(packedData) {
+ toBuffer(packedData, (error, buffer) => {
+ if (error) {
+ throw error;
+ }
+ super.send(buffer);
+ });
+ }
+
+ send(data) {
+ const packedData = BinaryPack.pack(data);
+
+ if (packedData.size > MAX_BUFFER_SIZE) {
+ const chunks = this.chunk(packedData);
+ for (let chunk of chunks) {
+ this.sendPackedData(BinaryPack.pack(chunk));
+ }
+ return;
+ } else {
+ this.sendPackedData(packedData);
+ }
+ }
+
+ // Converted from https://github.com/peers/peerjs/
+ chunk(blob) {
+ const chunks = [];
+ const size = blob.size;
+ const total = Math.ceil(size / MAX_BUFFER_SIZE);
+ const id = shortid.generate();
+
+ let index = 0;
+ let start = 0;
+
+ while (start < size) {
+ const end = Math.min(size, start + MAX_BUFFER_SIZE);
+ const slice = blob.slice(start, end);
+
+ const chunk = {
+ __chunked: true,
+ data: slice,
+ id,
+ index,
+ total,
+ };
+
+ chunks.push(chunk);
+ start = end;
+ index++;
+ }
+
+ return chunks;
+ }
+}
+
+export default Peer;
diff --git a/src/helpers/useSession.js b/src/helpers/useSession.js
index 0d73d9e..9ba2866 100644
--- a/src/helpers/useSession.js
+++ b/src/helpers/useSession.js
@@ -1,132 +1,86 @@
import { useEffect, useState } from "react";
-import Peer from "peerjs";
+import io from "socket.io-client";
-function useSession(onConnectionOpen, onConnectionSync) {
- const [peerId, setPeerId] = useState(null);
- const [peer, setPeer] = useState(null);
- const [connections, setConnections] = useState({});
+import { omit } from "../helpers/shared";
+import Peer from "../helpers/Peer";
- function addConnection(connection) {
- console.log("Adding connection", connection.peer);
- setConnections(prevConnnections => {
- console.log("Connections", {
- ...prevConnnections,
- [connection.peer]: connection
+const socket = io("http://localhost:9000");
+
+function useSession(partyId, onPeerConnected, onPeerDisconnected, onPeerData) {
+ useEffect(() => {
+ socket.emit("join party", partyId);
+ }, [partyId]);
+
+ const [peers, setPeers] = useState({});
+
+ useEffect(() => {
+ function addPeer(id, initiator) {
+ const peer = new Peer({ initiator, trickle: false });
+
+ peer.on("signal", (signal) => {
+ socket.emit("signal", JSON.stringify({ to: id, signal }));
});
- return {
- ...prevConnnections,
- [connection.peer]: connection
- };
- });
- }
- useEffect(() => {
- console.log("Creating peer");
- setPeer(new Peer());
- }, []);
+ peer.on("connect", () => {
+ onPeerConnected && onPeerConnected({ id, peer, initiator });
+ });
- useEffect(() => {
- function handleOpen(id) {
- console.log("Peer open", id);
- setPeerId(id);
+ peer.on("dataComplete", (data) => {
+ onPeerData && onPeerData({ id, peer, data });
+ });
+
+ peer.on("close", () => {
+ onPeerDisconnected && onPeerDisconnected(id);
+ });
+
+ peer.on("error", (err) => {
+ onPeerDisconnected && onPeerDisconnected(id);
+ console.error("error", err);
+ });
+
+ setPeers((prevPeers) => ({
+ ...prevPeers,
+ [id]: peer,
+ }));
}
- function handleConnection(connection) {
- connection.on("open", () => {
- console.log("incoming connection added", connection.peer);
- const metadata = connection.metadata;
- if (metadata.sync) {
- connection.send({
- id: "sync",
- data: { connections: Object.keys(connections) }
- });
- if (onConnectionSync) {
- onConnectionSync(connection);
- }
- }
+ function handlePartyMemberJoined(id) {
+ addPeer(id, false);
+ }
- addConnection(connection);
-
- if (onConnectionOpen) {
- onConnectionOpen(connection);
- }
- });
-
- function removeConnection() {
- console.log("removing connection", connection.peer);
- setConnections(prevConnections => {
- const { [connection.peer]: old, ...rest } = prevConnections;
- return rest;
- });
+ function handlePartyMemberLeft(id) {
+ if (id in peers) {
+ peers[id].destroy();
}
- connection.on("close", removeConnection);
- connection.on("error", error => {
- console.error("Data Connection error", error);
- removeConnection();
- });
+ setPeers((prevPeers) => omit(prevPeers, [id]));
}
- function handleError(error) {
- console.error("Peer error", error);
+ function handleJoinedParty(otherIds) {
+ for (let id of otherIds) {
+ addPeer(id, true);
+ }
}
- if (!peer) {
- return;
+ function handleSignal(data) {
+ const { from, signal } = JSON.parse(data);
+ if (from in peers) {
+ peers[from].signal(signal);
+ }
}
- peer.on("open", handleOpen);
- peer.on("connection", handleConnection);
- peer.on("error", handleError);
+ socket.on("party member joined", handlePartyMemberJoined);
+ socket.on("party member left", handlePartyMemberLeft);
+ socket.on("joined party", handleJoinedParty);
+ socket.on("signal", handleSignal);
return () => {
- peer.removeListener("open", handleOpen);
- peer.removeListener("connection", handleConnection);
- peer.removeListener("error", handleError);
+ socket.removeListener("party member joined", handlePartyMemberJoined);
+ socket.removeListener("party member left", handlePartyMemberLeft);
+ socket.removeListener("joined party", handleJoinedParty);
+ socket.removeListener("signal", handleSignal);
};
- }, [peer, peerId, connections, onConnectionOpen, onConnectionSync]);
+ }, [peers, onPeerConnected, onPeerDisconnected, onPeerData]);
- function connectTo(connectionId, payload) {
- console.log("Connecting to", connectionId);
- if (connectionId in connections) {
- return;
- }
- const connection = peer.connect(connectionId, {
- metadata: { sync: true }
- });
- addConnection(connection);
- connection.on("open", () => {
- connection.on("data", data => {
- if (data.id === "sync") {
- const { connections: syncConnections } = data.data;
- for (let syncId of syncConnections) {
- console.log("Syncing to", syncId);
- if (connectionId === syncId || syncId in connections) {
- continue;
- }
- const syncConnection = peer.connect(syncId, {
- metadata: { sync: false }
- });
- addConnection(syncConnection);
- syncConnection.on("open", () => {
- if (onConnectionOpen) {
- onConnectionOpen(syncConnection);
- }
- if (payload) {
- syncConnection.send(payload);
- }
- });
- }
- }
- });
- if (onConnectionOpen) {
- onConnectionOpen(connection);
- }
- if (payload) {
- connection.send(payload);
- }
- });
- }
-
- return { peer, peerId, connections, connectTo };
+ return { peers, id: socket.id };
}
export default useSession;
diff --git a/src/routes/Game.js b/src/routes/Game.js
index 18610f6..fc0d3b7 100644
--- a/src/routes/Game.js
+++ b/src/routes/Game.js
@@ -1,15 +1,8 @@
-import React, {
- useContext,
- useEffect,
- useState,
- useRef,
- useCallback
-} from "react";
+import React, { useState, useRef } from "react";
import { Flex } from "theme-ui";
import { omit } from "../helpers/shared";
-import GameContext from "../contexts/GameContext";
import useSession from "../helpers/useSession";
import { getRandomMonster } from "../helpers/monsters";
@@ -17,40 +10,22 @@ import Party from "../components/Party";
import Tokens from "../components/Tokens";
import Map from "../components/Map";
-const defaultNickname = getRandomMonster();
-
-function Game() {
- const { gameId } = useContext(GameContext);
- const handleConnectionOpenCallback = useCallback(handleConnectionOpen);
- const handleConnectionSyncCallback = useCallback(handleConnectionSync);
- const { peerId, connections, connectTo } = useSession(
- handleConnectionOpenCallback,
- handleConnectionSyncCallback
+function Game({ gameId }) {
+ const { peers, id } = useSession(
+ gameId,
+ handlePeerConnected,
+ handlePeerDisconnected,
+ handlePeerData
);
- const [nicknames, setNicknames] = useState({});
-
- useEffect(() => {
- // Initialize nickname state
- if (peerId !== null && !(peerId in nicknames)) {
- setNicknames({ [peerId]: defaultNickname });
- }
- if (gameId !== null && peerId !== null && !(gameId in connections)) {
- connectTo(gameId, {
- id: "nickname",
- data: { [peerId]: nicknames[peerId] || defaultNickname }
- });
- }
- }, [gameId, peerId, connectTo, connections, nicknames]);
-
const [mapSource, setMapSource] = useState(null);
const mapDataRef = useRef(null);
function handleMapChanged(mapData, mapSource) {
mapDataRef.current = mapData;
setMapSource(mapSource);
- for (let connection of Object.values(connections)) {
- connection.send({ id: "map", data: mapDataRef.current });
+ for (let peer of Object.values(peers)) {
+ peer.send({ id: "map", data: mapDataRef.current });
}
}
@@ -60,75 +35,75 @@ function Game() {
if (!mapSource) {
return;
}
- setMapTokens(prevMapTokens => ({
+ setMapTokens((prevMapTokens) => ({
...prevMapTokens,
- [token.id]: token
+ [token.id]: token,
}));
- for (let connection of Object.values(connections)) {
+ for (let peer of Object.values(peers)) {
const data = { [token.id]: token };
- connection.send({ id: "tokenEdit", data });
+ peer.send({ id: "tokenEdit", data });
}
}
function handleRemoveMapToken(token) {
- setMapTokens(prevMapTokens => {
+ setMapTokens((prevMapTokens) => {
const { [token.id]: old, ...rest } = prevMapTokens;
return rest;
});
- for (let connection of Object.values(connections)) {
+ for (let peer of Object.values(peers)) {
const data = { [token.id]: token };
- connection.send({ id: "tokenRemove", data });
+ peer.send({ id: "tokenRemove", data });
}
}
+ const [nickname, setNickname] = useState(getRandomMonster());
+ const [partyNicknames, setPartyNicknames] = useState({});
+
function handleNicknameChange(nickname) {
- setNicknames(prevNicknames => ({
- ...prevNicknames,
- [peerId]: nickname
- }));
- for (let connection of Object.values(connections)) {
- const data = { [peerId]: nickname };
- connection.send({ id: "nickname", data });
+ setNickname(nickname);
+ for (let peer of Object.values(peers)) {
+ const data = { [id]: nickname };
+ peer.send({ id: "nickname", data });
}
}
- function handleConnectionOpen(connection) {
- connection.on("data", data => {
- if (data.id === "map") {
- const blob = new Blob([data.data.file]);
- mapDataRef.current = { ...data.data, file: blob };
- setMapSource(URL.createObjectURL(mapDataRef.current.file));
+ function handlePeerConnected({ id, peer, initiator }) {
+ if (!initiator) {
+ if (mapSource) {
+ peer.send({ id: "map", data: mapDataRef.current });
}
- if (data.id === "tokenEdit") {
- setMapTokens(prevMapTokens => ({
- ...prevMapTokens,
- ...data.data
- }));
- }
- if (data.id === "tokenRemove") {
- setMapTokens(prevMapTokens =>
- omit(prevMapTokens, Object.keys(data.data))
- );
- }
- if (data.id === "nickname") {
- setNicknames(prevNicknames => ({
- ...prevNicknames,
- ...data.data
- }));
- }
- });
- connection.on("error", error => {
- console.error("Data Connection error", error);
- setNicknames(prevNicknames => omit(prevNicknames, [connection.peer]));
- });
+ peer.send({ id: "tokenEdit", data: mapTokens });
+ }
+ peer.send({ id: "nickname", data: { [id]: nickname } });
}
- function handleConnectionSync(connection) {
- if (mapSource) {
- connection.send({ id: "map", data: mapDataRef.current });
+ function handlePeerData({ id, peer, data }) {
+ if (data.id === "map") {
+ const blob = new Blob([data.data.file]);
+ mapDataRef.current = { ...data.data, file: blob };
+ setMapSource(URL.createObjectURL(mapDataRef.current.file));
}
- connection.send({ id: "tokenEdit", data: mapTokens });
- connection.send({ id: "nickname", data: nicknames });
+ if (data.id === "tokenEdit") {
+ setMapTokens((prevMapTokens) => ({
+ ...prevMapTokens,
+ ...data.data,
+ }));
+ }
+ if (data.id === "tokenRemove") {
+ setMapTokens((prevMapTokens) =>
+ omit(prevMapTokens, Object.keys(data.data))
+ );
+ }
+ if (data.id === "nickname") {
+ setPartyNicknames((prevNicknames) => ({
+ ...prevNicknames,
+ ...data.data,
+ }));
+ }
+ }
+
+ function handlePeerDisconnected(disconnectedId) {
+ setPartyNicknames((prevNicknames) => omit(prevNicknames, [disconnectedId]));
}
return (
@@ -137,8 +112,9 @@ function Game() {
sx={{ justifyContent: "space-between", flexGrow: 1, height: "100%" }}
>