From ae944ed0b8bee201b2b870d577236419907b42b0 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Fri, 29 Mar 2024 16:20:33 -0700 Subject: [PATCH 01/12] collab wip --- multiplayer/package-lock.json | 18 + multiplayer/package.json | 1 + multiplayer/src/components/CollabPage.tsx | 18 + multiplayer/src/components/JoinOrHost.tsx | 37 +- multiplayer/src/components/SignedInPage.tsx | 2 + multiplayer/src/epics/collab/index.ts | 1 + multiplayer/src/epics/collab/initState.ts | 7 + multiplayer/src/epics/hostCollabAsync.ts | 51 +++ multiplayer/src/epics/index.ts | 4 +- multiplayer/src/epics/joinCollabAsync.ts | 72 +++ multiplayer/src/epics/joinGameAsync.ts | 10 +- ...eDisconnected.ts => notifyDisconnected.ts} | 2 +- multiplayer/src/epics/signOutAsync.ts | 2 + multiplayer/src/index.tsx | 5 +- multiplayer/src/services/collabClient.ts | 411 ++++++++++++++++++ multiplayer/src/services/gameClient.ts | 10 +- multiplayer/src/state/actions.ts | 18 +- .../src/state/collab/CollabContext.tsx | 49 +++ multiplayer/src/state/collab/actions.ts | 37 ++ multiplayer/src/state/collab/index.ts | 2 + multiplayer/src/state/collab/reducer.ts | 30 ++ multiplayer/src/state/collab/state.ts | 9 + multiplayer/src/state/reducer.ts | 10 + multiplayer/src/state/state.ts | 3 + multiplayer/src/types/index.ts | 35 +- 25 files changed, 793 insertions(+), 51 deletions(-) create mode 100644 multiplayer/src/components/CollabPage.tsx create mode 100644 multiplayer/src/epics/collab/index.ts create mode 100644 multiplayer/src/epics/collab/initState.ts create mode 100644 multiplayer/src/epics/hostCollabAsync.ts create mode 100644 multiplayer/src/epics/joinCollabAsync.ts rename multiplayer/src/epics/{notifyGameDisconnected.ts => notifyDisconnected.ts} (97%) create mode 100644 multiplayer/src/services/collabClient.ts create mode 100644 multiplayer/src/state/collab/CollabContext.tsx create mode 100644 multiplayer/src/state/collab/actions.ts create mode 100644 multiplayer/src/state/collab/index.ts create mode 100644 multiplayer/src/state/collab/reducer.ts create mode 100644 multiplayer/src/state/collab/state.ts diff --git a/multiplayer/package-lock.json b/multiplayer/package-lock.json index bd26eed22edb..513b7dd9ea09 100644 --- a/multiplayer/package-lock.json +++ b/multiplayer/package-lock.json @@ -14,6 +14,7 @@ "@types/node": "^12.20.45", "engine.io-client": "^6.2.2", "framer-motion": "^6.5.1", + "jsonpath-plus": "^8.1.0", "nanoid": "^4.0.0", "qrcode.react": "^3.1.0", "react-scripts": "^5.0.1", @@ -11747,6 +11748,18 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonpath-plus": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-8.1.0.tgz", + "integrity": "sha512-qVTiuKztFGw0dGhYi3WNqvddx3/SHtyDT0xJaeyz4uP0d1tkpG+0y5uYQ4OcIo1TLAz3PE/qDOW9F0uDt3+CTw==", + "bin": { + "jsonpath": "bin/jsonpath-cli.js", + "jsonpath-plus": "bin/jsonpath-cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/jsonpointer": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", @@ -26079,6 +26092,11 @@ "universalify": "^2.0.0" } }, + "jsonpath-plus": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-8.1.0.tgz", + "integrity": "sha512-qVTiuKztFGw0dGhYi3WNqvddx3/SHtyDT0xJaeyz4uP0d1tkpG+0y5uYQ4OcIo1TLAz3PE/qDOW9F0uDt3+CTw==" + }, "jsonpointer": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", diff --git a/multiplayer/package.json b/multiplayer/package.json index 6337f65dc108..9cc6259ee0e1 100644 --- a/multiplayer/package.json +++ b/multiplayer/package.json @@ -9,6 +9,7 @@ "@types/node": "^12.20.45", "engine.io-client": "^6.2.2", "framer-motion": "^6.5.1", + "jsonpath-plus": "^8.1.0", "nanoid": "^4.0.0", "qrcode.react": "^3.1.0", "react-scripts": "^5.0.1", diff --git a/multiplayer/src/components/CollabPage.tsx b/multiplayer/src/components/CollabPage.tsx new file mode 100644 index 000000000000..2591ec78d86d --- /dev/null +++ b/multiplayer/src/components/CollabPage.tsx @@ -0,0 +1,18 @@ +import { useContext } from "react"; +import { AppStateContext } from "../state/AppStateContext"; + +export interface GamePageProps {} + +export default function Render(props: GamePageProps) { + const { state } = useContext(AppStateContext); + const { netMode, clientRole, collabInfo } = state; + + if (!collabInfo) return null; + if (netMode !== "connected") return null; + + return ( + <> + {collabInfo.joinCode} + + ); +} diff --git a/multiplayer/src/components/JoinOrHost.tsx b/multiplayer/src/components/JoinOrHost.tsx index 72e41afe64d7..e0cf7bb10d50 100644 --- a/multiplayer/src/components/JoinOrHost.tsx +++ b/multiplayer/src/components/JoinOrHost.tsx @@ -6,7 +6,7 @@ import { AppStateContext } from "../state/AppStateContext"; import { Button } from "../../../react-common/components/controls/Button"; import { Input } from "react-common/components/controls/Input"; import { Link } from "react-common/components/controls/Link"; -import { hostGameAsync, joinGameAsync } from "../epics"; +import { hostCollabAsync, joinCollabAsync } from "../epics"; import { resourceUrl } from "../util"; import TabButton from "./TabButton"; import HostGameButton from "./HostGameButton"; @@ -27,21 +27,19 @@ export default function Render() { const onJoinGameClick = async () => { if (joinCodeRef?.current?.value) { - await joinGameAsync(joinCodeRef.current.value); + await joinCollabAsync(joinCodeRef.current.value); } }; const onHostGameClick = async () => { - if (shareCodeRef?.current?.value) { - await hostGameAsync(shareCodeRef.current.value); - } + await hostCollabAsync(); }; const enterShareOrLink = lf("Enter share code or link"); const howToGetLink = lf("How do I get a share code or link?"); const moreGamesToPlay = lf("More games to play with your friends"); - const starterGames = targetConfig?.multiplayer?.games; - const showStarterGames = !!starterGames?.length; + const starterGames = targetConfig?.multiplayer?.games ?? []; + const showStarterGames = false; return (
@@ -50,11 +48,11 @@ export default function Render() {
- {lf("Join Game")} + {lf("Join Collab")}
{lf("Join")} @@ -65,11 +63,11 @@ export default function Render() { onClick={() => setCurrTab("join")} />
- {lf("Host Game")} + {lf("Host Collab")}
{lf("Host")} @@ -98,8 +96,8 @@ export default function Render() {
@@ -108,7 +106,7 @@ export default function Render() { href="/multiplayer#join-game" target="_blank" > - {lf("How do I get a game code?")} + {lf("How do I get a collab code?")}
@@ -117,6 +115,7 @@ export default function Render() {
-
- - {howToGetLink} - -
)}
diff --git a/multiplayer/src/components/SignedInPage.tsx b/multiplayer/src/components/SignedInPage.tsx index 0b5279179dd1..1bc26d0369af 100644 --- a/multiplayer/src/components/SignedInPage.tsx +++ b/multiplayer/src/components/SignedInPage.tsx @@ -2,6 +2,7 @@ import { useContext } from "react"; import { AppStateContext } from "../state/AppStateContext"; import JoinOrHost from "./JoinOrHost"; import GamePage from "./GamePage"; +import CollabPage from "./CollabPage"; export default function Render() { const { state } = useContext(AppStateContext); @@ -11,6 +12,7 @@ export default function Render() {
{netMode === "init" && } {netMode !== "init" && } + {netMode !== "init" && }
); } diff --git a/multiplayer/src/epics/collab/index.ts b/multiplayer/src/epics/collab/index.ts new file mode 100644 index 000000000000..8e8ff9f08a26 --- /dev/null +++ b/multiplayer/src/epics/collab/index.ts @@ -0,0 +1 @@ +export { initState } from "./initState"; diff --git a/multiplayer/src/epics/collab/initState.ts b/multiplayer/src/epics/collab/initState.ts new file mode 100644 index 000000000000..aaaa4edd8e2b --- /dev/null +++ b/multiplayer/src/epics/collab/initState.ts @@ -0,0 +1,7 @@ +import { collabStateAndDispatch } from "../../state/collab"; +import * as CollabActions from "../../state/collab/actions"; + +export function initState() { + const { dispatch } = collabStateAndDispatch(); + dispatch(CollabActions.init()); +} diff --git a/multiplayer/src/epics/hostCollabAsync.ts b/multiplayer/src/epics/hostCollabAsync.ts new file mode 100644 index 000000000000..1c89bd86f096 --- /dev/null +++ b/multiplayer/src/epics/hostCollabAsync.ts @@ -0,0 +1,51 @@ +import * as collabClient from "../services/collabClient"; +import { dispatch } from "../state"; +import { + dismissToast, + setNetMode, + setCollabInfo, + showToast, + setClientRole, +} from "../state/actions"; + +export async function hostCollabAsync() { + const connectingToast = showToast({ + type: "info", + text: lf("Connecting..."), + showSpinner: true, + }); + try { + dispatch(setNetMode("connecting")); + dispatch(connectingToast); + + const hostResult = await collabClient.hostCollabAsync(); + pxt.debug(hostResult); + + if (hostResult.success) { + dispatch( + showToast({ + type: "success", + text: lf("Connected!"), + timeoutMs: 5000, + }) + ); + dispatch(setClientRole("host")); + dispatch(setCollabInfo(hostResult)); + dispatch(setNetMode("connected")); + } else { + throw new Error(`host http response: ${hostResult.statusCode}`); + } + } catch (e: any) { + pxt.log(e.toString()); + dispatch( + showToast({ + type: "error", + text: lf("Something went wrong. Please try again."), + timeoutMs: 5000, + }) + ); + dispatch(setNetMode("init")); + } finally { + dispatch(dismissToast(connectingToast.toast.id)); + } +} diff --git a/multiplayer/src/epics/index.ts b/multiplayer/src/epics/index.ts index da85069a8d38..2e5d6509fe59 100644 --- a/multiplayer/src/epics/index.ts +++ b/multiplayer/src/epics/index.ts @@ -9,7 +9,7 @@ export { leaveGameAsync } from "./leaveGameAsync"; export { setGameModeAsync } from "./setGameModeAsync"; export { signInAsync } from "./signInAsync"; export { signOutAsync } from "./signOutAsync"; -export { notifyGameDisconnected } from "./notifyGameDisconnected"; +export { notifyDisconnected } from "./notifyDisconnected"; export { setPresenceAsync } from "./setPresenceAsync"; export { sendReactionAsync } from "./sendReactionAsync"; export { setReactionAsync } from "./setReactionAsync"; @@ -25,3 +25,5 @@ export { resumeGameAsync } from "./resumeGameAsync"; export { visibilityChanged } from "./visibilityChanged"; export { sendAbuseReportAsync } from "./sendAbuseReportAsync"; export { setCustomIconAsync } from "./setCustomIconAsync"; +export { hostCollabAsync } from "./hostCollabAsync"; +export { joinCollabAsync } from "./joinCollabAsync"; diff --git a/multiplayer/src/epics/joinCollabAsync.ts b/multiplayer/src/epics/joinCollabAsync.ts new file mode 100644 index 000000000000..198b86ad59a2 --- /dev/null +++ b/multiplayer/src/epics/joinCollabAsync.ts @@ -0,0 +1,72 @@ +import * as collabClient from "../services/collabClient"; +import { dispatch } from "../state"; +import { + dismissToast, + setNetMode, + setCollabInfo, + showToast, + setClientRole, +} from "../state/actions"; +import { HTTP_SESSION_FULL, HTTP_SESSION_NOT_FOUND } from "../types"; +import { cleanupJoinCode } from "../util"; +import { notifyDisconnected } from "."; + +export async function joinCollabAsync(joinCode: string | undefined) { + joinCode = cleanupJoinCode(joinCode); + if (!joinCode) { + return dispatch( + showToast({ + type: "error", + text: lf("Invalid join code. Please try again."), + timeoutMs: 5000, + }) + ); + } + const connectingToast = showToast({ + type: "info", + text: lf("Connecting..."), + showSpinner: true, + }); + try { + dispatch(setNetMode("connecting")); + dispatch(connectingToast); + + const joinResult = await collabClient.joinCollabAsync(joinCode); + pxt.debug(joinResult); + + if (joinResult.success) { + dispatch( + showToast({ + type: "success", + text: lf("Connected!"), + timeoutMs: 5000, + }) + ); + dispatch(setClientRole("guest")); + dispatch(setCollabInfo(joinResult)); + dispatch(setNetMode("connected")); + } else { + if (joinResult.statusCode === HTTP_SESSION_NOT_FOUND) { + notifyDisconnected("not-found"); + dispatch(setNetMode("init")); + } else if (joinResult.statusCode === HTTP_SESSION_FULL) { + // notification handled by collabClient + dispatch(setNetMode("init")); + } else { + throw new Error(`join http response: ${joinResult.statusCode}`); + } + } + } catch (e: any) { + pxt.log(e.toString()); + dispatch( + showToast({ + type: "error", + text: lf("Something went wrong. Please try again."), + timeoutMs: 5000, + }) + ); + dispatch(setNetMode("init")); + } finally { + dispatch(dismissToast(connectingToast.toast.id)); + } +} diff --git a/multiplayer/src/epics/joinGameAsync.ts b/multiplayer/src/epics/joinGameAsync.ts index 1b9b3c64e72d..06bd26142f6d 100644 --- a/multiplayer/src/epics/joinGameAsync.ts +++ b/multiplayer/src/epics/joinGameAsync.ts @@ -7,9 +7,9 @@ import { showToast, setClientRole, } from "../state/actions"; -import { HTTP_GAME_FULL, HTTP_GAME_NOT_FOUND } from "../types"; +import { HTTP_SESSION_FULL, HTTP_SESSION_NOT_FOUND } from "../types"; import { cleanupJoinCode } from "../util"; -import { notifyGameDisconnected } from "."; +import { notifyDisconnected } from "."; export async function joinGameAsync(joinCode: string | undefined) { joinCode = cleanupJoinCode(joinCode); @@ -46,10 +46,10 @@ export async function joinGameAsync(joinCode: string | undefined) { dispatch(setGameInfo(joinResult)); dispatch(setNetMode("connected")); } else { - if (joinResult.statusCode === HTTP_GAME_NOT_FOUND) { - notifyGameDisconnected("not-found"); + if (joinResult.statusCode === HTTP_SESSION_NOT_FOUND) { + notifyDisconnected("not-found"); dispatch(setNetMode("init")); - } else if (joinResult.statusCode === HTTP_GAME_FULL) { + } else if (joinResult.statusCode === HTTP_SESSION_FULL) { // notification handled by gameClient dispatch(setNetMode("init")); } else { diff --git a/multiplayer/src/epics/notifyGameDisconnected.ts b/multiplayer/src/epics/notifyDisconnected.ts similarity index 97% rename from multiplayer/src/epics/notifyGameDisconnected.ts rename to multiplayer/src/epics/notifyDisconnected.ts index d29ac12c4d85..4c378d174d20 100644 --- a/multiplayer/src/epics/notifyGameDisconnected.ts +++ b/multiplayer/src/epics/notifyDisconnected.ts @@ -2,7 +2,7 @@ import { dispatch } from "../state"; import { showToast, setNetMode } from "../state/actions"; import { GameOverReason } from "../types"; -export function notifyGameDisconnected(reason: GameOverReason | undefined) { +export function notifyDisconnected(reason: GameOverReason | undefined) { try { dispatch(setNetMode("init")); switch (reason) { diff --git a/multiplayer/src/epics/signOutAsync.ts b/multiplayer/src/epics/signOutAsync.ts index 7f107abd2452..6cd33c954a06 100644 --- a/multiplayer/src/epics/signOutAsync.ts +++ b/multiplayer/src/epics/signOutAsync.ts @@ -1,9 +1,11 @@ import * as gameClient from "../services/gameClient"; +import * as collabClient from "../services/collabClient"; import * as authClient from "../services/authClient"; export async function signOutAsync() { try { gameClient.destroy(); + collabClient.destroy(); await authClient.logoutAsync(); } catch (e) { } finally { diff --git a/multiplayer/src/index.tsx b/multiplayer/src/index.tsx index b4d3d02a58e5..9319b517732c 100644 --- a/multiplayer/src/index.tsx +++ b/multiplayer/src/index.tsx @@ -10,6 +10,7 @@ import ReactDOM from "react-dom"; import "./index.css"; import App from "./App"; import { AppStateProvider } from "./state/AppStateContext"; +import { CollabStateProvider } from "./state/collab"; function enableAnalytics() { pxt.analytics.enable(pxt.Util.userLanguage()); @@ -64,7 +65,9 @@ window.addEventListener("DOMContentLoaded", () => { ReactDOM.render( - + + + , document.getElementById("root") diff --git a/multiplayer/src/services/collabClient.ts b/multiplayer/src/services/collabClient.ts new file mode 100644 index 000000000000..721365160630 --- /dev/null +++ b/multiplayer/src/services/collabClient.ts @@ -0,0 +1,411 @@ +import { Socket } from "engine.io-client"; +import { SmartBuffer } from "smart-buffer"; +import * as authClient from "./authClient"; +import { + CollabInfo, + ClientRole, + Presence, + SessionOverReason, + CollabJoinResult, + HTTP_OK, + HTTP_SESSION_FULL, + HTTP_INTERNAL_SERVER_ERROR, + HTTP_IM_A_TEAPOT, +} from "../types"; +import { + notifyDisconnected, + setPresenceAsync, + playerJoinedAsync, + playerLeftAsync, +} from "../epics"; +import * as CollabEpics from "../epics/collab"; + +const COLLAB_HOST_PROD = "https://mp.makecode.com"; +const COLLAB_HOST_STAGING = "https://multiplayer.staging.pxt.io"; +const COLLAB_HOST_LOCALHOST = "http://localhost:8082"; +const COLLAB_HOST_DEV = COLLAB_HOST_LOCALHOST; +const COLLAB_HOST = (() => { + if (pxt.BrowserUtils.isLocalHostDev()) { + return COLLAB_HOST_DEV; + } else if (window.location.hostname === "arcade.makecode.com") { + return COLLAB_HOST_PROD; + } else { + return COLLAB_HOST_STAGING; + } +})(); + +export type CollabPlayer = { + clientId: string; + xp: number; + yp: number; + name: string; +}; + +class CollabClient { + sock: Socket | undefined; + screen: Buffer | undefined; + clientRole: ClientRole | undefined; + sessOverReason: SessionOverReason | undefined; + receivedJoinMessageInTimeHandler: ((a: any) => void) | undefined; + + constructor() {} + + destroy() { + try { + this.sock?.close(); + this.sock = undefined; + } catch (e) {} + } + + private sendMessage(msg: Protocol.Message | Buffer) { + if (msg instanceof Buffer) { + this.sock?.send(msg, {}, (err: any) => { + if (err) pxt.log("Error sending message. " + err.toString()); + }); + } else { + const payload = JSON.stringify(msg); + this.sock?.send(payload, {}, (err: any) => { + if (err) pxt.log("Error sending message. " + err.toString()); + }); + } + } + + private recvMessageAsync = async (payload: string | ArrayBuffer) => { + if (payload instanceof ArrayBuffer) { + //------------------------------------------------- + // Handle binary message + //-- + const reader = SmartBuffer.fromBuffer(Buffer.from(payload)); + const type = reader.readUInt16LE(); + switch (type) { + } + } else if (typeof payload === "string") { + //------------------------------------------------- + // Handle JSON message + //-- + const msg = JSON.parse(payload) as Protocol.Message; + switch (msg.type) { + case "joined": + return await this.recvJoinedMessageAsync(msg); + case "presence": + return await this.recvPresenceMessageAsync(msg); + case "player-joined": + return await this.recvPlayerJoinedMessageAsync(msg); + case "player-left": + return await this.recvPlayerLeftMessageAsync(msg); + } + } else { + throw new Error(`Unknown payload: ${payload}`); + } + }; + + private recvMessageWithJoinTimeout = async ( + payload: string | Buffer, + resolve: () => void + ) => { + try { + if (typeof payload === "string") { + const msg = JSON.parse(payload) as Protocol.Message; + if (msg.type === "joined") { + // We've joined the collab. Replace this handler with a direct call to recvMessageAsync + if (this.sock) { + this.sock.removeListener( + "message", + this.receivedJoinMessageInTimeHandler + ); + this.receivedJoinMessageInTimeHandler = undefined; + } + resolve(); + } + } + } catch (e) { + console.error("Error processing message", e); + destroyCollabClient(); + resolve(); + } + }; + + public async connectAsync(ticket: string) { + return new Promise((resolve, reject) => { + const joinTimeout = setTimeout(() => { + reject("Timed out connecting to collab server"); + destroyCollabClient(); + }, 20 * 1000); + this.sock = new Socket(COLLAB_HOST, { + transports: ["websocket"], // polling is unsupported + path: "/mp", + }); + this.sock.binaryType = "arraybuffer"; + this.sock.on("open", () => { + pxt.debug("socket opened"); + this.receivedJoinMessageInTimeHandler = async payload => { + await this.recvMessageWithJoinTimeout(payload, () => { + clearTimeout(joinTimeout); + resolve(); + }); + }; + this.sock?.on("message", this.receivedJoinMessageInTimeHandler); + this.sock?.on("message", async payload => { + try { + await this.recvMessageAsync(payload); + } catch (e) { + console.error("Error processing message", e); + destroyCollabClient(); + } + }); + this.sock?.on("close", () => { + pxt.debug("socket disconnected"); + notifyDisconnected(this.sessOverReason); + clearTimeout(joinTimeout); + resolve(); + }); + + this.sendMessage({ + type: "connect", + ticket, + version: Protocol.VERSION, + } as Protocol.ConnectMessage); + }); + }); + } + + public async hostCollabAsync(): Promise { + try { + const authToken = await authClient.authTokenAsync(); + + const hostRes = await fetch(`${COLLAB_HOST}/api/collab/host`, { + credentials: "include", + headers: { + Authorization: "mkcd " + authToken, + }, + }); + + if (hostRes.status !== HTTP_OK) { + return { + success: false, + statusCode: hostRes.status, + }; + } + + const collabInfo = (await hostRes.json()) as CollabInfo; + + if (!collabInfo?.joinTicket) + throw new Error("Collab server did not return a join ticket"); + + CollabEpics.initState(); + + await this.connectAsync(collabInfo.joinTicket!); + + return { + ...collabInfo, + success: true, + statusCode: hostRes.status, + }; + } catch (e) { + return { + success: false, + statusCode: HTTP_INTERNAL_SERVER_ERROR, + }; + } + } + + public async joinCollabAsync(joinCode: string): Promise { + try { + joinCode = encodeURIComponent(joinCode); + + const authToken = await authClient.authTokenAsync(); + + const joinRes = await fetch( + `${COLLAB_HOST}/api/collab/join/${joinCode}`, + { + credentials: "include", + headers: { + Authorization: "mkcd " + authToken, + }, + } + ); + + if (joinRes.status !== HTTP_OK) { + return { + success: false, + statusCode: joinRes.status, + }; + } + + const collabInfo = (await joinRes.json()) as CollabInfo; + + if (!collabInfo?.joinTicket) + throw new Error("Collab server did not return a join ticket"); + + CollabEpics.initState(); + + await this.connectAsync(collabInfo.joinTicket!); + + return { + ...collabInfo, + success: !this.sessOverReason, + statusCode: this.sessOverReason + ? this.sessOverReason === "full" + ? HTTP_SESSION_FULL + : HTTP_IM_A_TEAPOT + : joinRes.status, + }; + } catch (e) { + return { + success: false, + statusCode: HTTP_INTERNAL_SERVER_ERROR, + }; + } + } + + public async leaveCollabAsync(reason: SessionOverReason) { + this.sessOverReason = reason; + this.sock?.close(); + } + + private async recvJoinedMessageAsync(msg: Protocol.JoinedMessage) { + pxt.debug( + `Server said we're joined as "${msg.role}" in slot "${msg.slot}"` + ); + const { role } = msg; + + this.clientRole = role; + + // TODO: Set initial state + } + + private async recvPresenceMessageAsync(msg: Protocol.PresenceMessage) { + pxt.debug("Server sent presence"); + await setPresenceAsync(msg.presence); + } + + private async recvPlayerJoinedMessageAsync( + msg: Protocol.PlayerJoinedMessage + ) { + pxt.debug("Server sent player joined"); + if (this.clientRole === "host") { + //await this.sendCurrentScreenAsync(); // Workaround for server sometimes not sending the current screen to new players. Needs debugging. + } + await playerJoinedAsync(msg.clientId); + } + + private async recvPlayerLeftMessageAsync(msg: Protocol.PlayerLeftMessage) { + pxt.debug("Server sent player joined"); + await playerLeftAsync(msg.clientId); + } + + public kickPlayer(clientId: string) { + const msg: Protocol.KickPlayerMessage = { + type: "kick-player", + clientId, + }; + this.sendMessage(msg); + } + + public collabOver(reason: SessionOverReason) { + this.sessOverReason = reason; + destroyCollabClient(); + } +} + +let collabClient: CollabClient | undefined; + +function destroyCollabClient() { + collabClient?.destroy(); + collabClient = undefined; +} + +export async function hostCollabAsync(): Promise { + destroyCollabClient(); + collabClient = new CollabClient(); + const collabInfo = await collabClient.hostCollabAsync(); + return collabInfo; +} + +export async function joinCollabAsync( + joinCode: string +): Promise { + destroyCollabClient(); + collabClient = new CollabClient(); + const collabInfo = await collabClient.joinCollabAsync(joinCode); + return collabInfo; +} + +export async function leaveCollabAsync(reason: SessionOverReason) { + await collabClient?.leaveCollabAsync(reason); + destroyCollabClient(); +} + +export function kickPlayer(clientId: string) { + collabClient?.kickPlayer(clientId); +} + +export function collabOver(reason: SessionOverReason) { + collabClient?.collabOver(reason); +} + +export function destroy() { + destroyCollabClient(); +} + +//============================================================================= +// Network Messages +// Hiding these here for now to ensure they're not used outside of this module. +namespace Protocol { + // Procotol version. Updating this should be a rare occurance. We commit to + // backward compatibility, but in the case where we must make a breaking + // change, we can bump this version and let the server know which version + // we're compabible with. + export const VERSION = 2; + + type MessageBase = { + type: string; + }; + + export type ConnectMessage = MessageBase & { + type: "connect"; + ticket: string; + version: number; + }; + + export type PresenceMessage = MessageBase & { + type: "presence"; + presence: Presence; + }; + + export type JoinedMessage = MessageBase & { + type: "joined"; + role: ClientRole; + slot: number; + clientId: string; + }; + + export type PlayerJoinedMessage = MessageBase & { + type: "player-joined"; + clientId: string; + }; + + export type PlayerLeftMessage = MessageBase & { + type: "player-left"; + clientId: string; + }; + + export type KickPlayerMessage = MessageBase & { + type: "kick-player"; + clientId: string; + }; + + export type CollabOverMessage = MessageBase & { + type: "collab-over"; + reason: SessionOverReason; + }; + + export type Message = + | ConnectMessage + | PresenceMessage + | JoinedMessage + | PlayerJoinedMessage + | PlayerLeftMessage + | KickPlayerMessage + | CollabOverMessage; +} diff --git a/multiplayer/src/services/gameClient.ts b/multiplayer/src/services/gameClient.ts index 4cce1055b428..8c26ca4d1da8 100644 --- a/multiplayer/src/services/gameClient.ts +++ b/multiplayer/src/services/gameClient.ts @@ -16,7 +16,7 @@ import { GameOverReason, GameJoinResult, HTTP_OK, - HTTP_GAME_FULL, + HTTP_SESSION_FULL, HTTP_INTERNAL_SERVER_ERROR, HTTP_IM_A_TEAPOT, SimKey, @@ -24,7 +24,7 @@ import { IconType, } from "../types"; import { - notifyGameDisconnected, + notifyDisconnected, setGameModeAsync, setPresenceAsync, setReactionAsync, @@ -41,7 +41,7 @@ import { simDriver } from "./simHost"; const GAME_HOST_PROD = "https://mp.makecode.com"; const GAME_HOST_STAGING = "https://multiplayer.staging.pxt.io"; const GAME_HOST_LOCALHOST = "http://localhost:8082"; -const GAME_HOST_DEV = GAME_HOST_STAGING; +const GAME_HOST_DEV = GAME_HOST_LOCALHOST; const GAME_HOST = (() => { if (pxt.BrowserUtils.isLocalHostDev()) { return GAME_HOST_DEV; @@ -186,7 +186,7 @@ class GameClient { }); this.sock?.on("close", () => { pxt.debug("socket disconnected"); - notifyGameDisconnected(this.gameOverReason); + notifyDisconnected(this.gameOverReason); clearTimeout(joinTimeout); resolve(); }); @@ -278,7 +278,7 @@ class GameClient { success: !this.gameOverReason, statusCode: this.gameOverReason ? this.gameOverReason === "full" - ? HTTP_GAME_FULL + ? HTTP_SESSION_FULL : HTTP_IM_A_TEAPOT : joinRes.status, }; diff --git a/multiplayer/src/state/actions.ts b/multiplayer/src/state/actions.ts index 589b5c86d135..753b9fbaf2b7 100644 --- a/multiplayer/src/state/actions.ts +++ b/multiplayer/src/state/actions.ts @@ -1,5 +1,6 @@ import { nanoid } from "nanoid"; import { + ActionBase, GameInfo, GameMode, Toast, @@ -9,13 +10,9 @@ import { ModalType, GameMetadata, ClientRole, + CollabInfo, } from "../types"; -// Changes to app state are performed by dispatching actions to the reducer -type ActionBase = { - type: string; -}; - /** * Actions */ @@ -35,6 +32,11 @@ type SetNetMode = ActionBase & { mode: NetMode; }; +type SetCollabInfo = ActionBase & { + type: "SET_COLLAB_INFO"; + collabInfo: CollabInfo | undefined; +}; + type SetGameInfo = ActionBase & { type: "SET_GAME_INFO"; gameInfo: GameInfo | undefined; @@ -143,6 +145,7 @@ export type Action = | SetClientRole | SetNetMode | SetGameInfo + | SetCollabInfo | SetGameMetadata | SetGameId | SetPlayerSlot @@ -195,6 +198,11 @@ export const setGameInfo = (gameInfo: GameInfo): SetGameInfo => ({ gameInfo, }); +export const setCollabInfo = (collabInfo: CollabInfo): SetCollabInfo => ({ + type: "SET_COLLAB_INFO", + collabInfo, +}); + export const setGameMetadata = ( gameMetadata: GameMetadata ): SetGameMetadata => ({ diff --git a/multiplayer/src/state/collab/CollabContext.tsx b/multiplayer/src/state/collab/CollabContext.tsx new file mode 100644 index 000000000000..0a94db01f810 --- /dev/null +++ b/multiplayer/src/state/collab/CollabContext.tsx @@ -0,0 +1,49 @@ +import { createContext, useEffect, useReducer } from 'react'; +import { CollabState, initialState } from './state'; +import { reducer } from './reducer'; +import { CollabAction } from './actions'; + +let state: CollabState; +let dispatch: React.Dispatch; + +export function collabStateAndDispatch() { + return { state, dispatch }; +} + +type CollabContextProps = { + state: CollabState; + dispatch: React.Dispatch; +}; + +const initialCollabContextProps: CollabContextProps = { + state: undefined!, + dispatch: undefined!, +}; + +export const CollabContext = createContext(initialCollabContextProps); + +export function CollabStateProvider( + props: React.PropsWithChildren<{}> +): React.ReactElement { + // Create the application state and state change mechanism (dispatch) + const [state_, dispatch_] = useReducer(reducer, initialState); + + useEffect(() => { + // Make state and dispatch available outside the React context + state = state_; + dispatch = dispatch_; + }, [state_, dispatch_]); + + return ( + // Provide current state and dispatch mechanism to all child components + + {props.children} + + ); +} + diff --git a/multiplayer/src/state/collab/actions.ts b/multiplayer/src/state/collab/actions.ts new file mode 100644 index 000000000000..6844e802544f --- /dev/null +++ b/multiplayer/src/state/collab/actions.ts @@ -0,0 +1,37 @@ +import { ActionBase } from "../../types"; + +type Init = ActionBase & { + type: "INIT"; +}; + +type PlayerJoined = ActionBase & { + type: "PLAYER_JOINED"; + playerId: string; +}; + +type PlayerLeft = ActionBase & { + type: "PLAYER_LEFT"; + playerId: string; +}; + +export type CollabAction = Init | PlayerJoined | PlayerLeft; + +export function init(): Init { + return { + type: "INIT", + }; +} + +export function playerJoined(playerId: string): PlayerJoined { + return { + type: "PLAYER_JOINED", + playerId, + }; +} + +export function playerLeft(playerId: string): PlayerLeft { + return { + type: "PLAYER_LEFT", + playerId, + }; +} diff --git a/multiplayer/src/state/collab/index.ts b/multiplayer/src/state/collab/index.ts new file mode 100644 index 000000000000..4a9c4956aef1 --- /dev/null +++ b/multiplayer/src/state/collab/index.ts @@ -0,0 +1,2 @@ +export { collabStateAndDispatch } from "./CollabContext"; +export { CollabStateProvider, CollabContext } from "./CollabContext"; diff --git a/multiplayer/src/state/collab/reducer.ts b/multiplayer/src/state/collab/reducer.ts new file mode 100644 index 000000000000..0f7074ffa7e2 --- /dev/null +++ b/multiplayer/src/state/collab/reducer.ts @@ -0,0 +1,30 @@ +import { CollabState, initialState } from "./state"; +import { CollabAction } from "./actions"; + +export function reducer(state: CollabState, action: CollabAction): CollabState { + switch (action.type) { + case "INIT": + return { + ...initialState, + }; + case "PLAYER_JOINED": + return { + ...state, + players: { + ...state.players, + [action.playerId]: { + clientId: action.playerId, + name: "Player " + action.playerId, + xp: 0, + yp: 0, + }, + }, + }; + case "PLAYER_LEFT": + const { [action.playerId]: _, ...players } = state.players; + return { + ...state, + players, + }; + } +} diff --git a/multiplayer/src/state/collab/state.ts b/multiplayer/src/state/collab/state.ts new file mode 100644 index 000000000000..38e43b461c10 --- /dev/null +++ b/multiplayer/src/state/collab/state.ts @@ -0,0 +1,9 @@ +import { CollabPlayer } from "../../services/collabClient" + +export type CollabState = { + players: { [playerId: string]: CollabPlayer }; +}; + +export const initialState: CollabState = { + players: {}, +}; diff --git a/multiplayer/src/state/reducer.ts b/multiplayer/src/state/reducer.ts index 500e17178644..d0d18136b84c 100644 --- a/multiplayer/src/state/reducer.ts +++ b/multiplayer/src/state/reducer.ts @@ -34,6 +34,7 @@ export default function reducer(state: AppState, action: Action): AppState { gameState: undefined, gameMetadata: undefined, gamePaused: undefined, + collabInfo: undefined, presence: { ...defaultPresence }, modal: undefined, modalOpts: undefined, @@ -50,6 +51,14 @@ export default function reducer(state: AppState, action: Action): AppState { }, }; } + case "SET_COLLAB_INFO": { + return { + ...state, + collabInfo: { + ...action.collabInfo, + } + }; + } case "SET_GAME_METADATA": { return { ...state, @@ -77,6 +86,7 @@ export default function reducer(state: AppState, action: Action): AppState { playerSlot: undefined, gameState: undefined, gameMetadata: undefined, + collabInfo: undefined, }; } case "SET_GAME_MODE": { diff --git a/multiplayer/src/state/state.ts b/multiplayer/src/state/state.ts index 3e5ee8f79f61..b6fe2f13569f 100644 --- a/multiplayer/src/state/state.ts +++ b/multiplayer/src/state/state.ts @@ -7,6 +7,7 @@ import { ModalType, GameMetadata, ClientRole, + CollabInfo, } from "../types"; export type AppState = { @@ -19,6 +20,7 @@ export type AppState = { gameState: GameState | undefined; gameMetadata: GameMetadata | undefined; gamePaused: boolean | undefined; + collabInfo: CollabInfo | undefined; toasts: ToastWithId[]; presence: Presence; modal: ModalType | undefined; @@ -49,6 +51,7 @@ export const initialAppState: AppState = { gameState: undefined, gameMetadata: undefined, gamePaused: undefined, + collabInfo: undefined, toasts: [], presence: { ...defaultPresence }, modal: undefined, diff --git a/multiplayer/src/types/index.ts b/multiplayer/src/types/index.ts index 43cd5d18024e..6b6d52934f09 100644 --- a/multiplayer/src/types/index.ts +++ b/multiplayer/src/types/index.ts @@ -1,9 +1,13 @@ export const HTTP_OK = 200; -export const HTTP_GAME_FULL = 507; // Insuffient storage. Using this HTTP status code to indicate the game is full. -export const HTTP_GAME_NOT_FOUND = 404; // Not found. Using this HTTP status code to indicate the game was not found. +export const HTTP_SESSION_FULL = 507; // Insuffient storage. Using this HTTP status code to indicate the game is full. +export const HTTP_SESSION_NOT_FOUND = 404; // Not found. Using this HTTP status code to indicate the game was not found. export const HTTP_IM_A_TEAPOT = 418; // I'm a teapot. Using this HTTP status code to indicate look elsewhere for the reason. export const HTTP_INTERNAL_SERVER_ERROR = 500; +export type ActionBase = { + type: string; +}; + export type NetMode = "init" | "connecting" | "connected"; export type ModalType = | "sign-in" @@ -13,14 +17,28 @@ export type ModalType = export type ClientRole = "host" | "guest" | "none"; export type GameMode = "lobby" | "playing"; -export type GameOverReason = +export type SessionOverReason = | "kicked" | "ended" | "left" | "full" | "rejected" - | "not-found" - | "compile-failed"; + | "not-found"; +export type GameOverReason = SessionOverReason | "compile-failed"; + +export type NetResult = { + success: boolean; + statusCode: number; +}; + +export type CollabInfo = { + joinCode?: string; + joinTicket?: string; + slot?: number; + initialState?: string; +}; + +export type CollabJoinResult = Partial & NetResult; export type GameInfo = { joinCode?: string; @@ -29,10 +47,7 @@ export type GameInfo = { slot?: number; }; -export type GameJoinResult = Partial & { - success: boolean; - statusCode: number; -}; +export type GameJoinResult = Partial & NetResult; export type GameMetadata = { title: string; @@ -216,7 +231,7 @@ export namespace SimMultiplayer { content: "Connection"; slot: number; connected: boolean; - } + }; export type Message = | ImageMessage From 987d992805870391fc66b4afdebfb1dfe01ebc16 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Sat, 30 Mar 2024 22:37:52 -0700 Subject: [PATCH 02/12] collab wip --- multiplayer/package-lock.json | 726 +++++++++++++++++- multiplayer/package.json | 1 + multiplayer/src/components/AppModal.tsx | 8 +- .../src/components/ArcadeSimulator.tsx | 4 +- multiplayer/src/components/CollabPage.tsx | 177 ++++- multiplayer/src/components/GamePaused.tsx | 8 +- multiplayer/src/components/HeaderBar.tsx | 8 +- multiplayer/src/components/JoinCodeLabel.tsx | 4 +- multiplayer/src/components/JoinOrHost.tsx | 8 +- multiplayer/src/components/SignedInPage.tsx | 2 +- multiplayer/src/epics/collab/index.ts | 4 + .../src/epics/collab/recvPlayerJoined.ts | 9 + .../src/epics/collab/recvPlayerLeft.ts | 9 + .../src/epics/collab/recvSetPlayerValue.ts | 17 + .../src/epics/collab/recvUpdatePresence.ts | 8 + multiplayer/src/epics/joinCollabAsync.ts | 7 +- multiplayer/src/epics/leaveCollabAsync.ts | 15 + multiplayer/src/epics/sendAbuseReportAsync.ts | 8 +- multiplayer/src/hooks/useClickedOutside.ts | 3 +- multiplayer/src/index.css | 21 + multiplayer/src/index.tsx | 3 +- multiplayer/src/services/collabCanvas.ts | 211 +++++ multiplayer/src/services/collabClient.ts | 101 ++- multiplayer/src/services/gameClient.ts | 14 +- .../src/state/collab/CollabContext.tsx | 13 +- multiplayer/src/state/collab/actions.ts | 48 +- multiplayer/src/state/collab/reducer.ts | 45 +- multiplayer/src/state/collab/state.ts | 2 +- multiplayer/src/state/reducer.ts | 2 +- multiplayer/src/types/index.ts | 40 + multiplayer/src/util/index.ts | 58 +- 31 files changed, 1514 insertions(+), 70 deletions(-) create mode 100644 multiplayer/src/epics/collab/recvPlayerJoined.ts create mode 100644 multiplayer/src/epics/collab/recvPlayerLeft.ts create mode 100644 multiplayer/src/epics/collab/recvSetPlayerValue.ts create mode 100644 multiplayer/src/epics/collab/recvUpdatePresence.ts create mode 100644 multiplayer/src/epics/leaveCollabAsync.ts create mode 100644 multiplayer/src/services/collabCanvas.ts diff --git a/multiplayer/package-lock.json b/multiplayer/package-lock.json index 513b7dd9ea09..a5b9980142e0 100644 --- a/multiplayer/package-lock.json +++ b/multiplayer/package-lock.json @@ -16,6 +16,7 @@ "framer-motion": "^6.5.1", "jsonpath-plus": "^8.1.0", "nanoid": "^4.0.0", + "pixi.js": "^7.4.2", "qrcode.react": "^3.1.0", "react-scripts": "^5.0.1", "smart-buffer": "^4.2.0", @@ -3248,6 +3249,349 @@ "node": ">= 8" } }, + "node_modules/@pixi/accessibility": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/accessibility/-/accessibility-7.4.2.tgz", + "integrity": "sha512-R6VEolm8uyy1FB1F2qaLKxVbzXAFTZCF2ka8fl9lsz7We6ZfO4QpXv9ur7DvzratjCQUQVCKo0/V7xL5q1EV/g==", + "peerDependencies": { + "@pixi/core": "7.4.2", + "@pixi/display": "7.4.2", + "@pixi/events": "7.4.2" + } + }, + "node_modules/@pixi/app": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/app/-/app-7.4.2.tgz", + "integrity": "sha512-ugkH3kOgjT8P1mTMY29yCOgEh+KuVMAn8uBxeY0aMqaUgIMysfpnFv+Aepp2CtvI9ygr22NC+OiKl+u+eEaQHw==", + "peerDependencies": { + "@pixi/core": "7.4.2", + "@pixi/display": "7.4.2" + } + }, + "node_modules/@pixi/assets": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/assets/-/assets-7.4.2.tgz", + "integrity": "sha512-anxho59H9egZwoaEdM5aLvYyxoz6NCy3CaQIvNHD1bbGg8L16Ih0e26QSBR5fu53jl8OjT6M7s+p6n7uu4+fGA==", + "dependencies": { + "@types/css-font-loading-module": "^0.0.12" + }, + "peerDependencies": { + "@pixi/core": "7.4.2" + } + }, + "node_modules/@pixi/color": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/color/-/color-7.4.2.tgz", + "integrity": "sha512-av1LOvhHsiaW8+T4n/FgnOKHby55/w7VcA1HzPIHRBtEcsmxvSCDanT1HU2LslNhrxLPzyVx18nlmalOyt5OBg==", + "dependencies": { + "@pixi/colord": "^2.9.6" + } + }, + "node_modules/@pixi/colord": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/@pixi/colord/-/colord-2.9.6.tgz", + "integrity": "sha512-nezytU2pw587fQstUu1AsJZDVEynjskwOL+kibwcdxsMBFqPsFFNA7xl0ii/gXuDi6M0xj3mfRJj8pBSc2jCfA==" + }, + "node_modules/@pixi/compressed-textures": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/compressed-textures/-/compressed-textures-7.4.2.tgz", + "integrity": "sha512-VJrt7el6O4ZJSWkeOGXwrhJaiLg1UBhHB3fj42VR4YloYkAxpfd9K6s6IcbcVz7n9L48APKBMgHyaB2pX2Ck/A==", + "peerDependencies": { + "@pixi/assets": "7.4.2", + "@pixi/core": "7.4.2" + } + }, + "node_modules/@pixi/constants": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/constants/-/constants-7.4.2.tgz", + "integrity": "sha512-N9vn6Wpz5WIQg7ugUg2+SdqD2u2+NM0QthE8YzLJ4tLH2Iz+/TrnPKUJzeyIqbg3sxJG5ZpGGPiacqIBpy1KyA==" + }, + "node_modules/@pixi/core": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/core/-/core-7.4.2.tgz", + "integrity": "sha512-UbMtgSEnyCOFPzbE6ThB9qopXxbZ5GCof2ArB4FXOC5Xi/83MOIIYg5kf5M8689C5HJMhg2SrJu3xLKppF+CMg==", + "dependencies": { + "@pixi/color": "7.4.2", + "@pixi/constants": "7.4.2", + "@pixi/extensions": "7.4.2", + "@pixi/math": "7.4.2", + "@pixi/runner": "7.4.2", + "@pixi/settings": "7.4.2", + "@pixi/ticker": "7.4.2", + "@pixi/utils": "7.4.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/pixijs" + } + }, + "node_modules/@pixi/display": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/display/-/display-7.4.2.tgz", + "integrity": "sha512-DaD0J7gIlNlzO0Fdlby/0OH+tB5LtCY6rgFeCBKVDnzmn8wKW3zYZRenWBSFJ0Psx6vLqXYkSIM/rcokaKviIw==", + "peerDependencies": { + "@pixi/core": "7.4.2" + } + }, + "node_modules/@pixi/events": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/events/-/events-7.4.2.tgz", + "integrity": "sha512-Jw/w57heZjzZShIXL0bxOvKB+XgGIevyezhGtfF2ZSzQoSBWo+Fj1uE0QwKd0RIaXegZw/DhSmiMJSbNmcjifA==", + "peerDependencies": { + "@pixi/core": "7.4.2", + "@pixi/display": "7.4.2" + } + }, + "node_modules/@pixi/extensions": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/extensions/-/extensions-7.4.2.tgz", + "integrity": "sha512-Hmx2+O0yZ8XIvgomHM9GZEGcy9S9Dd8flmtOK5Aa3fXs/8v7xD08+ANQpN9ZqWU2Xs+C6UBlpqlt2BWALvKKKA==" + }, + "node_modules/@pixi/extract": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/extract/-/extract-7.4.2.tgz", + "integrity": "sha512-JOX27TRWjVEjauGBbF8PU7/g6LYXnivehdgqS5QlVDv1CNHTOrz/j3MdKcVWOhyZPbH5c9sh7lxyRxvd9AIuTQ==", + "peerDependencies": { + "@pixi/core": "7.4.2" + } + }, + "node_modules/@pixi/filter-alpha": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/filter-alpha/-/filter-alpha-7.4.2.tgz", + "integrity": "sha512-9OsKJ+yvY2wIcQXwswj5HQBiwNGymwmqdxfp7mo+nZSBoDmxUqvMZzE9UNJ3eUlswuNvNRO8zNOsQvwdz7WFww==", + "peerDependencies": { + "@pixi/core": "7.4.2" + } + }, + "node_modules/@pixi/filter-blur": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/filter-blur/-/filter-blur-7.4.2.tgz", + "integrity": "sha512-gOXBbIUx6CRZP1fmsis2wLzzSsofrqmIHhbf1gIkZMIQaLsc9T7brj+PaLTTiOiyJgnvGN5j20RZnkERWWKV0Q==", + "peerDependencies": { + "@pixi/core": "7.4.2" + } + }, + "node_modules/@pixi/filter-color-matrix": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/filter-color-matrix/-/filter-color-matrix-7.4.2.tgz", + "integrity": "sha512-ykZiR59Gvj80UKs9qm7jeUTKvn+wWk6HBVJOmJbK9jFK5juakDWp7BbH26U78Q61EWj97kI1FdfcbMkuQ7rqkA==", + "peerDependencies": { + "@pixi/core": "7.4.2" + } + }, + "node_modules/@pixi/filter-displacement": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/filter-displacement/-/filter-displacement-7.4.2.tgz", + "integrity": "sha512-QS/eWp/ivsxef3xapNeGwpPX7vrqQQeo99Fux4k5zsvplnNEsf91t6QYJLG776AbZEu/qh8VYRBA5raIVY/REw==", + "peerDependencies": { + "@pixi/core": "7.4.2" + } + }, + "node_modules/@pixi/filter-fxaa": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/filter-fxaa/-/filter-fxaa-7.4.2.tgz", + "integrity": "sha512-U/ptJgDsfs/r8y2a6gCaiPfDu2IFAxpQ4wtfmBpz6vRhqeE4kI8yNIUx5dZbui57zlsJaW0BNacOQxHU0vLkyQ==", + "peerDependencies": { + "@pixi/core": "7.4.2" + } + }, + "node_modules/@pixi/filter-noise": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/filter-noise/-/filter-noise-7.4.2.tgz", + "integrity": "sha512-Vy9ViBFhZEGh6xKkd3kFWErolZTwv1Y5Qb1bV7qPIYbvBECYsqzlR4uCrrjBV6KKm0PufpG/+NKC5vICZaqKzg==", + "peerDependencies": { + "@pixi/core": "7.4.2" + } + }, + "node_modules/@pixi/graphics": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/graphics/-/graphics-7.4.2.tgz", + "integrity": "sha512-jH4/Tum2RqWzHGzvlwEr7HIVduoLO57Ze705N2zQPkUD57TInn5911aGUeoua7f/wK8cTLGzgB9BzSo2kTdcHw==", + "peerDependencies": { + "@pixi/core": "7.4.2", + "@pixi/display": "7.4.2", + "@pixi/sprite": "7.4.2" + } + }, + "node_modules/@pixi/math": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/math/-/math-7.4.2.tgz", + "integrity": "sha512-7jHmCQoYk6e0rfSKjdNFOPl0wCcdgoraxgteXJTTHv3r0bMNx2pHD9FJ0VvocEUG7XHfj55O3+u7yItOAx0JaQ==" + }, + "node_modules/@pixi/mesh": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/mesh/-/mesh-7.4.2.tgz", + "integrity": "sha512-mEkKyQvvMrYXC3pahvH5WBIKtrtB63WixRr91ANFI7zXD+ESG6Ap6XtxMCJmXDQPwBDNk7SWVMiCflYuchG7kA==", + "peerDependencies": { + "@pixi/core": "7.4.2", + "@pixi/display": "7.4.2" + } + }, + "node_modules/@pixi/mesh-extras": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/mesh-extras/-/mesh-extras-7.4.2.tgz", + "integrity": "sha512-vNR/7wjxjs7sv9fGoKkHyU91ZAD+7EnMHBS5F3CVISlOIFxLi96NNZCB81oUIdky/90pHw40johd/4izR5zTyw==", + "peerDependencies": { + "@pixi/core": "7.4.2", + "@pixi/mesh": "7.4.2" + } + }, + "node_modules/@pixi/mixin-cache-as-bitmap": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/mixin-cache-as-bitmap/-/mixin-cache-as-bitmap-7.4.2.tgz", + "integrity": "sha512-6dgthi2ruUT/lervSrFDQ7vXkEsHo6CxdgV7W/wNdW1dqgQlKfDvO6FhjXzyIMRLSooUf5FoeluVtfsjkUIYrw==", + "peerDependencies": { + "@pixi/core": "7.4.2", + "@pixi/display": "7.4.2", + "@pixi/sprite": "7.4.2" + } + }, + "node_modules/@pixi/mixin-get-child-by-name": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/mixin-get-child-by-name/-/mixin-get-child-by-name-7.4.2.tgz", + "integrity": "sha512-0Cfw8JpQhsixprxiYph4Lj+B5n83Kk4ftNMXgM5xtZz+tVLz5s91qR0MqcdzwTGTJ7utVygiGmS4/3EfR/duRQ==", + "peerDependencies": { + "@pixi/display": "7.4.2" + } + }, + "node_modules/@pixi/mixin-get-global-position": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/mixin-get-global-position/-/mixin-get-global-position-7.4.2.tgz", + "integrity": "sha512-LcsahbVdX4DFS2IcGfNp4KaXuu7SjAwUp/flZSGIfstyKOKb5FWFgihtqcc9ZT4coyri3gs2JbILZub/zPZj1w==", + "peerDependencies": { + "@pixi/core": "7.4.2", + "@pixi/display": "7.4.2" + } + }, + "node_modules/@pixi/particle-container": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/particle-container/-/particle-container-7.4.2.tgz", + "integrity": "sha512-B78Qq86kt0lEa5WtB2YFIm3+PjhKfw9La9R++GBSgABl+g13s2UaZ6BIPxvY3JxWMdxPm4iPrQPFX1QWRN68mw==", + "peerDependencies": { + "@pixi/core": "7.4.2", + "@pixi/display": "7.4.2", + "@pixi/sprite": "7.4.2" + } + }, + "node_modules/@pixi/prepare": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/prepare/-/prepare-7.4.2.tgz", + "integrity": "sha512-PugyMzReCHXUzc3so9PPJj2OdHwibpUNWyqG4mWY2UUkb6c8NAGK1AnAPiscOvLilJcv/XQSFoNhX+N1jrvJEg==", + "peerDependencies": { + "@pixi/core": "7.4.2", + "@pixi/display": "7.4.2", + "@pixi/graphics": "7.4.2", + "@pixi/text": "7.4.2" + } + }, + "node_modules/@pixi/runner": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/runner/-/runner-7.4.2.tgz", + "integrity": "sha512-LPBpwym4vdyyDY5ucF4INQccaGyxztERyLTY1YN6aqJyyMmnc7iqXlIKt+a0euMBtNoLoxy6MWMvIuZj0JfFPA==" + }, + "node_modules/@pixi/settings": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/settings/-/settings-7.4.2.tgz", + "integrity": "sha512-pMN+L6aWgvUbwhFIL/BTHKe2ShYGPZ8h9wlVBnFHMtUcJcFLMF1B3lzuvCayZRepOphs6RY0TqvnDvVb585JhQ==", + "dependencies": { + "@pixi/constants": "7.4.2", + "@types/css-font-loading-module": "^0.0.12", + "ismobilejs": "^1.1.0" + } + }, + "node_modules/@pixi/sprite": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/sprite/-/sprite-7.4.2.tgz", + "integrity": "sha512-Ccf/OVQsB+HQV0Fyf5lwD+jk1jeU7uSIqEjbxenNNssmEdB7S5qlkTBV2EJTHT83+T6Z9OMOHsreJZerydpjeg==", + "peerDependencies": { + "@pixi/core": "7.4.2", + "@pixi/display": "7.4.2" + } + }, + "node_modules/@pixi/sprite-animated": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/sprite-animated/-/sprite-animated-7.4.2.tgz", + "integrity": "sha512-QPT6yxCUGOBN+98H3pyIZ1ZO6Y7BN1o0Q2IMZEsD1rNfZJrTYS3Q8VlCG5t2YlFlcB8j5iBo24bZb6FUxLOmsQ==", + "peerDependencies": { + "@pixi/core": "7.4.2", + "@pixi/sprite": "7.4.2" + } + }, + "node_modules/@pixi/sprite-tiling": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/sprite-tiling/-/sprite-tiling-7.4.2.tgz", + "integrity": "sha512-Z8PP6ewy3nuDYL+NeEdltHAhuucVgia33uzAitvH3OqqRSx6a6YRBFbNLUM9Sx+fBO2Lk3PpV1g6QZX+NE5LOg==", + "peerDependencies": { + "@pixi/core": "7.4.2", + "@pixi/display": "7.4.2", + "@pixi/sprite": "7.4.2" + } + }, + "node_modules/@pixi/spritesheet": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/spritesheet/-/spritesheet-7.4.2.tgz", + "integrity": "sha512-YIvHdpXW+AYp8vD0NkjJmrdnVHTZKidCnx6k8ATSuuvCT6O5Tuh2N/Ul2oDj4/QaePy0lVhyhAbZpJW00Jr7mQ==", + "peerDependencies": { + "@pixi/assets": "7.4.2", + "@pixi/core": "7.4.2" + } + }, + "node_modules/@pixi/text": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/text/-/text-7.4.2.tgz", + "integrity": "sha512-rZZWpJNsIQ8WoCWrcVg8Gi6L/PDakB941clo6dO3XjoII2ucoOUcnpe5HIkudxi2xPvS/8Bfq990gFEx50TP5A==", + "peerDependencies": { + "@pixi/core": "7.4.2", + "@pixi/sprite": "7.4.2" + } + }, + "node_modules/@pixi/text-bitmap": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/text-bitmap/-/text-bitmap-7.4.2.tgz", + "integrity": "sha512-lPBMJ83JnpFVL+6ckQ8KO8QmwdPm0z9Zs/M0NgFKH2F+BcjelRNnk80NI3O0qBDYSEDQIE+cFbKoZ213kf7zwA==", + "peerDependencies": { + "@pixi/assets": "7.4.2", + "@pixi/core": "7.4.2", + "@pixi/display": "7.4.2", + "@pixi/mesh": "7.4.2", + "@pixi/text": "7.4.2" + } + }, + "node_modules/@pixi/text-html": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/text-html/-/text-html-7.4.2.tgz", + "integrity": "sha512-duOu8oDYeDNuyPozj2DAsQ5VZBbRiwIXy78Gn7H2pCiEAefw/Uv5jJYwdgneKME0e1tOxz1eOUGKPcI6IJnZjw==", + "peerDependencies": { + "@pixi/core": "7.4.2", + "@pixi/display": "7.4.2", + "@pixi/sprite": "7.4.2", + "@pixi/text": "7.4.2" + } + }, + "node_modules/@pixi/ticker": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/ticker/-/ticker-7.4.2.tgz", + "integrity": "sha512-cAvxCh/KI6IW4m3tp2b+GQIf+DoSj9NNmPJmsOeEJ7LzvruG8Ps7SKI6CdjQob5WbceL1apBTDbqZ/f77hFDiQ==", + "dependencies": { + "@pixi/extensions": "7.4.2", + "@pixi/settings": "7.4.2", + "@pixi/utils": "7.4.2" + } + }, + "node_modules/@pixi/utils": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/utils/-/utils-7.4.2.tgz", + "integrity": "sha512-aU/itcyMC4TxFbmdngmak6ey4kC5c16Y5ntIYob9QnjNAfD/7GTsYIBnP6FqEAyO1eq0MjkAALxdONuay1BG3g==", + "dependencies": { + "@pixi/color": "7.4.2", + "@pixi/constants": "7.4.2", + "@pixi/settings": "7.4.2", + "@types/earcut": "^2.1.0", + "earcut": "^2.2.4", + "eventemitter3": "^4.0.0", + "url": "^0.11.0" + } + }, "node_modules/@pmmmwh/react-refresh-webpack-plugin": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.7.tgz", @@ -3707,6 +4051,16 @@ "@types/node": "*" } }, + "node_modules/@types/css-font-loading-module": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.12.tgz", + "integrity": "sha512-x2tZZYkSxXqWvTDgveSynfjq/T2HyiZHXb00j/+gy19yp70PHCizM48XFdjBCWH7eHBD0R5i/pw9yMBP/BH5uA==" + }, + "node_modules/@types/earcut": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@types/earcut/-/earcut-2.1.4.tgz", + "integrity": "sha512-qp3m9PPz4gULB9MhjGID7wpo3gJ4bTGXm7ltNDsmOvsPduTeHp8wSW9YckBj3mljeOh4F0m2z/0JKAALRKbmLQ==" + }, "node_modules/@types/eslint": { "version": "8.4.6", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.6.tgz", @@ -6703,6 +7057,11 @@ "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==" }, + "node_modules/earcut": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", + "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==" + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -9566,6 +9925,11 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, + "node_modules/ismobilejs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ismobilejs/-/ismobilejs-1.1.1.tgz", + "integrity": "sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw==" + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", @@ -12887,6 +13251,47 @@ "node": ">= 6" } }, + "node_modules/pixi.js": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-7.4.2.tgz", + "integrity": "sha512-TifqgHGNofO7UCEbdZJOpUu7dUnpu4YZ0o76kfCqxDa4RS8ITc9zjECCbtalmuNXkVhSEZmBKQvE7qhHMqw/xg==", + "dependencies": { + "@pixi/accessibility": "7.4.2", + "@pixi/app": "7.4.2", + "@pixi/assets": "7.4.2", + "@pixi/compressed-textures": "7.4.2", + "@pixi/core": "7.4.2", + "@pixi/display": "7.4.2", + "@pixi/events": "7.4.2", + "@pixi/extensions": "7.4.2", + "@pixi/extract": "7.4.2", + "@pixi/filter-alpha": "7.4.2", + "@pixi/filter-blur": "7.4.2", + "@pixi/filter-color-matrix": "7.4.2", + "@pixi/filter-displacement": "7.4.2", + "@pixi/filter-fxaa": "7.4.2", + "@pixi/filter-noise": "7.4.2", + "@pixi/graphics": "7.4.2", + "@pixi/mesh": "7.4.2", + "@pixi/mesh-extras": "7.4.2", + "@pixi/mixin-cache-as-bitmap": "7.4.2", + "@pixi/mixin-get-child-by-name": "7.4.2", + "@pixi/mixin-get-global-position": "7.4.2", + "@pixi/particle-container": "7.4.2", + "@pixi/prepare": "7.4.2", + "@pixi/sprite": "7.4.2", + "@pixi/sprite-animated": "7.4.2", + "@pixi/sprite-tiling": "7.4.2", + "@pixi/spritesheet": "7.4.2", + "@pixi/text": "7.4.2", + "@pixi/text-bitmap": "7.4.2", + "@pixi/text-html": "7.4.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/pixijs" + } + }, "node_modules/pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -14475,7 +14880,6 @@ "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", - "dev": true, "engines": { "node": ">=0.4.x" } @@ -16710,7 +17114,6 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", "integrity": "sha512-kbailJa29QrtXnxgq+DdCEGlbTeYM2eJUxsz6vjZavrCYPMIFHMKQmSKYAIuUK2i7hgPm28a8piX5NTUtM/LKQ==", - "dev": true, "dependencies": { "punycode": "1.3.2", "querystring": "0.2.0" @@ -16728,8 +17131,7 @@ "node_modules/url/node_modules/punycode": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==", - "dev": true + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" }, "node_modules/util": { "version": "0.12.5", @@ -19867,6 +20269,258 @@ "fastq": "^1.6.0" } }, + "@pixi/accessibility": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/accessibility/-/accessibility-7.4.2.tgz", + "integrity": "sha512-R6VEolm8uyy1FB1F2qaLKxVbzXAFTZCF2ka8fl9lsz7We6ZfO4QpXv9ur7DvzratjCQUQVCKo0/V7xL5q1EV/g==", + "requires": {} + }, + "@pixi/app": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/app/-/app-7.4.2.tgz", + "integrity": "sha512-ugkH3kOgjT8P1mTMY29yCOgEh+KuVMAn8uBxeY0aMqaUgIMysfpnFv+Aepp2CtvI9ygr22NC+OiKl+u+eEaQHw==", + "requires": {} + }, + "@pixi/assets": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/assets/-/assets-7.4.2.tgz", + "integrity": "sha512-anxho59H9egZwoaEdM5aLvYyxoz6NCy3CaQIvNHD1bbGg8L16Ih0e26QSBR5fu53jl8OjT6M7s+p6n7uu4+fGA==", + "requires": { + "@types/css-font-loading-module": "^0.0.12" + } + }, + "@pixi/color": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/color/-/color-7.4.2.tgz", + "integrity": "sha512-av1LOvhHsiaW8+T4n/FgnOKHby55/w7VcA1HzPIHRBtEcsmxvSCDanT1HU2LslNhrxLPzyVx18nlmalOyt5OBg==", + "requires": { + "@pixi/colord": "^2.9.6" + } + }, + "@pixi/colord": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/@pixi/colord/-/colord-2.9.6.tgz", + "integrity": "sha512-nezytU2pw587fQstUu1AsJZDVEynjskwOL+kibwcdxsMBFqPsFFNA7xl0ii/gXuDi6M0xj3mfRJj8pBSc2jCfA==" + }, + "@pixi/compressed-textures": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/compressed-textures/-/compressed-textures-7.4.2.tgz", + "integrity": "sha512-VJrt7el6O4ZJSWkeOGXwrhJaiLg1UBhHB3fj42VR4YloYkAxpfd9K6s6IcbcVz7n9L48APKBMgHyaB2pX2Ck/A==", + "requires": {} + }, + "@pixi/constants": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/constants/-/constants-7.4.2.tgz", + "integrity": "sha512-N9vn6Wpz5WIQg7ugUg2+SdqD2u2+NM0QthE8YzLJ4tLH2Iz+/TrnPKUJzeyIqbg3sxJG5ZpGGPiacqIBpy1KyA==" + }, + "@pixi/core": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/core/-/core-7.4.2.tgz", + "integrity": "sha512-UbMtgSEnyCOFPzbE6ThB9qopXxbZ5GCof2ArB4FXOC5Xi/83MOIIYg5kf5M8689C5HJMhg2SrJu3xLKppF+CMg==", + "requires": { + "@pixi/color": "7.4.2", + "@pixi/constants": "7.4.2", + "@pixi/extensions": "7.4.2", + "@pixi/math": "7.4.2", + "@pixi/runner": "7.4.2", + "@pixi/settings": "7.4.2", + "@pixi/ticker": "7.4.2", + "@pixi/utils": "7.4.2" + } + }, + "@pixi/display": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/display/-/display-7.4.2.tgz", + "integrity": "sha512-DaD0J7gIlNlzO0Fdlby/0OH+tB5LtCY6rgFeCBKVDnzmn8wKW3zYZRenWBSFJ0Psx6vLqXYkSIM/rcokaKviIw==", + "requires": {} + }, + "@pixi/events": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/events/-/events-7.4.2.tgz", + "integrity": "sha512-Jw/w57heZjzZShIXL0bxOvKB+XgGIevyezhGtfF2ZSzQoSBWo+Fj1uE0QwKd0RIaXegZw/DhSmiMJSbNmcjifA==", + "requires": {} + }, + "@pixi/extensions": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/extensions/-/extensions-7.4.2.tgz", + "integrity": "sha512-Hmx2+O0yZ8XIvgomHM9GZEGcy9S9Dd8flmtOK5Aa3fXs/8v7xD08+ANQpN9ZqWU2Xs+C6UBlpqlt2BWALvKKKA==" + }, + "@pixi/extract": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/extract/-/extract-7.4.2.tgz", + "integrity": "sha512-JOX27TRWjVEjauGBbF8PU7/g6LYXnivehdgqS5QlVDv1CNHTOrz/j3MdKcVWOhyZPbH5c9sh7lxyRxvd9AIuTQ==", + "requires": {} + }, + "@pixi/filter-alpha": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/filter-alpha/-/filter-alpha-7.4.2.tgz", + "integrity": "sha512-9OsKJ+yvY2wIcQXwswj5HQBiwNGymwmqdxfp7mo+nZSBoDmxUqvMZzE9UNJ3eUlswuNvNRO8zNOsQvwdz7WFww==", + "requires": {} + }, + "@pixi/filter-blur": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/filter-blur/-/filter-blur-7.4.2.tgz", + "integrity": "sha512-gOXBbIUx6CRZP1fmsis2wLzzSsofrqmIHhbf1gIkZMIQaLsc9T7brj+PaLTTiOiyJgnvGN5j20RZnkERWWKV0Q==", + "requires": {} + }, + "@pixi/filter-color-matrix": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/filter-color-matrix/-/filter-color-matrix-7.4.2.tgz", + "integrity": "sha512-ykZiR59Gvj80UKs9qm7jeUTKvn+wWk6HBVJOmJbK9jFK5juakDWp7BbH26U78Q61EWj97kI1FdfcbMkuQ7rqkA==", + "requires": {} + }, + "@pixi/filter-displacement": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/filter-displacement/-/filter-displacement-7.4.2.tgz", + "integrity": "sha512-QS/eWp/ivsxef3xapNeGwpPX7vrqQQeo99Fux4k5zsvplnNEsf91t6QYJLG776AbZEu/qh8VYRBA5raIVY/REw==", + "requires": {} + }, + "@pixi/filter-fxaa": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/filter-fxaa/-/filter-fxaa-7.4.2.tgz", + "integrity": "sha512-U/ptJgDsfs/r8y2a6gCaiPfDu2IFAxpQ4wtfmBpz6vRhqeE4kI8yNIUx5dZbui57zlsJaW0BNacOQxHU0vLkyQ==", + "requires": {} + }, + "@pixi/filter-noise": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/filter-noise/-/filter-noise-7.4.2.tgz", + "integrity": "sha512-Vy9ViBFhZEGh6xKkd3kFWErolZTwv1Y5Qb1bV7qPIYbvBECYsqzlR4uCrrjBV6KKm0PufpG/+NKC5vICZaqKzg==", + "requires": {} + }, + "@pixi/graphics": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/graphics/-/graphics-7.4.2.tgz", + "integrity": "sha512-jH4/Tum2RqWzHGzvlwEr7HIVduoLO57Ze705N2zQPkUD57TInn5911aGUeoua7f/wK8cTLGzgB9BzSo2kTdcHw==", + "requires": {} + }, + "@pixi/math": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/math/-/math-7.4.2.tgz", + "integrity": "sha512-7jHmCQoYk6e0rfSKjdNFOPl0wCcdgoraxgteXJTTHv3r0bMNx2pHD9FJ0VvocEUG7XHfj55O3+u7yItOAx0JaQ==" + }, + "@pixi/mesh": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/mesh/-/mesh-7.4.2.tgz", + "integrity": "sha512-mEkKyQvvMrYXC3pahvH5WBIKtrtB63WixRr91ANFI7zXD+ESG6Ap6XtxMCJmXDQPwBDNk7SWVMiCflYuchG7kA==", + "requires": {} + }, + "@pixi/mesh-extras": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/mesh-extras/-/mesh-extras-7.4.2.tgz", + "integrity": "sha512-vNR/7wjxjs7sv9fGoKkHyU91ZAD+7EnMHBS5F3CVISlOIFxLi96NNZCB81oUIdky/90pHw40johd/4izR5zTyw==", + "requires": {} + }, + "@pixi/mixin-cache-as-bitmap": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/mixin-cache-as-bitmap/-/mixin-cache-as-bitmap-7.4.2.tgz", + "integrity": "sha512-6dgthi2ruUT/lervSrFDQ7vXkEsHo6CxdgV7W/wNdW1dqgQlKfDvO6FhjXzyIMRLSooUf5FoeluVtfsjkUIYrw==", + "requires": {} + }, + "@pixi/mixin-get-child-by-name": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/mixin-get-child-by-name/-/mixin-get-child-by-name-7.4.2.tgz", + "integrity": "sha512-0Cfw8JpQhsixprxiYph4Lj+B5n83Kk4ftNMXgM5xtZz+tVLz5s91qR0MqcdzwTGTJ7utVygiGmS4/3EfR/duRQ==", + "requires": {} + }, + "@pixi/mixin-get-global-position": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/mixin-get-global-position/-/mixin-get-global-position-7.4.2.tgz", + "integrity": "sha512-LcsahbVdX4DFS2IcGfNp4KaXuu7SjAwUp/flZSGIfstyKOKb5FWFgihtqcc9ZT4coyri3gs2JbILZub/zPZj1w==", + "requires": {} + }, + "@pixi/particle-container": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/particle-container/-/particle-container-7.4.2.tgz", + "integrity": "sha512-B78Qq86kt0lEa5WtB2YFIm3+PjhKfw9La9R++GBSgABl+g13s2UaZ6BIPxvY3JxWMdxPm4iPrQPFX1QWRN68mw==", + "requires": {} + }, + "@pixi/prepare": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/prepare/-/prepare-7.4.2.tgz", + "integrity": "sha512-PugyMzReCHXUzc3so9PPJj2OdHwibpUNWyqG4mWY2UUkb6c8NAGK1AnAPiscOvLilJcv/XQSFoNhX+N1jrvJEg==", + "requires": {} + }, + "@pixi/runner": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/runner/-/runner-7.4.2.tgz", + "integrity": "sha512-LPBpwym4vdyyDY5ucF4INQccaGyxztERyLTY1YN6aqJyyMmnc7iqXlIKt+a0euMBtNoLoxy6MWMvIuZj0JfFPA==" + }, + "@pixi/settings": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/settings/-/settings-7.4.2.tgz", + "integrity": "sha512-pMN+L6aWgvUbwhFIL/BTHKe2ShYGPZ8h9wlVBnFHMtUcJcFLMF1B3lzuvCayZRepOphs6RY0TqvnDvVb585JhQ==", + "requires": { + "@pixi/constants": "7.4.2", + "@types/css-font-loading-module": "^0.0.12", + "ismobilejs": "^1.1.0" + } + }, + "@pixi/sprite": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/sprite/-/sprite-7.4.2.tgz", + "integrity": "sha512-Ccf/OVQsB+HQV0Fyf5lwD+jk1jeU7uSIqEjbxenNNssmEdB7S5qlkTBV2EJTHT83+T6Z9OMOHsreJZerydpjeg==", + "requires": {} + }, + "@pixi/sprite-animated": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/sprite-animated/-/sprite-animated-7.4.2.tgz", + "integrity": "sha512-QPT6yxCUGOBN+98H3pyIZ1ZO6Y7BN1o0Q2IMZEsD1rNfZJrTYS3Q8VlCG5t2YlFlcB8j5iBo24bZb6FUxLOmsQ==", + "requires": {} + }, + "@pixi/sprite-tiling": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/sprite-tiling/-/sprite-tiling-7.4.2.tgz", + "integrity": "sha512-Z8PP6ewy3nuDYL+NeEdltHAhuucVgia33uzAitvH3OqqRSx6a6YRBFbNLUM9Sx+fBO2Lk3PpV1g6QZX+NE5LOg==", + "requires": {} + }, + "@pixi/spritesheet": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/spritesheet/-/spritesheet-7.4.2.tgz", + "integrity": "sha512-YIvHdpXW+AYp8vD0NkjJmrdnVHTZKidCnx6k8ATSuuvCT6O5Tuh2N/Ul2oDj4/QaePy0lVhyhAbZpJW00Jr7mQ==", + "requires": {} + }, + "@pixi/text": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/text/-/text-7.4.2.tgz", + "integrity": "sha512-rZZWpJNsIQ8WoCWrcVg8Gi6L/PDakB941clo6dO3XjoII2ucoOUcnpe5HIkudxi2xPvS/8Bfq990gFEx50TP5A==", + "requires": {} + }, + "@pixi/text-bitmap": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/text-bitmap/-/text-bitmap-7.4.2.tgz", + "integrity": "sha512-lPBMJ83JnpFVL+6ckQ8KO8QmwdPm0z9Zs/M0NgFKH2F+BcjelRNnk80NI3O0qBDYSEDQIE+cFbKoZ213kf7zwA==", + "requires": {} + }, + "@pixi/text-html": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/text-html/-/text-html-7.4.2.tgz", + "integrity": "sha512-duOu8oDYeDNuyPozj2DAsQ5VZBbRiwIXy78Gn7H2pCiEAefw/Uv5jJYwdgneKME0e1tOxz1eOUGKPcI6IJnZjw==", + "requires": {} + }, + "@pixi/ticker": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/ticker/-/ticker-7.4.2.tgz", + "integrity": "sha512-cAvxCh/KI6IW4m3tp2b+GQIf+DoSj9NNmPJmsOeEJ7LzvruG8Ps7SKI6CdjQob5WbceL1apBTDbqZ/f77hFDiQ==", + "requires": { + "@pixi/extensions": "7.4.2", + "@pixi/settings": "7.4.2", + "@pixi/utils": "7.4.2" + } + }, + "@pixi/utils": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@pixi/utils/-/utils-7.4.2.tgz", + "integrity": "sha512-aU/itcyMC4TxFbmdngmak6ey4kC5c16Y5ntIYob9QnjNAfD/7GTsYIBnP6FqEAyO1eq0MjkAALxdONuay1BG3g==", + "requires": { + "@pixi/color": "7.4.2", + "@pixi/constants": "7.4.2", + "@pixi/settings": "7.4.2", + "@types/earcut": "^2.1.0", + "earcut": "^2.2.4", + "eventemitter3": "^4.0.0", + "url": "^0.11.0" + } + }, "@pmmmwh/react-refresh-webpack-plugin": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.7.tgz", @@ -20163,6 +20817,16 @@ "@types/node": "*" } }, + "@types/css-font-loading-module": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.12.tgz", + "integrity": "sha512-x2tZZYkSxXqWvTDgveSynfjq/T2HyiZHXb00j/+gy19yp70PHCizM48XFdjBCWH7eHBD0R5i/pw9yMBP/BH5uA==" + }, + "@types/earcut": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@types/earcut/-/earcut-2.1.4.tgz", + "integrity": "sha512-qp3m9PPz4gULB9MhjGID7wpo3gJ4bTGXm7ltNDsmOvsPduTeHp8wSW9YckBj3mljeOh4F0m2z/0JKAALRKbmLQ==" + }, "@types/eslint": { "version": "8.4.6", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.6.tgz", @@ -22445,6 +23109,11 @@ "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==" }, + "earcut": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", + "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==" + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -24493,6 +25162,11 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, + "ismobilejs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ismobilejs/-/ismobilejs-1.1.1.tgz", + "integrity": "sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw==" + }, "istanbul-lib-coverage": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", @@ -26945,6 +27619,43 @@ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==" }, + "pixi.js": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-7.4.2.tgz", + "integrity": "sha512-TifqgHGNofO7UCEbdZJOpUu7dUnpu4YZ0o76kfCqxDa4RS8ITc9zjECCbtalmuNXkVhSEZmBKQvE7qhHMqw/xg==", + "requires": { + "@pixi/accessibility": "7.4.2", + "@pixi/app": "7.4.2", + "@pixi/assets": "7.4.2", + "@pixi/compressed-textures": "7.4.2", + "@pixi/core": "7.4.2", + "@pixi/display": "7.4.2", + "@pixi/events": "7.4.2", + "@pixi/extensions": "7.4.2", + "@pixi/extract": "7.4.2", + "@pixi/filter-alpha": "7.4.2", + "@pixi/filter-blur": "7.4.2", + "@pixi/filter-color-matrix": "7.4.2", + "@pixi/filter-displacement": "7.4.2", + "@pixi/filter-fxaa": "7.4.2", + "@pixi/filter-noise": "7.4.2", + "@pixi/graphics": "7.4.2", + "@pixi/mesh": "7.4.2", + "@pixi/mesh-extras": "7.4.2", + "@pixi/mixin-cache-as-bitmap": "7.4.2", + "@pixi/mixin-get-child-by-name": "7.4.2", + "@pixi/mixin-get-global-position": "7.4.2", + "@pixi/particle-container": "7.4.2", + "@pixi/prepare": "7.4.2", + "@pixi/sprite": "7.4.2", + "@pixi/sprite-animated": "7.4.2", + "@pixi/sprite-tiling": "7.4.2", + "@pixi/spritesheet": "7.4.2", + "@pixi/text": "7.4.2", + "@pixi/text-bitmap": "7.4.2", + "@pixi/text-html": "7.4.2" + } + }, "pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -27914,8 +28625,7 @@ "querystring": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", - "dev": true + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==" }, "querystring-es3": { "version": "0.2.1", @@ -29574,7 +30284,6 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", "integrity": "sha512-kbailJa29QrtXnxgq+DdCEGlbTeYM2eJUxsz6vjZavrCYPMIFHMKQmSKYAIuUK2i7hgPm28a8piX5NTUtM/LKQ==", - "dev": true, "requires": { "punycode": "1.3.2", "querystring": "0.2.0" @@ -29583,8 +30292,7 @@ "punycode": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==", - "dev": true + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" } } }, diff --git a/multiplayer/package.json b/multiplayer/package.json index 9cc6259ee0e1..5409b9f91c3a 100644 --- a/multiplayer/package.json +++ b/multiplayer/package.json @@ -11,6 +11,7 @@ "framer-motion": "^6.5.1", "jsonpath-plus": "^8.1.0", "nanoid": "^4.0.0", + "pixi.js": "^7.4.2", "qrcode.react": "^3.1.0", "react-scripts": "^5.0.1", "smart-buffer": "^4.2.0", diff --git a/multiplayer/src/components/AppModal.tsx b/multiplayer/src/components/AppModal.tsx index 79c475c66c15..fd52701934fe 100644 --- a/multiplayer/src/components/AppModal.tsx +++ b/multiplayer/src/components/AppModal.tsx @@ -64,7 +64,9 @@ export default function Render() { onCancel={() => dispatch(clearModal())} >
- {lf("Kick this player? They will be blocked from rejoining the game.")} + {lf( + "Kick this player? They will be blocked from rejoining the game." + )}
); @@ -80,7 +82,9 @@ export default function Render() { onCancel={() => dispatch(clearModal())} >
- {lf("End the game? All players will be disconnected.")} + {lf( + "End the game? All players will be disconnected." + )}
); diff --git a/multiplayer/src/components/ArcadeSimulator.tsx b/multiplayer/src/components/ArcadeSimulator.tsx index 73f595bfad2f..59b74917c2c4 100644 --- a/multiplayer/src/components/ArcadeSimulator.tsx +++ b/multiplayer/src/components/ArcadeSimulator.tsx @@ -64,7 +64,9 @@ export default function Render() { useEffect(() => { const msgHandler = ( msg: MessageEvent< - SimMultiplayer.Message | pxsim.SimulatorStateMessage | pxsim.SimulatorTopLevelCodeFinishedMessage + | SimMultiplayer.Message + | pxsim.SimulatorStateMessage + | pxsim.SimulatorTopLevelCodeFinishedMessage > ) => { const { data } = msg; diff --git a/multiplayer/src/components/CollabPage.tsx b/multiplayer/src/components/CollabPage.tsx index 2591ec78d86d..a1c336b7a981 100644 --- a/multiplayer/src/components/CollabPage.tsx +++ b/multiplayer/src/components/CollabPage.tsx @@ -1,18 +1,183 @@ -import { useContext } from "react"; +import { useContext, useEffect, useState } from "react"; import { AppStateContext } from "../state/AppStateContext"; +import { Button } from "../../../react-common/components/controls/Button"; +import { leaveCollabAsync } from "../epics/leaveCollabAsync"; +import { BRUSH_COLORS, BRUSH_SIZES, BrushSize } from "../types"; +import { getCollabCanvas } from "../services/collabCanvas"; +import * as collabClient from "../services/collabClient"; +import * as CollabEpics from "../epics/collab"; +import { jsonReplacer } from "../util"; -export interface GamePageProps {} +export interface CollabPageProps {} -export default function Render(props: GamePageProps) { +export default function Render(props: CollabPageProps) { const { state } = useContext(AppStateContext); const { netMode, clientRole, collabInfo } = state; + const [canvasContainer, setCanvasContainer] = + useState(null); + const [selectedColorIndex, setSelectedColorIndex] = useState(0); + const [selectedSizeIndex, setSelectedSizeIndex] = useState(0); + const [mouseDown, setMouseDown] = useState(false); + + useEffect(() => { + const collabCanvas = getCollabCanvas(); + collabCanvas.reset(); + collabCanvas.addPlayerSprite(collabClient.getClientId()!, 0, 0, 0); + }, []); + + useEffect(() => { + const handleMouseDown = (e: MouseEvent) => { + if (!canvasContainer) return; + let canvasMouseX = e.clientX; + let canvasMouseY = e.clientY; + canvasMouseX -= canvasContainer.offsetLeft; + canvasMouseY -= canvasContainer.offsetTop; + canvasMouseX += canvasContainer.scrollLeft; + canvasMouseY += canvasContainer.scrollTop; + if (canvasMouseX < 0 || canvasMouseY < 0) return; + if (canvasMouseX >= canvasContainer.clientWidth) return; + if (canvasMouseY >= canvasContainer.clientHeight) return; + setMouseDown(true); + }; + const handleMouseUp = (e: MouseEvent) => { + setMouseDown(false); + }; + const handleMouseMove = (e: MouseEvent) => { + if (!canvasContainer) return; + // Send position of mouse on canvas + let canvasMouseX = e.clientX; + let canvasMouseY = e.clientY; + canvasMouseX -= canvasContainer.offsetLeft; + canvasMouseY -= canvasContainer.offsetTop; + canvasMouseX += canvasContainer.scrollLeft; + canvasMouseY += canvasContainer.scrollTop; + + // TODO: support canvas pan and zoom + + // Update local sprite position on canvas + getCollabCanvas().updatePlayerSpritePosition( + collabClient.getClientId()!, + canvasMouseX, + canvasMouseY + ); + const pos = { x: canvasMouseX, y: canvasMouseY }; + // Send position to server + collabClient.setPlayerValue("position", JSON.stringify(pos)); + // Update local player position in state + CollabEpics.recvSetPlayerValue( + collabClient.getClientId(), + "position", + JSON.stringify(pos, jsonReplacer) + ); + + if (!mouseDown) return; + // TODO: send draw message + }; + window.addEventListener("mousedown", handleMouseDown); + window.addEventListener("mouseup", handleMouseUp); + window.addEventListener("mousemove", handleMouseMove); + return () => { + window.removeEventListener("mousedown", handleMouseDown); + window.removeEventListener("mouseup", handleMouseUp); + window.removeEventListener("mousemove", handleMouseMove); + }; + }, [mouseDown, canvasContainer]); + + useEffect(() => { + if (canvasContainer && !canvasContainer.firstChild) { + const canvas = getCollabCanvas().view; + canvasContainer.append(canvas as any); + } + return () => { + if (canvasContainer) { + const canvas = getCollabCanvas().view; + canvasContainer.removeChild(canvas as any); + } + }; + }, [canvasContainer]); + if (!collabInfo) return null; if (netMode !== "connected") return null; + const leaveClick = () => { + leaveCollabAsync("left"); + }; + + const brushColorClicked = (color: string, index: number) => { + setSelectedColorIndex(index); + }; + + const brushSizeClicked = (bs: BrushSize, index: number) => { + setSelectedSizeIndex(index); + }; + + const handleCanvasContainerRef = (ref: HTMLDivElement) => { + setCanvasContainer(ref); + }; + return ( - <> - {collabInfo.joinCode} - +
+
+
+
+ {BRUSH_COLORS.map((bc, i) => ( +
brushColorClicked(bc, i)} + >
+ ))} +
+ {BRUSH_SIZES.map((bs, i) => ( +
brushSizeClicked(bs, i)} + >
+ ))} +
+
+
+
+ {collabInfo.joinCode} +
+
+
+
+
); } diff --git a/multiplayer/src/components/GamePaused.tsx b/multiplayer/src/components/GamePaused.tsx index 6923d7fbf5b5..aabd13df6bb7 100644 --- a/multiplayer/src/components/GamePaused.tsx +++ b/multiplayer/src/components/GamePaused.tsx @@ -19,7 +19,9 @@ export default function Render() { {lf("Game Paused")}
- {lf("The game is paused. Press the resume button to continue.")} + {lf( + "The game is paused. Press the resume button to continue." + )}
)} diff --git a/multiplayer/src/components/HeaderBar.tsx b/multiplayer/src/components/HeaderBar.tsx index cd2dd03c2d68..6760700111a8 100644 --- a/multiplayer/src/components/HeaderBar.tsx +++ b/multiplayer/src/components/HeaderBar.tsx @@ -110,9 +110,7 @@ export default function Render() { const getTargetLogo = (targetTheme: pxt.AppTheme) => { return (
{targetTheme.useTextLogo ? ( @@ -136,7 +134,9 @@ export default function Render() { ) : targetTheme.logo || targetTheme.portraitLogo ? ( {lf("{0} ) : ( diff --git a/multiplayer/src/components/JoinCodeLabel.tsx b/multiplayer/src/components/JoinCodeLabel.tsx index 87320e6292e0..467bf658f29b 100644 --- a/multiplayer/src/components/JoinCodeLabel.tsx +++ b/multiplayer/src/components/JoinCodeLabel.tsx @@ -6,7 +6,9 @@ export default function Render() { const { state } = useContext(AppStateContext); const joinCode = state.gameState?.joinCode; - const joinDeepLink = joinCode ? pxt.multiplayer.makeJoinLink(joinCode, true) : ""; + const joinDeepLink = joinCode + ? pxt.multiplayer.makeJoinLink(joinCode, true) + : ""; return (
{joinCode && ( diff --git a/multiplayer/src/components/JoinOrHost.tsx b/multiplayer/src/components/JoinOrHost.tsx index e0cf7bb10d50..a537da341eb9 100644 --- a/multiplayer/src/components/JoinOrHost.tsx +++ b/multiplayer/src/components/JoinOrHost.tsx @@ -148,8 +148,12 @@ export default function Render() { return ( diff --git a/multiplayer/src/components/SignedInPage.tsx b/multiplayer/src/components/SignedInPage.tsx index 1bc26d0369af..04aa8d7bfe58 100644 --- a/multiplayer/src/components/SignedInPage.tsx +++ b/multiplayer/src/components/SignedInPage.tsx @@ -9,7 +9,7 @@ export default function Render() { const { netMode } = state; return ( -
+
{netMode === "init" && } {netMode !== "init" && } {netMode !== "init" && } diff --git a/multiplayer/src/epics/collab/index.ts b/multiplayer/src/epics/collab/index.ts index 8e8ff9f08a26..d1488572cba3 100644 --- a/multiplayer/src/epics/collab/index.ts +++ b/multiplayer/src/epics/collab/index.ts @@ -1 +1,5 @@ export { initState } from "./initState"; +export { recvPlayerJoined } from "./recvPlayerJoined"; +export { recvPlayerLeft } from "./recvPlayerLeft"; +export { recvUpdatePresence } from "./recvUpdatePresence"; +export { recvSetPlayerValue } from "./recvSetPlayerValue"; diff --git a/multiplayer/src/epics/collab/recvPlayerJoined.ts b/multiplayer/src/epics/collab/recvPlayerJoined.ts new file mode 100644 index 000000000000..cb5e583450f6 --- /dev/null +++ b/multiplayer/src/epics/collab/recvPlayerJoined.ts @@ -0,0 +1,9 @@ +import { getCollabCanvas } from "../../services/collabCanvas"; +import { collabStateAndDispatch } from "../../state/collab"; +import * as CollabActions from "../../state/collab/actions"; + +export function recvPlayerJoined(playerId: string, kv?: Map) { + const { dispatch } = collabStateAndDispatch(); + dispatch(CollabActions.playerJoined(playerId, kv)); + getCollabCanvas().addPlayerSprite(playerId, 0, 0, 0); +} diff --git a/multiplayer/src/epics/collab/recvPlayerLeft.ts b/multiplayer/src/epics/collab/recvPlayerLeft.ts new file mode 100644 index 000000000000..9289934ad66c --- /dev/null +++ b/multiplayer/src/epics/collab/recvPlayerLeft.ts @@ -0,0 +1,9 @@ +import { getCollabCanvas } from "../../services/collabCanvas"; +import { collabStateAndDispatch } from "../../state/collab"; +import * as CollabActions from "../../state/collab/actions"; + +export function recvPlayerLeft(playerId: string) { + const { dispatch } = collabStateAndDispatch(); + dispatch(CollabActions.playerLeft(playerId)); + getCollabCanvas().removePlayerSprite(playerId); +} diff --git a/multiplayer/src/epics/collab/recvSetPlayerValue.ts b/multiplayer/src/epics/collab/recvSetPlayerValue.ts new file mode 100644 index 000000000000..b0b0a30c9d8e --- /dev/null +++ b/multiplayer/src/epics/collab/recvSetPlayerValue.ts @@ -0,0 +1,17 @@ +import { getCollabCanvas } from "../../services/collabCanvas"; +import { collabStateAndDispatch } from "../../state/collab"; +import * as CollabActions from "../../state/collab/actions"; + +export function recvSetPlayerValue( + playerId: string | undefined, + key: string, + value: string +) { + if (!playerId) return; + const { dispatch } = collabStateAndDispatch(); + dispatch(CollabActions.setPlayerValue(playerId, key, value)); + if (key === "position") { + const pos = JSON.parse(value); + getCollabCanvas().updatePlayerSpritePosition(playerId, pos.x, pos.y); + } +} diff --git a/multiplayer/src/epics/collab/recvUpdatePresence.ts b/multiplayer/src/epics/collab/recvUpdatePresence.ts new file mode 100644 index 000000000000..51d63cee1afe --- /dev/null +++ b/multiplayer/src/epics/collab/recvUpdatePresence.ts @@ -0,0 +1,8 @@ +import { collabStateAndDispatch } from "../../state/collab"; +import * as CollabActions from "../../state/collab/actions"; +import { Presence } from "../../types"; + +export function recvUpdatePresence(presence: Presence) { + const { dispatch } = collabStateAndDispatch(); + dispatch(CollabActions.updatePresence(presence)); +} diff --git a/multiplayer/src/epics/joinCollabAsync.ts b/multiplayer/src/epics/joinCollabAsync.ts index 198b86ad59a2..b786fedb4ee7 100644 --- a/multiplayer/src/epics/joinCollabAsync.ts +++ b/multiplayer/src/epics/joinCollabAsync.ts @@ -43,7 +43,12 @@ export async function joinCollabAsync(joinCode: string | undefined) { }) ); dispatch(setClientRole("guest")); - dispatch(setCollabInfo(joinResult)); + dispatch( + setCollabInfo({ + joinCode, + ...joinResult, + }) + ); dispatch(setNetMode("connected")); } else { if (joinResult.statusCode === HTTP_SESSION_NOT_FOUND) { diff --git a/multiplayer/src/epics/leaveCollabAsync.ts b/multiplayer/src/epics/leaveCollabAsync.ts new file mode 100644 index 000000000000..66bfab905444 --- /dev/null +++ b/multiplayer/src/epics/leaveCollabAsync.ts @@ -0,0 +1,15 @@ +import * as collabClient from "../services/collabClient"; +import { dispatch } from "../state"; +import { clearGameInfo, clearGameMetadata } from "../state/actions"; +import { SessionOverReason } from "../types"; + +export async function leaveCollabAsync(reason: SessionOverReason) { + try { + pxt.tickEvent("mp.leavecollab"); + await collabClient.leaveCollabAsync(reason); + dispatch(clearGameInfo()); + dispatch(clearGameMetadata()); + } catch (e) { + } finally { + } +} diff --git a/multiplayer/src/epics/sendAbuseReportAsync.ts b/multiplayer/src/epics/sendAbuseReportAsync.ts index 4dfae3868e95..ccab2c0dcdc2 100644 --- a/multiplayer/src/epics/sendAbuseReportAsync.ts +++ b/multiplayer/src/epics/sendAbuseReportAsync.ts @@ -15,7 +15,9 @@ export async function sendAbuseReportAsync(shareCode: string, text: string) { dispatch( showToast({ type: "success", - text: lf("Thank you for helping keep Microsoft MakeCode a friendly place!"), + text: lf( + "Thank you for helping keep Microsoft MakeCode a friendly place!" + ), icon: "✅", timeoutMs: 5000, }) @@ -27,7 +29,9 @@ export async function sendAbuseReportAsync(shareCode: string, text: string) { dispatch( showToast({ type: "error", - text: lf("Sorry, we couldn't send your report. Please try again later."), + text: lf( + "Sorry, we couldn't send your report. Please try again later." + ), timeoutMs: 5000, }) ); diff --git a/multiplayer/src/hooks/useClickedOutside.ts b/multiplayer/src/hooks/useClickedOutside.ts index a35b371bf742..65e76727d021 100644 --- a/multiplayer/src/hooks/useClickedOutside.ts +++ b/multiplayer/src/hooks/useClickedOutside.ts @@ -8,8 +8,7 @@ export function useClickedOutside( const handleMouseDown = (ev: Event) => { for (const ref of refs) { const el = ref?.current; - if (el && el.contains(ev.target as Node)) - return; + if (el && el.contains(ev.target as Node)) return; } cb?.(ev); }; diff --git a/multiplayer/src/index.css b/multiplayer/src/index.css index 21e3208f209c..7560ae1f4cf5 100644 --- a/multiplayer/src/index.css +++ b/multiplayer/src/index.css @@ -12,3 +12,24 @@ --you-tag-border-color: 0, 138, 169; --you-tag-bg-color: 205, 237, 244; } + +::-webkit-scrollbar { + width: 14px; + height: 14px; + background-color: rgba(255, 255, 255, 0); + cursor: initial; +} + +::-webkit-scrollbar-thumb { + height: 0; + border: 2px solid rgba(255, 255, 255, 0); + border-radius: 99px; + background-clip: padding-box; + background-color: rgb(206, 205, 206); + cursor: initial; +} + +::-webkit-scrollbar-thumb:hover { + border-width: 1px; + cursor: initial; +} diff --git a/multiplayer/src/index.tsx b/multiplayer/src/index.tsx index 9319b517732c..8d97c98a4e1d 100644 --- a/multiplayer/src/index.tsx +++ b/multiplayer/src/index.tsx @@ -38,8 +38,7 @@ window.addEventListener("DOMContentLoaded", () => { const bundle = (window as any).pxtTargetBundle as pxt.TargetBundle; pxt.options.debug = /dbg=1/i.test(window.location.href); - if (pxt.options.debug) - pxt.debug = console.debug; + if (pxt.options.debug) pxt.debug = console.debug; pxt.setupWebConfig((window as any).pxtConfig || pxt.webConfig); pxt.setAppTarget(bundle); diff --git a/multiplayer/src/services/collabCanvas.ts b/multiplayer/src/services/collabCanvas.ts new file mode 100644 index 000000000000..d072b26c10e2 --- /dev/null +++ b/multiplayer/src/services/collabCanvas.ts @@ -0,0 +1,211 @@ +import * as Pixi from "pixi.js"; +import { flattenVerts } from "../util"; + +const shaderPrograms = new Map(); + +export function addShaderProgram(name: string, vert: string, frag: string) { + shaderPrograms.set(name, Pixi.Program.from(vert, frag)); +} + +function getShaderProgram(name: string): Pixi.Program { + const pgm = shaderPrograms.get(name); + if (pgm) return pgm; + console.error(`shader program not found: "${name}"`); + return shaderPrograms.get("$$missing_shader$$")!; +} + +export const CommonVertexShaderGlobals = ` + precision mediump float; + attribute vec2 aVerts; + attribute vec2 aUvs; + uniform mat3 translationMatrix; + uniform mat3 projectionMatrix; + varying vec2 vUvs; + varying vec2 vVerts; +`; +export const BasicVertexShader = + CommonVertexShaderGlobals + + ` + void main() { + vUvs = aUvs; + vVerts = aVerts; + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVerts, 1.0)).xy, 0.0, 1.0); + }`; + +export const CommonFragmentShaderGlobals = ` + precision mediump float; + varying vec2 vUvs; + varying vec2 vVerts; + `; + +addShaderProgram( + "$$missing_shader$$", + BasicVertexShader, + CommonFragmentShaderGlobals + + ` + void main() { + vec2 uv = vUvs; + uv = floor(uv * 10.); + vec3 color1 = vec3(0.4, 0.0, 0.0); + vec3 color2 = vec3(0.0, 0.4, 0.4); + vec3 outColor = mod(uv.x + uv.y, 2.) < 0.5 ? color1 : color2; + gl_FragColor = vec4(outColor.rgb, 0.5); + }` +); + +addShaderProgram( + "textured_colored", + BasicVertexShader, + CommonFragmentShaderGlobals + + ` + uniform sampler2D uSampler2; + uniform vec3 uColor; + uniform float uAlpha; + void main() { + vec2 uv = vUvs; + gl_FragColor = texture2D(uSampler2, uv) * vec4(uColor.rgb, uAlpha); + }` +); + +addShaderProgram( + "grid", + BasicVertexShader, + CommonFragmentShaderGlobals + + ` + void main() { + vec2 uv = vUvs; + vec2 vert = vVerts; + bool a = int(mod(vert.x, 20.0)) == 0; + bool b = int(mod(vert.y, 20.0)) == 0; + gl_FragColor = ((a && !b) || (!a && b)) ? vec4(0.,0.,0.,0.1) : vec4(0.,0.,0.,0.); + }` +); + +export const CANVAS_SIZE = 4000; + +type CanvasPlayer = { + imgId: number; + sprite: Pixi.Sprite; +}; + +class CollabCanvas { + private app: Pixi.Application; + private root: Pixi.Graphics; + private playerSprites: Map = new Map(); + + public get view() { + return this.app.view; + } + + constructor() { + this.app = new Pixi.Application({ + width: CANVAS_SIZE, + height: CANVAS_SIZE, + backgroundColor: 0xffffff, + antialias: true, + clearBeforeRender: true, + }); + + // Set up initial canvas dimensions + const canv = this.app.view as HTMLCanvasElement; + canv.style.width = canv.style.minWidth = CANVAS_SIZE + "px"; + canv.style.height = canv.style.minHeight = CANVAS_SIZE + "px"; + + this.root = new Pixi.Graphics(); + this.app.stage.addChild(this.root as any); + + this.addBackgroundGrid(); + } + + public reset() { + this.app.stage.removeChildren(); + this.root = new Pixi.Graphics(); + this.playerSprites.clear(); + this.app.stage.addChild(this.root as any); + this.addBackgroundGrid(); + } + + private addBackgroundGrid() { + const geom = new Pixi.Geometry(); + geom.addAttribute( + "aVerts", + flattenVerts([ + { x: 0, y: 0 }, + { x: 0, y: CANVAS_SIZE }, + { x: CANVAS_SIZE, y: CANVAS_SIZE }, + { x: CANVAS_SIZE, y: 0 }, + ]), + 2 + ); + geom.addAttribute( + "aUvs", + flattenVerts([ + { x: 0, y: 0 }, + { x: 0, y: 1 }, + { x: 1, y: 1 }, + { x: 1, y: 0 }, + ]), + 2 + ); + geom.addIndex([0, 1, 2, 0, 2, 3]); + const pgm = getShaderProgram("grid"); + const shader = new Pixi.Shader(pgm); + const mesh = new Pixi.Mesh(geom, shader); + mesh.zIndex = -100; // behind everything + this.root.addChild(mesh as any); + } + + public addPlayerSprite( + playerId: string, + x: number, + y: number, + imgId: number + ) { + const sprite = Pixi.Sprite.from( + `hackathon/rt-collab/sprites/sprite-${imgId}.png` + ); + sprite.anchor.set(0.5); + sprite.position.set(x, y); + sprite.zIndex = 100; // in front of everything + const player: CanvasPlayer = { imgId, sprite }; + this.playerSprites.set(playerId, player); + this.root.addChild(sprite as any); + } + + public removePlayerSprite(playerId: string) { + const player = this.playerSprites.get(playerId); + if (player) { + this.root.removeChild(player.sprite as any); + this.playerSprites.delete(playerId); + } + } + + public updatePlayerSpritePosition(playerId: string, x: number, y: number) { + const player = this.playerSprites.get(playerId); + if (player) { + player.sprite.position.set(x, y); + } + } + + public updatePlayerSpriteImage(playerId: string, imgId: number) { + const player = this.playerSprites.get(playerId); + if (player && player.imgId !== imgId) { + player.sprite.texture = Pixi.Texture.from( + `sprites/sprite-${imgId}.png` + ); + } + } +} + +let _instance: CollabCanvas; + +function ensureInstance(): CollabCanvas { + if (!_instance) { + _instance = new CollabCanvas(); + } + return _instance; +} + +export function getCollabCanvas() { + return ensureInstance(); +} diff --git a/multiplayer/src/services/collabClient.ts b/multiplayer/src/services/collabClient.ts index 721365160630..25e13138e29a 100644 --- a/multiplayer/src/services/collabClient.ts +++ b/multiplayer/src/services/collabClient.ts @@ -19,6 +19,7 @@ import { playerLeftAsync, } from "../epics"; import * as CollabEpics from "../epics/collab"; +import { jsonReplacer, jsonReviver } from "../util"; const COLLAB_HOST_PROD = "https://mp.makecode.com"; const COLLAB_HOST_STAGING = "https://multiplayer.staging.pxt.io"; @@ -36,15 +37,14 @@ const COLLAB_HOST = (() => { export type CollabPlayer = { clientId: string; - xp: number; - yp: number; - name: string; + kv: Map; }; class CollabClient { sock: Socket | undefined; screen: Buffer | undefined; clientRole: ClientRole | undefined; + clientId: string | undefined; sessOverReason: SessionOverReason | undefined; receivedJoinMessageInTimeHandler: ((a: any) => void) | undefined; @@ -63,7 +63,7 @@ class CollabClient { if (err) pxt.log("Error sending message. " + err.toString()); }); } else { - const payload = JSON.stringify(msg); + const payload = JSON.stringify(msg, jsonReplacer); this.sock?.send(payload, {}, (err: any) => { if (err) pxt.log("Error sending message. " + err.toString()); }); @@ -83,7 +83,7 @@ class CollabClient { //------------------------------------------------- // Handle JSON message //-- - const msg = JSON.parse(payload) as Protocol.Message; + const msg = JSON.parse(payload, jsonReviver) as Protocol.Message; switch (msg.type) { case "joined": return await this.recvJoinedMessageAsync(msg); @@ -93,6 +93,10 @@ class CollabClient { return await this.recvPlayerJoinedMessageAsync(msg); case "player-left": return await this.recvPlayerLeftMessageAsync(msg); + case "set-player-value": + return await this.recvSetPlayerValueMessageAsync(msg); + case "set-session-value": + return await this.recvSetSessionValueMessageAsync(msg); } } else { throw new Error(`Unknown payload: ${payload}`); @@ -267,7 +271,7 @@ class CollabClient { pxt.debug( `Server said we're joined as "${msg.role}" in slot "${msg.slot}"` ); - const { role } = msg; + const { role, clientId } = msg; this.clientRole = role; @@ -287,11 +291,26 @@ class CollabClient { //await this.sendCurrentScreenAsync(); // Workaround for server sometimes not sending the current screen to new players. Needs debugging. } await playerJoinedAsync(msg.clientId); + CollabEpics.recvPlayerJoined(msg.clientId); } private async recvPlayerLeftMessageAsync(msg: Protocol.PlayerLeftMessage) { pxt.debug("Server sent player joined"); await playerLeftAsync(msg.clientId); + CollabEpics.recvPlayerLeft(msg.clientId); + } + + private async recvSetPlayerValueMessageAsync( + msg: Protocol.SetPlayerValueMessage + ) { + pxt.debug(`Recv set player value: ${msg.key} = ${msg.value}`); + CollabEpics.recvSetPlayerValue(msg.clientId!, msg.key, msg.value); + } + + private async recvSetSessionValueMessageAsync( + msg: Protocol.SetSessionValueMessage + ) { + pxt.debug(`Recv set session value: ${msg.key} = ${msg.value}`); } public kickPlayer(clientId: string) { @@ -306,18 +325,43 @@ class CollabClient { this.sessOverReason = reason; destroyCollabClient(); } + + public setPlayerValue(key: string, value: string) { + const msg: Protocol.SetPlayerValueMessage = { + type: "set-player-value", + key, + value, + }; + this.sendMessage(msg); + } + + public setSessionValue(key: string, value: string) { + const msg: Protocol.SetSessionValueMessage = { + type: "set-session-value", + key, + value, + }; + this.sendMessage(msg); + } } -let collabClient: CollabClient | undefined; +let _collabClient: CollabClient | undefined; + +function ensureCollabClient(): CollabClient { + if (!_collabClient) { + _collabClient = new CollabClient(); + } + return _collabClient; +} function destroyCollabClient() { - collabClient?.destroy(); - collabClient = undefined; + _collabClient?.destroy(); + _collabClient = undefined; } export async function hostCollabAsync(): Promise { destroyCollabClient(); - collabClient = new CollabClient(); + const collabClient = ensureCollabClient(); const collabInfo = await collabClient.hostCollabAsync(); return collabInfo; } @@ -326,24 +370,41 @@ export async function joinCollabAsync( joinCode: string ): Promise { destroyCollabClient(); - collabClient = new CollabClient(); + const collabClient = ensureCollabClient(); const collabInfo = await collabClient.joinCollabAsync(joinCode); return collabInfo; } export async function leaveCollabAsync(reason: SessionOverReason) { + const collabClient = ensureCollabClient(); await collabClient?.leaveCollabAsync(reason); destroyCollabClient(); } export function kickPlayer(clientId: string) { + const collabClient = ensureCollabClient(); collabClient?.kickPlayer(clientId); } export function collabOver(reason: SessionOverReason) { + const collabClient = ensureCollabClient(); collabClient?.collabOver(reason); } +export function setPlayerValue(key: string, value: string) { + const collabClient = ensureCollabClient(); + collabClient?.setPlayerValue(key, value); +} + +export function setSessionValue(key: string, value: string) { + const collabClient = ensureCollabClient(); + collabClient?.setSessionValue(key, value); +} + +export function getClientId() { + return _collabClient?.clientId; +} + export function destroy() { destroyCollabClient(); } @@ -400,6 +461,20 @@ namespace Protocol { reason: SessionOverReason; }; + export type SetPlayerValueMessage = MessageBase & { + type: "set-player-value"; + key: string; + value: string; + clientId?: string; // only set on received messages + }; + + export type SetSessionValueMessage = MessageBase & { + type: "set-session-value"; + key: string; + value: string; + clientId?: string; // only set on received messages + }; + export type Message = | ConnectMessage | PresenceMessage @@ -407,5 +482,7 @@ namespace Protocol { | PlayerJoinedMessage | PlayerLeftMessage | KickPlayerMessage - | CollabOverMessage; + | CollabOverMessage + | SetPlayerValueMessage + | SetSessionValueMessage; } diff --git a/multiplayer/src/services/gameClient.ts b/multiplayer/src/services/gameClient.ts index 8c26ca4d1da8..219f735d1d2f 100644 --- a/multiplayer/src/services/gameClient.ts +++ b/multiplayer/src/services/gameClient.ts @@ -63,8 +63,7 @@ class GameClient { receivedJoinMessageInTimeHandler: ((a: any) => void) | undefined; paused: boolean = false; - constructor() { - } + constructor() {} destroy() { try { @@ -131,7 +130,7 @@ class GameClient { } else { throw new Error(`Unknown payload: ${payload}`); } - } + }; private recvMessageWithJoinTimeout = async ( payload: string | Buffer, @@ -143,7 +142,10 @@ class GameClient { if (msg.type === "joined") { // We've joined the game. Replace this handler with a direct call to recvMessageAsync if (this.sock) { - this.sock.removeListener("message", this.receivedJoinMessageInTimeHandler); + this.sock.removeListener( + "message", + this.receivedJoinMessageInTimeHandler + ); this.receivedJoinMessageInTimeHandler = undefined; } resolve(); @@ -154,7 +156,7 @@ class GameClient { destroyGameClient(); resolve(); } - } + }; public async connectAsync(ticket: string) { return new Promise((resolve, reject) => { @@ -174,7 +176,7 @@ class GameClient { clearTimeout(joinTimeout); resolve(); }); - } + }; this.sock?.on("message", this.receivedJoinMessageInTimeHandler); this.sock?.on("message", async payload => { try { diff --git a/multiplayer/src/state/collab/CollabContext.tsx b/multiplayer/src/state/collab/CollabContext.tsx index 0a94db01f810..4b38f423a2b6 100644 --- a/multiplayer/src/state/collab/CollabContext.tsx +++ b/multiplayer/src/state/collab/CollabContext.tsx @@ -1,7 +1,7 @@ -import { createContext, useEffect, useReducer } from 'react'; -import { CollabState, initialState } from './state'; -import { reducer } from './reducer'; -import { CollabAction } from './actions'; +import { createContext, useEffect, useReducer } from "react"; +import { CollabState, initialState } from "./state"; +import { reducer } from "./reducer"; +import { CollabAction } from "./actions"; let state: CollabState; let dispatch: React.Dispatch; @@ -20,7 +20,9 @@ const initialCollabContextProps: CollabContextProps = { dispatch: undefined!, }; -export const CollabContext = createContext(initialCollabContextProps); +export const CollabContext = createContext( + initialCollabContextProps +); export function CollabStateProvider( props: React.PropsWithChildren<{}> @@ -46,4 +48,3 @@ export function CollabStateProvider( ); } - diff --git a/multiplayer/src/state/collab/actions.ts b/multiplayer/src/state/collab/actions.ts index 6844e802544f..9913ca0caca2 100644 --- a/multiplayer/src/state/collab/actions.ts +++ b/multiplayer/src/state/collab/actions.ts @@ -1,4 +1,4 @@ -import { ActionBase } from "../../types"; +import { ActionBase, Presence } from "../../types"; type Init = ActionBase & { type: "INIT"; @@ -7,6 +7,7 @@ type Init = ActionBase & { type PlayerJoined = ActionBase & { type: "PLAYER_JOINED"; playerId: string; + kv?: Map; }; type PlayerLeft = ActionBase & { @@ -14,7 +15,24 @@ type PlayerLeft = ActionBase & { playerId: string; }; -export type CollabAction = Init | PlayerJoined | PlayerLeft; +type SetPlayerValue = ActionBase & { + type: "SET_PLAYER_VALUE"; + playerId: string; + key: string; + value: string; +}; + +type UpdatePresence = ActionBase & { + type: "UPDATE_PRESENCE"; + presence: Presence; +}; + +export type CollabAction = + | Init + | PlayerJoined + | PlayerLeft + | SetPlayerValue + | UpdatePresence; export function init(): Init { return { @@ -22,10 +40,14 @@ export function init(): Init { }; } -export function playerJoined(playerId: string): PlayerJoined { +export function playerJoined( + playerId: string, + kv?: Map +): PlayerJoined { return { type: "PLAYER_JOINED", playerId, + kv, }; } @@ -35,3 +57,23 @@ export function playerLeft(playerId: string): PlayerLeft { playerId, }; } + +export function setPlayerValue( + playerId: string, + key: string, + value: string +): SetPlayerValue { + return { + type: "SET_PLAYER_VALUE", + playerId, + key, + value, + }; +} + +export function updatePresence(presence: Presence): UpdatePresence { + return { + type: "UPDATE_PRESENCE", + presence, + }; +} diff --git a/multiplayer/src/state/collab/reducer.ts b/multiplayer/src/state/collab/reducer.ts index 0f7074ffa7e2..4a2941023c0e 100644 --- a/multiplayer/src/state/collab/reducer.ts +++ b/multiplayer/src/state/collab/reducer.ts @@ -7,24 +7,59 @@ export function reducer(state: CollabState, action: CollabAction): CollabState { return { ...initialState, }; - case "PLAYER_JOINED": + case "PLAYER_JOINED": { return { ...state, players: { ...state.players, [action.playerId]: { clientId: action.playerId, - name: "Player " + action.playerId, - xp: 0, - yp: 0, + kv: action.kv ? action.kv : new Map(), }, }, }; - case "PLAYER_LEFT": + } + case "PLAYER_LEFT": { const { [action.playerId]: _, ...players } = state.players; return { ...state, players, }; + } + case "SET_PLAYER_VALUE": { + const player = state.players[action.playerId]; + if (player) { + return { + ...state, + players: { + ...state.players, + [action.playerId]: { + ...player, + kv: new Map(player.kv).set( + action.key, + action.value + ), + }, + }, + }; + } else { + return state; + } + } + case "UPDATE_PRESENCE": { + const players = { ...state.players }; + action.presence.users.forEach(user => { + const player = players[user.id]; + if (!player) return; + players[user.id] = { + ...player, + kv: user.kv ? user.kv : player.kv, + }; + }); + return { + ...state, + players, + }; + } } } diff --git a/multiplayer/src/state/collab/state.ts b/multiplayer/src/state/collab/state.ts index 38e43b461c10..ef561f644f9e 100644 --- a/multiplayer/src/state/collab/state.ts +++ b/multiplayer/src/state/collab/state.ts @@ -1,4 +1,4 @@ -import { CollabPlayer } from "../../services/collabClient" +import { CollabPlayer } from "../../services/collabClient"; export type CollabState = { players: { [playerId: string]: CollabPlayer }; diff --git a/multiplayer/src/state/reducer.ts b/multiplayer/src/state/reducer.ts index d0d18136b84c..a1811daac67b 100644 --- a/multiplayer/src/state/reducer.ts +++ b/multiplayer/src/state/reducer.ts @@ -56,7 +56,7 @@ export default function reducer(state: AppState, action: Action): AppState { ...state, collabInfo: { ...action.collabInfo, - } + }, }; } case "SET_GAME_METADATA": { diff --git a/multiplayer/src/types/index.ts b/multiplayer/src/types/index.ts index 6b6d52934f09..053903ba98b5 100644 --- a/multiplayer/src/types/index.ts +++ b/multiplayer/src/types/index.ts @@ -103,6 +103,45 @@ export enum SimKey { TogglePause = -4, } +// https://lospec.com/palette-list/gems-in-the-forrest +export const BRUSH_COLORS = [ + "#ff3282", + "#5b1284", + "#3171ee", + "#4ff5fc", + "#aefdd5", +]; + +export type BrushSizeType = "sm" | "md" | "lg"; + +export type BrushSize = { + sz: BrushSizeType; + px: number; +}; + +export const BRUSH_SIZES: BrushSize[] = [ + { sz: "sm", px: 22 }, + { sz: "md", px: 32 }, + { sz: "lg", px: 42 }, +]; + +export type BrushModeType = "draw" | "move"; + +export type BrushMode = { + mode: BrushModeType; + icon: string; +}; + +export const BRUSH_MODES: BrushMode[] = [ + { mode: "draw", icon: "🖌️" }, + { mode: "move", icon: "🤚" }, +]; + +export type Vec2Like = { + x: number; + y: number; +}; + export function buttonStateToString(state: ButtonState): string | undefined { switch (state) { case ButtonState.Pressed: @@ -178,6 +217,7 @@ export type UserInfo = { id: string; slot: number; role: ClientRole; + kv?: Map; }; export type Presence = { diff --git a/multiplayer/src/util/index.ts b/multiplayer/src/util/index.ts index fd25787ef004..e22171d6fd08 100644 --- a/multiplayer/src/util/index.ts +++ b/multiplayer/src/util/index.ts @@ -1,4 +1,5 @@ import zlib from "zlib"; +import { Vec2Like } from "../types"; export function isLocal() { return window.location.hostname === "localhost"; @@ -72,8 +73,63 @@ export function cleanupShareCode( export function resourceUrl(path: string | undefined): string | undefined { if (!path) return; - if (pxt.BrowserUtils.isLocalHostDev() && !(path.startsWith('https:') || path.startsWith('data:'))) { + if ( + pxt.BrowserUtils.isLocalHostDev() && + !(path.startsWith("https:") || path.startsWith("data:")) + ) { return pxt.appTarget?.appTheme.homeUrl + path; } return path; } + +export function throttle) => ReturnType>( + func: F, + waitFor: number +): F { + let timeout: NodeJS.Timeout | undefined; + let previousTime = 0; + return function (this: ThisParameterType, ...args: Parameters) { + const context = this; + const currentTime = Date.now(); + const timeSinceLastCall = currentTime - previousTime; + const timeRemaining = waitFor - timeSinceLastCall; + if (timeRemaining <= 0) { + previousTime = currentTime; + func.apply(context, args); + } else if (!timeout) { + timeout = setTimeout(() => { + previousTime = Date.now(); + timeout = undefined; + func.apply(context, args); + }, timeRemaining); + } + } as F; +} + +export function flattenVerts(verts: Vec2Like[]): number[] { + const flatVerts: number[] = []; + for (const v of verts) { + flatVerts.push(v.x, v.y); + } + return flatVerts; +} + +export function jsonReplacer(key: any, value: any) { + if (value instanceof Map) { + return { + [".dataType"]: "Map", + value: Array.from(value.entries()), // or with spread: value: [...value] + }; + } else { + return value; + } +} + +export function jsonReviver(key: any, value: any) { + if (typeof value === "object" && value !== null) { + if (value[".dataType"] === "Map") { + return new Map(value.value); + } + } + return value; +} From 10537940b4d027a4592db83b49e4877e2340bfc3 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Sat, 30 Mar 2024 23:41:11 -0700 Subject: [PATCH 03/12] collab wip --- multiplayer/src/components/CollabPage.tsx | 106 +++++++++++++++++- multiplayer/src/epics/collab/initState.ts | 1 + .../src/epics/collab/recvSetPlayerValue.ts | 2 + .../src/epics/collab/recvUpdatePresence.ts | 20 +++- multiplayer/src/services/collabCanvas.ts | 10 +- multiplayer/src/services/collabClient.ts | 2 + multiplayer/src/state/collab/index.ts | 7 ++ multiplayer/src/state/collab/reducer.ts | 6 +- 8 files changed, 144 insertions(+), 10 deletions(-) diff --git a/multiplayer/src/components/CollabPage.tsx b/multiplayer/src/components/CollabPage.tsx index a1c336b7a981..0e6a39ccb2fd 100644 --- a/multiplayer/src/components/CollabPage.tsx +++ b/multiplayer/src/components/CollabPage.tsx @@ -18,6 +18,7 @@ export default function Render(props: CollabPageProps) { useState(null); const [selectedColorIndex, setSelectedColorIndex] = useState(0); const [selectedSizeIndex, setSelectedSizeIndex] = useState(0); + const [selectedIconIndex, setSelectedIconIndex] = useState(0); const [mouseDown, setMouseDown] = useState(false); useEffect(() => { @@ -52,7 +53,7 @@ export default function Render(props: CollabPageProps) { canvasMouseY -= canvasContainer.offsetTop; canvasMouseX += canvasContainer.scrollLeft; canvasMouseY += canvasContainer.scrollTop; - + // TODO: support canvas pan and zoom // Update local sprite position on canvas @@ -112,10 +113,18 @@ export default function Render(props: CollabPageProps) { setSelectedSizeIndex(index); }; + const iconClicked = (index: number) => { + setSelectedIconIndex(index); + getCollabCanvas().updatePlayerSpriteImage(collabClient.getClientId()!, index); + collabClient.setPlayerValue("imgId", JSON.stringify(index)); + }; + const handleCanvasContainerRef = (ref: HTMLDivElement) => { setCanvasContainer(ref); }; + const SELECTED_COLOR = "#1e293b"; + return (
@@ -132,7 +141,7 @@ export default function Render(props: CollabPageProps) { backgroundColor: bc, outline: i === selectedColorIndex - ? "3px solid #1e293b" + ? "3px solid " + SELECTED_COLOR : undefined, outlineOffset: "1px", }} @@ -151,13 +160,104 @@ export default function Render(props: CollabPageProps) { height: bs.px + "px", outline: i === selectedSizeIndex - ? "3px solid #1e293b" + ? "3px solid " + SELECTED_COLOR : undefined, outlineOffset: "1px", }} onClick={() => brushSizeClicked(bs, i)} >
))} +
+
+
iconClicked(0)} + className="tw-w-8 tw-h-8 tw-rounded-md tw-cursor-pointer tw-border-slate-800 tw-border" + style={{ + backgroundImage: + "url(hackathon/rt-collab/sprites/sprite-0.png)", + objectFit: "cover", + outline: + 0 === selectedIconIndex + ? "3px solid " + SELECTED_COLOR + : undefined, + outlineOffset: "1px", + }} + >
+
iconClicked(1)} + className="tw-w-8 tw-h-8 tw-rounded-md tw-cursor-pointer tw-border-slate-800 tw-border" + style={{ + backgroundImage: + "url(hackathon/rt-collab/sprites/sprite-1.png)", + objectFit: "cover", + outline: + 1 === selectedIconIndex + ? "3px solid " + SELECTED_COLOR + : undefined, + outlineOffset: "1px", + }} + >
+
+
+
iconClicked(2)} + className="tw-w-8 tw-h-8 tw-rounded-md tw-cursor-pointer tw-border-slate-800 tw-border" + style={{ + backgroundImage: + "url(hackathon/rt-collab/sprites/sprite-2.png)", + objectFit: "cover", + outline: + 2 === selectedIconIndex + ? "3px solid " + SELECTED_COLOR + : undefined, + outlineOffset: "1px", + }} + >
+
iconClicked(3)} + className="tw-w-8 tw-h-8 tw-rounded-md tw-cursor-pointer tw-border-slate-800 tw-border" + style={{ + backgroundImage: + "url(hackathon/rt-collab/sprites/sprite-3.png)", + objectFit: "cover", + outline: + 3 === selectedIconIndex + ? "3px solid " + SELECTED_COLOR + : undefined, + outlineOffset: "1px", + }} + >
+
+
+
iconClicked(4)} + className="tw-w-8 tw-h-8 tw-rounded-md tw-cursor-pointer tw-border-slate-800 tw-border" + style={{ + backgroundImage: + "url(hackathon/rt-collab/sprites/sprite-4.png)", + objectFit: "cover", + outline: + 4 === selectedIconIndex + ? "3px solid " + SELECTED_COLOR + : undefined, + outlineOffset: "1px", + }} + >
+
iconClicked(5)} + className="tw-w-8 tw-h-8 tw-rounded-md tw-cursor-pointer tw-border-slate-800 tw-border" + style={{ + backgroundImage: + "url(hackathon/rt-collab/sprites/sprite-5.png)", + objectFit: "cover", + outline: + 5 === selectedIconIndex + ? "3px solid " + SELECTED_COLOR + : undefined, + outlineOffset: "1px", + }} + >
+
{ + const { state } = collabStateAndDispatch(); + getCollabPlayers(state).forEach(player => { + let x = 0; + let y = 0; + let imgId = 0; + if (player.kv.has("position")) { + const pos = JSON.parse(player.kv.get("position")!); + x = pos.x; + y = pos.y; + } + if (player.kv.has("imgId")) { + imgId = parseInt(player.kv.get("imgId")!); + } + getCollabCanvas().addPlayerSprite(player.clientId, x, y, imgId); + }); + }, 1); } diff --git a/multiplayer/src/services/collabCanvas.ts b/multiplayer/src/services/collabCanvas.ts index d072b26c10e2..53b83c25e4ba 100644 --- a/multiplayer/src/services/collabCanvas.ts +++ b/multiplayer/src/services/collabCanvas.ts @@ -118,10 +118,9 @@ class CollabCanvas { } public reset() { - this.app.stage.removeChildren(); - this.root = new Pixi.Graphics(); + this.root.children.forEach(child => child.destroy()); + this.root.removeChildren(); this.playerSprites.clear(); - this.app.stage.addChild(this.root as any); this.addBackgroundGrid(); } @@ -161,6 +160,8 @@ class CollabCanvas { y: number, imgId: number ) { + if (!playerId) return; + if (this.playerSprites.has(playerId)) return; const sprite = Pixi.Sprite.from( `hackathon/rt-collab/sprites/sprite-${imgId}.png` ); @@ -190,8 +191,9 @@ class CollabCanvas { public updatePlayerSpriteImage(playerId: string, imgId: number) { const player = this.playerSprites.get(playerId); if (player && player.imgId !== imgId) { + player.imgId = imgId; player.sprite.texture = Pixi.Texture.from( - `sprites/sprite-${imgId}.png` + `hackathon/rt-collab/sprites/sprite-${imgId}.png` ); } } diff --git a/multiplayer/src/services/collabClient.ts b/multiplayer/src/services/collabClient.ts index 25e13138e29a..66fe6551c0e0 100644 --- a/multiplayer/src/services/collabClient.ts +++ b/multiplayer/src/services/collabClient.ts @@ -274,6 +274,7 @@ class CollabClient { const { role, clientId } = msg; this.clientRole = role; + this.clientId = clientId; // TODO: Set initial state } @@ -281,6 +282,7 @@ class CollabClient { private async recvPresenceMessageAsync(msg: Protocol.PresenceMessage) { pxt.debug("Server sent presence"); await setPresenceAsync(msg.presence); + CollabEpics.recvUpdatePresence(msg.presence); } private async recvPlayerJoinedMessageAsync( diff --git a/multiplayer/src/state/collab/index.ts b/multiplayer/src/state/collab/index.ts index 4a9c4956aef1..5459c35e470b 100644 --- a/multiplayer/src/state/collab/index.ts +++ b/multiplayer/src/state/collab/index.ts @@ -1,2 +1,9 @@ +import { CollabPlayer } from "../../services/collabClient"; +import { CollabState } from "./state"; + export { collabStateAndDispatch } from "./CollabContext"; export { CollabStateProvider, CollabContext } from "./CollabContext"; + +export function getCollabPlayers(state: CollabState): CollabPlayer[] { + return Object.values(state.players); +} diff --git a/multiplayer/src/state/collab/reducer.ts b/multiplayer/src/state/collab/reducer.ts index 4a2941023c0e..73b1f7c6079b 100644 --- a/multiplayer/src/state/collab/reducer.ts +++ b/multiplayer/src/state/collab/reducer.ts @@ -49,8 +49,10 @@ export function reducer(state: CollabState, action: CollabAction): CollabState { case "UPDATE_PRESENCE": { const players = { ...state.players }; action.presence.users.forEach(user => { - const player = players[user.id]; - if (!player) return; + const player = players[user.id] ?? { + clientId: user.id, + kv: new Map(), + } players[user.id] = { ...player, kv: user.kv ? user.kv : player.kv, From 5ba933e042e4df5620cb786806338747fdfd9f7b Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Sun, 31 Mar 2024 15:59:22 -0700 Subject: [PATCH 04/12] collab wip --- multiplayer/package-lock.json | 14 ++ multiplayer/package.json | 1 + multiplayer/src/components/CollabPage.tsx | 206 +++++++++--------- multiplayer/src/constants.ts | 35 +++ multiplayer/src/epics/collab/index.ts | 4 + multiplayer/src/epics/collab/initState.ts | 4 +- .../src/epics/collab/recvDelPlayerValue.ts | 9 + .../src/epics/collab/recvDelSessionValue.ts | 11 + .../src/epics/collab/recvSessionState.ts | 12 + .../src/epics/collab/recvSetPlayerValue.ts | 5 +- .../src/epics/collab/recvSetSessionState.ts | 19 ++ .../src/epics/collab/recvSetSessionValue.ts | 17 ++ multiplayer/src/services/collabCanvas.ts | 41 +++- multiplayer/src/services/collabClient.ts | 47 +++- multiplayer/src/state/collab/actions.ts | 60 ++++- multiplayer/src/state/collab/reducer.ts | 41 +++- multiplayer/src/state/collab/state.ts | 2 + multiplayer/src/types/index.ts | 4 +- multiplayer/src/util/index.ts | 8 + 19 files changed, 420 insertions(+), 120 deletions(-) create mode 100644 multiplayer/src/constants.ts create mode 100644 multiplayer/src/epics/collab/recvDelPlayerValue.ts create mode 100644 multiplayer/src/epics/collab/recvDelSessionValue.ts create mode 100644 multiplayer/src/epics/collab/recvSessionState.ts create mode 100644 multiplayer/src/epics/collab/recvSetSessionState.ts create mode 100644 multiplayer/src/epics/collab/recvSetSessionValue.ts diff --git a/multiplayer/package-lock.json b/multiplayer/package-lock.json index a5b9980142e0..e910df12a390 100644 --- a/multiplayer/package-lock.json +++ b/multiplayer/package-lock.json @@ -8,6 +8,7 @@ "name": "@arcade/multiplayer", "version": "0.1.0", "dependencies": { + "@ctrl/tinycolor": "^4.0.3", "@fortawesome/free-regular-svg-icons": "^6.2.0", "@fortawesome/free-solid-svg-icons": "^6.2.0", "@fortawesome/react-fontawesome": "^0.2.0", @@ -2182,6 +2183,14 @@ "postcss-selector-parser": "^6.0.10" } }, + "node_modules/@ctrl/tinycolor": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.0.3.tgz", + "integrity": "sha512-e9nEVehVJwkymQpkGhdSNzLT2Lr9UTTby+JePq4Z2SxBbOQjY7pLgSouAaXvfaGQVSAaY0U4eJdwfSDmCbItcw==", + "engines": { + "node": ">=14" + } + }, "node_modules/@emotion/is-prop-valid": { "version": "0.8.8", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", @@ -19445,6 +19454,11 @@ "integrity": "sha512-IkpVW/ehM1hWKln4fCA3NzJU8KwD+kIOvPZA4cqxoJHtE21CCzjyp+Kxbu0i5I4tBNOlXPL9mjwnWlL0VEG4Fg==", "requires": {} }, + "@ctrl/tinycolor": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.0.3.tgz", + "integrity": "sha512-e9nEVehVJwkymQpkGhdSNzLT2Lr9UTTby+JePq4Z2SxBbOQjY7pLgSouAaXvfaGQVSAaY0U4eJdwfSDmCbItcw==" + }, "@emotion/is-prop-valid": { "version": "0.8.8", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", diff --git a/multiplayer/package.json b/multiplayer/package.json index 5409b9f91c3a..477f5f23b19e 100644 --- a/multiplayer/package.json +++ b/multiplayer/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { + "@ctrl/tinycolor": "^4.0.3", "@fortawesome/free-regular-svg-icons": "^6.2.0", "@fortawesome/free-solid-svg-icons": "^6.2.0", "@fortawesome/react-fontawesome": "^0.2.0", diff --git a/multiplayer/src/components/CollabPage.tsx b/multiplayer/src/components/CollabPage.tsx index 0e6a39ccb2fd..8b809729fbd0 100644 --- a/multiplayer/src/components/CollabPage.tsx +++ b/multiplayer/src/components/CollabPage.tsx @@ -2,11 +2,13 @@ import { useContext, useEffect, useState } from "react"; import { AppStateContext } from "../state/AppStateContext"; import { Button } from "../../../react-common/components/controls/Button"; import { leaveCollabAsync } from "../epics/leaveCollabAsync"; -import { BRUSH_COLORS, BRUSH_SIZES, BrushSize } from "../types"; +import { BRUSH_COLORS, BRUSH_SIZES, BrushSize, Vec2Like } from "../types"; import { getCollabCanvas } from "../services/collabCanvas"; import * as collabClient from "../services/collabClient"; import * as CollabEpics from "../epics/collab"; -import { jsonReplacer } from "../util"; +import { dist, distSq, jsonReplacer } from "../util"; +import { BRUSH_PROPS, PLAYER_SPRITE_DATAURLS } from "../constants"; +import { nanoid } from "nanoid"; export interface CollabPageProps {} @@ -20,6 +22,7 @@ export default function Render(props: CollabPageProps) { const [selectedSizeIndex, setSelectedSizeIndex] = useState(0); const [selectedIconIndex, setSelectedIconIndex] = useState(0); const [mouseDown, setMouseDown] = useState(false); + const [lastPosition, setLastPosition] = useState({ x: 0, y: 0 }); useEffect(() => { const collabCanvas = getCollabCanvas(); @@ -36,10 +39,35 @@ export default function Render(props: CollabPageProps) { canvasMouseY -= canvasContainer.offsetTop; canvasMouseX += canvasContainer.scrollLeft; canvasMouseY += canvasContainer.scrollTop; - if (canvasMouseX < 0 || canvasMouseY < 0) return; - if (canvasMouseX >= canvasContainer.clientWidth) return; - if (canvasMouseY >= canvasContainer.clientHeight) return; + if (canvasMouseX < canvasContainer.scrollLeft) return; + if (canvasMouseY < canvasContainer.scrollTop) return; + if ( + canvasMouseX >= + canvasContainer.scrollLeft + canvasContainer.clientWidth + ) + return; + if ( + canvasMouseY >= + canvasContainer.scrollTop + canvasContainer.clientHeight + ) + return; setMouseDown(true); + setLastPosition({ x: canvasMouseX, y: canvasMouseY }); + getCollabCanvas().addPaintSprite( + canvasMouseX, + canvasMouseY, + selectedSizeIndex, + selectedColorIndex + ); + collabClient.setSessionValue( + "s:" + nanoid(), + JSON.stringify({ + x: canvasMouseX, + y: canvasMouseY, + s: selectedSizeIndex, + c: selectedColorIndex, + }) + ); }; const handleMouseUp = (e: MouseEvent) => { setMouseDown(false); @@ -62,18 +90,46 @@ export default function Render(props: CollabPageProps) { canvasMouseX, canvasMouseY ); - const pos = { x: canvasMouseX, y: canvasMouseY }; + const newPos = { x: canvasMouseX, y: canvasMouseY }; + const newPosStr = JSON.stringify(newPos); // Send position to server - collabClient.setPlayerValue("position", JSON.stringify(pos)); + collabClient.setPlayerValue("position", newPosStr); // Update local player position in state CollabEpics.recvSetPlayerValue( collabClient.getClientId(), "position", - JSON.stringify(pos, jsonReplacer) + newPosStr ); if (!mouseDown) return; - // TODO: send draw message + const brushSize = BRUSH_PROPS[selectedSizeIndex].size; + const brushStep = brushSize / 3; + const posDist = dist(lastPosition, newPos); + if (posDist > brushStep) { + const numSteps = Math.ceil(posDist / brushStep); + const stepX = (newPos.x - lastPosition.x) / numSteps; + const stepY = (newPos.y - lastPosition.y) / numSteps; + for (let i = 0; i < numSteps; i++) { + const x = lastPosition.x + stepX * i; + const y = lastPosition.y + stepY * i; + getCollabCanvas().addPaintSprite( + x, + y, + selectedSizeIndex, + selectedColorIndex + ); + collabClient.setSessionValue( + "s:" + nanoid(), + JSON.stringify({ + x, + y, + s: selectedSizeIndex, + c: selectedColorIndex, + }) + ); + } + setLastPosition(newPos); + } }; window.addEventListener("mousedown", handleMouseDown); window.addEventListener("mouseup", handleMouseUp); @@ -83,7 +139,13 @@ export default function Render(props: CollabPageProps) { window.removeEventListener("mouseup", handleMouseUp); window.removeEventListener("mousemove", handleMouseMove); }; - }, [mouseDown, canvasContainer]); + }, [ + mouseDown, + canvasContainer, + lastPosition, + selectedColorIndex, + selectedSizeIndex, + ]); useEffect(() => { if (canvasContainer && !canvasContainer.firstChild) { @@ -115,7 +177,10 @@ export default function Render(props: CollabPageProps) { const iconClicked = (index: number) => { setSelectedIconIndex(index); - getCollabCanvas().updatePlayerSpriteImage(collabClient.getClientId()!, index); + getCollabCanvas().updatePlayerSpriteImage( + collabClient.getClientId()!, + index + ); collabClient.setPlayerValue("imgId", JSON.stringify(index)); }; @@ -123,6 +188,29 @@ export default function Render(props: CollabPageProps) { setCanvasContainer(ref); }; + const SpriteRow: React.FC = ({ children }) => ( +
{children}
+ ); + + const SpriteImg: React.FC<{ imgId: number }> = ({ imgId }) => ( +
iconClicked(imgId)} + className="tw-w-8 tw-h-8 tw-rounded-md tw-cursor-pointer tw-border-slate-800 tw-border tw-bg-slate-100" + style={{ + backgroundImage: `url(${PLAYER_SPRITE_DATAURLS[imgId]})`, + backgroundRepeat: "no-repeat", + backgroundPosition: "center", + padding: "1.1rem", + outline: + imgId === selectedIconIndex + ? "3px solid " + SELECTED_COLOR + : undefined, + outlineOffset: "1px", + imageRendering: "pixelated", + }} + >
+ ); + const SELECTED_COLOR = "#1e293b"; return ( @@ -168,96 +256,12 @@ export default function Render(props: CollabPageProps) { >
))}
-
-
iconClicked(0)} - className="tw-w-8 tw-h-8 tw-rounded-md tw-cursor-pointer tw-border-slate-800 tw-border" - style={{ - backgroundImage: - "url(hackathon/rt-collab/sprites/sprite-0.png)", - objectFit: "cover", - outline: - 0 === selectedIconIndex - ? "3px solid " + SELECTED_COLOR - : undefined, - outlineOffset: "1px", - }} - >
-
iconClicked(1)} - className="tw-w-8 tw-h-8 tw-rounded-md tw-cursor-pointer tw-border-slate-800 tw-border" - style={{ - backgroundImage: - "url(hackathon/rt-collab/sprites/sprite-1.png)", - objectFit: "cover", - outline: - 1 === selectedIconIndex - ? "3px solid " + SELECTED_COLOR - : undefined, - outlineOffset: "1px", - }} - >
-
-
-
iconClicked(2)} - className="tw-w-8 tw-h-8 tw-rounded-md tw-cursor-pointer tw-border-slate-800 tw-border" - style={{ - backgroundImage: - "url(hackathon/rt-collab/sprites/sprite-2.png)", - objectFit: "cover", - outline: - 2 === selectedIconIndex - ? "3px solid " + SELECTED_COLOR - : undefined, - outlineOffset: "1px", - }} - >
-
iconClicked(3)} - className="tw-w-8 tw-h-8 tw-rounded-md tw-cursor-pointer tw-border-slate-800 tw-border" - style={{ - backgroundImage: - "url(hackathon/rt-collab/sprites/sprite-3.png)", - objectFit: "cover", - outline: - 3 === selectedIconIndex - ? "3px solid " + SELECTED_COLOR - : undefined, - outlineOffset: "1px", - }} - >
-
-
-
iconClicked(4)} - className="tw-w-8 tw-h-8 tw-rounded-md tw-cursor-pointer tw-border-slate-800 tw-border" - style={{ - backgroundImage: - "url(hackathon/rt-collab/sprites/sprite-4.png)", - objectFit: "cover", - outline: - 4 === selectedIconIndex - ? "3px solid " + SELECTED_COLOR - : undefined, - outlineOffset: "1px", - }} - >
-
iconClicked(5)} - className="tw-w-8 tw-h-8 tw-rounded-md tw-cursor-pointer tw-border-slate-800 tw-border" - style={{ - backgroundImage: - "url(hackathon/rt-collab/sprites/sprite-5.png)", - objectFit: "cover", - outline: - 5 === selectedIconIndex - ? "3px solid " + SELECTED_COLOR - : undefined, - outlineOffset: "1px", - }} - >
-
+ {[0, 2, 4].map(imgId => ( + + + + + ))}
) { const { dispatch } = collabStateAndDispatch(); - dispatch(CollabActions.init()); + dispatch(CollabActions.init(kv)); } diff --git a/multiplayer/src/epics/collab/recvDelPlayerValue.ts b/multiplayer/src/epics/collab/recvDelPlayerValue.ts new file mode 100644 index 000000000000..78c6847076d8 --- /dev/null +++ b/multiplayer/src/epics/collab/recvDelPlayerValue.ts @@ -0,0 +1,9 @@ +import { getCollabCanvas } from "../../services/collabCanvas"; +import { collabStateAndDispatch } from "../../state/collab"; +import * as CollabActions from "../../state/collab/actions"; + +export function recvDelPlayerValue(playerId: string | undefined, key: string) { + if (!playerId) return; + const { dispatch } = collabStateAndDispatch(); + dispatch(CollabActions.delPlayerValue(playerId, key)); +} diff --git a/multiplayer/src/epics/collab/recvDelSessionValue.ts b/multiplayer/src/epics/collab/recvDelSessionValue.ts new file mode 100644 index 000000000000..c22d9f1173d8 --- /dev/null +++ b/multiplayer/src/epics/collab/recvDelSessionValue.ts @@ -0,0 +1,11 @@ +import { getCollabCanvas } from "../../services/collabCanvas"; +import { collabStateAndDispatch } from "../../state/collab"; +import * as CollabActions from "../../state/collab/actions"; + +export function recvDelSessionValue(key: string) { + const { dispatch } = collabStateAndDispatch(); + dispatch(CollabActions.delSessionValue(key)); + if (key.startsWith("s:")) { + //getCollabCanvas().removePaintSprite(key); + } +} diff --git a/multiplayer/src/epics/collab/recvSessionState.ts b/multiplayer/src/epics/collab/recvSessionState.ts new file mode 100644 index 000000000000..d7cd5c84c929 --- /dev/null +++ b/multiplayer/src/epics/collab/recvSessionState.ts @@ -0,0 +1,12 @@ +import { getCollabCanvas } from "../../services/collabCanvas"; +import { collabStateAndDispatch } from "../../state/collab"; +import * as CollabActions from "../../state/collab/actions"; + +export function recvSetSessionValue(key: string, value: string) { + const { dispatch } = collabStateAndDispatch(); + dispatch(CollabActions.setSessionValue(key, value)); + if (key.startsWith("s:")) { + const sprite = JSON.parse(value); + //getCollabCanvas().addPaintSprite(s); + } +} diff --git a/multiplayer/src/epics/collab/recvSetPlayerValue.ts b/multiplayer/src/epics/collab/recvSetPlayerValue.ts index 776383175ec8..c73c13b22662 100644 --- a/multiplayer/src/epics/collab/recvSetPlayerValue.ts +++ b/multiplayer/src/epics/collab/recvSetPlayerValue.ts @@ -14,6 +14,9 @@ export function recvSetPlayerValue( const pos = JSON.parse(value); getCollabCanvas().updatePlayerSpritePosition(playerId, pos.x, pos.y); } else if (key === "imgId") { - getCollabCanvas().updatePlayerSpriteImage(playerId, parseInt(JSON.parse(value))); + getCollabCanvas().updatePlayerSpriteImage( + playerId, + parseInt(JSON.parse(value)) + ); } } diff --git a/multiplayer/src/epics/collab/recvSetSessionState.ts b/multiplayer/src/epics/collab/recvSetSessionState.ts new file mode 100644 index 000000000000..633d49847bfe --- /dev/null +++ b/multiplayer/src/epics/collab/recvSetSessionState.ts @@ -0,0 +1,19 @@ +import { getCollabCanvas } from "../../services/collabCanvas"; +import { collabStateAndDispatch } from "../../state/collab"; +import * as CollabActions from "../../state/collab/actions"; + +export function recvSetSessionState(sessKv: Map) { + const { dispatch } = collabStateAndDispatch(); + dispatch(CollabActions.setSessionState(sessKv)); + sessKv.forEach((value, key) => { + if (key.startsWith("s:")) { + const sprite = JSON.parse(value); + getCollabCanvas().addPaintSprite( + sprite.x, + sprite.y, + sprite.s, + sprite.c + ); + } + }); +} diff --git a/multiplayer/src/epics/collab/recvSetSessionValue.ts b/multiplayer/src/epics/collab/recvSetSessionValue.ts new file mode 100644 index 000000000000..98e6e8dda82a --- /dev/null +++ b/multiplayer/src/epics/collab/recvSetSessionValue.ts @@ -0,0 +1,17 @@ +import { getCollabCanvas } from "../../services/collabCanvas"; +import { collabStateAndDispatch } from "../../state/collab"; +import * as CollabActions from "../../state/collab/actions"; + +export function recvSetSessionValue(key: string, value: string) { + const { dispatch } = collabStateAndDispatch(); + dispatch(CollabActions.setSessionValue(key, value)); + if (key.startsWith("s:")) { + const sprite = JSON.parse(value); + getCollabCanvas().addPaintSprite( + sprite.x, + sprite.y, + sprite.s, + sprite.c + ); + } +} diff --git a/multiplayer/src/services/collabCanvas.ts b/multiplayer/src/services/collabCanvas.ts index 53b83c25e4ba..5c65977dd894 100644 --- a/multiplayer/src/services/collabCanvas.ts +++ b/multiplayer/src/services/collabCanvas.ts @@ -1,5 +1,8 @@ import * as Pixi from "pixi.js"; +import { TinyColor } from "@ctrl/tinycolor"; import { flattenVerts } from "../util"; +import { BRUSH_PROPS, PLAYER_SPRITE_DATAURLS } from "../constants"; +import { BRUSH_COLORS } from "../types"; const shaderPrograms = new Map(); @@ -92,6 +95,7 @@ class CollabCanvas { private app: Pixi.Application; private root: Pixi.Graphics; private playerSprites: Map = new Map(); + private brushTextures: Map = new Map(); public get view() { return this.app.view; @@ -105,16 +109,22 @@ class CollabCanvas { antialias: true, clearBeforeRender: true, }); - // Set up initial canvas dimensions const canv = this.app.view as HTMLCanvasElement; canv.style.width = canv.style.minWidth = CANVAS_SIZE + "px"; canv.style.height = canv.style.minHeight = CANVAS_SIZE + "px"; this.root = new Pixi.Graphics(); + this.root.sortableChildren = true; this.app.stage.addChild(this.root as any); this.addBackgroundGrid(); + + // Set up brush textures + BRUSH_PROPS.map(brush => { + const texture = Pixi.Texture.from(brush.dataUrl); + this.brushTextures.set(brush.name, texture); + }); } public reset() { @@ -162,9 +172,7 @@ class CollabCanvas { ) { if (!playerId) return; if (this.playerSprites.has(playerId)) return; - const sprite = Pixi.Sprite.from( - `hackathon/rt-collab/sprites/sprite-${imgId}.png` - ); + const sprite = Pixi.Sprite.from(PLAYER_SPRITE_DATAURLS[imgId]); sprite.anchor.set(0.5); sprite.position.set(x, y); sprite.zIndex = 100; // in front of everything @@ -189,14 +197,37 @@ class CollabCanvas { } public updatePlayerSpriteImage(playerId: string, imgId: number) { + if (!playerId) return; const player = this.playerSprites.get(playerId); if (player && player.imgId !== imgId) { player.imgId = imgId; player.sprite.texture = Pixi.Texture.from( - `hackathon/rt-collab/sprites/sprite-${imgId}.png` + PLAYER_SPRITE_DATAURLS[imgId] ); } } + + public addPaintSprite( + x: number, + y: number, + brushIndex: number, + colorIndex: number + ) { + const sprite = new Pixi.Sprite(); + const brush = BRUSH_PROPS[brushIndex]; + if (!brush) return; + if (!this.brushTextures.has(brush.name)) return; + const color = BRUSH_COLORS[colorIndex]; + if (!color) return; + sprite.texture = this.brushTextures.get(brush.name)!; + sprite.anchor.set(0.5); + sprite.position.set(x, y); + sprite.width = brush.size; + sprite.height = brush.size; + sprite.tint = new TinyColor(color).toNumber(); + sprite.zIndex = 0; // behind players + this.root.addChild(sprite as any); + } } let _instance: CollabCanvas; diff --git a/multiplayer/src/services/collabClient.ts b/multiplayer/src/services/collabClient.ts index 66fe6551c0e0..6638d84db17a 100644 --- a/multiplayer/src/services/collabClient.ts +++ b/multiplayer/src/services/collabClient.ts @@ -95,8 +95,12 @@ class CollabClient { return await this.recvPlayerLeftMessageAsync(msg); case "set-player-value": return await this.recvSetPlayerValueMessageAsync(msg); + case "del-player-value": + return await this.recvDelPlayerValueMessageAsync(msg); case "set-session-value": return await this.recvSetSessionValueMessageAsync(msg); + case "del-session-value": + return await this.recvDelSessionValueMessageAsync(msg); } } else { throw new Error(`Unknown payload: ${payload}`); @@ -196,7 +200,7 @@ class CollabClient { if (!collabInfo?.joinTicket) throw new Error("Collab server did not return a join ticket"); - CollabEpics.initState(); + CollabEpics.initState(new Map()); await this.connectAsync(collabInfo.joinTicket!); @@ -241,7 +245,7 @@ class CollabClient { if (!collabInfo?.joinTicket) throw new Error("Collab server did not return a join ticket"); - CollabEpics.initState(); + CollabEpics.initState(new Map()); await this.connectAsync(collabInfo.joinTicket!); @@ -271,12 +275,13 @@ class CollabClient { pxt.debug( `Server said we're joined as "${msg.role}" in slot "${msg.slot}"` ); - const { role, clientId } = msg; + const { role, clientId, sessKv } = msg; this.clientRole = role; this.clientId = clientId; // TODO: Set initial state + CollabEpics.recvSetSessionState(sessKv); } private async recvPresenceMessageAsync(msg: Protocol.PresenceMessage) { @@ -305,14 +310,29 @@ class CollabClient { private async recvSetPlayerValueMessageAsync( msg: Protocol.SetPlayerValueMessage ) { - pxt.debug(`Recv set player value: ${msg.key} = ${msg.value}`); + //pxt.debug(`Recv set player value: ${msg.key} = ${msg.value}`); CollabEpics.recvSetPlayerValue(msg.clientId!, msg.key, msg.value); } + private async recvDelPlayerValueMessageAsync( + msg: Protocol.DelPlayerValueMessage + ) { + //pxt.debug(`Recv del player value: ${msg.key}`); + CollabEpics.recvDelPlayerValue(msg.clientId!, msg.key); + } + private async recvSetSessionValueMessageAsync( msg: Protocol.SetSessionValueMessage ) { - pxt.debug(`Recv set session value: ${msg.key} = ${msg.value}`); + //pxt.debug(`Recv set session value: ${msg.key} = ${msg.value}`); + CollabEpics.recvSetSessionValue(msg.key, msg.value); + } + + private async recvDelSessionValueMessageAsync( + msg: Protocol.DelSessionValueMessage + ) { + //pxt.debug(`Recv del session value: ${msg.key}`); + CollabEpics.recvDelSessionValue(msg.key); } public kickPlayer(clientId: string) { @@ -441,6 +461,7 @@ namespace Protocol { role: ClientRole; slot: number; clientId: string; + sessKv: Map; }; export type PlayerJoinedMessage = MessageBase & { @@ -470,11 +491,21 @@ namespace Protocol { clientId?: string; // only set on received messages }; + export type DelPlayerValueMessage = MessageBase & { + type: "del-player-value"; + key: string; + clientId?: string; // only set on received messages + }; + export type SetSessionValueMessage = MessageBase & { type: "set-session-value"; key: string; value: string; - clientId?: string; // only set on received messages + }; + + export type DelSessionValueMessage = MessageBase & { + type: "del-session-value"; + key: string; }; export type Message = @@ -486,5 +517,7 @@ namespace Protocol { | KickPlayerMessage | CollabOverMessage | SetPlayerValueMessage - | SetSessionValueMessage; + | DelPlayerValueMessage + | SetSessionValueMessage + | DelSessionValueMessage; } diff --git a/multiplayer/src/state/collab/actions.ts b/multiplayer/src/state/collab/actions.ts index 9913ca0caca2..16d2d10566f2 100644 --- a/multiplayer/src/state/collab/actions.ts +++ b/multiplayer/src/state/collab/actions.ts @@ -2,6 +2,7 @@ import { ActionBase, Presence } from "../../types"; type Init = ActionBase & { type: "INIT"; + kv: Map; }; type PlayerJoined = ActionBase & { @@ -22,6 +23,28 @@ type SetPlayerValue = ActionBase & { value: string; }; +type DelPlayerValue = ActionBase & { + type: "DEL_PLAYER_VALUE"; + playerId: string; + key: string; +}; + +type SetSessionValue = ActionBase & { + type: "SET_SESSION_VALUE"; + key: string; + value: string; +}; + +type DelSessionValue = ActionBase & { + type: "DEL_SESSION_VALUE"; + key: string; +}; + +type SetSessionState = ActionBase & { + type: "SET_SESSION_STATE"; + sessKv: Map; +}; + type UpdatePresence = ActionBase & { type: "UPDATE_PRESENCE"; presence: Presence; @@ -32,11 +55,16 @@ export type CollabAction = | PlayerJoined | PlayerLeft | SetPlayerValue + | DelPlayerValue + | SetSessionValue + | DelSessionValue + | SetSessionState | UpdatePresence; -export function init(): Init { +export function init(kv: Map): Init { return { type: "INIT", + kv, }; } @@ -71,6 +99,36 @@ export function setPlayerValue( }; } +export function delPlayerValue(playerId: string, key: string): DelPlayerValue { + return { + type: "DEL_PLAYER_VALUE", + playerId, + key, + }; +} + +export function setSessionValue(key: string, value: string): SetSessionValue { + return { + type: "SET_SESSION_VALUE", + key, + value, + }; +} + +export function delSessionValue(key: string): DelSessionValue { + return { + type: "DEL_SESSION_VALUE", + key, + }; +} + +export function setSessionState(sessKv: Map): SetSessionState { + return { + type: "SET_SESSION_STATE", + sessKv, + }; +} + export function updatePresence(presence: Presence): UpdatePresence { return { type: "UPDATE_PRESENCE", diff --git a/multiplayer/src/state/collab/reducer.ts b/multiplayer/src/state/collab/reducer.ts index 73b1f7c6079b..76a5d69c23fc 100644 --- a/multiplayer/src/state/collab/reducer.ts +++ b/multiplayer/src/state/collab/reducer.ts @@ -46,13 +46,52 @@ export function reducer(state: CollabState, action: CollabAction): CollabState { return state; } } + case "DEL_PLAYER_VALUE": { + const player = state.players[action.playerId]; + if (player) { + const kv = new Map(player.kv); + kv.delete(action.key); + return { + ...state, + players: { + ...state.players, + [action.playerId]: { + ...player, + kv, + }, + }, + }; + } else { + return state; + } + } + case "SET_SESSION_VALUE": { + return { + ...state, + kv: new Map(state.kv).set(action.key, action.value), + }; + } + case "DEL_SESSION_VALUE": { + const kv = new Map(state.kv); + kv.delete(action.key); + return { + ...state, + kv, + }; + } + case "SET_SESSION_STATE": { + return { + ...state, + kv: action.sessKv, + }; + } case "UPDATE_PRESENCE": { const players = { ...state.players }; action.presence.users.forEach(user => { const player = players[user.id] ?? { clientId: user.id, kv: new Map(), - } + }; players[user.id] = { ...player, kv: user.kv ? user.kv : player.kv, diff --git a/multiplayer/src/state/collab/state.ts b/multiplayer/src/state/collab/state.ts index ef561f644f9e..0220542c1bc8 100644 --- a/multiplayer/src/state/collab/state.ts +++ b/multiplayer/src/state/collab/state.ts @@ -2,8 +2,10 @@ import { CollabPlayer } from "../../services/collabClient"; export type CollabState = { players: { [playerId: string]: CollabPlayer }; + kv: Map; }; export const initialState: CollabState = { players: {}, + kv: new Map(), }; diff --git a/multiplayer/src/types/index.ts b/multiplayer/src/types/index.ts index 053903ba98b5..41422c816fa3 100644 --- a/multiplayer/src/types/index.ts +++ b/multiplayer/src/types/index.ts @@ -120,9 +120,9 @@ export type BrushSize = { }; export const BRUSH_SIZES: BrushSize[] = [ - { sz: "sm", px: 22 }, + { sz: "sm", px: 16 }, { sz: "md", px: 32 }, - { sz: "lg", px: 42 }, + { sz: "lg", px: 48 }, ]; export type BrushModeType = "draw" | "move"; diff --git a/multiplayer/src/util/index.ts b/multiplayer/src/util/index.ts index e22171d6fd08..749bc1c6b5c2 100644 --- a/multiplayer/src/util/index.ts +++ b/multiplayer/src/util/index.ts @@ -133,3 +133,11 @@ export function jsonReviver(key: any, value: any) { } return value; } + +export function distSq(a: Vec2Like, b: Vec2Like) { + return (a.x - b.x) ** 2 + (a.y - b.y) ** 2; +} + +export function dist(a: Vec2Like, b: Vec2Like) { + return Math.sqrt(distSq(a, b)); +} From 2269d69b2d2b30263cf269a01f100001a5188808 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Sun, 31 Mar 2024 16:04:16 -0700 Subject: [PATCH 05/12] prettier --- multiplayer/.prettierrc | 3 +- multiplayer/src/components/AppModal.tsx | 25 +-- .../src/components/ArcadeSimulator.tsx | 15 +- multiplayer/src/components/CollabPage.tsx | 76 ++------ multiplayer/src/components/CopyButton.tsx | 19 +- multiplayer/src/components/EditGameButton.tsx | 10 +- multiplayer/src/components/GamePage.tsx | 6 +- multiplayer/src/components/GamePaused.tsx | 25 +-- multiplayer/src/components/HeaderBar.tsx | 43 +---- multiplayer/src/components/HostGameButton.tsx | 13 +- multiplayer/src/components/HostLobby.tsx | 10 +- multiplayer/src/components/JoinCodeLabel.tsx | 4 +- multiplayer/src/components/JoinLobby.tsx | 4 +- multiplayer/src/components/JoinOrHost.tsx | 29 +-- .../src/components/KeyboardControlsInfo.tsx | 12 +- multiplayer/src/components/Popup.tsx | 3 +- multiplayer/src/components/Presence.tsx | 34 +--- .../src/components/ReactionEmitter.tsx | 5 +- .../src/components/ReactionParticle.tsx | 16 +- multiplayer/src/components/Reactions.tsx | 4 +- multiplayer/src/components/TabButton.tsx | 12 +- multiplayer/src/components/Toast.tsx | 29 +-- multiplayer/src/components/icons/UserIcon.tsx | 8 +- .../src/components/modals/ConfirmModal.tsx | 3 +- .../src/epics/collab/recvSetPlayerValue.ts | 11 +- .../src/epics/collab/recvSetSessionState.ts | 7 +- .../src/epics/collab/recvSetSessionValue.ts | 7 +- multiplayer/src/epics/hostCollabAsync.ts | 8 +- multiplayer/src/epics/hostGameAsync.ts | 8 +- multiplayer/src/epics/joinCollabAsync.ts | 8 +- multiplayer/src/epics/joinGameAsync.ts | 8 +- multiplayer/src/epics/sendAbuseReportAsync.ts | 19 +- multiplayer/src/epics/sendReactionAsync.ts | 5 +- multiplayer/src/epics/setCustomIconAsync.ts | 16 +- multiplayer/src/epics/setGameMetadataAsync.ts | 11 +- multiplayer/src/epics/setGameModeAsync.ts | 13 +- multiplayer/src/epics/setUserProfileAsync.ts | 4 +- multiplayer/src/hooks/useClickedOutside.ts | 5 +- multiplayer/src/hooks/useVisibilityChange.ts | 5 +- multiplayer/src/services/authClient.ts | 8 +- multiplayer/src/services/collabCanvas.ts | 18 +- multiplayer/src/services/collabClient.ts | 66 ++----- multiplayer/src/services/gameClient.ts | 179 +++++------------- multiplayer/src/services/simHost.ts | 72 ++----- multiplayer/src/state/AppStateContext.tsx | 8 +- multiplayer/src/state/actions.ts | 38 +--- .../src/state/collab/CollabContext.tsx | 8 +- multiplayer/src/state/collab/actions.ts | 11 +- multiplayer/src/state/collab/reducer.ts | 5 +- multiplayer/src/state/reducer.ts | 6 +- multiplayer/src/types/index.ts | 36 +--- multiplayer/src/util/index.ts | 18 +- 52 files changed, 222 insertions(+), 794 deletions(-) diff --git a/multiplayer/.prettierrc b/multiplayer/.prettierrc index 06c8d264b740..f5b0ff961c72 100644 --- a/multiplayer/.prettierrc +++ b/multiplayer/.prettierrc @@ -1,5 +1,6 @@ { "arrowParens": "avoid", "semi": true, - "tabWidth": 4 + "tabWidth": 4, + "printWidth": 120 } \ No newline at end of file diff --git a/multiplayer/src/components/AppModal.tsx b/multiplayer/src/components/AppModal.tsx index fd52701934fe..6576e1229de3 100644 --- a/multiplayer/src/components/AppModal.tsx +++ b/multiplayer/src/components/AppModal.tsx @@ -1,12 +1,7 @@ import { useContext, useState } from "react"; import { Textarea } from "react-common/components/controls/Textarea"; import { SignInModal } from "react-common/components/profile/SignInModal"; -import { - signInAsync, - kickPlayer, - leaveGameAsync, - sendAbuseReportAsync, -} from "../epics"; +import { signInAsync, kickPlayer, leaveGameAsync, sendAbuseReportAsync } from "../epics"; import { clearModal } from "../state/actions"; import { AppStateContext, dispatch } from "../state/AppStateContext"; import ConfirmModal from "./modals/ConfirmModal"; @@ -24,10 +19,8 @@ export default function Render() { onClose={() => dispatch(clearModal())} onSignIn={async (provider, rememberMe) => { const params: pxt.Map = {}; - if (deepLinks?.shareCode) - params["host"] = deepLinks.shareCode; - if (deepLinks?.joinCode) - params["join"] = deepLinks.joinCode; + if (deepLinks?.shareCode) params["host"] = deepLinks.shareCode; + if (deepLinks?.joinCode) params["join"] = deepLinks.joinCode; await signInAsync(provider.id, rememberMe, { params }); }} dialogMessages={state.modalOpts.dialogMessages} @@ -63,11 +56,7 @@ export default function Render() { }} onCancel={() => dispatch(clearModal())} > -
- {lf( - "Kick this player? They will be blocked from rejoining the game." - )} -
+
{lf("Kick this player? They will be blocked from rejoining the game.")}
); case "leave-game": @@ -81,11 +70,7 @@ export default function Render() { }} onCancel={() => dispatch(clearModal())} > -
- {lf( - "End the game? All players will be disconnected." - )} -
+
{lf("End the game? All players will be disconnected.")}
); } else { diff --git a/multiplayer/src/components/ArcadeSimulator.tsx b/multiplayer/src/components/ArcadeSimulator.tsx index 59b74917c2c4..266dab551d96 100644 --- a/multiplayer/src/components/ArcadeSimulator.tsx +++ b/multiplayer/src/components/ArcadeSimulator.tsx @@ -4,13 +4,7 @@ import { SimMultiplayer } from "../types"; import * as gameClient from "../services/gameClient"; // eslint-disable-next-line import/no-unassigned-import import "./ArcadeSimulator.css"; -import { - simDriver, - preloadSim, - simulateAsync, - buildSimJsInfo, - RunOptions, -} from "../services/simHost"; +import { simDriver, preloadSim, simulateAsync, buildSimJsInfo, RunOptions } from "../services/simHost"; import { state as currentState } from "../state"; let builtSimJsInfo: Promise | undefined; @@ -64,9 +58,7 @@ export default function Render() { useEffect(() => { const msgHandler = ( msg: MessageEvent< - | SimMultiplayer.Message - | pxsim.SimulatorStateMessage - | pxsim.SimulatorTopLevelCodeFinishedMessage + SimMultiplayer.Message | pxsim.SimulatorStateMessage | pxsim.SimulatorTopLevelCodeFinishedMessage > ) => { const { data } = msg; @@ -77,8 +69,7 @@ export default function Render() { // Once the simulator is ready, if this player is a guest, pass initial screen to simulator const { state: simState } = data; if (simState === "running" && clientRole === "guest") { - const { image, palette } = - gameClient.getCurrentScreen(); + const { image, palette } = gameClient.getCurrentScreen(); if (image) { simDriver()?.postMessage({ type: "multiplayer", diff --git a/multiplayer/src/components/CollabPage.tsx b/multiplayer/src/components/CollabPage.tsx index 8b809729fbd0..656628665657 100644 --- a/multiplayer/src/components/CollabPage.tsx +++ b/multiplayer/src/components/CollabPage.tsx @@ -16,8 +16,7 @@ export default function Render(props: CollabPageProps) { const { state } = useContext(AppStateContext); const { netMode, clientRole, collabInfo } = state; - const [canvasContainer, setCanvasContainer] = - useState(null); + const [canvasContainer, setCanvasContainer] = useState(null); const [selectedColorIndex, setSelectedColorIndex] = useState(0); const [selectedSizeIndex, setSelectedSizeIndex] = useState(0); const [selectedIconIndex, setSelectedIconIndex] = useState(0); @@ -41,24 +40,11 @@ export default function Render(props: CollabPageProps) { canvasMouseY += canvasContainer.scrollTop; if (canvasMouseX < canvasContainer.scrollLeft) return; if (canvasMouseY < canvasContainer.scrollTop) return; - if ( - canvasMouseX >= - canvasContainer.scrollLeft + canvasContainer.clientWidth - ) - return; - if ( - canvasMouseY >= - canvasContainer.scrollTop + canvasContainer.clientHeight - ) - return; + if (canvasMouseX >= canvasContainer.scrollLeft + canvasContainer.clientWidth) return; + if (canvasMouseY >= canvasContainer.scrollTop + canvasContainer.clientHeight) return; setMouseDown(true); setLastPosition({ x: canvasMouseX, y: canvasMouseY }); - getCollabCanvas().addPaintSprite( - canvasMouseX, - canvasMouseY, - selectedSizeIndex, - selectedColorIndex - ); + getCollabCanvas().addPaintSprite(canvasMouseX, canvasMouseY, selectedSizeIndex, selectedColorIndex); collabClient.setSessionValue( "s:" + nanoid(), JSON.stringify({ @@ -85,21 +71,13 @@ export default function Render(props: CollabPageProps) { // TODO: support canvas pan and zoom // Update local sprite position on canvas - getCollabCanvas().updatePlayerSpritePosition( - collabClient.getClientId()!, - canvasMouseX, - canvasMouseY - ); + getCollabCanvas().updatePlayerSpritePosition(collabClient.getClientId()!, canvasMouseX, canvasMouseY); const newPos = { x: canvasMouseX, y: canvasMouseY }; const newPosStr = JSON.stringify(newPos); // Send position to server collabClient.setPlayerValue("position", newPosStr); // Update local player position in state - CollabEpics.recvSetPlayerValue( - collabClient.getClientId(), - "position", - newPosStr - ); + CollabEpics.recvSetPlayerValue(collabClient.getClientId(), "position", newPosStr); if (!mouseDown) return; const brushSize = BRUSH_PROPS[selectedSizeIndex].size; @@ -112,12 +90,7 @@ export default function Render(props: CollabPageProps) { for (let i = 0; i < numSteps; i++) { const x = lastPosition.x + stepX * i; const y = lastPosition.y + stepY * i; - getCollabCanvas().addPaintSprite( - x, - y, - selectedSizeIndex, - selectedColorIndex - ); + getCollabCanvas().addPaintSprite(x, y, selectedSizeIndex, selectedColorIndex); collabClient.setSessionValue( "s:" + nanoid(), JSON.stringify({ @@ -139,13 +112,7 @@ export default function Render(props: CollabPageProps) { window.removeEventListener("mouseup", handleMouseUp); window.removeEventListener("mousemove", handleMouseMove); }; - }, [ - mouseDown, - canvasContainer, - lastPosition, - selectedColorIndex, - selectedSizeIndex, - ]); + }, [mouseDown, canvasContainer, lastPosition, selectedColorIndex, selectedSizeIndex]); useEffect(() => { if (canvasContainer && !canvasContainer.firstChild) { @@ -177,10 +144,7 @@ export default function Render(props: CollabPageProps) { const iconClicked = (index: number) => { setSelectedIconIndex(index); - getCollabCanvas().updatePlayerSpriteImage( - collabClient.getClientId()!, - index - ); + getCollabCanvas().updatePlayerSpriteImage(collabClient.getClientId()!, index); collabClient.setPlayerValue("imgId", JSON.stringify(index)); }; @@ -188,9 +152,7 @@ export default function Render(props: CollabPageProps) { setCanvasContainer(ref); }; - const SpriteRow: React.FC = ({ children }) => ( -
{children}
- ); + const SpriteRow: React.FC = ({ children }) =>
{children}
; const SpriteImg: React.FC<{ imgId: number }> = ({ imgId }) => (
brushColorClicked(bc, i)} @@ -242,14 +198,10 @@ export default function Render(props: CollabPageProps) { key={bs.sz} className="tw-rounded-full tw-cursor-pointer tw-border-slate-800 tw-border" style={{ - backgroundColor: - BRUSH_COLORS[selectedColorIndex], + backgroundColor: BRUSH_COLORS[selectedColorIndex], width: bs.px + "px", height: bs.px + "px", - outline: - i === selectedSizeIndex - ? "3px solid " + SELECTED_COLOR - : undefined, + outline: i === selectedSizeIndex ? "3px solid " + SELECTED_COLOR : undefined, outlineOffset: "1px", }} onClick={() => brushSizeClicked(bs, i)} diff --git a/multiplayer/src/components/CopyButton.tsx b/multiplayer/src/components/CopyButton.tsx index cb8bbc3c31a6..20812b570627 100644 --- a/multiplayer/src/components/CopyButton.tsx +++ b/multiplayer/src/components/CopyButton.tsx @@ -46,28 +46,15 @@ export default function Render(props: { }, [copySuccessful]); return ( - ); } diff --git a/multiplayer/src/components/EditGameButton.tsx b/multiplayer/src/components/EditGameButton.tsx index 81a297767dde..3ad34679e9b8 100644 --- a/multiplayer/src/components/EditGameButton.tsx +++ b/multiplayer/src/components/EditGameButton.tsx @@ -6,9 +6,7 @@ export default function Render() { const { state } = useContext(AppStateContext); const gameId = state.gameState?.gameId; - const remixUrl = gameId - ? `${pxt.webConfig.relprefix.replace(/-+$/, "")}#pub:${gameId}` - : undefined; + const remixUrl = gameId ? `${pxt.webConfig.relprefix.replace(/-+$/, "")}#pub:${gameId}` : undefined; function handleEditGameClick() { if (remixUrl) { @@ -21,11 +19,7 @@ export default function Render() {
- } + label={
{lf("Edit Game")}
} onClick={handleEditGameClick} className="tw-border-2 tw-border-slate-400 tw-border-solid tw-p-2 tw-bg-slate-100 hover:tw-bg-slate-200 active:tw-bg-slate-300 tw-ease-linear tw-duration-[50ms] tw-pr-1 sm:tw-pr-3" /> diff --git a/multiplayer/src/components/GamePage.tsx b/multiplayer/src/components/GamePage.tsx index fc6ec1ab0147..fea4b63e26f2 100644 --- a/multiplayer/src/components/GamePage.tsx +++ b/multiplayer/src/components/GamePage.tsx @@ -26,11 +26,7 @@ export default function Render(props: GamePageProps) { )}
diff --git a/multiplayer/src/components/GamePaused.tsx b/multiplayer/src/components/GamePaused.tsx index aabd13df6bb7..91c7ddd24687 100644 --- a/multiplayer/src/components/GamePaused.tsx +++ b/multiplayer/src/components/GamePaused.tsx @@ -15,33 +15,18 @@ export default function Render() { <> {gamePaused && clientRole === "host" && (
-
- {lf("Game Paused")} -
-
- {lf( - "The game is paused. Press the resume button to continue." - )} -
+
{lf("Game Paused")}
+
{lf("The game is paused. Press the resume button to continue.")}
-
)} {gamePaused && clientRole !== "host" && (
-
- {lf("Game Paused")} -
+
{lf("Game Paused")}
- {lf( - "The game is paused. Please wait for the host to resume the game." - )} + {lf("The game is paused. Please wait for the host to resume the game.")}
)} diff --git a/multiplayer/src/components/HeaderBar.tsx b/multiplayer/src/components/HeaderBar.tsx index 6760700111a8..a23b27f019e5 100644 --- a/multiplayer/src/components/HeaderBar.tsx +++ b/multiplayer/src/components/HeaderBar.tsx @@ -5,10 +5,7 @@ import { useContext } from "react"; import { Button } from "../../../react-common/components/controls/Button"; import { MenuBar } from "../../../react-common/components/controls/MenuBar"; -import { - MenuDropdown, - MenuItem, -} from "../../../react-common/components/controls/MenuDropdown"; +import { MenuDropdown, MenuItem } from "../../../react-common/components/controls/MenuDropdown"; import { signOutAsync } from "../epics"; import { showModal } from "../state/actions"; import { AppStateContext } from "../state/AppStateContext"; @@ -53,15 +50,9 @@ export default function Render() { pxt.tickEvent("mp.home"); // relprefix looks like "/beta---", need to chop off the hyphens and slash - let rel = pxt.webConfig?.relprefix.substr( - 0, - pxt.webConfig.relprefix.length - 3 - ); + let rel = pxt.webConfig?.relprefix.substr(0, pxt.webConfig.relprefix.length - 3); if (pxt.appTarget.appTheme.homeUrl && rel) { - if ( - pxt.appTarget.appTheme.homeUrl?.lastIndexOf("/") === - pxt.appTarget.appTheme.homeUrl?.length - 1 - ) { + if (pxt.appTarget.appTheme.homeUrl?.lastIndexOf("/") === pxt.appTarget.appTheme.homeUrl?.length - 1) { rel = rel.substr(1); } window.open(pxt.appTarget.appTheme.homeUrl + rel, "_self"); @@ -99,9 +90,7 @@ export default function Render() { /> ) : ( - - {targetTheme.organization} - + {targetTheme.organization} )}
); @@ -109,34 +98,20 @@ export default function Render() { const getTargetLogo = (targetTheme: pxt.AppTheme) => { return ( -
+
{targetTheme.useTextLogo ? ( [ - + {targetTheme.organizationText} , - - {targetTheme.organizationShortText || - targetTheme.organizationText} + + {targetTheme.organizationShortText || targetTheme.organizationText} , ] ) : targetTheme.logo || targetTheme.portraitLogo ? ( {lf("{0} ) : ( diff --git a/multiplayer/src/components/HostGameButton.tsx b/multiplayer/src/components/HostGameButton.tsx index a83d853ba763..cc2f026428a3 100644 --- a/multiplayer/src/components/HostGameButton.tsx +++ b/multiplayer/src/components/HostGameButton.tsx @@ -2,12 +2,7 @@ import { Button } from "react-common/components/controls/Button"; import { hostGameAsync } from "../epics/hostGameAsync"; import { cleanupShareCode } from "../util"; -export default function Render(props: { - shareId: string; - title: string; - subtitle: string; - image?: string; -}) { +export default function Render(props: { shareId: string; title: string; subtitle: string; image?: string }) { const { title, subtitle, image } = props; const shareCode = cleanupShareCode(props.shareId)!; @@ -28,11 +23,7 @@ export default function Render(props: { title={buttonTitle} label={ <> - {thumbnailAltText} + {thumbnailAltText}
{pxt.U.rlf(title)}
diff --git a/multiplayer/src/components/HostLobby.tsx b/multiplayer/src/components/HostLobby.tsx index 6a6a8e6f8304..0acf5044a0ae 100644 --- a/multiplayer/src/components/HostLobby.tsx +++ b/multiplayer/src/components/HostLobby.tsx @@ -62,9 +62,7 @@ export default function Render() {
-
- {lf("or scan with phone")} -
+
{lf("or scan with phone")}
diff --git a/multiplayer/src/components/Toast.tsx b/multiplayer/src/components/Toast.tsx index 7fa4050ceb67..635a1c9bc998 100644 --- a/multiplayer/src/components/Toast.tsx +++ b/multiplayer/src/components/Toast.tsx @@ -43,10 +43,7 @@ function Toast(props: ToastWithId) { useEffect(() => { let t1: NodeJS.Timeout, t2: NodeJS.Timeout; if (props.timeoutMs) { - t1 = setTimeout( - () => dispatch(dismissToast(props.id)), - SLIDER_DELAY_MS + props.timeoutMs - ); + t1 = setTimeout(() => dispatch(dismissToast(props.id)), SLIDER_DELAY_MS + props.timeoutMs); t2 = setTimeout(() => setSliderActive(true), SLIDER_DELAY_MS); } return () => { @@ -68,10 +65,9 @@ function Toast(props: ToastWithId) { className={ `tw-flex tw-flex-col tw-mr-0 md:tw-mr-4 tw-border-none tw-rounded tw-shadow-md tw-overflow-hidden tw-pointer-events-none ` + - [ - props.textColorClass || "text-black", - props.backgroundColorClass || backgroundColors[props.type], - ].join(" ") + [props.textColorClass || "text-black", props.backgroundColorClass || backgroundColors[props.type]].join( + " " + ) } >
@@ -87,13 +83,9 @@ function Toast(props: ToastWithId) { )}
{props.text && ( -
- {props.text} -
- )} - {props.detail && ( -
{props.detail}
+
{props.text}
)} + {props.detail &&
{props.detail}
} {props.jsx &&
{props.jsx}
}
{!props.hideDismissBtn && !props.showSpinner && ( @@ -103,9 +95,7 @@ function Toast(props: ToastWithId) { >
@@ -114,10 +104,7 @@ function Toast(props: ToastWithId) { {props.showSpinner && (
- +
)} diff --git a/multiplayer/src/components/icons/UserIcon.tsx b/multiplayer/src/components/icons/UserIcon.tsx index 3092745a9b3a..5e69f512b5b3 100644 --- a/multiplayer/src/components/icons/UserIcon.tsx +++ b/multiplayer/src/components/icons/UserIcon.tsx @@ -15,13 +15,7 @@ export default function Render(props: { slot: number; dataUri?: string }) { } return ( - + any; }> ) { - const { title, confirmLabel, cancelLabel, children, onConfirm, onCancel } = - props; + const { title, confirmLabel, cancelLabel, children, onConfirm, onCancel } = props; const actions: ModalAction[] = [ { diff --git a/multiplayer/src/epics/collab/recvSetPlayerValue.ts b/multiplayer/src/epics/collab/recvSetPlayerValue.ts index c73c13b22662..8b7da4d23dfd 100644 --- a/multiplayer/src/epics/collab/recvSetPlayerValue.ts +++ b/multiplayer/src/epics/collab/recvSetPlayerValue.ts @@ -2,11 +2,7 @@ import { getCollabCanvas } from "../../services/collabCanvas"; import { collabStateAndDispatch } from "../../state/collab"; import * as CollabActions from "../../state/collab/actions"; -export function recvSetPlayerValue( - playerId: string | undefined, - key: string, - value: string -) { +export function recvSetPlayerValue(playerId: string | undefined, key: string, value: string) { if (!playerId) return; const { dispatch } = collabStateAndDispatch(); dispatch(CollabActions.setPlayerValue(playerId, key, value)); @@ -14,9 +10,6 @@ export function recvSetPlayerValue( const pos = JSON.parse(value); getCollabCanvas().updatePlayerSpritePosition(playerId, pos.x, pos.y); } else if (key === "imgId") { - getCollabCanvas().updatePlayerSpriteImage( - playerId, - parseInt(JSON.parse(value)) - ); + getCollabCanvas().updatePlayerSpriteImage(playerId, parseInt(JSON.parse(value))); } } diff --git a/multiplayer/src/epics/collab/recvSetSessionState.ts b/multiplayer/src/epics/collab/recvSetSessionState.ts index 633d49847bfe..76811fa960fa 100644 --- a/multiplayer/src/epics/collab/recvSetSessionState.ts +++ b/multiplayer/src/epics/collab/recvSetSessionState.ts @@ -8,12 +8,7 @@ export function recvSetSessionState(sessKv: Map) { sessKv.forEach((value, key) => { if (key.startsWith("s:")) { const sprite = JSON.parse(value); - getCollabCanvas().addPaintSprite( - sprite.x, - sprite.y, - sprite.s, - sprite.c - ); + getCollabCanvas().addPaintSprite(sprite.x, sprite.y, sprite.s, sprite.c); } }); } diff --git a/multiplayer/src/epics/collab/recvSetSessionValue.ts b/multiplayer/src/epics/collab/recvSetSessionValue.ts index 98e6e8dda82a..da3b349260a1 100644 --- a/multiplayer/src/epics/collab/recvSetSessionValue.ts +++ b/multiplayer/src/epics/collab/recvSetSessionValue.ts @@ -7,11 +7,6 @@ export function recvSetSessionValue(key: string, value: string) { dispatch(CollabActions.setSessionValue(key, value)); if (key.startsWith("s:")) { const sprite = JSON.parse(value); - getCollabCanvas().addPaintSprite( - sprite.x, - sprite.y, - sprite.s, - sprite.c - ); + getCollabCanvas().addPaintSprite(sprite.x, sprite.y, sprite.s, sprite.c); } } diff --git a/multiplayer/src/epics/hostCollabAsync.ts b/multiplayer/src/epics/hostCollabAsync.ts index 1c89bd86f096..96c88ec8bc9c 100644 --- a/multiplayer/src/epics/hostCollabAsync.ts +++ b/multiplayer/src/epics/hostCollabAsync.ts @@ -1,12 +1,6 @@ import * as collabClient from "../services/collabClient"; import { dispatch } from "../state"; -import { - dismissToast, - setNetMode, - setCollabInfo, - showToast, - setClientRole, -} from "../state/actions"; +import { dismissToast, setNetMode, setCollabInfo, showToast, setClientRole } from "../state/actions"; export async function hostCollabAsync() { const connectingToast = showToast({ diff --git a/multiplayer/src/epics/hostGameAsync.ts b/multiplayer/src/epics/hostGameAsync.ts index e91b7c1beec7..e11390a98138 100644 --- a/multiplayer/src/epics/hostGameAsync.ts +++ b/multiplayer/src/epics/hostGameAsync.ts @@ -1,12 +1,6 @@ import * as gameClient from "../services/gameClient"; import { dispatch } from "../state"; -import { - dismissToast, - setNetMode, - setGameInfo, - showToast, - setClientRole, -} from "../state/actions"; +import { dismissToast, setNetMode, setGameInfo, showToast, setClientRole } from "../state/actions"; import { cleanupShareCode } from "../util"; export async function hostGameAsync(shareCode: string | undefined) { diff --git a/multiplayer/src/epics/joinCollabAsync.ts b/multiplayer/src/epics/joinCollabAsync.ts index b786fedb4ee7..fd03d8914a78 100644 --- a/multiplayer/src/epics/joinCollabAsync.ts +++ b/multiplayer/src/epics/joinCollabAsync.ts @@ -1,12 +1,6 @@ import * as collabClient from "../services/collabClient"; import { dispatch } from "../state"; -import { - dismissToast, - setNetMode, - setCollabInfo, - showToast, - setClientRole, -} from "../state/actions"; +import { dismissToast, setNetMode, setCollabInfo, showToast, setClientRole } from "../state/actions"; import { HTTP_SESSION_FULL, HTTP_SESSION_NOT_FOUND } from "../types"; import { cleanupJoinCode } from "../util"; import { notifyDisconnected } from "."; diff --git a/multiplayer/src/epics/joinGameAsync.ts b/multiplayer/src/epics/joinGameAsync.ts index 06bd26142f6d..36c7494f4ec1 100644 --- a/multiplayer/src/epics/joinGameAsync.ts +++ b/multiplayer/src/epics/joinGameAsync.ts @@ -1,12 +1,6 @@ import * as gameClient from "../services/gameClient"; import { dispatch } from "../state"; -import { - dismissToast, - setNetMode, - setGameInfo, - showToast, - setClientRole, -} from "../state/actions"; +import { dismissToast, setNetMode, setGameInfo, showToast, setClientRole } from "../state/actions"; import { HTTP_SESSION_FULL, HTTP_SESSION_NOT_FOUND } from "../types"; import { cleanupJoinCode } from "../util"; import { notifyDisconnected } from "."; diff --git a/multiplayer/src/epics/sendAbuseReportAsync.ts b/multiplayer/src/epics/sendAbuseReportAsync.ts index ccab2c0dcdc2..0750de91a447 100644 --- a/multiplayer/src/epics/sendAbuseReportAsync.ts +++ b/multiplayer/src/epics/sendAbuseReportAsync.ts @@ -4,20 +4,15 @@ import { showToast } from "../state/actions"; export async function sendAbuseReportAsync(shareCode: string, text: string) { try { pxt.tickEvent("mp.reportabuse"); - const res = await fetch( - `https://makecode.com/api/${shareCode}/abusereports`, - { - method: "POST", - body: JSON.stringify({ text }), - } - ); + const res = await fetch(`https://makecode.com/api/${shareCode}/abusereports`, { + method: "POST", + body: JSON.stringify({ text }), + }); if (res.status === 200) { dispatch( showToast({ type: "success", - text: lf( - "Thank you for helping keep Microsoft MakeCode a friendly place!" - ), + text: lf("Thank you for helping keep Microsoft MakeCode a friendly place!"), icon: "✅", timeoutMs: 5000, }) @@ -29,9 +24,7 @@ export async function sendAbuseReportAsync(shareCode: string, text: string) { dispatch( showToast({ type: "error", - text: lf( - "Sorry, we couldn't send your report. Please try again later." - ), + text: lf("Sorry, we couldn't send your report. Please try again later."), timeoutMs: 5000, }) ); diff --git a/multiplayer/src/epics/sendReactionAsync.ts b/multiplayer/src/epics/sendReactionAsync.ts index db8abbc9c4da..0e1041eba97b 100644 --- a/multiplayer/src/epics/sendReactionAsync.ts +++ b/multiplayer/src/epics/sendReactionAsync.ts @@ -5,10 +5,7 @@ const TIMEOUT_PER_REACTION = 250; const lastSentTime: number[] = []; export async function sendReactionAsync(index: number) { try { - if ( - !lastSentTime[index] || - lastSentTime[index] + TIMEOUT_PER_REACTION < Date.now() - ) { + if (!lastSentTime[index] || lastSentTime[index] + TIMEOUT_PER_REACTION < Date.now()) { lastSentTime[index] = Date.now(); const reaction = Reactions[index]; pxt.tickEvent("mp.sendreaction", { reaction: reaction.name }); diff --git a/multiplayer/src/epics/setCustomIconAsync.ts b/multiplayer/src/epics/setCustomIconAsync.ts index f845ce3e2d00..811b6bfd37f1 100644 --- a/multiplayer/src/epics/setCustomIconAsync.ts +++ b/multiplayer/src/epics/setCustomIconAsync.ts @@ -1,21 +1,11 @@ import { IconType } from "../types"; import { dispatch } from "../state"; -import { - setPresenceIconOverride, - setReactionIconOverride, -} from "../state/actions"; +import { setPresenceIconOverride, setReactionIconOverride } from "../state/actions"; -export async function setCustomIconAsync( - iconType: IconType, - slot: number, - pngDataUri?: string -) { +export async function setCustomIconAsync(iconType: IconType, slot: number, pngDataUri?: string) { try { - const actionHandler = - iconType === IconType.Player - ? setPresenceIconOverride - : setReactionIconOverride; + const actionHandler = iconType === IconType.Player ? setPresenceIconOverride : setReactionIconOverride; dispatch(actionHandler(slot, pngDataUri)); } catch (e) { } finally { diff --git a/multiplayer/src/epics/setGameMetadataAsync.ts b/multiplayer/src/epics/setGameMetadataAsync.ts index c007a9e82d7f..7b9ef0986da0 100644 --- a/multiplayer/src/epics/setGameMetadataAsync.ts +++ b/multiplayer/src/epics/setGameMetadataAsync.ts @@ -1,14 +1,7 @@ import { dispatch } from "../state"; -import { - setGameId, - setGameMetadata, - setNetMode, - showToast, -} from "../state/actions"; +import { setGameId, setGameMetadata, setNetMode, showToast } from "../state/actions"; -export async function setGameMetadataAsync( - shareCode: string -): Promise { +export async function setGameMetadataAsync(shareCode: string): Promise { try { const metaUri = `https://makecode.com/api/${shareCode}`; //Fetch the game metadata diff --git a/multiplayer/src/epics/setGameModeAsync.ts b/multiplayer/src/epics/setGameModeAsync.ts index 69613b38341f..a590c372495d 100644 --- a/multiplayer/src/epics/setGameModeAsync.ts +++ b/multiplayer/src/epics/setGameModeAsync.ts @@ -1,18 +1,9 @@ import { GameMode } from "../types"; import { dispatch, state } from "../state"; -import { - clearModal, - setGameMode, - setPlayerSlot, - showToast, -} from "../state/actions"; +import { clearModal, setGameMode, setPlayerSlot, showToast } from "../state/actions"; import { pauseGameAsync } from "."; -export async function setGameModeAsync( - gameMode: GameMode, - gamePaused: boolean, - slot?: number -) { +export async function setGameModeAsync(gameMode: GameMode, gamePaused: boolean, slot?: number) { const { clientRole } = state; try { diff --git a/multiplayer/src/epics/setUserProfileAsync.ts b/multiplayer/src/epics/setUserProfileAsync.ts index 046a82b9945f..0e85274feec4 100644 --- a/multiplayer/src/epics/setUserProfileAsync.ts +++ b/multiplayer/src/epics/setUserProfileAsync.ts @@ -1,9 +1,7 @@ import { dispatch } from "../state"; import { clearUserProfile, setUserProfile } from "../state/actions"; -export async function setUserProfileAsync( - profile: pxt.auth.UserProfile | undefined -) { +export async function setUserProfileAsync(profile: pxt.auth.UserProfile | undefined) { try { if (profile) { dispatch(setUserProfile(profile)); diff --git a/multiplayer/src/hooks/useClickedOutside.ts b/multiplayer/src/hooks/useClickedOutside.ts index 65e76727d021..cd4d4f3a4581 100644 --- a/multiplayer/src/hooks/useClickedOutside.ts +++ b/multiplayer/src/hooks/useClickedOutside.ts @@ -1,9 +1,6 @@ import { useEffect, RefObject } from "react"; -export function useClickedOutside( - refs: RefObject[], - cb: (ev?: Event) => any -) { +export function useClickedOutside(refs: RefObject[], cb: (ev?: Event) => any) { useEffect(() => { const handleMouseDown = (ev: Event) => { for (const ref of refs) { diff --git a/multiplayer/src/hooks/useVisibilityChange.ts b/multiplayer/src/hooks/useVisibilityChange.ts index b2d1e8e29930..a7658faafdf1 100644 --- a/multiplayer/src/hooks/useVisibilityChange.ts +++ b/multiplayer/src/hooks/useVisibilityChange.ts @@ -7,10 +7,7 @@ export function useVisibilityChange(cb: (visible: boolean) => any) { }; document.addEventListener("visibilitychange", onVisibilityChange); return () => { - document.removeEventListener( - "visibilitychange", - onVisibilityChange - ); + document.removeEventListener("visibilitychange", onVisibilityChange); }; }, []); } diff --git a/multiplayer/src/services/authClient.ts b/multiplayer/src/services/authClient.ts index 92d1ec0b6a31..86b929c11e03 100644 --- a/multiplayer/src/services/authClient.ts +++ b/multiplayer/src/services/authClient.ts @@ -17,9 +17,7 @@ class AuthClient extends pxt.auth.AuthClient { } await setUserProfileAsync(state.profile); } - protected onUserPreferencesChanged( - diff: ts.pxtc.jsonPatch.PatchOperation[] - ): Promise { + protected onUserPreferencesChanged(diff: ts.pxtc.jsonPatch.PatchOperation[]): Promise { return Promise.resolve(); } protected onStateCleared(): Promise { @@ -53,9 +51,7 @@ export async function clientAsync(): Promise { return authClientPromise; } -export async function authCheckAsync(): Promise< - pxt.auth.UserProfile | undefined -> { +export async function authCheckAsync(): Promise { const cli = await clientAsync(); const query = pxt.Util.parseQueryString(window.location.href); if (query["authcallback"]) { diff --git a/multiplayer/src/services/collabCanvas.ts b/multiplayer/src/services/collabCanvas.ts index 5c65977dd894..6fbdf8bdf439 100644 --- a/multiplayer/src/services/collabCanvas.ts +++ b/multiplayer/src/services/collabCanvas.ts @@ -164,12 +164,7 @@ class CollabCanvas { this.root.addChild(mesh as any); } - public addPlayerSprite( - playerId: string, - x: number, - y: number, - imgId: number - ) { + public addPlayerSprite(playerId: string, x: number, y: number, imgId: number) { if (!playerId) return; if (this.playerSprites.has(playerId)) return; const sprite = Pixi.Sprite.from(PLAYER_SPRITE_DATAURLS[imgId]); @@ -201,18 +196,11 @@ class CollabCanvas { const player = this.playerSprites.get(playerId); if (player && player.imgId !== imgId) { player.imgId = imgId; - player.sprite.texture = Pixi.Texture.from( - PLAYER_SPRITE_DATAURLS[imgId] - ); + player.sprite.texture = Pixi.Texture.from(PLAYER_SPRITE_DATAURLS[imgId]); } } - public addPaintSprite( - x: number, - y: number, - brushIndex: number, - colorIndex: number - ) { + public addPaintSprite(x: number, y: number, brushIndex: number, colorIndex: number) { const sprite = new Pixi.Sprite(); const brush = BRUSH_PROPS[brushIndex]; if (!brush) return; diff --git a/multiplayer/src/services/collabClient.ts b/multiplayer/src/services/collabClient.ts index 6638d84db17a..ceb48318871e 100644 --- a/multiplayer/src/services/collabClient.ts +++ b/multiplayer/src/services/collabClient.ts @@ -12,12 +12,7 @@ import { HTTP_INTERNAL_SERVER_ERROR, HTTP_IM_A_TEAPOT, } from "../types"; -import { - notifyDisconnected, - setPresenceAsync, - playerJoinedAsync, - playerLeftAsync, -} from "../epics"; +import { notifyDisconnected, setPresenceAsync, playerJoinedAsync, playerLeftAsync } from "../epics"; import * as CollabEpics from "../epics/collab"; import { jsonReplacer, jsonReviver } from "../util"; @@ -107,20 +102,14 @@ class CollabClient { } }; - private recvMessageWithJoinTimeout = async ( - payload: string | Buffer, - resolve: () => void - ) => { + private recvMessageWithJoinTimeout = async (payload: string | Buffer, resolve: () => void) => { try { if (typeof payload === "string") { const msg = JSON.parse(payload) as Protocol.Message; if (msg.type === "joined") { // We've joined the collab. Replace this handler with a direct call to recvMessageAsync if (this.sock) { - this.sock.removeListener( - "message", - this.receivedJoinMessageInTimeHandler - ); + this.sock.removeListener("message", this.receivedJoinMessageInTimeHandler); this.receivedJoinMessageInTimeHandler = undefined; } resolve(); @@ -197,8 +186,7 @@ class CollabClient { const collabInfo = (await hostRes.json()) as CollabInfo; - if (!collabInfo?.joinTicket) - throw new Error("Collab server did not return a join ticket"); + if (!collabInfo?.joinTicket) throw new Error("Collab server did not return a join ticket"); CollabEpics.initState(new Map()); @@ -223,15 +211,12 @@ class CollabClient { const authToken = await authClient.authTokenAsync(); - const joinRes = await fetch( - `${COLLAB_HOST}/api/collab/join/${joinCode}`, - { - credentials: "include", - headers: { - Authorization: "mkcd " + authToken, - }, - } - ); + const joinRes = await fetch(`${COLLAB_HOST}/api/collab/join/${joinCode}`, { + credentials: "include", + headers: { + Authorization: "mkcd " + authToken, + }, + }); if (joinRes.status !== HTTP_OK) { return { @@ -242,8 +227,7 @@ class CollabClient { const collabInfo = (await joinRes.json()) as CollabInfo; - if (!collabInfo?.joinTicket) - throw new Error("Collab server did not return a join ticket"); + if (!collabInfo?.joinTicket) throw new Error("Collab server did not return a join ticket"); CollabEpics.initState(new Map()); @@ -272,9 +256,7 @@ class CollabClient { } private async recvJoinedMessageAsync(msg: Protocol.JoinedMessage) { - pxt.debug( - `Server said we're joined as "${msg.role}" in slot "${msg.slot}"` - ); + pxt.debug(`Server said we're joined as "${msg.role}" in slot "${msg.slot}"`); const { role, clientId, sessKv } = msg; this.clientRole = role; @@ -290,9 +272,7 @@ class CollabClient { CollabEpics.recvUpdatePresence(msg.presence); } - private async recvPlayerJoinedMessageAsync( - msg: Protocol.PlayerJoinedMessage - ) { + private async recvPlayerJoinedMessageAsync(msg: Protocol.PlayerJoinedMessage) { pxt.debug("Server sent player joined"); if (this.clientRole === "host") { //await this.sendCurrentScreenAsync(); // Workaround for server sometimes not sending the current screen to new players. Needs debugging. @@ -307,30 +287,22 @@ class CollabClient { CollabEpics.recvPlayerLeft(msg.clientId); } - private async recvSetPlayerValueMessageAsync( - msg: Protocol.SetPlayerValueMessage - ) { + private async recvSetPlayerValueMessageAsync(msg: Protocol.SetPlayerValueMessage) { //pxt.debug(`Recv set player value: ${msg.key} = ${msg.value}`); CollabEpics.recvSetPlayerValue(msg.clientId!, msg.key, msg.value); } - private async recvDelPlayerValueMessageAsync( - msg: Protocol.DelPlayerValueMessage - ) { + private async recvDelPlayerValueMessageAsync(msg: Protocol.DelPlayerValueMessage) { //pxt.debug(`Recv del player value: ${msg.key}`); CollabEpics.recvDelPlayerValue(msg.clientId!, msg.key); } - private async recvSetSessionValueMessageAsync( - msg: Protocol.SetSessionValueMessage - ) { + private async recvSetSessionValueMessageAsync(msg: Protocol.SetSessionValueMessage) { //pxt.debug(`Recv set session value: ${msg.key} = ${msg.value}`); CollabEpics.recvSetSessionValue(msg.key, msg.value); } - private async recvDelSessionValueMessageAsync( - msg: Protocol.DelSessionValueMessage - ) { + private async recvDelSessionValueMessageAsync(msg: Protocol.DelSessionValueMessage) { //pxt.debug(`Recv del session value: ${msg.key}`); CollabEpics.recvDelSessionValue(msg.key); } @@ -388,9 +360,7 @@ export async function hostCollabAsync(): Promise { return collabInfo; } -export async function joinCollabAsync( - joinCode: string -): Promise { +export async function joinCollabAsync(joinCode: string): Promise { destroyCollabClient(); const collabClient = ensureCollabClient(); const collabInfo = await collabClient.joinCollabAsync(joinCode); diff --git a/multiplayer/src/services/gameClient.ts b/multiplayer/src/services/gameClient.ts index 219f735d1d2f..b34d43ad9cb4 100644 --- a/multiplayer/src/services/gameClient.ts +++ b/multiplayer/src/services/gameClient.ts @@ -132,20 +132,14 @@ class GameClient { } }; - private recvMessageWithJoinTimeout = async ( - payload: string | Buffer, - resolve: () => void - ) => { + private recvMessageWithJoinTimeout = async (payload: string | Buffer, resolve: () => void) => { try { if (typeof payload === "string") { const msg = JSON.parse(payload) as Protocol.Message; if (msg.type === "joined") { // We've joined the game. Replace this handler with a direct call to recvMessageAsync if (this.sock) { - this.sock.removeListener( - "message", - this.receivedJoinMessageInTimeHandler - ); + this.sock.removeListener("message", this.receivedJoinMessageInTimeHandler); this.receivedJoinMessageInTimeHandler = undefined; } resolve(); @@ -208,15 +202,12 @@ class GameClient { const authToken = await authClient.authTokenAsync(); - const hostRes = await fetch( - `${GAME_HOST}/api/game/host/${shareCode}`, - { - credentials: "include", - headers: { - Authorization: "mkcd " + authToken, - }, - } - ); + const hostRes = await fetch(`${GAME_HOST}/api/game/host/${shareCode}`, { + credentials: "include", + headers: { + Authorization: "mkcd " + authToken, + }, + }); if (hostRes.status !== HTTP_OK) { return { @@ -227,8 +218,7 @@ class GameClient { const gameInfo = (await hostRes.json()) as GameInfo; - if (!gameInfo?.joinTicket) - throw new Error("Game server did not return a join ticket"); + if (!gameInfo?.joinTicket) throw new Error("Game server did not return a join ticket"); await this.connectAsync(gameInfo.joinTicket!); @@ -251,15 +241,12 @@ class GameClient { const authToken = await authClient.authTokenAsync(); - const joinRes = await fetch( - `${GAME_HOST}/api/game/join/${joinCode}`, - { - credentials: "include", - headers: { - Authorization: "mkcd " + authToken, - }, - } - ); + const joinRes = await fetch(`${GAME_HOST}/api/game/join/${joinCode}`, { + credentials: "include", + headers: { + Authorization: "mkcd " + authToken, + }, + }); if (joinRes.status !== HTTP_OK) { return { @@ -270,8 +257,7 @@ class GameClient { const gameInfo = (await joinRes.json()) as GameInfo; - if (!gameInfo?.joinTicket) - throw new Error("Game server did not return a join ticket"); + if (!gameInfo?.joinTicket) throw new Error("Game server did not return a join ticket"); await this.connectAsync(gameInfo.joinTicket!); @@ -311,9 +297,7 @@ class GameClient { } private async recvJoinedMessageAsync(msg: Protocol.JoinedMessage) { - pxt.debug( - `Server said we're joined as "${msg.role}" in slot "${msg.slot}"` - ); + pxt.debug(`Server said we're joined as "${msg.role}" in slot "${msg.slot}"`); const { gameMode, gamePaused, shareCode, role } = msg; this.clientRole = role; @@ -339,9 +323,7 @@ class GameClient { await setReactionAsync(msg.clientId!, msg.index); } - private async recvPlayerJoinedMessageAsync( - msg: Protocol.PlayerJoinedMessage - ) { + private async recvPlayerJoinedMessageAsync(msg: Protocol.PlayerJoinedMessage) { pxt.debug("Server sent player joined"); if (this.clientRole === "host") { await this.sendCurrentScreenAsync(); // Workaround for server sometimes not sending the current screen to new players. Needs debugging. @@ -372,8 +354,7 @@ class GameClient { } private async recvCompressedScreenMessageAsync(reader: SmartBuffer) { - const { zippedData, isDelta } = - Protocol.Binary.unpackCompressedScreenMessage(reader); + const { zippedData, isDelta } = Protocol.Binary.unpackCompressedScreenMessage(reader); const screen = await gunzipAsync(zippedData); if (!isDelta) { @@ -400,17 +381,10 @@ class GameClient { } private async recvInputMessageAsync(reader: SmartBuffer) { - const { button, state, slot } = - Protocol.Binary.unpackInputMessage(reader); + const { button, state, slot } = Protocol.Binary.unpackInputMessage(reader); const stringifiedState = buttonStateToString(state); - if ( - button <= SimKey.None || - button >= SimKey.Menu || - slot < 2 || - !stringifiedState - ) - return; + if (button <= SimKey.None || button >= SimKey.Menu || slot < 2 || !stringifiedState) return; this.postToSimFrame({ type: "multiplayer", @@ -422,8 +396,7 @@ class GameClient { } private async recvAudioMessageAsync(reader: SmartBuffer) { - const { instruction, soundbuf } = - Protocol.Binary.unpackAudioMessage(reader); + const { instruction, soundbuf } = Protocol.Binary.unpackAudioMessage(reader); this.postToSimFrame({ type: "multiplayer", content: "Audio", @@ -433,15 +406,10 @@ class GameClient { } private async recvIconMessageAsync(reader: SmartBuffer) { - const { iconType, iconSlot, iconBuffer } = - Protocol.Binary.unpackIconMessage(reader); + const { iconType, iconSlot, iconBuffer } = Protocol.Binary.unpackIconMessage(reader); if (iconType === IconType.Player && iconSlot >= 1 && iconSlot <= 4) { - } else if ( - iconType === IconType.Reaction && - iconSlot >= 1 && - iconSlot <= 6 - ) { + } else if (iconType === IconType.Reaction && iconSlot >= 1 && iconSlot <= 6) { } else { // unhandled icon type or invalid slot, ignore. return; @@ -451,10 +419,7 @@ class GameClient { const unzipped = await gunzipAsync(iconBuffer); const iconPalette = unzipped.slice(0, PALETTE_BUFFER_SIZE); const iconImage = unzipped.slice(PALETTE_BUFFER_SIZE); - const iconPngDataUri = pxt.convertUint8BufferToPngUri( - iconPalette, - iconImage - ); + const iconPngDataUri = pxt.convertUint8BufferToPngUri(iconPalette, iconImage); setCustomIconAsync(iconType, iconSlot, iconPngDataUri); } else { @@ -467,27 +432,15 @@ class GameClient { simDriver()?.postMessage(msg); } - public async sendInputAsync( - button: number, - state: "Pressed" | "Released" | "Held" - ) { + public async sendInputAsync(button: number, state: "Pressed" | "Released" | "Held") { if (this.paused) return; - const buffer = Protocol.Binary.packInputMessage( - button, - stringToButtonState(state)! - ); + const buffer = Protocol.Binary.packInputMessage(button, stringToButtonState(state)!); this.sendMessage(buffer); } - public async sendAudioAsync( - instruction: "playinstructions" | "muteallchannels", - soundbuf?: Uint8Array - ) { + public async sendAudioAsync(instruction: "playinstructions" | "muteallchannels", soundbuf?: Uint8Array) { if (this.paused) return; - const buffer = Protocol.Binary.packAudioMessage( - stringToAudioInstruction(instruction)!, - Buffer.from(soundbuf!) - ); + const buffer = Protocol.Binary.packAudioMessage(stringToAudioInstruction(instruction)!, Buffer.from(soundbuf!)); this.sendMessage(buffer); } @@ -504,19 +457,12 @@ class GameClient { zippedIconBuffer = await gzipAsync(iconDataBuf); } - const msgBuffer = Protocol.Binary.packIconMessage( - type, - slot, - zippedIconBuffer - ); + const msgBuffer = Protocol.Binary.packIconMessage(type, slot, zippedIconBuffer); this.sendMessage(msgBuffer); } - public async sendScreenUpdateAsync( - image: Uint8Array, - palette: Uint8Array | undefined - ) { + public async sendScreenUpdateAsync(image: Uint8Array, palette: Uint8Array | undefined) { const DELTAS_ENABLED = true; const buffers: Buffer[] = []; @@ -597,12 +543,7 @@ class GameClient { const image = this.screen.slice(0, SCREEN_BUFFER_SIZE); const palette = this.screen.length >= SCREEN_BUFFER_SIZE + PALETTE_BUFFER_SIZE - ? this.screen - .slice( - SCREEN_BUFFER_SIZE, - SCREEN_BUFFER_SIZE + PALETTE_BUFFER_SIZE - ) - .map(v => v) + ? this.screen.slice(SCREEN_BUFFER_SIZE, SCREEN_BUFFER_SIZE + PALETTE_BUFFER_SIZE).map(v => v) : undefined; return { @@ -641,16 +582,11 @@ export async function startPostingRandomKeys() { const currentState = new Array(SimKey.B).fill(0); return setInterval(() => { const key = Math.floor(Math.random() * SimKey.B); - gameClient?.sendInputAsync( - key + 1, - states[currentState[key]++ % 2] as any - ); + gameClient?.sendInputAsync(key + 1, states[currentState[key]++ % 2] as any); }, 300); } -export async function hostGameAsync( - shareCode: string -): Promise { +export async function hostGameAsync(shareCode: string): Promise { destroyGameClient(); gameClient = new GameClient(); const gameInfo = await gameClient.hostGameAsync(shareCode); @@ -677,33 +613,19 @@ export async function sendReactionAsync(index: number) { await gameClient?.sendReactionAsync(index); } -export async function sendInputAsync( - button: number, - state: "Pressed" | "Released" | "Held" -) { +export async function sendInputAsync(button: number, state: "Pressed" | "Released" | "Held") { await gameClient?.sendInputAsync(button, state); } -export async function sendAudioAsync( - instruction: "playinstructions" | "muteallchannels", - soundbuf?: Uint8Array -) { +export async function sendAudioAsync(instruction: "playinstructions" | "muteallchannels", soundbuf?: Uint8Array) { await gameClient?.sendAudioAsync(instruction, soundbuf); } -export async function sendIconAsync( - iconType: IconType, - slot: number, - palette: Uint8Array, - icon: Uint8Array -) { +export async function sendIconAsync(iconType: IconType, slot: number, palette: Uint8Array, icon: Uint8Array) { await gameClient?.sendIconAsync(iconType, slot, palette, icon); } -export async function sendScreenUpdateAsync( - img: Uint8Array, - palette: Uint8Array -) { +export async function sendScreenUpdateAsync(img: Uint8Array, palette: Uint8Array) { await gameClient?.sendScreenUpdateAsync(img, palette); } @@ -867,10 +789,7 @@ namespace Protocol { } // Input - export function packInputMessage( - button: number, - state: ButtonState - ): Buffer { + export function packInputMessage(button: number, state: ButtonState): Buffer { const writer = new SmartBuffer(); writer.writeUInt16LE(MessageType.Input); writer.writeUInt16LE(button); @@ -894,10 +813,7 @@ namespace Protocol { } // CompressedScreen - export function packCompressedScreenMessage( - zippedData: Buffer, - isDelta: boolean - ): Buffer { + export function packCompressedScreenMessage(zippedData: Buffer, isDelta: boolean): Buffer { const writer = new SmartBuffer(); writer.writeUInt16LE(MessageType.CompressedScreen); writer.writeUInt8(isDelta ? 1 : 0); @@ -918,10 +834,7 @@ namespace Protocol { } // Audio - export function packAudioMessage( - instruction: number, - soundbuf?: Buffer - ): Buffer { + export function packAudioMessage(instruction: number, soundbuf?: Buffer): Buffer { const writer = new SmartBuffer(); writer.writeUInt16LE(MessageType.Audio); writer.writeUInt8(instruction); @@ -936,9 +849,7 @@ namespace Protocol { } { // `type` field has already been read const instruction = reader.readUInt8(); - const soundbuf = reader.remaining() - ? reader.readBuffer() - : undefined; + const soundbuf = reader.remaining() ? reader.readBuffer() : undefined; return { instruction, soundbuf, @@ -946,11 +857,7 @@ namespace Protocol { } // Icon - export function packIconMessage( - iconType: IconType, - iconSlot: number, - iconBuffer?: Buffer - ): Buffer { + export function packIconMessage(iconType: IconType, iconSlot: number, iconBuffer?: Buffer): Buffer { const writer = new SmartBuffer(); writer.writeUInt16LE(MessageType.Icon); writer.writeUInt8(iconType); diff --git a/multiplayer/src/services/simHost.ts b/multiplayer/src/services/simHost.ts index be52c0706446..40c9f8dc6a1b 100644 --- a/multiplayer/src/services/simHost.ts +++ b/multiplayer/src/services/simHost.ts @@ -23,13 +23,9 @@ if (!pxt.react.getTilemapProject) { } async function loadPackageAsync(runOpts: RunOptions) { - const verspec = - runOpts.mpRole === "server" ? `pub:${runOpts.id}` : "empty:clientprj"; + const verspec = runOpts.mpRole === "server" ? `pub:${runOpts.id}` : "empty:clientprj"; const previousMainPackage = mainPkg(); - if ( - previousMainPackage?._verspec !== verspec || - verspec.startsWith("pub:S") - ) { + if (previousMainPackage?._verspec !== verspec || verspec.startsWith("pub:S")) { const mp = mainPkg(true /** force refresh */); // we want this to be cached only within the scope of a single call to loadpackageasync, // as the file can be requested multiple times while loading. @@ -130,11 +126,7 @@ class PkgHost implements pxt.Host { return null as any as string; } - patchDependencies( - cfg: pxt.PackageConfig, - name: string, - repoId: string - ): boolean { + patchDependencies(cfg: pxt.PackageConfig, name: string, repoId: string): boolean { if (!repoId) return false; // check that the same package hasn't been added yet const repo = pxt.github.parseRepoId(repoId); @@ -161,17 +153,13 @@ class PkgHost implements pxt.Host { let proto = pkg.verProtocol(); let cached: pxt.Map | undefined = undefined; // cache resolve github packages - if (proto == "github" || proto == "pub") - cached = this.githubPackageCache[pkg._verspec]; + if (proto == "github" || proto == "pub") cached = this.githubPackageCache[pkg._verspec]; let epkg = getEditorPkg(pkg); - return ( - cached ? Promise.resolve(cached) : pkg.commonDownloadAsync() - ).then(resp => { + return (cached ? Promise.resolve(cached) : pkg.commonDownloadAsync()).then(resp => { if (resp) { if ((proto == "github" || proto == "pub") && !cached) - this.githubPackageCache[pkg._verspec] = - pxt.Util.clone(resp); + this.githubPackageCache[pkg._verspec] = pxt.Util.clone(resp); epkg.setFiles(resp); return Promise.resolve(); } @@ -181,9 +169,7 @@ class PkgHost implements pxt.Host { } if (dependencies && dependencies.length) { const files = getEditorPkg(pkg).files; - const cfg = JSON.parse( - files[pxt.CONFIG_NAME] - ) as pxt.PackageConfig; + const cfg = JSON.parse(files[pxt.CONFIG_NAME]) as pxt.PackageConfig; dependencies.forEach((d: string) => { this.addPackageToConfig(cfg, d); }); @@ -192,9 +178,7 @@ class PkgHost implements pxt.Host { return Promise.resolve(); } else if (proto == "docs") { let files = emptyPrjFiles(); - let cfg = JSON.parse( - files[pxt.CONFIG_NAME] - ) as pxt.PackageConfig; + let cfg = JSON.parse(files[pxt.CONFIG_NAME]) as pxt.PackageConfig; // load all dependencies pkg.verArgument() .split(",") @@ -213,9 +197,7 @@ class PkgHost implements pxt.Host { pxt.log(`skipping invalid pkg ${pkg.id}`); return Promise.resolve(); } else { - return Promise.reject( - `Cannot download ${pkg.version()}; unknown protocol` - ); + return Promise.reject(`Cannot download ${pkg.version()}; unknown protocol`); } }); } @@ -260,10 +242,7 @@ function setStoredState(runOpts: RunOptions, key: string, value: any) { window.localStorage.setItem(id, JSON.stringify(storedState)); } catch (e) {} } -function workerOpAsync( - op: T, - arg: pxtc.service.OpArg -): Promise { +function workerOpAsync(op: T, arg: pxtc.service.OpArg): Promise { const startTm = Date.now(); pxt.debug("worker op: " + op); return pxt.worker @@ -278,9 +257,7 @@ function workerOpAsync( return res; }); } -export async function compileAsync( - updateOptions?: (ops: pxtc.CompileOptions) => void -) { +export async function compileAsync(updateOptions?: (ops: pxtc.CompileOptions) => void) { const opts = await getCompileOptionsAsync(); if (updateOptions) updateOptions(opts); const resp = (await workerOpAsync("compile", { @@ -367,8 +344,7 @@ export async function simulateAsync( const runOptions = initDriverAndOptions(container, runOpts, builtSimJS); const driver = simDriver(container)!; - driver.options.messageSimulators = - pxt.appTarget?.simulator?.messageSimulators; + driver.options.messageSimulators = pxt.appTarget?.simulator?.messageSimulators; driver.options.onSimulatorCommand = msg => { if (msg.command === "restart") { runOptions.storedState = getStoredState(runOpts); @@ -387,9 +363,7 @@ export async function simulateAsync( return builtSimJS; } -export async function buildSimJsInfo( - runOpts: RunOptions -): Promise { +export async function buildSimJsInfo(runOpts: RunOptions): Promise { await loadPackageAsync(runOpts); let didUpgrade = false; @@ -398,8 +372,7 @@ export async function buildSimJsInfo( opts.computeUsedParts = true; opts.breakpoints = true; - if (runOpts.mpRole == "client") - opts.fileSystem[pxt.MAIN_TS] = "multiplayer.init()"; + if (runOpts.mpRole == "client") opts.fileSystem[pxt.MAIN_TS] = "multiplayer.init()"; // Api info needed for py2ts conversion, if project is shared in Python if (opts.target.preferredEditor === pxt.PYTHON_PROJECT_NAME) { @@ -417,16 +390,10 @@ export async function buildSimJsInfo( if ( sharedTargetVersion && currentTargetVersion && - pxt.semver.cmp( - pxt.semver.parse(sharedTargetVersion), - pxt.semver.parse(currentTargetVersion) - ) < 0 + pxt.semver.cmp(pxt.semver.parse(sharedTargetVersion), pxt.semver.parse(currentTargetVersion)) < 0 ) { for (const fileName of Object.keys(opts.fileSystem)) { - if ( - !pxt.Util.startsWith(fileName, "pxt_modules") && - pxt.Util.endsWith(fileName, ".ts") - ) { + if (!pxt.Util.startsWith(fileName, "pxt_modules") && pxt.Util.endsWith(fileName, ".ts")) { didUpgrade = true; opts.fileSystem[fileName] = pxt.patching.patchJavaScript( sharedTargetVersion, @@ -438,12 +405,9 @@ export async function buildSimJsInfo( }); if (compileResult.diagnostics?.length > 0 && didUpgrade) { - pxt.log( - "Compile with upgrade rules failed, trying again with original code" - ); + pxt.log("Compile with upgrade rules failed, trying again with original code"); compileResult = await compileAsync(opts => { - if (runOpts.mpRole === "client") - opts.fileSystem[pxt.MAIN_TS] = "multiplayer.init()"; + if (runOpts.mpRole === "client") opts.fileSystem[pxt.MAIN_TS] = "multiplayer.init()"; }); } diff --git a/multiplayer/src/state/AppStateContext.tsx b/multiplayer/src/state/AppStateContext.tsx index 40a2886e35ea..220c3afadecd 100644 --- a/multiplayer/src/state/AppStateContext.tsx +++ b/multiplayer/src/state/AppStateContext.tsx @@ -17,17 +17,13 @@ const initialAppStateContextProps: AppStateContextProps = { dispatch: undefined!, }; -export const AppStateContext = createContext( - initialAppStateContextProps -); +export const AppStateContext = createContext(initialAppStateContextProps); export type AppStateProviderProps = { // This is where the app would inject any initial state at startup }; -export function AppStateProvider( - props: React.PropsWithChildren -): React.ReactElement { +export function AppStateProvider(props: React.PropsWithChildren): React.ReactElement { // Create the application state and state change mechanism (dispatch) const [state_, dispatch_] = useReducer(reducer, initialAppState); diff --git a/multiplayer/src/state/actions.ts b/multiplayer/src/state/actions.ts index 753b9fbaf2b7..8aff4db8b086 100644 --- a/multiplayer/src/state/actions.ts +++ b/multiplayer/src/state/actions.ts @@ -169,9 +169,7 @@ export type Action = * Action creators */ -export const setUserProfile = ( - profile?: pxt.auth.UserProfile -): SetUserProfile => ({ +export const setUserProfile = (profile?: pxt.auth.UserProfile): SetUserProfile => ({ type: "SET_USER_PROFILE", profile, }); @@ -181,9 +179,7 @@ export const clearUserProfile = (): SetUserProfile => ({ profile: undefined, }); -export const setClientRole = ( - clientRole: ClientRole | undefined -): SetClientRole => ({ +export const setClientRole = (clientRole: ClientRole | undefined): SetClientRole => ({ type: "SET_CLIENT_ROLE", clientRole, }); @@ -203,9 +199,7 @@ export const setCollabInfo = (collabInfo: CollabInfo): SetCollabInfo => ({ collabInfo, }); -export const setGameMetadata = ( - gameMetadata: GameMetadata -): SetGameMetadata => ({ +export const setGameMetadata = (gameMetadata: GameMetadata): SetGameMetadata => ({ type: "SET_GAME_METADATA", gameMetadata, }); @@ -252,11 +246,7 @@ export const setPresence = (presence: Presence): SetPresence => ({ presence, }); -export const setReaction = ( - clientId: string, - reactionId: string, - index: number -): SetReaction => ({ +export const setReaction = (clientId: string, reactionId: string, index: number): SetReaction => ({ type: "SET_REACTION", clientId, reactionId, @@ -268,10 +258,7 @@ export const clearReaction = (clientId: string): ClearReaction => ({ clientId, }); -export const showModal = ( - modalType: ModalType, - modalOpts?: any -): ShowModal => ({ +export const showModal = (modalType: ModalType, modalOpts?: any): ShowModal => ({ type: "SHOW_MODAL", modalType, modalOpts, @@ -281,10 +268,7 @@ export const clearModal = (): ClearModal => ({ type: "CLEAR_MODAL", }); -export const setDeepLinks = ( - shareCode: string | undefined, - joinCode: string | undefined -): SetDeepLinks => ({ +export const setDeepLinks = (shareCode: string | undefined, joinCode: string | undefined): SetDeepLinks => ({ type: "SET_DEEP_LINKS", shareCode, joinCode, @@ -305,19 +289,13 @@ export const setTargetConfig = (trgCfg: pxt.TargetConfig): SetTargetConfig => ({ targetConfig: JSON.parse(JSON.stringify(trgCfg)), }); -export const setPresenceIconOverride = ( - slot: number, - icon?: string -): SetPresenceIconOverride => ({ +export const setPresenceIconOverride = (slot: number, icon?: string): SetPresenceIconOverride => ({ type: "SET_PRESENCE_ICON_OVERRIDE", slot, icon, }); -export const setReactionIconOverride = ( - slot: number, - icon?: string -): SetReactionIconOverride => ({ +export const setReactionIconOverride = (slot: number, icon?: string): SetReactionIconOverride => ({ type: "SET_REACTION_ICON_OVERRIDE", slot, icon, diff --git a/multiplayer/src/state/collab/CollabContext.tsx b/multiplayer/src/state/collab/CollabContext.tsx index 4b38f423a2b6..eceb6ce329e0 100644 --- a/multiplayer/src/state/collab/CollabContext.tsx +++ b/multiplayer/src/state/collab/CollabContext.tsx @@ -20,13 +20,9 @@ const initialCollabContextProps: CollabContextProps = { dispatch: undefined!, }; -export const CollabContext = createContext( - initialCollabContextProps -); +export const CollabContext = createContext(initialCollabContextProps); -export function CollabStateProvider( - props: React.PropsWithChildren<{}> -): React.ReactElement { +export function CollabStateProvider(props: React.PropsWithChildren<{}>): React.ReactElement { // Create the application state and state change mechanism (dispatch) const [state_, dispatch_] = useReducer(reducer, initialState); diff --git a/multiplayer/src/state/collab/actions.ts b/multiplayer/src/state/collab/actions.ts index 16d2d10566f2..b0bd7fd9c863 100644 --- a/multiplayer/src/state/collab/actions.ts +++ b/multiplayer/src/state/collab/actions.ts @@ -68,10 +68,7 @@ export function init(kv: Map): Init { }; } -export function playerJoined( - playerId: string, - kv?: Map -): PlayerJoined { +export function playerJoined(playerId: string, kv?: Map): PlayerJoined { return { type: "PLAYER_JOINED", playerId, @@ -86,11 +83,7 @@ export function playerLeft(playerId: string): PlayerLeft { }; } -export function setPlayerValue( - playerId: string, - key: string, - value: string -): SetPlayerValue { +export function setPlayerValue(playerId: string, key: string, value: string): SetPlayerValue { return { type: "SET_PLAYER_VALUE", playerId, diff --git a/multiplayer/src/state/collab/reducer.ts b/multiplayer/src/state/collab/reducer.ts index 76a5d69c23fc..1e21e96b298e 100644 --- a/multiplayer/src/state/collab/reducer.ts +++ b/multiplayer/src/state/collab/reducer.ts @@ -35,10 +35,7 @@ export function reducer(state: CollabState, action: CollabAction): CollabState { ...state.players, [action.playerId]: { ...player, - kv: new Map(player.kv).set( - action.key, - action.value - ), + kv: new Map(player.kv).set(action.key, action.value), }, }, }; diff --git a/multiplayer/src/state/reducer.ts b/multiplayer/src/state/reducer.ts index a1811daac67b..3b578a3b543f 100644 --- a/multiplayer/src/state/reducer.ts +++ b/multiplayer/src/state/reducer.ts @@ -179,8 +179,7 @@ export default function reducer(state: AppState, action: Action): AppState { }; } case "SET_PRESENCE_ICON_OVERRIDE": { - let nextPresenceIcon = - state.gameState?.presenceIconOverrides?.slice() || []; + let nextPresenceIcon = state.gameState?.presenceIconOverrides?.slice() || []; nextPresenceIcon[action.slot] = action.icon; return { ...state, @@ -191,8 +190,7 @@ export default function reducer(state: AppState, action: Action): AppState { }; } case "SET_REACTION_ICON_OVERRIDE": { - let nextReactionIcons = - state.gameState?.reactionIconOverrides?.slice() || []; + let nextReactionIcons = state.gameState?.reactionIconOverrides?.slice() || []; nextReactionIcons[action.slot] = action.icon; return { ...state, diff --git a/multiplayer/src/types/index.ts b/multiplayer/src/types/index.ts index 41422c816fa3..60652fd3681f 100644 --- a/multiplayer/src/types/index.ts +++ b/multiplayer/src/types/index.ts @@ -9,21 +9,11 @@ export type ActionBase = { }; export type NetMode = "init" | "connecting" | "connected"; -export type ModalType = - | "sign-in" - | "report-abuse" - | "kick-player" - | "leave-game"; +export type ModalType = "sign-in" | "report-abuse" | "kick-player" | "leave-game"; export type ClientRole = "host" | "guest" | "none"; export type GameMode = "lobby" | "playing"; -export type SessionOverReason = - | "kicked" - | "ended" - | "left" - | "full" - | "rejected" - | "not-found"; +export type SessionOverReason = "kicked" | "ended" | "left" | "full" | "rejected" | "not-found"; export type GameOverReason = SessionOverReason | "compile-failed"; export type NetResult = { @@ -104,13 +94,7 @@ export enum SimKey { } // https://lospec.com/palette-list/gems-in-the-forrest -export const BRUSH_COLORS = [ - "#ff3282", - "#5b1284", - "#3171ee", - "#4ff5fc", - "#aefdd5", -]; +export const BRUSH_COLORS = ["#ff3282", "#5b1284", "#3171ee", "#4ff5fc", "#aefdd5"]; export type BrushSizeType = "sm" | "md" | "lg"; @@ -169,9 +153,7 @@ export enum AudioInstruction { PlayInstruction = 1, } -export function audioInstructionToString( - state: AudioInstruction -): string | undefined { +export function audioInstructionToString(state: AudioInstruction): string | undefined { switch (state) { case AudioInstruction.MuteAllChannels: return "muteallchannels"; @@ -180,9 +162,7 @@ export function audioInstructionToString( } } -export function stringToAudioInstruction( - state: string -): AudioInstruction | undefined { +export function stringToAudioInstruction(state: string): AudioInstruction | undefined { switch (state) { case "muteallchannels": return AudioInstruction.MuteAllChannels; @@ -273,9 +253,5 @@ export namespace SimMultiplayer { connected: boolean; }; - export type Message = - | ImageMessage - | AudioMessage - | InputMessage - | MultiplayerIconMessage; + export type Message = ImageMessage | AudioMessage | InputMessage | MultiplayerIconMessage; } diff --git a/multiplayer/src/util/index.ts b/multiplayer/src/util/index.ts index 749bc1c6b5c2..a7055ea697e5 100644 --- a/multiplayer/src/util/index.ts +++ b/multiplayer/src/util/index.ts @@ -43,9 +43,7 @@ export function gunzipAsync(data: zlib.InputType): Promise { }); } -export function cleanupJoinCode( - joinCode: string | undefined -): string | undefined { +export function cleanupJoinCode(joinCode: string | undefined): string | undefined { try { const url = new URL(joinCode || ""); if (url.searchParams.has("join")) { @@ -58,9 +56,7 @@ export function cleanupJoinCode( return joinCode; } -export function cleanupShareCode( - shareCode: string | undefined -): string | undefined { +export function cleanupShareCode(shareCode: string | undefined): string | undefined { try { const url = new URL(shareCode || ""); if (url.searchParams.has("host")) { @@ -73,19 +69,13 @@ export function cleanupShareCode( export function resourceUrl(path: string | undefined): string | undefined { if (!path) return; - if ( - pxt.BrowserUtils.isLocalHostDev() && - !(path.startsWith("https:") || path.startsWith("data:")) - ) { + if (pxt.BrowserUtils.isLocalHostDev() && !(path.startsWith("https:") || path.startsWith("data:"))) { return pxt.appTarget?.appTheme.homeUrl + path; } return path; } -export function throttle) => ReturnType>( - func: F, - waitFor: number -): F { +export function throttle) => ReturnType>(func: F, waitFor: number): F { let timeout: NodeJS.Timeout | undefined; let previousTime = 0; return function (this: ThisParameterType, ...args: Parameters) { From ed105e447fbec5408b3cac196b38cde4d8a2b541 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Sun, 31 Mar 2024 18:38:34 -0700 Subject: [PATCH 06/12] collab wip --- multiplayer/src/components/CollabPage.tsx | 144 +++++++++++++++--- .../src/epics/collab/recvDelPlayerValue.ts | 7 +- .../src/epics/collab/recvDelSessionValue.ts | 6 +- .../src/epics/collab/recvSetPlayerValue.ts | 11 +- .../src/epics/collab/recvSetSessionState.ts | 2 +- .../src/epics/collab/recvSetSessionValue.ts | 6 +- multiplayer/src/services/collabCanvas.ts | 12 +- multiplayer/src/services/collabClient.ts | 40 ++++- multiplayer/src/services/gameClient.ts | 2 +- 9 files changed, 189 insertions(+), 41 deletions(-) diff --git a/multiplayer/src/components/CollabPage.tsx b/multiplayer/src/components/CollabPage.tsx index 656628665657..fdc78e40325f 100644 --- a/multiplayer/src/components/CollabPage.tsx +++ b/multiplayer/src/components/CollabPage.tsx @@ -1,4 +1,4 @@ -import { useContext, useEffect, useState } from "react"; +import { useContext, useEffect, useRef, useState } from "react"; import { AppStateContext } from "../state/AppStateContext"; import { Button } from "../../../react-common/components/controls/Button"; import { leaveCollabAsync } from "../epics/leaveCollabAsync"; @@ -6,14 +6,17 @@ import { BRUSH_COLORS, BRUSH_SIZES, BrushSize, Vec2Like } from "../types"; import { getCollabCanvas } from "../services/collabCanvas"; import * as collabClient from "../services/collabClient"; import * as CollabEpics from "../epics/collab"; +import * as CollabActions from "../state/collab/actions"; import { dist, distSq, jsonReplacer } from "../util"; import { BRUSH_PROPS, PLAYER_SPRITE_DATAURLS } from "../constants"; import { nanoid } from "nanoid"; +import { CollabContext } from "../state/collab"; export interface CollabPageProps {} export default function Render(props: CollabPageProps) { const { state } = useContext(AppStateContext); + const { dispatch: collabDispatch } = useContext(CollabContext); const { netMode, clientRole, collabInfo } = state; const [canvasContainer, setCanvasContainer] = useState(null); @@ -22,6 +25,9 @@ export default function Render(props: CollabPageProps) { const [selectedIconIndex, setSelectedIconIndex] = useState(0); const [mouseDown, setMouseDown] = useState(false); const [lastPosition, setLastPosition] = useState({ x: 0, y: 0 }); + const [brushAlpha, setBrushAlpha] = useState(0.5); + const [alphaSliderRef, setAlphaSliderRef] = useState(null); + const undoStack = useRef(new Array>()); useEffect(() => { const collabCanvas = getCollabCanvas(); @@ -29,6 +35,30 @@ export default function Render(props: CollabPageProps) { collabCanvas.addPlayerSprite(collabClient.getClientId()!, 0, 0, 0); }, []); + const undo = () => { + const undoSet = undoStack.current.pop(); + if (undoSet) { + undoSet.forEach(spriteId => { + getCollabCanvas().removePaintSprite(spriteId); + collabDispatch(CollabActions.delSessionValue("s:" + spriteId)); + collabClient.delSessionValue("s:" + spriteId); + }); + } + }; + + useEffect(() => { + const handleKeydown = (e: KeyboardEvent) => { + // handle ctrl-z + if (e.key === "z" && e.ctrlKey) { + undo(); + } + }; + window.addEventListener("keydown", handleKeydown); + return () => { + window.removeEventListener("keydown", handleKeydown); + }; + }, []); + useEffect(() => { const handleMouseDown = (e: MouseEvent) => { if (!canvasContainer) return; @@ -44,14 +74,26 @@ export default function Render(props: CollabPageProps) { if (canvasMouseY >= canvasContainer.scrollTop + canvasContainer.clientHeight) return; setMouseDown(true); setLastPosition({ x: canvasMouseX, y: canvasMouseY }); - getCollabCanvas().addPaintSprite(canvasMouseX, canvasMouseY, selectedSizeIndex, selectedColorIndex); + const spriteId = nanoid(); + const undoSet = new Set(); + undoSet.add(spriteId); + undoStack.current.push(undoSet); + getCollabCanvas().addPaintSprite( + spriteId, + canvasMouseX, + canvasMouseY, + selectedSizeIndex, + selectedColorIndex, + brushAlpha + ); collabClient.setSessionValue( - "s:" + nanoid(), + "s:" + spriteId, JSON.stringify({ x: canvasMouseX, y: canvasMouseY, s: selectedSizeIndex, c: selectedColorIndex, + a: brushAlpha, }) ); }; @@ -76,28 +118,32 @@ export default function Render(props: CollabPageProps) { const newPosStr = JSON.stringify(newPos); // Send position to server collabClient.setPlayerValue("position", newPosStr); - // Update local player position in state - CollabEpics.recvSetPlayerValue(collabClient.getClientId(), "position", newPosStr); + // Update local player position in state (may not be needed) + collabDispatch(CollabActions.setPlayerValue(collabClient.getClientId()!, "position", newPosStr)); if (!mouseDown) return; + const undoSet = undoStack.current[undoStack.current.length - 1]; const brushSize = BRUSH_PROPS[selectedSizeIndex].size; - const brushStep = brushSize / 3; + const brushStep = brushSize / 5; const posDist = dist(lastPosition, newPos); if (posDist > brushStep) { const numSteps = Math.ceil(posDist / brushStep); const stepX = (newPos.x - lastPosition.x) / numSteps; const stepY = (newPos.y - lastPosition.y) / numSteps; for (let i = 0; i < numSteps; i++) { + const spriteId = nanoid(); + undoSet.add(spriteId); const x = lastPosition.x + stepX * i; const y = lastPosition.y + stepY * i; - getCollabCanvas().addPaintSprite(x, y, selectedSizeIndex, selectedColorIndex); + getCollabCanvas().addPaintSprite(spriteId, x, y, selectedSizeIndex, selectedColorIndex, brushAlpha); collabClient.setSessionValue( - "s:" + nanoid(), + "s:" + spriteId, JSON.stringify({ x, y, s: selectedSizeIndex, c: selectedColorIndex, + a: brushAlpha, }) ); } @@ -112,7 +158,7 @@ export default function Render(props: CollabPageProps) { window.removeEventListener("mouseup", handleMouseUp); window.removeEventListener("mousemove", handleMouseMove); }; - }, [mouseDown, canvasContainer, lastPosition, selectedColorIndex, selectedSizeIndex]); + }, [mouseDown, canvasContainer, lastPosition, selectedColorIndex, selectedSizeIndex, brushAlpha]); useEffect(() => { if (canvasContainer && !canvasContainer.firstChild) { @@ -127,6 +173,36 @@ export default function Render(props: CollabPageProps) { }; }, [canvasContainer]); + useEffect(() => { + let mouseIsDown = false; + if (!alphaSliderRef) return; + const handleMouseMove = (e: MouseEvent) => { + if (!mouseIsDown) return; + if (!alphaSliderRef) return; + let alphaMouseY = e.clientY; + alphaMouseY -= alphaSliderRef.offsetTop; + alphaMouseY += alphaSliderRef.scrollTop; + if (alphaMouseY < alphaSliderRef.scrollTop) return; + if (alphaMouseY >= alphaSliderRef.scrollTop + alphaSliderRef.clientHeight) return; + setBrushAlpha(alphaMouseY / alphaSliderRef.clientHeight); + }; + const handleMouseDown = (e: MouseEvent) => { + mouseIsDown = true; + handleMouseMove(e); + }; + const handleMouseUp = (e: MouseEvent) => { + mouseIsDown = false; + }; + alphaSliderRef.addEventListener("mousedown", handleMouseDown); + alphaSliderRef.addEventListener("mousemove", handleMouseMove); + alphaSliderRef.addEventListener("mouseup", handleMouseUp); + return () => { + alphaSliderRef?.removeEventListener("mousedown", handleMouseDown); + alphaSliderRef?.removeEventListener("mousemove", handleMouseMove); + alphaSliderRef?.removeEventListener("mouseup", handleMouseUp); + }; + }, [alphaSliderRef]); + if (!collabInfo) return null; if (netMode !== "connected") return null; @@ -152,6 +228,14 @@ export default function Render(props: CollabPageProps) { setCanvasContainer(ref); }; + const handleAlphaSliderRef = (ref: HTMLDivElement) => { + setAlphaSliderRef(ref); + }; + + const handleUndoClick = () => { + undo(); + }; + const SpriteRow: React.FC = ({ children }) =>
{children}
; const SpriteImg: React.FC<{ imgId: number }> = ({ imgId }) => ( @@ -193,20 +277,36 @@ export default function Render(props: CollabPageProps) { >
))}
- {BRUSH_SIZES.map((bs, i) => ( +
+
+ {BRUSH_SIZES.map((bs, i) => ( +
brushSizeClicked(bs, i)} + >
+ ))} +
brushSizeClicked(bs, i)} - >
- ))} + > +
+
+
{[0, 2, 4].map(imgId => ( @@ -214,6 +314,10 @@ export default function Render(props: CollabPageProps) { ))} +
+
+ +
) { sessKv.forEach((value, key) => { if (key.startsWith("s:")) { const sprite = JSON.parse(value); - getCollabCanvas().addPaintSprite(sprite.x, sprite.y, sprite.s, sprite.c); + getCollabCanvas().addPaintSprite(key, sprite.x, sprite.y, sprite.s, sprite.c, sprite.a); } }); } diff --git a/multiplayer/src/epics/collab/recvSetSessionValue.ts b/multiplayer/src/epics/collab/recvSetSessionValue.ts index da3b349260a1..1f72dfad4a18 100644 --- a/multiplayer/src/epics/collab/recvSetSessionValue.ts +++ b/multiplayer/src/epics/collab/recvSetSessionValue.ts @@ -1,12 +1,14 @@ import { getCollabCanvas } from "../../services/collabCanvas"; import { collabStateAndDispatch } from "../../state/collab"; import * as CollabActions from "../../state/collab/actions"; +import * as collabClient from "../../services/collabClient"; -export function recvSetSessionValue(key: string, value: string) { +export function recvSetSessionValue(key: string, value: string, senderId: string) { + if (senderId === collabClient.getClientId()) return; const { dispatch } = collabStateAndDispatch(); dispatch(CollabActions.setSessionValue(key, value)); if (key.startsWith("s:")) { const sprite = JSON.parse(value); - getCollabCanvas().addPaintSprite(sprite.x, sprite.y, sprite.s, sprite.c); + getCollabCanvas().addPaintSprite(key, sprite.x, sprite.y, sprite.s, sprite.c, sprite.a); } } diff --git a/multiplayer/src/services/collabCanvas.ts b/multiplayer/src/services/collabCanvas.ts index 6fbdf8bdf439..5aef60718a1f 100644 --- a/multiplayer/src/services/collabCanvas.ts +++ b/multiplayer/src/services/collabCanvas.ts @@ -200,22 +200,32 @@ class CollabCanvas { } } - public addPaintSprite(x: number, y: number, brushIndex: number, colorIndex: number) { + public addPaintSprite(key: string, x: number, y: number, brushIndex: number, colorIndex: number, alpha: number) { const sprite = new Pixi.Sprite(); const brush = BRUSH_PROPS[brushIndex]; if (!brush) return; if (!this.brushTextures.has(brush.name)) return; const color = BRUSH_COLORS[colorIndex]; if (!color) return; + sprite.name = key; sprite.texture = this.brushTextures.get(brush.name)!; sprite.anchor.set(0.5); sprite.position.set(x, y); sprite.width = brush.size; sprite.height = brush.size; sprite.tint = new TinyColor(color).toNumber(); + sprite.alpha = alpha; + sprite.blendMode = Pixi.BLEND_MODES.NORMAL; sprite.zIndex = 0; // behind players this.root.addChild(sprite as any); } + + public removePaintSprite(key: string) { + const sprite = this.root.children.find(child => child.name === key); + if (sprite) { + this.root.removeChild(sprite as any); + } + } } let _instance: CollabCanvas; diff --git a/multiplayer/src/services/collabClient.ts b/multiplayer/src/services/collabClient.ts index ceb48318871e..5110fa06bc8a 100644 --- a/multiplayer/src/services/collabClient.ts +++ b/multiplayer/src/services/collabClient.ts @@ -17,9 +17,9 @@ import * as CollabEpics from "../epics/collab"; import { jsonReplacer, jsonReviver } from "../util"; const COLLAB_HOST_PROD = "https://mp.makecode.com"; -const COLLAB_HOST_STAGING = "https://multiplayer.staging.pxt.io"; +const COLLAB_HOST_STAGING = "https://dev.multiplayer.staging.pxt.io"; const COLLAB_HOST_LOCALHOST = "http://localhost:8082"; -const COLLAB_HOST_DEV = COLLAB_HOST_LOCALHOST; +const COLLAB_HOST_DEV = COLLAB_HOST_STAGING; const COLLAB_HOST = (() => { if (pxt.BrowserUtils.isLocalHostDev()) { return COLLAB_HOST_DEV; @@ -289,22 +289,22 @@ class CollabClient { private async recvSetPlayerValueMessageAsync(msg: Protocol.SetPlayerValueMessage) { //pxt.debug(`Recv set player value: ${msg.key} = ${msg.value}`); - CollabEpics.recvSetPlayerValue(msg.clientId!, msg.key, msg.value); + CollabEpics.recvSetPlayerValue(msg.key, msg.value, msg.clientId!); } private async recvDelPlayerValueMessageAsync(msg: Protocol.DelPlayerValueMessage) { //pxt.debug(`Recv del player value: ${msg.key}`); - CollabEpics.recvDelPlayerValue(msg.clientId!, msg.key); + CollabEpics.recvDelPlayerValue(msg.key, msg.clientId!); } private async recvSetSessionValueMessageAsync(msg: Protocol.SetSessionValueMessage) { //pxt.debug(`Recv set session value: ${msg.key} = ${msg.value}`); - CollabEpics.recvSetSessionValue(msg.key, msg.value); + CollabEpics.recvSetSessionValue(msg.key, msg.value, msg.clientId!); } private async recvDelSessionValueMessageAsync(msg: Protocol.DelSessionValueMessage) { //pxt.debug(`Recv del session value: ${msg.key}`); - CollabEpics.recvDelSessionValue(msg.key); + CollabEpics.recvDelSessionValue(msg.key, msg.clientId!); } public kickPlayer(clientId: string) { @@ -329,6 +329,14 @@ class CollabClient { this.sendMessage(msg); } + public delPlayerValue(key: string) { + const msg: Protocol.DelPlayerValueMessage = { + type: "del-player-value", + key, + }; + this.sendMessage(msg); + } + public setSessionValue(key: string, value: string) { const msg: Protocol.SetSessionValueMessage = { type: "set-session-value", @@ -337,6 +345,14 @@ class CollabClient { }; this.sendMessage(msg); } + + public delSessionValue(key: string) { + const msg: Protocol.DelSessionValueMessage = { + type: "del-session-value", + key, + }; + this.sendMessage(msg); + } } let _collabClient: CollabClient | undefined; @@ -388,11 +404,21 @@ export function setPlayerValue(key: string, value: string) { collabClient?.setPlayerValue(key, value); } +export function delPlayerValue(key: string) { + const collabClient = ensureCollabClient(); + collabClient?.delPlayerValue(key); +} + export function setSessionValue(key: string, value: string) { const collabClient = ensureCollabClient(); collabClient?.setSessionValue(key, value); } +export function delSessionValue(key: string) { + const collabClient = ensureCollabClient(); + collabClient?.delSessionValue(key); +} + export function getClientId() { return _collabClient?.clientId; } @@ -471,11 +497,13 @@ namespace Protocol { type: "set-session-value"; key: string; value: string; + clientId?: string; // only set on received messages }; export type DelSessionValueMessage = MessageBase & { type: "del-session-value"; key: string; + clientId?: string; // only set on received messages }; export type Message = diff --git a/multiplayer/src/services/gameClient.ts b/multiplayer/src/services/gameClient.ts index b34d43ad9cb4..59fb794ff0f6 100644 --- a/multiplayer/src/services/gameClient.ts +++ b/multiplayer/src/services/gameClient.ts @@ -41,7 +41,7 @@ import { simDriver } from "./simHost"; const GAME_HOST_PROD = "https://mp.makecode.com"; const GAME_HOST_STAGING = "https://multiplayer.staging.pxt.io"; const GAME_HOST_LOCALHOST = "http://localhost:8082"; -const GAME_HOST_DEV = GAME_HOST_LOCALHOST; +const GAME_HOST_DEV = GAME_HOST_STAGING; const GAME_HOST = (() => { if (pxt.BrowserUtils.isLocalHostDev()) { return GAME_HOST_DEV; From f5e55829d4b95dd03475fab99edb0c85ceab5bb1 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Mon, 1 Apr 2024 12:18:07 -0700 Subject: [PATCH 07/12] collab wip --- multiplayer/src/components/CollabPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/multiplayer/src/components/CollabPage.tsx b/multiplayer/src/components/CollabPage.tsx index fdc78e40325f..a5c216fac17f 100644 --- a/multiplayer/src/components/CollabPage.tsx +++ b/multiplayer/src/components/CollabPage.tsx @@ -25,7 +25,7 @@ export default function Render(props: CollabPageProps) { const [selectedIconIndex, setSelectedIconIndex] = useState(0); const [mouseDown, setMouseDown] = useState(false); const [lastPosition, setLastPosition] = useState({ x: 0, y: 0 }); - const [brushAlpha, setBrushAlpha] = useState(0.5); + const [brushAlpha, setBrushAlpha] = useState(0.25); const [alphaSliderRef, setAlphaSliderRef] = useState(null); const undoStack = useRef(new Array>()); From 9fac2813d042cac7666524313f119570f7d770aa Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 3 Apr 2024 15:57:18 -0700 Subject: [PATCH 08/12] binary messages --- multiplayer/src/components/CollabPage.tsx | 6 +- multiplayer/src/services/collabClient.ts | 197 ++++++++++++++-------- 2 files changed, 131 insertions(+), 72 deletions(-) diff --git a/multiplayer/src/components/CollabPage.tsx b/multiplayer/src/components/CollabPage.tsx index a5c216fac17f..b4384556b24c 100644 --- a/multiplayer/src/components/CollabPage.tsx +++ b/multiplayer/src/components/CollabPage.tsx @@ -7,13 +7,15 @@ import { getCollabCanvas } from "../services/collabCanvas"; import * as collabClient from "../services/collabClient"; import * as CollabEpics from "../epics/collab"; import * as CollabActions from "../state/collab/actions"; -import { dist, distSq, jsonReplacer } from "../util"; +import { dist, distSq, jsonReplacer, throttle } from "../util"; import { BRUSH_PROPS, PLAYER_SPRITE_DATAURLS } from "../constants"; import { nanoid } from "nanoid"; import { CollabContext } from "../state/collab"; export interface CollabPageProps {} +const setPlayerValueThrottled = throttle(collabClient.setPlayerValue, 100); + export default function Render(props: CollabPageProps) { const { state } = useContext(AppStateContext); const { dispatch: collabDispatch } = useContext(CollabContext); @@ -117,7 +119,7 @@ export default function Render(props: CollabPageProps) { const newPos = { x: canvasMouseX, y: canvasMouseY }; const newPosStr = JSON.stringify(newPos); // Send position to server - collabClient.setPlayerValue("position", newPosStr); + setPlayerValueThrottled("position", newPosStr); // Update local player position in state (may not be needed) collabDispatch(CollabActions.setPlayerValue(collabClient.getClientId()!, "position", newPosStr)); diff --git a/multiplayer/src/services/collabClient.ts b/multiplayer/src/services/collabClient.ts index 5110fa06bc8a..fddd3eecf09d 100644 --- a/multiplayer/src/services/collabClient.ts +++ b/multiplayer/src/services/collabClient.ts @@ -73,6 +73,14 @@ class CollabClient { const reader = SmartBuffer.fromBuffer(Buffer.from(payload)); const type = reader.readUInt16LE(); switch (type) { + case Protocol.Binary.MessageType.SetPlayerValue: + return await this.recvSetPlayerValueMessageAsync(reader); + case Protocol.Binary.MessageType.DelPlayerValue: + return await this.recvDelPlayerValueMessageAsync(reader); + case Protocol.Binary.MessageType.SetSessionValue: + return await this.recvSetSessionValueMessageAsync(reader); + case Protocol.Binary.MessageType.DelSessionValue: + return await this.recvDelSessionValueMessageAsync(reader); } } else if (typeof payload === "string") { //------------------------------------------------- @@ -88,14 +96,6 @@ class CollabClient { return await this.recvPlayerJoinedMessageAsync(msg); case "player-left": return await this.recvPlayerLeftMessageAsync(msg); - case "set-player-value": - return await this.recvSetPlayerValueMessageAsync(msg); - case "del-player-value": - return await this.recvDelPlayerValueMessageAsync(msg); - case "set-session-value": - return await this.recvSetSessionValueMessageAsync(msg); - case "del-session-value": - return await this.recvDelSessionValueMessageAsync(msg); } } else { throw new Error(`Unknown payload: ${payload}`); @@ -287,24 +287,28 @@ class CollabClient { CollabEpics.recvPlayerLeft(msg.clientId); } - private async recvSetPlayerValueMessageAsync(msg: Protocol.SetPlayerValueMessage) { + private async recvSetPlayerValueMessageAsync(reader: SmartBuffer) { //pxt.debug(`Recv set player value: ${msg.key} = ${msg.value}`); - CollabEpics.recvSetPlayerValue(msg.key, msg.value, msg.clientId!); + const { key, value, clientId } = Protocol.Binary.unpackSetPlayerValueMessage(reader); + CollabEpics.recvSetPlayerValue(key, value, clientId); } - private async recvDelPlayerValueMessageAsync(msg: Protocol.DelPlayerValueMessage) { + private async recvDelPlayerValueMessageAsync(reader: SmartBuffer) { //pxt.debug(`Recv del player value: ${msg.key}`); - CollabEpics.recvDelPlayerValue(msg.key, msg.clientId!); + const { key, clientId } = Protocol.Binary.unpackDelPlayerValueMessage(reader); + CollabEpics.recvDelPlayerValue(key, clientId); } - private async recvSetSessionValueMessageAsync(msg: Protocol.SetSessionValueMessage) { + private async recvSetSessionValueMessageAsync(reader: SmartBuffer) { //pxt.debug(`Recv set session value: ${msg.key} = ${msg.value}`); - CollabEpics.recvSetSessionValue(msg.key, msg.value, msg.clientId!); + const { key, value, clientId } = Protocol.Binary.unpackSetSessionValueMessage(reader); + CollabEpics.recvSetSessionValue(key, value, clientId); } - private async recvDelSessionValueMessageAsync(msg: Protocol.DelSessionValueMessage) { + private async recvDelSessionValueMessageAsync(reader: SmartBuffer) { //pxt.debug(`Recv del session value: ${msg.key}`); - CollabEpics.recvDelSessionValue(msg.key, msg.clientId!); + const { key, clientId } = Protocol.Binary.unpackDelSessionValueMessage(reader); + CollabEpics.recvDelSessionValue(key, clientId); } public kickPlayer(clientId: string) { @@ -321,37 +325,23 @@ class CollabClient { } public setPlayerValue(key: string, value: string) { - const msg: Protocol.SetPlayerValueMessage = { - type: "set-player-value", - key, - value, - }; - this.sendMessage(msg); + const buff = Protocol.Binary.packSetPlayerValueMessage(key, value); + this.sendMessage(buff); } public delPlayerValue(key: string) { - const msg: Protocol.DelPlayerValueMessage = { - type: "del-player-value", - key, - }; - this.sendMessage(msg); + const buff = Protocol.Binary.packDelPlayerValueMessage(key); + this.sendMessage(buff); } public setSessionValue(key: string, value: string) { - const msg: Protocol.SetSessionValueMessage = { - type: "set-session-value", - key, - value, - }; - this.sendMessage(msg); + const buff = Protocol.Binary.packSetSessionValueMessage(key, value); + this.sendMessage(buff); } public delSessionValue(key: string) { - const msg: Protocol.DelSessionValueMessage = { - type: "del-session-value", - key, - }; - this.sendMessage(msg); + const buff = Protocol.Binary.packDelSessionValueMessage(key); + this.sendMessage(buff); } } @@ -480,32 +470,6 @@ namespace Protocol { reason: SessionOverReason; }; - export type SetPlayerValueMessage = MessageBase & { - type: "set-player-value"; - key: string; - value: string; - clientId?: string; // only set on received messages - }; - - export type DelPlayerValueMessage = MessageBase & { - type: "del-player-value"; - key: string; - clientId?: string; // only set on received messages - }; - - export type SetSessionValueMessage = MessageBase & { - type: "set-session-value"; - key: string; - value: string; - clientId?: string; // only set on received messages - }; - - export type DelSessionValueMessage = MessageBase & { - type: "del-session-value"; - key: string; - clientId?: string; // only set on received messages - }; - export type Message = | ConnectMessage | PresenceMessage @@ -513,9 +477,102 @@ namespace Protocol { | PlayerJoinedMessage | PlayerLeftMessage | KickPlayerMessage - | CollabOverMessage - | SetPlayerValueMessage - | DelPlayerValueMessage - | SetSessionValueMessage - | DelSessionValueMessage; -} + | CollabOverMessage; + + export namespace Binary { + export enum MessageType { + SetPlayerValue = 16, + DelPlayerValue = 17, + SetSessionValue = 18, + DelSessionValue = 19, + } + + // Collab:SetPlayerValue + export function packSetPlayerValueMessage(key: string, value: string): Buffer { + const writer = new SmartBuffer(); + writer.writeUInt16LE(MessageType.SetPlayerValue); + writer.writeStringNT(key); + writer.writeStringNT(value); + return writer.toBuffer(); + } + export function unpackSetPlayerValueMessage(reader: SmartBuffer): { + key: string; + value: string; + clientId: string; + } { + // `type` field has already been read + const key = reader.readStringNT(); + const value = reader.readStringNT(); + const clientId = reader.readStringNT(); + return { + key, + value, + clientId, + }; + } + + // Collab:DelPlayerValue + export function packDelPlayerValueMessage(key: string): Buffer { + const writer = new SmartBuffer(); + writer.writeUInt16LE(MessageType.DelPlayerValue); + writer.writeStringNT(key); + return writer.toBuffer(); + } + export function unpackDelPlayerValueMessage(reader: SmartBuffer): { + key: string; + clientId: string; + } { + // `type` field has already been read + const key = reader.readStringNT(); + const clientId = reader.readStringNT(); + return { + key, + clientId, + }; + } + + // Collab:SetSessionValue + export function packSetSessionValueMessage(key: string, value: string): Buffer { + const writer = new SmartBuffer(); + writer.writeUInt16LE(MessageType.SetSessionValue); + writer.writeStringNT(key); + writer.writeStringNT(value); + return writer.toBuffer(); + } + export function unpackSetSessionValueMessage(reader: SmartBuffer): { + key: string; + value: string; + clientId: string; + } { + // `type` field has already been read + const key = reader.readStringNT(); + const value = reader.readStringNT(); + const clientId = reader.readStringNT(); + return { + key, + value, + clientId, + }; + } + + // Collab:DelSessionValue + export function packDelSessionValueMessage(key: string): Buffer { + const writer = new SmartBuffer(); + writer.writeUInt16LE(MessageType.DelSessionValue); + writer.writeStringNT(key); + return writer.toBuffer(); + } + export function unpackDelSessionValueMessage(reader: SmartBuffer): { + key: string; + clientId: string; + } { + // `type` field has already been read + const key = reader.readStringNT(); + const clientId = reader.readStringNT(); + return { + key, + clientId, + }; + } + } + } From e800dd7507baa4cb5669d93d5a70174119c4fa26 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 3 Apr 2024 16:03:39 -0700 Subject: [PATCH 09/12] prettier --- multiplayer/src/services/collabClient.ts | 190 +++++++++++------------ 1 file changed, 95 insertions(+), 95 deletions(-) diff --git a/multiplayer/src/services/collabClient.ts b/multiplayer/src/services/collabClient.ts index fddd3eecf09d..3c62ac8eb5df 100644 --- a/multiplayer/src/services/collabClient.ts +++ b/multiplayer/src/services/collabClient.ts @@ -479,100 +479,100 @@ namespace Protocol { | KickPlayerMessage | CollabOverMessage; - export namespace Binary { - export enum MessageType { - SetPlayerValue = 16, - DelPlayerValue = 17, - SetSessionValue = 18, - DelSessionValue = 19, - } - - // Collab:SetPlayerValue - export function packSetPlayerValueMessage(key: string, value: string): Buffer { - const writer = new SmartBuffer(); - writer.writeUInt16LE(MessageType.SetPlayerValue); - writer.writeStringNT(key); - writer.writeStringNT(value); - return writer.toBuffer(); - } - export function unpackSetPlayerValueMessage(reader: SmartBuffer): { - key: string; - value: string; - clientId: string; - } { - // `type` field has already been read - const key = reader.readStringNT(); - const value = reader.readStringNT(); - const clientId = reader.readStringNT(); - return { - key, - value, - clientId, - }; - } - - // Collab:DelPlayerValue - export function packDelPlayerValueMessage(key: string): Buffer { - const writer = new SmartBuffer(); - writer.writeUInt16LE(MessageType.DelPlayerValue); - writer.writeStringNT(key); - return writer.toBuffer(); - } - export function unpackDelPlayerValueMessage(reader: SmartBuffer): { - key: string; - clientId: string; - } { - // `type` field has already been read - const key = reader.readStringNT(); - const clientId = reader.readStringNT(); - return { - key, - clientId, - }; - } - - // Collab:SetSessionValue - export function packSetSessionValueMessage(key: string, value: string): Buffer { - const writer = new SmartBuffer(); - writer.writeUInt16LE(MessageType.SetSessionValue); - writer.writeStringNT(key); - writer.writeStringNT(value); - return writer.toBuffer(); - } - export function unpackSetSessionValueMessage(reader: SmartBuffer): { - key: string; - value: string; - clientId: string; - } { - // `type` field has already been read - const key = reader.readStringNT(); - const value = reader.readStringNT(); - const clientId = reader.readStringNT(); - return { - key, - value, - clientId, - }; - } - - // Collab:DelSessionValue - export function packDelSessionValueMessage(key: string): Buffer { - const writer = new SmartBuffer(); - writer.writeUInt16LE(MessageType.DelSessionValue); - writer.writeStringNT(key); - return writer.toBuffer(); - } - export function unpackDelSessionValueMessage(reader: SmartBuffer): { - key: string; - clientId: string; - } { - // `type` field has already been read - const key = reader.readStringNT(); - const clientId = reader.readStringNT(); - return { - key, - clientId, - }; - } + export namespace Binary { + export enum MessageType { + SetPlayerValue = 16, + DelPlayerValue = 17, + SetSessionValue = 18, + DelSessionValue = 19, + } + + // Collab:SetPlayerValue + export function packSetPlayerValueMessage(key: string, value: string): Buffer { + const writer = new SmartBuffer(); + writer.writeUInt16LE(MessageType.SetPlayerValue); + writer.writeStringNT(key); + writer.writeStringNT(value); + return writer.toBuffer(); + } + export function unpackSetPlayerValueMessage(reader: SmartBuffer): { + key: string; + value: string; + clientId: string; + } { + // `type` field has already been read + const key = reader.readStringNT(); + const value = reader.readStringNT(); + const clientId = reader.readStringNT(); + return { + key, + value, + clientId, + }; + } + + // Collab:DelPlayerValue + export function packDelPlayerValueMessage(key: string): Buffer { + const writer = new SmartBuffer(); + writer.writeUInt16LE(MessageType.DelPlayerValue); + writer.writeStringNT(key); + return writer.toBuffer(); + } + export function unpackDelPlayerValueMessage(reader: SmartBuffer): { + key: string; + clientId: string; + } { + // `type` field has already been read + const key = reader.readStringNT(); + const clientId = reader.readStringNT(); + return { + key, + clientId, + }; + } + + // Collab:SetSessionValue + export function packSetSessionValueMessage(key: string, value: string): Buffer { + const writer = new SmartBuffer(); + writer.writeUInt16LE(MessageType.SetSessionValue); + writer.writeStringNT(key); + writer.writeStringNT(value); + return writer.toBuffer(); + } + export function unpackSetSessionValueMessage(reader: SmartBuffer): { + key: string; + value: string; + clientId: string; + } { + // `type` field has already been read + const key = reader.readStringNT(); + const value = reader.readStringNT(); + const clientId = reader.readStringNT(); + return { + key, + value, + clientId, + }; + } + + // Collab:DelSessionValue + export function packDelSessionValueMessage(key: string): Buffer { + const writer = new SmartBuffer(); + writer.writeUInt16LE(MessageType.DelSessionValue); + writer.writeStringNT(key); + return writer.toBuffer(); + } + export function unpackDelSessionValueMessage(reader: SmartBuffer): { + key: string; + clientId: string; + } { + // `type` field has already been read + const key = reader.readStringNT(); + const clientId = reader.readStringNT(); + return { + key, + clientId, + }; } } +} From 118a627f58aa01591d6a7665fb8cbc7fa56e4999 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 3 Apr 2024 16:54:52 -0700 Subject: [PATCH 10/12] decomment --- multiplayer/src/util/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/multiplayer/src/util/index.ts b/multiplayer/src/util/index.ts index a7055ea697e5..58038c85bfe9 100644 --- a/multiplayer/src/util/index.ts +++ b/multiplayer/src/util/index.ts @@ -108,7 +108,7 @@ export function jsonReplacer(key: any, value: any) { if (value instanceof Map) { return { [".dataType"]: "Map", - value: Array.from(value.entries()), // or with spread: value: [...value] + value: Array.from(value.entries()), }; } else { return value; From f98c04dddd0a29b626c9bba55c11e5073656a7bd Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 4 Apr 2024 11:59:54 -0700 Subject: [PATCH 11/12] don't re-render on every network message --- multiplayer/src/components/CollabPage.tsx | 6 - multiplayer/src/components/JoinOrHost.tsx | 151 +---------------- .../src/components/JoinOrHostCollab.tsx | 152 +++++++++++++++++ multiplayer/src/components/JoinOrHostGame.tsx | 158 ++++++++++++++++++ multiplayer/src/epics/collab/connected.ts | 5 + multiplayer/src/epics/collab/index.ts | 4 +- multiplayer/src/epics/collab/initState.ts | 8 - .../src/epics/collab/recvDelPlayerValue.ts | 4 - .../src/epics/collab/recvDelSessionValue.ts | 4 - .../src/epics/collab/recvPlayerJoined.ts | 4 - .../src/epics/collab/recvPlayerLeft.ts | 4 - multiplayer/src/epics/collab/recvPresence.ts | 8 + .../src/epics/collab/recvSessionState.ts | 12 -- .../src/epics/collab/recvSetPlayerValue.ts | 4 - .../src/epics/collab/recvSetSessionState.ts | 4 - .../src/epics/collab/recvSetSessionValue.ts | 4 - .../src/epics/collab/recvUpdatePresence.ts | 26 --- multiplayer/src/index.tsx | 5 +- multiplayer/src/services/collabClient.ts | 8 +- .../src/state/collab/CollabContext.tsx | 46 ----- multiplayer/src/state/collab/actions.ts | 130 -------------- multiplayer/src/state/collab/index.ts | 9 - multiplayer/src/state/collab/reducer.ts | 103 ------------ multiplayer/src/state/collab/state.ts | 11 -- multiplayer/src/state/state.ts | 2 + 25 files changed, 334 insertions(+), 538 deletions(-) create mode 100644 multiplayer/src/components/JoinOrHostCollab.tsx create mode 100644 multiplayer/src/components/JoinOrHostGame.tsx create mode 100644 multiplayer/src/epics/collab/connected.ts delete mode 100644 multiplayer/src/epics/collab/initState.ts create mode 100644 multiplayer/src/epics/collab/recvPresence.ts delete mode 100644 multiplayer/src/epics/collab/recvSessionState.ts delete mode 100644 multiplayer/src/epics/collab/recvUpdatePresence.ts delete mode 100644 multiplayer/src/state/collab/CollabContext.tsx delete mode 100644 multiplayer/src/state/collab/actions.ts delete mode 100644 multiplayer/src/state/collab/index.ts delete mode 100644 multiplayer/src/state/collab/reducer.ts delete mode 100644 multiplayer/src/state/collab/state.ts diff --git a/multiplayer/src/components/CollabPage.tsx b/multiplayer/src/components/CollabPage.tsx index b4384556b24c..5c1b639527bd 100644 --- a/multiplayer/src/components/CollabPage.tsx +++ b/multiplayer/src/components/CollabPage.tsx @@ -6,11 +6,9 @@ import { BRUSH_COLORS, BRUSH_SIZES, BrushSize, Vec2Like } from "../types"; import { getCollabCanvas } from "../services/collabCanvas"; import * as collabClient from "../services/collabClient"; import * as CollabEpics from "../epics/collab"; -import * as CollabActions from "../state/collab/actions"; import { dist, distSq, jsonReplacer, throttle } from "../util"; import { BRUSH_PROPS, PLAYER_SPRITE_DATAURLS } from "../constants"; import { nanoid } from "nanoid"; -import { CollabContext } from "../state/collab"; export interface CollabPageProps {} @@ -18,7 +16,6 @@ const setPlayerValueThrottled = throttle(collabClient.setPlayerValue, 100); export default function Render(props: CollabPageProps) { const { state } = useContext(AppStateContext); - const { dispatch: collabDispatch } = useContext(CollabContext); const { netMode, clientRole, collabInfo } = state; const [canvasContainer, setCanvasContainer] = useState(null); @@ -42,7 +39,6 @@ export default function Render(props: CollabPageProps) { if (undoSet) { undoSet.forEach(spriteId => { getCollabCanvas().removePaintSprite(spriteId); - collabDispatch(CollabActions.delSessionValue("s:" + spriteId)); collabClient.delSessionValue("s:" + spriteId); }); } @@ -120,8 +116,6 @@ export default function Render(props: CollabPageProps) { const newPosStr = JSON.stringify(newPos); // Send position to server setPlayerValueThrottled("position", newPosStr); - // Update local player position in state (may not be needed) - collabDispatch(CollabActions.setPlayerValue(collabClient.getClientId()!, "position", newPosStr)); if (!mouseDown) return; const undoSet = undoStack.current[undoStack.current.length - 1]; diff --git a/multiplayer/src/components/JoinOrHost.tsx b/multiplayer/src/components/JoinOrHost.tsx index 3ef0f111c90c..52e267865532 100644 --- a/multiplayer/src/components/JoinOrHost.tsx +++ b/multiplayer/src/components/JoinOrHost.tsx @@ -1,152 +1,9 @@ -import { useCallback, useContext, useEffect, useRef, useState } from "react"; - -import { dispatch } from "../state"; -import { setTargetConfig } from "../state/actions"; +import JoinOrHostGame from "./JoinOrHostGame"; +import JoinOrHostCollab from "./JoinOrHostCollab"; +import { useContext } from "react"; import { AppStateContext } from "../state/AppStateContext"; -import { Button } from "../../../react-common/components/controls/Button"; -import { Input } from "react-common/components/controls/Input"; -import { Link } from "react-common/components/controls/Link"; -import { hostCollabAsync, joinCollabAsync } from "../epics"; -import { resourceUrl } from "../util"; -import TabButton from "./TabButton"; -import HostGameButton from "./HostGameButton"; export default function Render() { const { state } = useContext(AppStateContext); - const { targetConfig } = state; - const [currTab, setCurrTab] = useState<"join" | "host">("join"); - const joinCodeRef = useRef(); - const shareCodeRef = useRef(); - - const setJoinCodeRef = useCallback((ref: HTMLInputElement) => { - joinCodeRef.current = ref; - }, []); - const setShareCodeRef = useCallback((ref: HTMLInputElement) => { - shareCodeRef.current = ref; - }, []); - - const onJoinGameClick = async () => { - if (joinCodeRef?.current?.value) { - await joinCollabAsync(joinCodeRef.current.value); - } - }; - const onHostGameClick = async () => { - await hostCollabAsync(); - }; - - const enterShareOrLink = lf("Enter share code or link"); - const howToGetLink = lf("How do I get a share code or link?"); - const moreGamesToPlay = lf("More games to play with your friends"); - - const starterGames = targetConfig?.multiplayer?.games ?? []; - const showStarterGames = false; - - return ( -
-
-
-
-
- -
{lf("Join Collab")}
-
{lf("Join")}
- - } - selected={currTab === "join"} - onClick={() => setCurrTab("join")} - /> - -
{lf("Host Collab")}
-
{lf("Host")}
- - } - selected={currTab === "host"} - onClick={() => setCurrTab("host")} - /> -
-
- {currTab === "join" && ( -
-
- -
-
-
-
- - {lf("How do I get a collab code?")} - -
-
- )} - {currTab === "host" && ( -
-
- -
-
-
-
- )} -
-
-
-
- {showStarterGames && ( -
-
{moreGamesToPlay}
-
- {starterGames.map((game, i) => { - return ( - - ); - })} -
-
- )} -
- ); + return state.collabMode ? : ; } diff --git a/multiplayer/src/components/JoinOrHostCollab.tsx b/multiplayer/src/components/JoinOrHostCollab.tsx new file mode 100644 index 000000000000..3ef0f111c90c --- /dev/null +++ b/multiplayer/src/components/JoinOrHostCollab.tsx @@ -0,0 +1,152 @@ +import { useCallback, useContext, useEffect, useRef, useState } from "react"; + +import { dispatch } from "../state"; +import { setTargetConfig } from "../state/actions"; +import { AppStateContext } from "../state/AppStateContext"; +import { Button } from "../../../react-common/components/controls/Button"; +import { Input } from "react-common/components/controls/Input"; +import { Link } from "react-common/components/controls/Link"; +import { hostCollabAsync, joinCollabAsync } from "../epics"; +import { resourceUrl } from "../util"; +import TabButton from "./TabButton"; +import HostGameButton from "./HostGameButton"; + +export default function Render() { + const { state } = useContext(AppStateContext); + const { targetConfig } = state; + const [currTab, setCurrTab] = useState<"join" | "host">("join"); + const joinCodeRef = useRef(); + const shareCodeRef = useRef(); + + const setJoinCodeRef = useCallback((ref: HTMLInputElement) => { + joinCodeRef.current = ref; + }, []); + const setShareCodeRef = useCallback((ref: HTMLInputElement) => { + shareCodeRef.current = ref; + }, []); + + const onJoinGameClick = async () => { + if (joinCodeRef?.current?.value) { + await joinCollabAsync(joinCodeRef.current.value); + } + }; + const onHostGameClick = async () => { + await hostCollabAsync(); + }; + + const enterShareOrLink = lf("Enter share code or link"); + const howToGetLink = lf("How do I get a share code or link?"); + const moreGamesToPlay = lf("More games to play with your friends"); + + const starterGames = targetConfig?.multiplayer?.games ?? []; + const showStarterGames = false; + + return ( +
+
+
+
+
+ +
{lf("Join Collab")}
+
{lf("Join")}
+ + } + selected={currTab === "join"} + onClick={() => setCurrTab("join")} + /> + +
{lf("Host Collab")}
+
{lf("Host")}
+ + } + selected={currTab === "host"} + onClick={() => setCurrTab("host")} + /> +
+
+ {currTab === "join" && ( +
+
+ +
+
+
+
+ + {lf("How do I get a collab code?")} + +
+
+ )} + {currTab === "host" && ( +
+
+ +
+
+
+
+ )} +
+
+
+
+ {showStarterGames && ( +
+
{moreGamesToPlay}
+
+ {starterGames.map((game, i) => { + return ( + + ); + })} +
+
+ )} +
+ ); +} diff --git a/multiplayer/src/components/JoinOrHostGame.tsx b/multiplayer/src/components/JoinOrHostGame.tsx new file mode 100644 index 000000000000..8c1c231d80a7 --- /dev/null +++ b/multiplayer/src/components/JoinOrHostGame.tsx @@ -0,0 +1,158 @@ +import { useCallback, useContext, useEffect, useRef, useState } from "react"; + +import { dispatch } from "../state"; +import { setTargetConfig } from "../state/actions"; +import { AppStateContext } from "../state/AppStateContext"; +import { Button } from "../../../react-common/components/controls/Button"; +import { Input } from "react-common/components/controls/Input"; +import { Link } from "react-common/components/controls/Link"; +import { hostGameAsync, joinGameAsync } from "../epics"; +import { resourceUrl } from "../util"; +import TabButton from "./TabButton"; +import HostGameButton from "./HostGameButton"; + +export default function Render() { + const { state } = useContext(AppStateContext); + const { targetConfig } = state; + const [currTab, setCurrTab] = useState<"join" | "host">("join"); + const joinCodeRef = useRef(); + const shareCodeRef = useRef(); + + const setJoinCodeRef = useCallback((ref: HTMLInputElement) => { + joinCodeRef.current = ref; + }, []); + const setShareCodeRef = useCallback((ref: HTMLInputElement) => { + shareCodeRef.current = ref; + }, []); + + const onJoinGameClick = async () => { + if (joinCodeRef?.current?.value) { + await joinGameAsync(joinCodeRef.current.value); + } + }; + const onHostGameClick = async () => { + if (shareCodeRef?.current?.value) { + await hostGameAsync(shareCodeRef.current.value); + } + }; + + const enterShareOrLink = lf("Enter share code or link"); + const howToGetLink = lf("How do I get a share code or link?"); + const moreGamesToPlay = lf("More games to play with your friends"); + + const starterGames = targetConfig?.multiplayer?.games; + const showStarterGames = !!starterGames?.length; + + return ( +
+
+
+
+
+ +
{lf("Join Game")}
+
{lf("Join")}
+ + } + selected={currTab === "join"} + onClick={() => setCurrTab("join")} + /> + +
{lf("Host Game")}
+
{lf("Host")}
+ + } + selected={currTab === "host"} + onClick={() => setCurrTab("host")} + /> +
+
+ {currTab === "join" && ( +
+
+ +
+
+
+
+ + {lf("How do I get a game code?")} + +
+
+ )} + {currTab === "host" && ( +
+
+ +
+
+
+
+ + {howToGetLink} + +
+
+ )} +
+
+
+
+ {showStarterGames && ( +
+
{moreGamesToPlay}
+
+ {starterGames.map((game, i) => { + return ( + + ); + })} +
+
+ )} +
+ ); +} diff --git a/multiplayer/src/epics/collab/connected.ts b/multiplayer/src/epics/collab/connected.ts new file mode 100644 index 000000000000..837e9bfe8c17 --- /dev/null +++ b/multiplayer/src/epics/collab/connected.ts @@ -0,0 +1,5 @@ +import { getCollabCanvas } from "../../services/collabCanvas"; + +export function connected(clientId: string) { + getCollabCanvas().addPlayerSprite(clientId, 0, 0, 0); +} diff --git a/multiplayer/src/epics/collab/index.ts b/multiplayer/src/epics/collab/index.ts index 3b9afb118ed9..e2d964be79cf 100644 --- a/multiplayer/src/epics/collab/index.ts +++ b/multiplayer/src/epics/collab/index.ts @@ -1,9 +1,9 @@ -export { initState } from "./initState"; +export { connected } from "./connected"; export { recvPlayerJoined } from "./recvPlayerJoined"; export { recvPlayerLeft } from "./recvPlayerLeft"; -export { recvUpdatePresence } from "./recvUpdatePresence"; export { recvSetPlayerValue } from "./recvSetPlayerValue"; export { recvDelPlayerValue } from "./recvDelPlayerValue"; export { recvSetSessionValue } from "./recvSetSessionValue"; export { recvDelSessionValue } from "./recvDelSessionValue"; export { recvSetSessionState } from "./recvSetSessionState"; +export { recvPresence } from "./recvPresence"; diff --git a/multiplayer/src/epics/collab/initState.ts b/multiplayer/src/epics/collab/initState.ts deleted file mode 100644 index d79578edab16..000000000000 --- a/multiplayer/src/epics/collab/initState.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { getCollabCanvas } from "../../services/collabCanvas"; -import { collabStateAndDispatch } from "../../state/collab"; -import * as CollabActions from "../../state/collab/actions"; - -export function initState(kv: Map) { - const { dispatch } = collabStateAndDispatch(); - dispatch(CollabActions.init(kv)); -} diff --git a/multiplayer/src/epics/collab/recvDelPlayerValue.ts b/multiplayer/src/epics/collab/recvDelPlayerValue.ts index f5ef4f045a57..23d303005779 100644 --- a/multiplayer/src/epics/collab/recvDelPlayerValue.ts +++ b/multiplayer/src/epics/collab/recvDelPlayerValue.ts @@ -1,10 +1,6 @@ import { getCollabCanvas } from "../../services/collabCanvas"; -import { collabStateAndDispatch } from "../../state/collab"; -import * as CollabActions from "../../state/collab/actions"; import * as collabClient from "../../services/collabClient"; export function recvDelPlayerValue(key: string, senderId: string) { if (senderId === collabClient.getClientId()) return; - const { dispatch } = collabStateAndDispatch(); - dispatch(CollabActions.delPlayerValue(senderId, key)); } diff --git a/multiplayer/src/epics/collab/recvDelSessionValue.ts b/multiplayer/src/epics/collab/recvDelSessionValue.ts index 82fbdc769830..3e782e938a4d 100644 --- a/multiplayer/src/epics/collab/recvDelSessionValue.ts +++ b/multiplayer/src/epics/collab/recvDelSessionValue.ts @@ -1,12 +1,8 @@ import { getCollabCanvas } from "../../services/collabCanvas"; -import { collabStateAndDispatch } from "../../state/collab"; -import * as CollabActions from "../../state/collab/actions"; import * as collabClient from "../../services/collabClient"; export function recvDelSessionValue(key: string, senderId: string) { if (senderId === collabClient.getClientId()) return; - const { dispatch } = collabStateAndDispatch(); - dispatch(CollabActions.delSessionValue(key)); if (key.startsWith("s:")) { getCollabCanvas().removePaintSprite(key); } diff --git a/multiplayer/src/epics/collab/recvPlayerJoined.ts b/multiplayer/src/epics/collab/recvPlayerJoined.ts index cb5e583450f6..9a78fbecdda5 100644 --- a/multiplayer/src/epics/collab/recvPlayerJoined.ts +++ b/multiplayer/src/epics/collab/recvPlayerJoined.ts @@ -1,9 +1,5 @@ import { getCollabCanvas } from "../../services/collabCanvas"; -import { collabStateAndDispatch } from "../../state/collab"; -import * as CollabActions from "../../state/collab/actions"; export function recvPlayerJoined(playerId: string, kv?: Map) { - const { dispatch } = collabStateAndDispatch(); - dispatch(CollabActions.playerJoined(playerId, kv)); getCollabCanvas().addPlayerSprite(playerId, 0, 0, 0); } diff --git a/multiplayer/src/epics/collab/recvPlayerLeft.ts b/multiplayer/src/epics/collab/recvPlayerLeft.ts index 9289934ad66c..5bf6bdb0ad1a 100644 --- a/multiplayer/src/epics/collab/recvPlayerLeft.ts +++ b/multiplayer/src/epics/collab/recvPlayerLeft.ts @@ -1,9 +1,5 @@ import { getCollabCanvas } from "../../services/collabCanvas"; -import { collabStateAndDispatch } from "../../state/collab"; -import * as CollabActions from "../../state/collab/actions"; export function recvPlayerLeft(playerId: string) { - const { dispatch } = collabStateAndDispatch(); - dispatch(CollabActions.playerLeft(playerId)); getCollabCanvas().removePlayerSprite(playerId); } diff --git a/multiplayer/src/epics/collab/recvPresence.ts b/multiplayer/src/epics/collab/recvPresence.ts new file mode 100644 index 000000000000..aa0b459aa258 --- /dev/null +++ b/multiplayer/src/epics/collab/recvPresence.ts @@ -0,0 +1,8 @@ +import { getCollabCanvas } from "../../services/collabCanvas"; +import { Presence } from "../../types"; + +export function recvPresence(presence: Presence) { + presence.users.forEach((user) => { + getCollabCanvas().addPlayerSprite(user.id, 0, 0, 0); + }); +} diff --git a/multiplayer/src/epics/collab/recvSessionState.ts b/multiplayer/src/epics/collab/recvSessionState.ts deleted file mode 100644 index d7cd5c84c929..000000000000 --- a/multiplayer/src/epics/collab/recvSessionState.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { getCollabCanvas } from "../../services/collabCanvas"; -import { collabStateAndDispatch } from "../../state/collab"; -import * as CollabActions from "../../state/collab/actions"; - -export function recvSetSessionValue(key: string, value: string) { - const { dispatch } = collabStateAndDispatch(); - dispatch(CollabActions.setSessionValue(key, value)); - if (key.startsWith("s:")) { - const sprite = JSON.parse(value); - //getCollabCanvas().addPaintSprite(s); - } -} diff --git a/multiplayer/src/epics/collab/recvSetPlayerValue.ts b/multiplayer/src/epics/collab/recvSetPlayerValue.ts index 9e4dfa4327c6..d6f843113713 100644 --- a/multiplayer/src/epics/collab/recvSetPlayerValue.ts +++ b/multiplayer/src/epics/collab/recvSetPlayerValue.ts @@ -1,12 +1,8 @@ import { getCollabCanvas } from "../../services/collabCanvas"; -import { collabStateAndDispatch } from "../../state/collab"; -import * as CollabActions from "../../state/collab/actions"; import * as collabClient from "../../services/collabClient"; export function recvSetPlayerValue(key: string, value: string, senderId: string) { if (senderId === collabClient.getClientId()) return; - const { dispatch } = collabStateAndDispatch(); - dispatch(CollabActions.setPlayerValue(senderId, key, value)); if (key === "position") { const pos = JSON.parse(value); getCollabCanvas().updatePlayerSpritePosition(senderId, pos.x, pos.y); diff --git a/multiplayer/src/epics/collab/recvSetSessionState.ts b/multiplayer/src/epics/collab/recvSetSessionState.ts index 09234b0331fb..4d6fe8e94fb5 100644 --- a/multiplayer/src/epics/collab/recvSetSessionState.ts +++ b/multiplayer/src/epics/collab/recvSetSessionState.ts @@ -1,10 +1,6 @@ import { getCollabCanvas } from "../../services/collabCanvas"; -import { collabStateAndDispatch } from "../../state/collab"; -import * as CollabActions from "../../state/collab/actions"; export function recvSetSessionState(sessKv: Map) { - const { dispatch } = collabStateAndDispatch(); - dispatch(CollabActions.setSessionState(sessKv)); sessKv.forEach((value, key) => { if (key.startsWith("s:")) { const sprite = JSON.parse(value); diff --git a/multiplayer/src/epics/collab/recvSetSessionValue.ts b/multiplayer/src/epics/collab/recvSetSessionValue.ts index 1f72dfad4a18..456303caa920 100644 --- a/multiplayer/src/epics/collab/recvSetSessionValue.ts +++ b/multiplayer/src/epics/collab/recvSetSessionValue.ts @@ -1,12 +1,8 @@ import { getCollabCanvas } from "../../services/collabCanvas"; -import { collabStateAndDispatch } from "../../state/collab"; -import * as CollabActions from "../../state/collab/actions"; import * as collabClient from "../../services/collabClient"; export function recvSetSessionValue(key: string, value: string, senderId: string) { if (senderId === collabClient.getClientId()) return; - const { dispatch } = collabStateAndDispatch(); - dispatch(CollabActions.setSessionValue(key, value)); if (key.startsWith("s:")) { const sprite = JSON.parse(value); getCollabCanvas().addPaintSprite(key, sprite.x, sprite.y, sprite.s, sprite.c, sprite.a); diff --git a/multiplayer/src/epics/collab/recvUpdatePresence.ts b/multiplayer/src/epics/collab/recvUpdatePresence.ts deleted file mode 100644 index 9878ca3acd9e..000000000000 --- a/multiplayer/src/epics/collab/recvUpdatePresence.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { getCollabCanvas } from "../../services/collabCanvas"; -import { collabStateAndDispatch, getCollabPlayers } from "../../state/collab"; -import * as CollabActions from "../../state/collab/actions"; -import { Presence } from "../../types"; - -export function recvUpdatePresence(presence: Presence) { - const { dispatch } = collabStateAndDispatch(); - dispatch(CollabActions.updatePresence(presence)); - setTimeout(() => { - const { state } = collabStateAndDispatch(); - getCollabPlayers(state).forEach(player => { - let x = 0; - let y = 0; - let imgId = 0; - if (player.kv.has("position")) { - const pos = JSON.parse(player.kv.get("position")!); - x = pos.x; - y = pos.y; - } - if (player.kv.has("imgId")) { - imgId = parseInt(player.kv.get("imgId")!); - } - getCollabCanvas().addPlayerSprite(player.clientId, x, y, imgId); - }); - }, 1); -} diff --git a/multiplayer/src/index.tsx b/multiplayer/src/index.tsx index 8d97c98a4e1d..8ad8abd8750f 100644 --- a/multiplayer/src/index.tsx +++ b/multiplayer/src/index.tsx @@ -10,7 +10,6 @@ import ReactDOM from "react-dom"; import "./index.css"; import App from "./App"; import { AppStateProvider } from "./state/AppStateContext"; -import { CollabStateProvider } from "./state/collab"; function enableAnalytics() { pxt.analytics.enable(pxt.Util.userLanguage()); @@ -64,9 +63,7 @@ window.addEventListener("DOMContentLoaded", () => { ReactDOM.render( - - - + , document.getElementById("root") diff --git a/multiplayer/src/services/collabClient.ts b/multiplayer/src/services/collabClient.ts index 3c62ac8eb5df..ff47573f9f3e 100644 --- a/multiplayer/src/services/collabClient.ts +++ b/multiplayer/src/services/collabClient.ts @@ -188,8 +188,6 @@ class CollabClient { if (!collabInfo?.joinTicket) throw new Error("Collab server did not return a join ticket"); - CollabEpics.initState(new Map()); - await this.connectAsync(collabInfo.joinTicket!); return { @@ -229,8 +227,6 @@ class CollabClient { if (!collabInfo?.joinTicket) throw new Error("Collab server did not return a join ticket"); - CollabEpics.initState(new Map()); - await this.connectAsync(collabInfo.joinTicket!); return { @@ -262,14 +258,14 @@ class CollabClient { this.clientRole = role; this.clientId = clientId; - // TODO: Set initial state + CollabEpics.connected(clientId); CollabEpics.recvSetSessionState(sessKv); } private async recvPresenceMessageAsync(msg: Protocol.PresenceMessage) { pxt.debug("Server sent presence"); await setPresenceAsync(msg.presence); - CollabEpics.recvUpdatePresence(msg.presence); + CollabEpics.recvPresence(msg.presence); } private async recvPlayerJoinedMessageAsync(msg: Protocol.PlayerJoinedMessage) { diff --git a/multiplayer/src/state/collab/CollabContext.tsx b/multiplayer/src/state/collab/CollabContext.tsx deleted file mode 100644 index eceb6ce329e0..000000000000 --- a/multiplayer/src/state/collab/CollabContext.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { createContext, useEffect, useReducer } from "react"; -import { CollabState, initialState } from "./state"; -import { reducer } from "./reducer"; -import { CollabAction } from "./actions"; - -let state: CollabState; -let dispatch: React.Dispatch; - -export function collabStateAndDispatch() { - return { state, dispatch }; -} - -type CollabContextProps = { - state: CollabState; - dispatch: React.Dispatch; -}; - -const initialCollabContextProps: CollabContextProps = { - state: undefined!, - dispatch: undefined!, -}; - -export const CollabContext = createContext(initialCollabContextProps); - -export function CollabStateProvider(props: React.PropsWithChildren<{}>): React.ReactElement { - // Create the application state and state change mechanism (dispatch) - const [state_, dispatch_] = useReducer(reducer, initialState); - - useEffect(() => { - // Make state and dispatch available outside the React context - state = state_; - dispatch = dispatch_; - }, [state_, dispatch_]); - - return ( - // Provide current state and dispatch mechanism to all child components - - {props.children} - - ); -} diff --git a/multiplayer/src/state/collab/actions.ts b/multiplayer/src/state/collab/actions.ts deleted file mode 100644 index b0bd7fd9c863..000000000000 --- a/multiplayer/src/state/collab/actions.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { ActionBase, Presence } from "../../types"; - -type Init = ActionBase & { - type: "INIT"; - kv: Map; -}; - -type PlayerJoined = ActionBase & { - type: "PLAYER_JOINED"; - playerId: string; - kv?: Map; -}; - -type PlayerLeft = ActionBase & { - type: "PLAYER_LEFT"; - playerId: string; -}; - -type SetPlayerValue = ActionBase & { - type: "SET_PLAYER_VALUE"; - playerId: string; - key: string; - value: string; -}; - -type DelPlayerValue = ActionBase & { - type: "DEL_PLAYER_VALUE"; - playerId: string; - key: string; -}; - -type SetSessionValue = ActionBase & { - type: "SET_SESSION_VALUE"; - key: string; - value: string; -}; - -type DelSessionValue = ActionBase & { - type: "DEL_SESSION_VALUE"; - key: string; -}; - -type SetSessionState = ActionBase & { - type: "SET_SESSION_STATE"; - sessKv: Map; -}; - -type UpdatePresence = ActionBase & { - type: "UPDATE_PRESENCE"; - presence: Presence; -}; - -export type CollabAction = - | Init - | PlayerJoined - | PlayerLeft - | SetPlayerValue - | DelPlayerValue - | SetSessionValue - | DelSessionValue - | SetSessionState - | UpdatePresence; - -export function init(kv: Map): Init { - return { - type: "INIT", - kv, - }; -} - -export function playerJoined(playerId: string, kv?: Map): PlayerJoined { - return { - type: "PLAYER_JOINED", - playerId, - kv, - }; -} - -export function playerLeft(playerId: string): PlayerLeft { - return { - type: "PLAYER_LEFT", - playerId, - }; -} - -export function setPlayerValue(playerId: string, key: string, value: string): SetPlayerValue { - return { - type: "SET_PLAYER_VALUE", - playerId, - key, - value, - }; -} - -export function delPlayerValue(playerId: string, key: string): DelPlayerValue { - return { - type: "DEL_PLAYER_VALUE", - playerId, - key, - }; -} - -export function setSessionValue(key: string, value: string): SetSessionValue { - return { - type: "SET_SESSION_VALUE", - key, - value, - }; -} - -export function delSessionValue(key: string): DelSessionValue { - return { - type: "DEL_SESSION_VALUE", - key, - }; -} - -export function setSessionState(sessKv: Map): SetSessionState { - return { - type: "SET_SESSION_STATE", - sessKv, - }; -} - -export function updatePresence(presence: Presence): UpdatePresence { - return { - type: "UPDATE_PRESENCE", - presence, - }; -} diff --git a/multiplayer/src/state/collab/index.ts b/multiplayer/src/state/collab/index.ts deleted file mode 100644 index 5459c35e470b..000000000000 --- a/multiplayer/src/state/collab/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { CollabPlayer } from "../../services/collabClient"; -import { CollabState } from "./state"; - -export { collabStateAndDispatch } from "./CollabContext"; -export { CollabStateProvider, CollabContext } from "./CollabContext"; - -export function getCollabPlayers(state: CollabState): CollabPlayer[] { - return Object.values(state.players); -} diff --git a/multiplayer/src/state/collab/reducer.ts b/multiplayer/src/state/collab/reducer.ts deleted file mode 100644 index 1e21e96b298e..000000000000 --- a/multiplayer/src/state/collab/reducer.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { CollabState, initialState } from "./state"; -import { CollabAction } from "./actions"; - -export function reducer(state: CollabState, action: CollabAction): CollabState { - switch (action.type) { - case "INIT": - return { - ...initialState, - }; - case "PLAYER_JOINED": { - return { - ...state, - players: { - ...state.players, - [action.playerId]: { - clientId: action.playerId, - kv: action.kv ? action.kv : new Map(), - }, - }, - }; - } - case "PLAYER_LEFT": { - const { [action.playerId]: _, ...players } = state.players; - return { - ...state, - players, - }; - } - case "SET_PLAYER_VALUE": { - const player = state.players[action.playerId]; - if (player) { - return { - ...state, - players: { - ...state.players, - [action.playerId]: { - ...player, - kv: new Map(player.kv).set(action.key, action.value), - }, - }, - }; - } else { - return state; - } - } - case "DEL_PLAYER_VALUE": { - const player = state.players[action.playerId]; - if (player) { - const kv = new Map(player.kv); - kv.delete(action.key); - return { - ...state, - players: { - ...state.players, - [action.playerId]: { - ...player, - kv, - }, - }, - }; - } else { - return state; - } - } - case "SET_SESSION_VALUE": { - return { - ...state, - kv: new Map(state.kv).set(action.key, action.value), - }; - } - case "DEL_SESSION_VALUE": { - const kv = new Map(state.kv); - kv.delete(action.key); - return { - ...state, - kv, - }; - } - case "SET_SESSION_STATE": { - return { - ...state, - kv: action.sessKv, - }; - } - case "UPDATE_PRESENCE": { - const players = { ...state.players }; - action.presence.users.forEach(user => { - const player = players[user.id] ?? { - clientId: user.id, - kv: new Map(), - }; - players[user.id] = { - ...player, - kv: user.kv ? user.kv : player.kv, - }; - }); - return { - ...state, - players, - }; - } - } -} diff --git a/multiplayer/src/state/collab/state.ts b/multiplayer/src/state/collab/state.ts deleted file mode 100644 index 0220542c1bc8..000000000000 --- a/multiplayer/src/state/collab/state.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { CollabPlayer } from "../../services/collabClient"; - -export type CollabState = { - players: { [playerId: string]: CollabPlayer }; - kv: Map; -}; - -export const initialState: CollabState = { - players: {}, - kv: new Map(), -}; diff --git a/multiplayer/src/state/state.ts b/multiplayer/src/state/state.ts index b6fe2f13569f..c4e60b90a2f6 100644 --- a/multiplayer/src/state/state.ts +++ b/multiplayer/src/state/state.ts @@ -39,6 +39,7 @@ export type AppState = { | undefined; }; targetConfig: pxt.TargetConfig | undefined; + collabMode: boolean; // ?collab=1 in URL }; export const initialAppState: AppState = { @@ -60,4 +61,5 @@ export const initialAppState: AppState = { deepLinks: {}, reactions: {}, targetConfig: undefined, + collabMode: /[?|&]collab=1/i.test(window.location.href), }; From 0efa3aaae20637b1073adb4d1e409e91f280a798 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 4 Apr 2024 12:58:13 -0700 Subject: [PATCH 12/12] preserve original url params --- pxtlib/auth.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pxtlib/auth.ts b/pxtlib/auth.ts index 3fa33ba3cc80..5cf5c7acb9cc 100644 --- a/pxtlib/auth.ts +++ b/pxtlib/auth.ts @@ -193,6 +193,7 @@ namespace pxt.auth { key: genId(), callbackState, callbackPathname: window.location.pathname, + callbackParams: pxt.U.parseQueryString(window.location.search), idp, persistent }; @@ -643,6 +644,7 @@ namespace pxt.auth { key: string; callbackState: CallbackState; callbackPathname: string; + callbackParams?: pxt.Map; idp: pxt.IdentityProviderId; authCodeVerifier?: string; persistent: boolean; @@ -724,7 +726,7 @@ namespace pxt.auth { // Clear url parameters and redirect to the callback location. const hash = callbackState.hash.startsWith('#') ? callbackState.hash : `#${callbackState.hash}`; - const params = pxt.Util.stringifyQueryString('', callbackState.params); + const params = pxt.Util.stringifyQueryString('', { ...callbackState.params, ...loginState.callbackParams }); const pathname = loginState.callbackPathname.startsWith('/') ? loginState.callbackPathname : `/${loginState.callbackPathname}`; const redirect = `${pathname}${params}${hash}`; window.location.href = redirect;