Files
grungnet/src/network/NetworkedMapAndTokens.js
T

495 lines
13 KiB
JavaScript
Raw Normal View History

import React, { useState, useEffect, useRef } from "react";
2021-04-01 10:55:45 +11:00
import { useToasts } from "react-toast-notifications";
import { useMapData } from "../contexts/MapDataContext";
import { useMapLoading } from "../contexts/MapLoadingContext";
import { useAuth } from "../contexts/AuthContext";
import { useDatabase } from "../contexts/DatabaseContext";
import { useParty } from "../contexts/PartyContext";
import { useAssets } from "../contexts/AssetsContext";
import { omit } from "../helpers/shared";
import useDebounce from "../hooks/useDebounce";
import useNetworkedState from "../hooks/useNetworkedState";
// Load session for auto complete
// eslint-disable-next-line no-unused-vars
import Session from "./Session";
import Map from "../components/map/Map";
2021-05-01 14:32:02 +10:00
import TokenBar from "../components/token/TokenBar";
import GlobalImageDrop from "../components/image/GlobalImageDrop";
const defaultMapActions = {
mapDrawActions: [],
mapDrawActionIndex: -1,
fogDrawActions: [],
fogDrawActionIndex: -1,
};
/**
* @typedef {object} NetworkedMapProps
* @property {Session} session
*/
/**
* @param {NetworkedMapProps} props
*/
function NetworkedMapAndTokens({ session }) {
2021-04-01 10:55:45 +11:00
const { addToast } = useToasts();
const { userId } = useAuth();
const partyState = useParty();
const { assetLoadStart, assetProgressUpdate, isLoading } = useMapLoading();
const { updateMapState } = useMapData();
const { getAsset, putAsset } = useAssets();
const [currentMap, setCurrentMap] = useState(null);
2020-12-05 17:16:06 +11:00
const [currentMapState, setCurrentMapState] = useNetworkedState(
null,
session,
"map_state",
2021-01-22 06:59:49 +11:00
500,
true,
"mapId"
2020-12-05 17:16:06 +11:00
);
const [assetManifest, setAssetManifest] = useNetworkedState(
2021-01-21 13:51:23 +11:00
null,
session,
2021-01-03 12:12:13 +11:00
"manifest",
2021-01-22 06:59:49 +11:00
500,
true,
"mapId"
);
async function loadAssetManifestFromMap(map, mapState) {
const assets = {};
const { owner } = map;
let processedTokens = new Set();
for (let tokenState of Object.values(mapState.tokens)) {
if (
tokenState.file &&
!processedTokens.has(tokenState.file) &&
tokenState.owner === owner
) {
processedTokens.add(tokenState.file);
assets[tokenState.file] = { id: tokenState.file, owner };
}
}
if (map.type === "file") {
assets[map.thumbnail] = { id: map.thumbnail, owner };
const qualityId = map.resolutions[map.quality];
if (qualityId) {
assets[qualityId] = { id: qualityId, owner };
} else {
assets[map.file] = { id: map.file, owner };
}
}
setAssetManifest({ mapId: map.id, assets }, true, true);
}
function addAssetsIfNeeded(assets) {
setAssetManifest((prevManifest) => {
if (prevManifest?.assets) {
2021-06-06 11:03:07 +10:00
let newAssets = { ...prevManifest.assets };
for (let asset of assets) {
const id = asset.id;
2021-06-06 11:03:07 +10:00
const exists = id in newAssets;
if (!exists) {
2021-06-06 11:03:07 +10:00
newAssets[id] = asset;
}
}
2021-06-06 11:03:07 +10:00
return { ...prevManifest, assets: newAssets };
}
return prevManifest;
});
}
// Keep track of assets we are already requesting to prevent from loading them multiple times
const requestingAssetsRef = useRef(new Set());
useEffect(() => {
if (!assetManifest || !userId) {
return;
}
async function requestAssetsIfNeeded() {
for (let asset of Object.values(assetManifest.assets)) {
if (
asset.owner === userId ||
requestingAssetsRef.current.has(asset.id)
) {
continue;
}
const owner = Object.values(partyState).find(
(player) => player.userId === asset.owner
);
// Ensure requests are added before any async operation to prevent them from sending twice
requestingAssetsRef.current.add(asset.id);
const cachedAsset = await getAsset(asset.id);
if (!owner) {
// Add no owner toast if we don't have asset in out cache
if (!cachedAsset) {
// TODO: Stop toast from appearing multiple times
addToast("Unable to find owner for asset");
2021-04-01 10:55:45 +11:00
}
requestingAssetsRef.current.delete(asset.id);
continue;
}
if (cachedAsset) {
requestingAssetsRef.current.delete(asset.id);
} else {
assetLoadStart(asset.id);
session.sendTo(owner.sessionId, "assetRequest", asset);
}
}
}
requestAssetsIfNeeded();
}, [
assetManifest,
partyState,
session,
userId,
addToast,
getAsset,
assetLoadStart,
]);
/**
* Map state
*/
const { database } = useDatabase();
// 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
) {
updateMapState(debouncedMapState.mapId, debouncedMapState);
}
}, [currentMap, debouncedMapState, userId, database, updateMapState]);
async function handleMapChange(newMap, newMapState) {
2021-01-04 09:48:08 +11:00
// Clear map before sending new one
setCurrentMap(null);
session.socket?.emit("map", null);
setCurrentMapState(newMapState, true, true);
setCurrentMap(newMap);
session.socket?.emit("map", newMap);
if (!newMap || !newMapState) {
2021-03-26 15:48:55 +11:00
setAssetManifest(null, true, true);
return;
}
await loadAssetManifestFromMap(newMap, newMapState);
}
2021-02-22 17:14:12 +11:00
function handleMapReset(newMapState) {
setCurrentMapState(newMapState, true, true);
2021-02-22 17:14:12 +11:00
setMapActions(defaultMapActions);
}
const [mapActions, setMapActions] = useState(defaultMapActions);
function addMapActions(actions, indexKey, actionsKey, shapesKey) {
setMapActions((prevMapActions) => {
const newActions = [
...prevMapActions[actionsKey].slice(0, prevMapActions[indexKey] + 1),
...actions,
];
const newIndex = newActions.length - 1;
return {
...prevMapActions,
[actionsKey]: newActions,
[indexKey]: newIndex,
};
});
// Update map state by performing the actions on it
setCurrentMapState((prevMapState) => {
if (prevMapState) {
let shapes = prevMapState[shapesKey];
for (let action of actions) {
shapes = action.execute(shapes);
}
return {
...prevMapState,
[shapesKey]: shapes,
};
}
});
}
function updateActionIndex(change, indexKey, actionsKey, shapesKey) {
const prevIndex = mapActions[indexKey];
const newIndex = Math.min(
Math.max(mapActions[indexKey] + change, -1),
mapActions[actionsKey].length - 1
);
setMapActions((prevMapActions) => ({
...prevMapActions,
[indexKey]: newIndex,
}));
// Update map state by either performing the actions or undoing them
setCurrentMapState((prevMapState) => {
if (prevMapState) {
let shapes = prevMapState[shapesKey];
if (prevIndex < newIndex) {
// Redo
for (let i = prevIndex + 1; i < newIndex + 1; i++) {
let action = mapActions[actionsKey][i];
shapes = action.execute(shapes);
}
} else {
// Undo
for (let i = prevIndex; i > newIndex; i--) {
let action = mapActions[actionsKey][i];
shapes = action.undo(shapes);
}
}
return {
...prevMapState,
[shapesKey]: shapes,
};
}
});
return newIndex;
}
function handleMapDraw(action) {
addMapActions(
[action],
"mapDrawActionIndex",
"mapDrawActions",
"drawShapes"
);
}
function handleMapDrawUndo() {
updateActionIndex(-1, "mapDrawActionIndex", "mapDrawActions", "drawShapes");
}
function handleMapDrawRedo() {
updateActionIndex(1, "mapDrawActionIndex", "mapDrawActions", "drawShapes");
}
function handleFogDraw(action) {
addMapActions(
[action],
"fogDrawActionIndex",
"fogDrawActions",
"fogShapes"
);
}
function handleFogDrawUndo() {
updateActionIndex(-1, "fogDrawActionIndex", "fogDrawActions", "fogShapes");
}
function handleFogDrawRedo() {
updateActionIndex(1, "fogDrawActionIndex", "fogDrawActions", "fogShapes");
}
2021-02-11 23:09:00 +11:00
// If map changes clear map actions
const previousMapIdRef = useRef();
useEffect(() => {
2021-02-11 23:09:00 +11:00
if (currentMap && currentMap.id !== previousMapIdRef.current) {
setMapActions(defaultMapActions);
2021-02-11 23:09:00 +11:00
previousMapIdRef.current = currentMap.id;
}
2021-02-11 23:09:00 +11:00
}, [currentMap]);
2020-11-04 15:03:34 +11:00
function handleNoteChange(note) {
2020-11-03 17:15:39 +11:00
setCurrentMapState((prevMapState) => ({
...prevMapState,
notes: {
...prevMapState.notes,
[note.id]: note,
},
}));
}
2020-11-05 14:41:33 +11:00
function handleNoteRemove(noteId) {
setCurrentMapState((prevMapState) => ({
...prevMapState,
notes: omit(prevMapState.notes, [noteId]),
}));
}
/**
* Token state
*/
async function handleMapTokensStateCreate(tokenStates) {
if (!currentMap || !currentMapState) {
return;
}
let assets = [];
for (let tokenState of tokenStates) {
if (tokenState.type === "file") {
assets.push({ id: tokenState.file, owner: tokenState.owner });
}
}
if (assets.length > 0) {
addAssetsIfNeeded(assets);
}
setCurrentMapState((prevMapState) => {
let newMapTokens = { ...prevMapState.tokens };
for (let tokenState of tokenStates) {
newMapTokens[tokenState.id] = tokenState;
}
return { ...prevMapState, tokens: newMapTokens };
});
}
function handleMapTokenStateChange(change) {
if (!currentMapState) {
return;
}
setCurrentMapState((prevMapState) => {
let tokens = { ...prevMapState.tokens };
for (let id in change) {
if (id in tokens) {
tokens[id] = { ...tokens[id], ...change[id] };
}
}
return {
...prevMapState,
tokens,
};
});
}
function handleMapTokenStateRemove(tokenState) {
setCurrentMapState((prevMapState) => {
const { [tokenState.id]: old, ...rest } = prevMapState.tokens;
return { ...prevMapState, tokens: rest };
});
}
useEffect(() => {
async function handlePeerData({ id, data, reply }) {
if (id === "assetRequest") {
const asset = await getAsset(data.id);
reply("assetResponse", asset, undefined, asset.id);
}
if (id === "assetResponse") {
await putAsset(data);
requestingAssetsRef.current.delete(data.id);
}
}
function handlePeerDataProgress({ id, total, count }) {
assetProgressUpdate({ id, total, count });
}
async function handleSocketMap(map) {
if (map) {
setCurrentMap(map);
} else {
setCurrentMap(null);
}
2020-12-05 17:16:06 +11:00
}
session.on("peerData", handlePeerData);
session.on("peerDataProgress", handlePeerDataProgress);
session.socket?.on("map", handleSocketMap);
return () => {
session.off("peerData", handlePeerData);
session.off("peerDataProgress", handlePeerDataProgress);
session.socket?.off("map", handleSocketMap);
};
});
2020-07-17 15:57:52 +10:00
const canChangeMap = !isLoading;
const canEditMapDrawing =
currentMap &&
currentMapState &&
(currentMapState.editFlags.includes("drawing") ||
currentMap.owner === userId);
const canEditFogDrawing =
currentMap &&
currentMapState &&
(currentMapState.editFlags.includes("fog") || currentMap.owner === userId);
2020-11-05 16:21:52 +11:00
const canEditNotes =
currentMap &&
currentMapState &&
2020-11-05 16:21:52 +11:00
(currentMapState.editFlags.includes("notes") ||
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 &&
currentMap &&
!currentMapState.editFlags.includes("tokens") &&
currentMap.owner !== userId
) {
for (let token of Object.values(currentMapState.tokens)) {
if (token.owner !== userId) {
disabledMapTokens[token.id] = true;
}
}
}
return (
<GlobalImageDrop
onMapChange={handleMapChange}
onMapTokensStateCreate={handleMapTokensStateCreate}
>
<Map
map={currentMap}
mapState={currentMapState}
mapActions={mapActions}
onMapTokenStateChange={handleMapTokenStateChange}
onMapTokenStateRemove={handleMapTokenStateRemove}
onMapChange={handleMapChange}
2021-02-22 17:14:12 +11:00
onMapReset={handleMapReset}
onMapDraw={handleMapDraw}
onMapDrawUndo={handleMapDrawUndo}
onMapDrawRedo={handleMapDrawRedo}
onFogDraw={handleFogDraw}
onFogDrawUndo={handleFogDrawUndo}
onFogDrawRedo={handleFogDrawRedo}
2020-11-04 15:03:34 +11:00
onMapNoteChange={handleNoteChange}
2020-11-05 14:41:33 +11:00
onMapNoteRemove={handleNoteRemove}
allowMapDrawing={canEditMapDrawing}
allowFogDrawing={canEditFogDrawing}
2020-07-17 15:57:52 +10:00
allowMapChange={canChangeMap}
2020-11-05 16:21:52 +11:00
allowNoteEditing={canEditNotes}
disabledTokens={disabledMapTokens}
2020-07-28 17:59:26 +10:00
session={session}
/>
<TokenBar onMapTokensStateCreate={handleMapTokensStateCreate} />
</GlobalImageDrop>
);
}
export default NetworkedMapAndTokens;