From 0aaa4394f630a0fe04545f5a705d637a47e7ddd8 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Tue, 29 Jul 2025 13:56:13 -0700 Subject: [PATCH 01/32] init: multiplayer subsystem --- fission/bun.lock | 13 + fission/package.json | 1 + .../systems/multiplayer/MultiplayerSystem.ts | 98 +++++++ fission/src/systems/multiplayer/types.ts | 49 ++++ multiplayer/.gitignore | 34 +++ multiplayer/README.md | 15 ++ multiplayer/bun.lock | 244 ++++++++++++++++++ multiplayer/package.json | 13 + multiplayer/server.ts | 15 ++ multiplayer/tsconfig.json | 29 +++ 10 files changed, 511 insertions(+) create mode 100644 fission/src/systems/multiplayer/MultiplayerSystem.ts create mode 100644 fission/src/systems/multiplayer/types.ts create mode 100644 multiplayer/.gitignore create mode 100644 multiplayer/README.md create mode 100644 multiplayer/bun.lock create mode 100644 multiplayer/package.json create mode 100644 multiplayer/server.ts create mode 100644 multiplayer/tsconfig.json diff --git a/fission/bun.lock b/fission/bun.lock index 8e753c0c59..35843e34c1 100644 --- a/fission/bun.lock +++ b/fission/bun.lock @@ -19,6 +19,7 @@ "colord": "^2.9.3", "framer-motion": "^10.18.0", "lygia": "^1.3.3", + "peerjs": "^1.5.5", "playwright": "^1.54.0", "postprocessing": "^6.37.6", "react": "^18.3.1", @@ -273,6 +274,8 @@ "@monogrid/gainmap-js": ["@monogrid/gainmap-js@3.1.0", "", { "dependencies": { "promise-worker-transferable": "^1.0.4" }, "peerDependencies": { "three": ">= 0.159.0" } }, "sha512-Obb0/gEd/HReTlg8ttaYk+0m62gQJmCblMOjHSMHRrBP2zdfKMHLCRbh/6ex9fSUJMKdjjIEiohwkbGD3wj2Nw=="], + "@msgpack/msgpack": ["@msgpack/msgpack@2.8.0", "", {}, "sha512-h9u4u/jiIRKbq25PM+zymTyW6bhTzELvOoUd+AvYriWOAKpLGnIamaET3pnHYoI5iYphAHBI4ayx0MehR+VVPQ=="], + "@mui/base": ["@mui/base@5.0.0-dev.20240529-082515-213b5e33ab", "", { "dependencies": { "@babel/runtime": "^7.24.6", "@floating-ui/react-dom": "^2.0.8", "@mui/types": "^7.2.14-dev.20240529-082515-213b5e33ab", "@mui/utils": "^6.0.0-dev.20240529-082515-213b5e33ab", "@popperjs/core": "^2.11.8", "clsx": "^2.1.1", "prop-types": "^15.8.1" }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0", "react": "^17.0.0 || ^18.0.0", "react-dom": "^17.0.0 || ^18.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-3ic6fc6BHstgM+MGqJEVx3zt9g5THxVXm3VVFUfdeplPqAWWgW2QoKfZDLT10s+pi+MAkpgEBP0kgRidf81Rsw=="], "@mui/core-downloads-tracker": ["@mui/core-downloads-tracker@5.17.1", "", {}, "sha512-OcZj+cs6EfUD39IoPBOgN61zf1XFVY+imsGoBDwXeSq2UHJZE3N59zzBOVjclck91Ne3e9gudONOeILvHCIhUA=="], @@ -817,6 +820,8 @@ "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + "eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], + "expect-type": ["expect-type@1.2.1", "", {}, "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], @@ -1185,6 +1190,10 @@ "pathval": ["pathval@2.0.0", "", {}, "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA=="], + "peerjs": ["peerjs@1.5.5", "", { "dependencies": { "@msgpack/msgpack": "^2.8.0", "eventemitter3": "^4.0.7", "peerjs-js-binarypack": "^2.1.0", "webrtc-adapter": "^9.0.0" } }, "sha512-viMUCPDL6CSfOu0ZqVcFqbWRXNHIbv2lPqNbrBIjbFYrflebOjItJ4hPfhjnuUCstqciHVu9vVJ7jFqqKi/EuQ=="], + + "peerjs-js-binarypack": ["peerjs-js-binarypack@2.1.0", "", {}, "sha512-YIwCC+pTzp3Bi8jPI9UFKO0t0SLo6xALnHkiNt/iUFmUUZG0fEEmEyFKvjsDKweiFitzHRyhuh6NvyJZ4nNxMg=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], @@ -1353,6 +1362,8 @@ "scheduler": ["scheduler@0.21.0", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ=="], + "sdp": ["sdp@3.2.1", "", {}, "sha512-lwsAIzOPlH8/7IIjjz3K0zYBk7aBVVcvjMwt3M4fLxpjMYyy7i3I97SLHebgn4YBjirkzfp3RvRDWSKsh/+WFw=="], + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], @@ -1545,6 +1556,8 @@ "webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], + "webrtc-adapter": ["webrtc-adapter@9.0.3", "", { "dependencies": { "sdp": "^3.2.0" } }, "sha512-5fALBcroIl31OeXAdd1YUntxiZl1eHlZZWzNg3U4Fn+J9/cGL3eT80YlrsWGvj2ojuz1rZr2OXkgCzIxAZ7vRQ=="], + "whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="], "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], diff --git a/fission/package.json b/fission/package.json index 7a69e93113..04ed48e979 100644 --- a/fission/package.json +++ b/fission/package.json @@ -37,6 +37,7 @@ "colord": "^2.9.3", "framer-motion": "^10.18.0", "lygia": "^1.3.3", + "peerjs": "^1.5.5", "playwright": "^1.54.0", "postprocessing": "^6.37.6", "react": "^18.3.1", diff --git a/fission/src/systems/multiplayer/MultiplayerSystem.ts b/fission/src/systems/multiplayer/MultiplayerSystem.ts new file mode 100644 index 0000000000..0adeb05523 --- /dev/null +++ b/fission/src/systems/multiplayer/MultiplayerSystem.ts @@ -0,0 +1,98 @@ +import Peer, { DataConnection } from "peerjs"; +import type { InitData, Message } from "./types"; + +class PeerConnection { + peer: Peer; + connection?: DataConnection; + clientId: string; + connected: boolean = false; + otherPeers: string[] = []; + initialization: InitData; + handlePeerMessage: (data: Message) => void; + + constructor( + handlePeerMessage: (data: Message) => void, + initialization: Omit, + ) { + this.clientId = this.generateClientId(); + this.peer = new Peer(this.clientId, { + host: "localhost", + port: 9000, + path: "/connect", + }); + this.initialization = { ...initialization, clientId: this.clientId }; + this.handlePeerMessage = handlePeerMessage; + + this.peer.on("open", (id: string) => { + console.log(`Client connected: ID - ${id}`); + this.connectToPeer(); // Replace 'some-peer-id' with the actual peer ID + }); + + this.peer.on("connection", (conn) => { + this.connection = conn; + this.setupConnectionHandlers(); + }); + } + + connectToPeer() { + this.peer.listAllPeers((peers) => { + console.log(`Peers: ${peers}`); + peers + .filter((peer) => peer !== this.clientId) + .forEach((peer) => this.otherPeers.push(peer as string)); + if (this.otherPeers.length > 0) + this.connection = this.peer.connect(this.otherPeers[0]!); + console.log(`Connection: ${this.connection?.peer}`); + this.setupConnectionHandlers(); + }); + } + + setupConnectionHandlers() { + if (!this.connection) return; + + this.connection.on("open", () => { + this.connected = true; + console.log("Connection opened"); + this.send({ type: "init", data: this.initialization }); + }); + + this.connection.on("data", (data: any) => { + this.handlePeerMessage(data); + }); + + this.connection.on("close", () => { + this.connected = false; + this.handlePeerMessage({ type: "robotLeft", data: { robotId: "" } }); + console.log("Connection closed"); + }); + + // this.connection.on("disconnected", () => { + // this.handlePeerMessage({ type: "robotLeft", data: { robotId: "" } }); + // }) + + this.connection.on("error", (err: Error) => { + console.error("Connection error:", err); + }); + } + + send(message: Message) { + if (!this.connection || !this.connected) return; + this.connection.send(message); + } + + generateClientId(): string { + return generateId("client"); + } + + getOtherPeerId(): string | null { + if (!this.connection) return null; + + return this.connection.peer; + } +} + +function generateId(root: string): string { + return `${root}-${Math.random().toString(36).substring(2, 9)}`; +} + +export default PeerConnection; diff --git a/fission/src/systems/multiplayer/types.ts b/fission/src/systems/multiplayer/types.ts new file mode 100644 index 0000000000..a0df03ee9a --- /dev/null +++ b/fission/src/systems/multiplayer/types.ts @@ -0,0 +1,49 @@ +export type Metrics = { + startTime: number; + totalFrames: number; + frameTimes: number[]; + inputsSent: number; + messagesReceived: number; + bytesReceived: number; + bytesSent: number; + connectionTime: number; + averageFPS: number; + networkStats: { + packetsLost: number; + roundTripTimes: number[]; + jitter: number; + }; +}; + +// TODO: Update this with Physics States instead of robots +// export type Message = +// | { type: "init"; data: InitData } +// | { type: "gameState"; data: GameStateData } +// | { type: "collision"; data: CollisionData } +// | { type: "robotJoined"; data: RobotJoinedData } +// | { type: "robotLeft"; data: RobotLeftData } +// | { type: "ping"; data: PingData } +// | { type: "pong"; data: PingData }; +// +// export type InitData = { +// clientId: string; +// robotId: string; +// worldSize: { height: number; width: number }; +// robots: Robot[]; +// }; +// +// export type GameStateData = { +// sequence: number; +// otherRobots: Robot[]; +// timestamp: number; +// }; +// +// export type CollisionData = { +// sequence: number; +// robots: Robot[]; +// timestamp: number; +// }; +// +// export type RobotJoinedData = Robot; +// export type RobotLeftData = { robotId: string }; +// export type PingData = { timestamp: number }; diff --git a/multiplayer/.gitignore b/multiplayer/.gitignore new file mode 100644 index 0000000000..a14702c409 --- /dev/null +++ b/multiplayer/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/multiplayer/README.md b/multiplayer/README.md new file mode 100644 index 0000000000..dfc757b0f4 --- /dev/null +++ b/multiplayer/README.md @@ -0,0 +1,15 @@ +# multiplayer + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run +``` + +This project was created using `bun init` in bun v1.2.16. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/multiplayer/bun.lock b/multiplayer/bun.lock new file mode 100644 index 0000000000..ce4e6033ba --- /dev/null +++ b/multiplayer/bun.lock @@ -0,0 +1,244 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "multiplayer", + "dependencies": { + "peer": "^1.0.2", + }, + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], + + "@types/bun": ["@types/bun@1.2.19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="], + + "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], + + "@types/express": ["@types/express@4.17.23", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "*" } }, "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ=="], + + "@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.6", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A=="], + + "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], + + "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], + + "@types/node": ["@types/node@24.1.0", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w=="], + + "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], + + "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], + + "@types/react": ["@types/react@19.1.9", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA=="], + + "@types/send": ["@types/send@0.17.5", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w=="], + + "@types/serve-static": ["@types/serve-static@1.15.8", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "*" } }, "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg=="], + + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + + "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="], + + "body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="], + + "bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="], + + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + + "cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="], + + "cookie-signature": ["cookie-signature@1.0.6", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="], + + "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], + + "debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + + "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + + "express": ["express@4.21.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA=="], + + "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], + + "finalhandler": ["finalhandler@1.3.1", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", "statuses": "2.0.1", "unpipe": "~1.0.0" } }, "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ=="], + + "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], + + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + + "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], + + "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], + + "merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], + + "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="], + + "mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], + + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], + + "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + + "path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="], + + "peer": ["peer@1.0.2", "", { "dependencies": { "@types/express": "^4.17.3", "@types/ws": "^7.2.3 || ^8.0.0", "cors": "^2.8.5", "express": "^4.17.1", "node-fetch": "^3.3.0", "ws": "^7.2.3 || ^8.0.0", "yargs": "^17.6.2" }, "bin": { "peerjs": "dist/bin/peerjs.js" } }, "sha512-ZObVEhAaoskd3KuSxr5DJLM8QuqQW4w3i0MqrI8H7Bzz8DjRC3DjUg2XtQQGfdc36+8Xk+wIPT/tL5wE+KnIqg=="], + + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + + "qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="], + + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="], + + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="], + + "serve-static": ["serve-static@1.16.2", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.19.0" } }, "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], + + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + + "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], + + "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + + "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], + + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + + "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], + + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + + "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], + + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], + + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], + + "send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + } +} diff --git a/multiplayer/package.json b/multiplayer/package.json new file mode 100644 index 0000000000..570f17285a --- /dev/null +++ b/multiplayer/package.json @@ -0,0 +1,13 @@ +{ + "name": "multiplayer", + "private": true, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + }, + "dependencies": { + "peer": "^1.0.2" + } +} diff --git a/multiplayer/server.ts b/multiplayer/server.ts new file mode 100644 index 0000000000..8544169e2f --- /dev/null +++ b/multiplayer/server.ts @@ -0,0 +1,15 @@ +import { PeerServer } from "peer"; + +export const PORT = 9000; + +const peerServer = PeerServer({ + path: "/", + port: PORT, + allow_discovery: true, +}); + +console.log(`WebRTC Connection Server Running on Port ${PORT}`); + +peerServer.on("connection", (client) => { + console.log(`Connection with client ${client.getId()}`); +}); diff --git a/multiplayer/tsconfig.json b/multiplayer/tsconfig.json new file mode 100644 index 0000000000..bfa0fead54 --- /dev/null +++ b/multiplayer/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} From 9ec9239533b1575a75dd83479e4de5c58812a3c0 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Tue, 29 Jul 2025 15:25:21 -0700 Subject: [PATCH 02/32] feat: update multiplayer system to use fission types --- fission/src/mirabuf/MirabufSceneObject.ts | 13 +- .../systems/multiplayer/MultiplayerSystem.ts | 148 ++++++++++++------ fission/src/systems/multiplayer/types.ts | 80 ++++++---- 3 files changed, 156 insertions(+), 85 deletions(-) diff --git a/fission/src/mirabuf/MirabufSceneObject.ts b/fission/src/mirabuf/MirabufSceneObject.ts index 1b1fb72f44..8ff1f00d20 100644 --- a/fission/src/mirabuf/MirabufSceneObject.ts +++ b/fission/src/mirabuf/MirabufSceneObject.ts @@ -655,7 +655,11 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier { * * @returns the object containing the width (x), height (y), and depth (z) dimensions in meters. */ - public getDimensionsWithoutRotation(): { width: number; height: number; depth: number } { + public getDimensionsWithoutRotation(): { + width: number + height: number + depth: number + } { const rootNodeId = this.getRootNodeId() if (!rootNodeId) { console.warn("No root node found for robot, using regular dimensions") @@ -821,7 +825,10 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier { } public getSupplierData(): ContextData { - const data: ContextData = { title: this.miraType == MiraType.ROBOT ? "A Robot" : "A Field", items: [] } + const data: ContextData = { + title: this.miraType == MiraType.ROBOT ? "A Robot" : "A Field", + items: [], + } data.items.push( { @@ -912,6 +919,8 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier { objectCollidedWith.robotLastInContactWith = this } } + + public getMultiplayerData(): MultiplayerObjectData {} } export async function createMirabuf( diff --git a/fission/src/systems/multiplayer/MultiplayerSystem.ts b/fission/src/systems/multiplayer/MultiplayerSystem.ts index 0adeb05523..ebc97ef543 100644 --- a/fission/src/systems/multiplayer/MultiplayerSystem.ts +++ b/fission/src/systems/multiplayer/MultiplayerSystem.ts @@ -1,93 +1,139 @@ import Peer, { DataConnection } from "peerjs"; -import type { InitData, Message } from "./types"; +import type { + ClientInfo as ClientInfo, + InitMultiplayerObjectData, + Message, +} from "./types"; +import PhysicsSystem from "../physics/PhysicsSystem"; +import MirabufSceneObject from "@/mirabuf/MirabufSceneObject"; +import World from "../World"; +import { PORT } from "../../../../multiplayer/server"; class PeerConnection { - peer: Peer; - connection?: DataConnection; - clientId: string; + serverConnection: Peer; + connections: DataConnection[] = []; + + info: ClientInfo; + isHost: boolean; connected: boolean = false; otherPeers: string[] = []; - initialization: InitData; + handlePeerMessage: (data: Message) => void; constructor( handlePeerMessage: (data: Message) => void, - initialization: Omit, + displayName: string = generateId("guest"), + isHost: boolean = false, ) { - this.clientId = this.generateClientId(); - this.peer = new Peer(this.clientId, { - host: "localhost", - port: 9000, + this.isHost = isHost; + this.info = { clientId: generateId("client"), displayName }; + this.serverConnection = new Peer(this.info.clientId, { + host: window.location.hostname, + port: PORT, path: "/connect", }); - this.initialization = { ...initialization, clientId: this.clientId }; + this.handlePeerMessage = handlePeerMessage; - this.peer.on("open", (id: string) => { + this.serverConnection.on("open", (id: string) => { console.log(`Client connected: ID - ${id}`); this.connectToPeer(); // Replace 'some-peer-id' with the actual peer ID }); - this.peer.on("connection", (conn) => { - this.connection = conn; + this.serverConnection.on("connection", (conn) => { + this.connections.push(conn); this.setupConnectionHandlers(); }); } connectToPeer() { - this.peer.listAllPeers((peers) => { + this.serverConnection.listAllPeers((peers) => { console.log(`Peers: ${peers}`); peers - .filter((peer) => peer !== this.clientId) + .filter((peer) => peer !== this.info.clientId) .forEach((peer) => this.otherPeers.push(peer as string)); if (this.otherPeers.length > 0) - this.connection = this.peer.connect(this.otherPeers[0]!); - console.log(`Connection: ${this.connection?.peer}`); + this.connections.push( + this.serverConnection.connect(this.otherPeers[0]!), + ); + console.log( + `Connections: ${this.connections?.map((c) => c.connectionId)}`, + ); this.setupConnectionHandlers(); }); } - setupConnectionHandlers() { - if (!this.connection) return; - - this.connection.on("open", () => { - this.connected = true; - console.log("Connection opened"); - this.send({ type: "init", data: this.initialization }); - }); - - this.connection.on("data", (data: any) => { - this.handlePeerMessage(data); - }); - - this.connection.on("close", () => { - this.connected = false; - this.handlePeerMessage({ type: "robotLeft", data: { robotId: "" } }); - console.log("Connection closed"); - }); - - // this.connection.on("disconnected", () => { - // this.handlePeerMessage({ type: "robotLeft", data: { robotId: "" } }); - // }) + // Called by the host, initializes the world with some defined set of objects, robots can be spawned in later + initWorld(physicsSystem: PhysicsSystem) { + const sceneObjects: InitMultiplayerObjectData[] = [ + ...World.sceneRenderer.sceneObjects.entries(), + ] + .filter( + (sceneObjectPair): sceneObjectPair is [number, MirabufSceneObject] => + sceneObjectPair[1] instanceof MirabufSceneObject, + ) + .map(([key, sceneObject]) => { + return { + key, + sceneObject, + }; + }); + const message: Message = { + type: "init", + data: { physicsSystem, objects: sceneObjects }, + }; + this.connections.forEach((c) => c.send(message)); + } - this.connection.on("error", (err: Error) => { - console.error("Connection error:", err); + setupConnectionHandlers() { + if (this.connections.length === 0) return; + + this.connections.forEach((connection) => { + connection.on("open", () => { + this.connected = true; + console.log("Connection opened"); + this.emit({ type: "info", data: this.info }); + }); + + connection.on("data", (data: unknown) => { + this.handlePeerMessage(data as Message); + }); + + connection.on("close", () => { + this.connected = false; + this.handlePeerMessage({ + type: "robotLeft", + data: { sceneObjectKey: 0 }, // TODO Get actual sceneObjectKey + }); + console.log("Connection closed"); + }); + + // this.connection.on("disconnected", () => { + // this.handlePeerMessage({ type: "robotLeft", data: { robotId: "" } }); + // }) + + connection.on("error", (err: Error) => { + console.error("Connection error:", err); + }); }); } - send(message: Message) { - if (!this.connection || !this.connected) return; - this.connection.send(message); + emit(message: Message) { + this.connections.forEach((connection) => connection.send(message)); } - generateClientId(): string { - return generateId("client"); - } + send(message: Message, clientId: string) { + if (this.connections.length === 0 || !this.connected) return; + const connection = this.connections.find( + (conn) => conn.connectionId === clientId, + ); + if (!connection) return; - getOtherPeerId(): string | null { - if (!this.connection) return null; + connection.send(message); + } - return this.connection.peer; + getOtherPeerIds(): string[] { + return this.connections.map((c) => c.peer); } } diff --git a/fission/src/systems/multiplayer/types.ts b/fission/src/systems/multiplayer/types.ts index a0df03ee9a..6868150129 100644 --- a/fission/src/systems/multiplayer/types.ts +++ b/fission/src/systems/multiplayer/types.ts @@ -1,3 +1,8 @@ +import MirabufInstance from "@/mirabuf/MirabufInstance"; +import Mechanism from "../physics/Mechanism"; +import PhysicsSystem from "../physics/PhysicsSystem"; +import MirabufSceneObject from "@/mirabuf/MirabufSceneObject"; + export type Metrics = { startTime: number; totalFrames: number; @@ -15,35 +20,46 @@ export type Metrics = { }; }; -// TODO: Update this with Physics States instead of robots -// export type Message = -// | { type: "init"; data: InitData } -// | { type: "gameState"; data: GameStateData } -// | { type: "collision"; data: CollisionData } -// | { type: "robotJoined"; data: RobotJoinedData } -// | { type: "robotLeft"; data: RobotLeftData } -// | { type: "ping"; data: PingData } -// | { type: "pong"; data: PingData }; -// -// export type InitData = { -// clientId: string; -// robotId: string; -// worldSize: { height: number; width: number }; -// robots: Robot[]; -// }; -// -// export type GameStateData = { -// sequence: number; -// otherRobots: Robot[]; -// timestamp: number; -// }; -// -// export type CollisionData = { -// sequence: number; -// robots: Robot[]; -// timestamp: number; -// }; -// -// export type RobotJoinedData = Robot; -// export type RobotLeftData = { robotId: string }; -// export type PingData = { timestamp: number }; +export type Message = + // Represents the initial information given in thee lobby or smth + | { type: "info"; data: ClientInfo } + | { type: "init"; data: InitData } + | { type: "update"; data: UpdateMultiplayerObjectData[] } + | { type: "collision"; data: CollisionData } + | { type: "newObject"; data: InitMultiplayerObjectData } + | { type: "robotLeft"; data: RobotLeftData } + | { type: "ping"; data: PingData } + | { type: "pong"; data: PingData }; + +export type ClientInfo = { + displayName: string; + clientId: string; +}; + +// TODO: Figure out if InitMultiplayerObjectData is still necessary +export type InitData = { + physicsSystem: PhysicsSystem; + objects: InitMultiplayerObjectData[]; // We need to send the entire scene object with rendering data and configuration (for fields and such) +}; + +export type UpdateMultiplayerObjectData = { + sceneObjectKey: number; + mechanism: Mechanism; + instance: MirabufInstance; +}; + +export type InitMultiplayerObjectData = { + key: number; // TODO Check if we actually have to sync up keys (i think it's best if we do) + sceneObject: MirabufSceneObject; +}; + +export type CollisionData = { + physicsSystem: PhysicsSystem; + sceneObject: Map; +}; + +export type RobotLeftData = { + sceneObjectKey: number; +}; + +export type PingData = { timestamp: number }; From 02bbb4f7ace32ed667f64a2a5afd44139510362c Mon Sep 17 00:00:00 2001 From: Zach Rutman Date: Tue, 29 Jul 2025 16:03:26 -0700 Subject: [PATCH 03/32] feat: base multiplayer lobby system --- fission/src/Synthesis.tsx | 24 ++ fission/src/Window.d.ts | 1 + .../systems/multiplayer/MultiplayerSystem.ts | 309 ++++++++++-------- fission/src/ui/modals/MainMenuModal.tsx | 24 +- multiplayer/package.json | 3 + multiplayer/server.ts | 15 +- 6 files changed, 227 insertions(+), 149 deletions(-) diff --git a/fission/src/Synthesis.tsx b/fission/src/Synthesis.tsx index 743c148b47..5e68f0849b 100644 --- a/fission/src/Synthesis.tsx +++ b/fission/src/Synthesis.tsx @@ -71,6 +71,8 @@ import GraphicsSettings from "./ui/panels/GraphicsSettingsPanel.tsx" import AutoTestPanel from "./ui/panels/simulation/AutoTestPanel.tsx" import WiringPanel from "./ui/panels/simulation/WiringPanel.tsx" import WSViewPanel from "./ui/panels/WSViewPanel.tsx" +import MultiplayerSystem from "@/systems/multiplayer/MultiplayerSystem.ts"; +import PeerConnection from "@/systems/multiplayer/MultiplayerSystem.ts"; const Synthesis: React.FC = () => { const { openModal, closeModal, getActiveModalElement, registerModal, activeModalId } = @@ -111,6 +113,10 @@ const Synthesis: React.FC = () => { World.sceneRenderer.updateSkyboxColors(defaultTheme) }} + + startMultiplayerCallback={async () => { + + }} /> ), }) @@ -122,6 +128,24 @@ const Synthesis: React.FC = () => { window.close() return } + const startMultiplayer = async () => { + const roomId = urlParams.get("roomId") + let client: PeerConnection + if (roomId) { + client = await MultiplayerSystem.create((peer, msg) => { + console.log({peer, msg}) + }, roomId) + } else { + client = await MultiplayerSystem.createHost((peer, msg) => { + console.log({peer, msg}) + }) + } + console.log({room: client.roomId}) + console.log(client) + window.multiplayer = client + } + void startMultiplayer() + openModal("main-menu") // Cleanup return () => { diff --git a/fission/src/Window.d.ts b/fission/src/Window.d.ts index d8c67b5981..de3bbd5ba1 100644 --- a/fission/src/Window.d.ts +++ b/fission/src/Window.d.ts @@ -1,4 +1,5 @@ declare interface Window { convertAuthToken(code: string): void gtag: () => void + multiplayer:unknown // todo: remove } diff --git a/fission/src/systems/multiplayer/MultiplayerSystem.ts b/fission/src/systems/multiplayer/MultiplayerSystem.ts index ebc97ef543..ee552136ee 100644 --- a/fission/src/systems/multiplayer/MultiplayerSystem.ts +++ b/fission/src/systems/multiplayer/MultiplayerSystem.ts @@ -1,144 +1,177 @@ -import Peer, { DataConnection } from "peerjs"; -import type { - ClientInfo as ClientInfo, - InitMultiplayerObjectData, - Message, -} from "./types"; -import PhysicsSystem from "../physics/PhysicsSystem"; -import MirabufSceneObject from "@/mirabuf/MirabufSceneObject"; -import World from "../World"; -import { PORT } from "../../../../multiplayer/server"; +import Peer, { DataConnection } from "peerjs" +import MirabufSceneObject from "@/mirabuf/MirabufSceneObject" +import PhysicsSystem from "../physics/PhysicsSystem" +import World from "../World" +import type { ClientInfo, InitMultiplayerObjectData, Message } from "./types" class PeerConnection { - serverConnection: Peer; - connections: DataConnection[] = []; - - info: ClientInfo; - isHost: boolean; - connected: boolean = false; - otherPeers: string[] = []; - - handlePeerMessage: (data: Message) => void; - - constructor( - handlePeerMessage: (data: Message) => void, - displayName: string = generateId("guest"), - isHost: boolean = false, - ) { - this.isHost = isHost; - this.info = { clientId: generateId("client"), displayName }; - this.serverConnection = new Peer(this.info.clientId, { - host: window.location.hostname, - port: PORT, - path: "/connect", - }); - - this.handlePeerMessage = handlePeerMessage; - - this.serverConnection.on("open", (id: string) => { - console.log(`Client connected: ID - ${id}`); - this.connectToPeer(); // Replace 'some-peer-id' with the actual peer ID - }); - - this.serverConnection.on("connection", (conn) => { - this.connections.push(conn); - this.setupConnectionHandlers(); - }); - } - - connectToPeer() { - this.serverConnection.listAllPeers((peers) => { - console.log(`Peers: ${peers}`); - peers - .filter((peer) => peer !== this.info.clientId) - .forEach((peer) => this.otherPeers.push(peer as string)); - if (this.otherPeers.length > 0) - this.connections.push( - this.serverConnection.connect(this.otherPeers[0]!), - ); - console.log( - `Connections: ${this.connections?.map((c) => c.connectionId)}`, - ); - this.setupConnectionHandlers(); - }); - } - - // Called by the host, initializes the world with some defined set of objects, robots can be spawned in later - initWorld(physicsSystem: PhysicsSystem) { - const sceneObjects: InitMultiplayerObjectData[] = [ - ...World.sceneRenderer.sceneObjects.entries(), - ] - .filter( - (sceneObjectPair): sceneObjectPair is [number, MirabufSceneObject] => - sceneObjectPair[1] instanceof MirabufSceneObject, - ) - .map(([key, sceneObject]) => { - return { - key, - sceneObject, - }; - }); - const message: Message = { - type: "init", - data: { physicsSystem, objects: sceneObjects }, - }; - this.connections.forEach((c) => c.send(message)); - } - - setupConnectionHandlers() { - if (this.connections.length === 0) return; - - this.connections.forEach((connection) => { - connection.on("open", () => { - this.connected = true; - console.log("Connection opened"); - this.emit({ type: "info", data: this.info }); - }); - - connection.on("data", (data: unknown) => { - this.handlePeerMessage(data as Message); - }); - - connection.on("close", () => { - this.connected = false; - this.handlePeerMessage({ - type: "robotLeft", - data: { sceneObjectKey: 0 }, // TODO Get actual sceneObjectKey - }); - console.log("Connection closed"); - }); - - // this.connection.on("disconnected", () => { - // this.handlePeerMessage({ type: "robotLeft", data: { robotId: "" } }); - // }) - - connection.on("error", (err: Error) => { - console.error("Connection error:", err); - }); - }); - } - - emit(message: Message) { - this.connections.forEach((connection) => connection.send(message)); - } - - send(message: Message, clientId: string) { - if (this.connections.length === 0 || !this.connected) return; - const connection = this.connections.find( - (conn) => conn.connectionId === clientId, - ); - if (!connection) return; - - connection.send(message); - } - - getOtherPeerIds(): string[] { - return this.connections.map((c) => c.peer); - } + readonly client: Peer + readonly roomId: string + readonly connections: DataConnection[] = [] + readonly clientId: string + isHost: boolean = false + info: ClientInfo + readonly handlePeerMessage: (peer: string, data: Message) => void + + public static async create( + handlePeerMessage: (peer: string, data: Message) => void, + roomId: string + ): Promise { + const clientId = await generateId(roomId) + return new PeerConnection(handlePeerMessage, roomId, clientId) + } + + public static async createHost(handlePeerMessage: (peer: string, data: Message) => void): Promise { + const room = Math.random().toString(10).substring(2, 9) + return this.create(handlePeerMessage, room) + } + + private constructor(handlePeerMessage: (peer: string, data: Message) => void, roomId: string, clientId: string) { + this.roomId = roomId + this.clientId = clientId + this.info = { clientId: this.clientId, displayName: this.clientId } + this.client = new Peer(this.clientId, { + host: window.location.hostname, + port: 9000, + path: "/", + }) + this.client.on("error", console.log) + this.client.on("disconnected", console.log) + this.client.on("call", console.log) + this.client.on("close", console.log) + this.handlePeerMessage = handlePeerMessage + + this.client.on("open", async (id: string) => { + console.log(`Broker connection opened: ID - ${id}`) + await this.connectToRoom() + }) + + this.client.on("connection", async conn => { + console.log("Receiving Connection: ", conn.peer) + if (conn.metadata.authHash != (await createSha256Hash(this.roomId + conn.peer + this.clientId))) { + conn.close() + console.warn("Blocking unauthorized connection from " + conn.peer) + return + } + this.setupConnectionHandlers(conn) + }) + } + + async connectToRoom() { + const roomHash = await createSha256Hash(this.roomId) + + const peersPromise = new Promise(resolve => this.client.listAllPeers(resolve)) + const peers = await peersPromise + + console.log(`Peers: ${peers}`) + + await Promise.all( + peers + .filter(peer => peer !== this.clientId && peer.split("-")[1] == roomHash) + .map(async peer => { + const conn = this.client.connect(peer, { + metadata: { + authHash: await createSha256Hash(this.roomId + this.clientId + peer), + }, + }) + this.setupConnectionHandlers(conn) + + console.log(`Initiating Connection: ${peer}`) + }) + ) + } + + // Called by the host, initializes the world with some defined set of objects, robots can be spawned in later + initWorld(physicsSystem: PhysicsSystem) { + const sceneObjects: InitMultiplayerObjectData[] = [...World.sceneRenderer.sceneObjects.entries()] + .filter( + (sceneObjectPair): sceneObjectPair is [number, MirabufSceneObject] => + sceneObjectPair[1] instanceof MirabufSceneObject + ) + .map(([key, sceneObject]) => { + return { + key, + sceneObject, + } + }) + const message: Message = { + type: "init", + data: { physicsSystem, objects: sceneObjects }, + } + this.connections.forEach(c => c.send(message)) + } + + setupConnectionHandlers(conn: DataConnection) { + if (this.connections.includes(conn)) { + console.warn("Setting up connection for", conn.peer, "again") + return + } + conn.on("open", async () => { + console.log("Connection opened") + this.connections.push(conn) + await this.send(conn.peer, { type: "info", data: this.info }) + }) + + conn.on("data", (data: unknown) => { + this.handlePeerMessage(conn.peer, data as Message) + }) + + conn.on("close", () => { + this.handlePeerMessage(conn.peer, { type: "robotLeft", data: { sceneObjectKey: 0 } }) // TODO Get actual sceneObjectKey + this.connections.splice( + this.connections.findIndex(c => c == conn), + 1 + ) + console.log("Connection closed:", conn.peer) + }) + conn.on("iceStateChanged", console.log) + + conn.on("error", (err: Error) => { + console.error("Connection error:", err) + }) + } + + async send(peer: string, message: Message) { + const conn = this.connections.find(c => c.peer == peer) + if (!conn) { + console.warn("Can't find peer", peer) + return + } + await conn.send(message) + } + async broadcast(message: Message) { + return await Promise.all(this.connections.map(connection => connection.send(message))) + } + + getOtherPeerIds(): string[] { + return this.connections.map(c => c.peer) + } } -function generateId(root: string): string { - return `${root}-${Math.random().toString(36).substring(2, 9)}`; +const localStorageKey = "multiplayer_clientid" + +async function generateId(roomId?: string): Promise { + let id = + (import.meta.env.DEV ? new URLSearchParams(window.location.search).get("uid") : undefined) ?? + window.localStorage.getItem(localStorageKey) + if (id == null) { + id = `client_${Math.random().toString(36).substring(2, 9)}` + window.localStorage.setItem(localStorageKey, id) + } + if (roomId) { + return `${id}-${await createSha256Hash(roomId)}` + } + return id } -export default PeerConnection; +async function createSha256Hash(msg: string) { + const msgBuffer = new TextEncoder().encode(msg) + const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer) + const hashArray = Array.from(new Uint8Array(hashBuffer)) + return hashArray + .slice(0, 8) + .map(b => b.toString(16).padStart(2, "0")) + .join("") +} + +export default PeerConnection diff --git a/fission/src/ui/modals/MainMenuModal.tsx b/fission/src/ui/modals/MainMenuModal.tsx index 1ee1115f88..ad679ab2c6 100644 --- a/fission/src/ui/modals/MainMenuModal.tsx +++ b/fission/src/ui/modals/MainMenuModal.tsx @@ -1,15 +1,18 @@ import React from "react" import Button from "@/components/Button.tsx" -import { globalAddToast } from "@/components/GlobalUIControls.ts" -import Modal, { ModalPropsImpl } from "@/components/Modal" -import { SynthesisIcons } from "../components/StyledComponents" -import { useModalControlContext } from "../helpers/UseModalManager" +import Modal, {ModalPropsImpl} from "@/components/Modal" +import {SynthesisIcons} from "../components/StyledComponents" +import {useModalControlContext} from "../helpers/UseModalManager" -const MainMenuModal: React.FC void }> = ({ - modalId, - startSingleplayerCallback, -}) => { - const { closeModal } = useModalControlContext() +const MainMenuModal: React.FC void, + startMultiplayerCallback: () => void +}> = ({ + modalId, + startSingleplayerCallback, + startMultiplayerCallback + }) => { + const {closeModal} = useModalControlContext() return ( { - globalAddToast("error", "Not Supported", "Multiplayer is not yet supported. Come back soon!") + closeModal() + startMultiplayerCallback() }} className="w-full mt-1 mb-3" /> diff --git a/multiplayer/package.json b/multiplayer/package.json index 570f17285a..fa017c4a93 100644 --- a/multiplayer/package.json +++ b/multiplayer/package.json @@ -1,6 +1,9 @@ { "name": "multiplayer", "private": true, + "scripts": { + "start": "bun server.ts" + }, "devDependencies": { "@types/bun": "latest" }, diff --git a/multiplayer/server.ts b/multiplayer/server.ts index 8544169e2f..a1d371b1ee 100644 --- a/multiplayer/server.ts +++ b/multiplayer/server.ts @@ -9,7 +9,20 @@ const peerServer = PeerServer({ }); console.log(`WebRTC Connection Server Running on Port ${PORT}`); - +const connected = new Set() peerServer.on("connection", (client) => { console.log(`Connection with client ${client.getId()}`); + connected.add(client.getId()) }); +peerServer.on("disconnect", (client) => { + console.log(`Client ${client.getId()} disconnecting`) + + connected.delete(client.getId()) +}) + +setInterval(() => { + let string = "" + connected.forEach((id) => string+=id+" ") + console.log(string) +}, 15000) +// peerServer.on("") \ No newline at end of file From 21ebb2d3bd8c7cd654fb96b87f13b620ddf177f7 Mon Sep 17 00:00:00 2001 From: Zach Rutman Date: Tue, 29 Jul 2025 16:19:42 -0700 Subject: [PATCH 04/32] fix: make initWorld use broadcast --- fission/src/systems/multiplayer/MultiplayerSystem.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/fission/src/systems/multiplayer/MultiplayerSystem.ts b/fission/src/systems/multiplayer/MultiplayerSystem.ts index ee552136ee..10c828086e 100644 --- a/fission/src/systems/multiplayer/MultiplayerSystem.ts +++ b/fission/src/systems/multiplayer/MultiplayerSystem.ts @@ -82,7 +82,7 @@ class PeerConnection { } // Called by the host, initializes the world with some defined set of objects, robots can be spawned in later - initWorld(physicsSystem: PhysicsSystem) { + async initWorld(physicsSystem: PhysicsSystem) { const sceneObjects: InitMultiplayerObjectData[] = [...World.sceneRenderer.sceneObjects.entries()] .filter( (sceneObjectPair): sceneObjectPair is [number, MirabufSceneObject] => @@ -94,11 +94,10 @@ class PeerConnection { sceneObject, } }) - const message: Message = { - type: "init", - data: { physicsSystem, objects: sceneObjects }, - } - this.connections.forEach(c => c.send(message)) + await this.broadcast({ + type: "init", + data: { physicsSystem, objects: sceneObjects }, + }) } setupConnectionHandlers(conn: DataConnection) { From cf5330c58d7b2f8975ef441d73a24bee782edd95 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Wed, 30 Jul 2025 08:22:34 -0700 Subject: [PATCH 05/32] Merge --- fission/src/Synthesis.tsx | 20 +--- fission/src/Window.d.ts | 2 +- fission/src/mirabuf/MirabufSceneObject.ts | 8 +- fission/src/systems/World.ts | 10 ++ .../systems/multiplayer/MultiplayerSystem.ts | 108 ++++++++++++++---- fission/src/systems/multiplayer/types.ts | 97 ++++++++-------- fission/src/systems/scene/SceneRenderer.ts | 7 +- fission/src/ui/modals/MainMenuModal.tsx | 22 ++-- 8 files changed, 173 insertions(+), 101 deletions(-) diff --git a/fission/src/Synthesis.tsx b/fission/src/Synthesis.tsx index 5e68f0849b..64ea9720b5 100644 --- a/fission/src/Synthesis.tsx +++ b/fission/src/Synthesis.tsx @@ -71,8 +71,7 @@ import GraphicsSettings from "./ui/panels/GraphicsSettingsPanel.tsx" import AutoTestPanel from "./ui/panels/simulation/AutoTestPanel.tsx" import WiringPanel from "./ui/panels/simulation/WiringPanel.tsx" import WSViewPanel from "./ui/panels/WSViewPanel.tsx" -import MultiplayerSystem from "@/systems/multiplayer/MultiplayerSystem.ts"; -import PeerConnection from "@/systems/multiplayer/MultiplayerSystem.ts"; +import MultiplayerSystem from "@/systems/multiplayer/MultiplayerSystem.ts" const Synthesis: React.FC = () => { const { openModal, closeModal, getActiveModalElement, registerModal, activeModalId } = @@ -113,10 +112,7 @@ const Synthesis: React.FC = () => { World.sceneRenderer.updateSkyboxColors(defaultTheme) }} - - startMultiplayerCallback={async () => { - - }} + startMultiplayerCallback={async () => {}} /> ), }) @@ -130,17 +126,13 @@ const Synthesis: React.FC = () => { } const startMultiplayer = async () => { const roomId = urlParams.get("roomId") - let client: PeerConnection + let client: MultiplayerSystem if (roomId) { - client = await MultiplayerSystem.create((peer, msg) => { - console.log({peer, msg}) - }, roomId) + client = await MultiplayerSystem.create(roomId) } else { - client = await MultiplayerSystem.createHost((peer, msg) => { - console.log({peer, msg}) - }) + client = await MultiplayerSystem.createHost() } - console.log({room: client.roomId}) + console.log({ room: client.roomId }) console.log(client) window.multiplayer = client } diff --git a/fission/src/Window.d.ts b/fission/src/Window.d.ts index de3bbd5ba1..6815c0552f 100644 --- a/fission/src/Window.d.ts +++ b/fission/src/Window.d.ts @@ -1,5 +1,5 @@ declare interface Window { convertAuthToken(code: string): void gtag: () => void - multiplayer:unknown // todo: remove + multiplayer: unknown // todo: remove } diff --git a/fission/src/mirabuf/MirabufSceneObject.ts b/fission/src/mirabuf/MirabufSceneObject.ts index 8ff1f00d20..63b5d529ce 100644 --- a/fission/src/mirabuf/MirabufSceneObject.ts +++ b/fission/src/mirabuf/MirabufSceneObject.ts @@ -114,6 +114,12 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier { public set ejectorActive(a: boolean) { this._ejectorActive = a } + public set mechanism(a: Mechanism) { + this._mechanism = a + } + public set mirabufInstance(a: MirabufInstance) { + this.mirabufInstance = a + } get mirabufInstance() { return this._mirabufInstance @@ -919,8 +925,6 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier { objectCollidedWith.robotLastInContactWith = this } } - - public getMultiplayerData(): MultiplayerObjectData {} } export async function createMirabuf( diff --git a/fission/src/systems/World.ts b/fission/src/systems/World.ts index e8db010709..762b3d6ee6 100644 --- a/fission/src/systems/World.ts +++ b/fission/src/systems/World.ts @@ -7,6 +7,7 @@ import DragModeSystem from "./scene/DragModeSystem" import SceneRenderer from "./scene/SceneRenderer" import SimulationSystem from "./simulation/SimulationSystem" import RobotDimensionTracker from "./match_mode/RobotDimensionTracker" +import MultiplayerSystem from "./multiplayer/MultiplayerSystem" class World { private static _isAlive: boolean = false @@ -17,6 +18,7 @@ class World { private static _physicsSystem: PhysicsSystem private static _simulationSystem: SimulationSystem private static _inputSystem: InputSystem + private static _multiplayerSystem?: MultiplayerSystem private static _analyticsSystem: AnalyticsSystem | undefined = undefined private static _dragModeSystem: DragModeSystem private static _performanceMonitorSystem: PerformanceMonitoringSystem @@ -50,6 +52,9 @@ class World { public static get inputSystem() { return World._inputSystem } + public static get multiplayerSystem() { + return World._multiplayerSystem + } public static get analyticsSystem() { return World._analyticsSystem } @@ -57,6 +62,10 @@ class World { return World._dragModeSystem } + public static set physicsSystem(system: PhysicsSystem) { + World.physicsSystem = system + } + public static resetAccumTimes() { this._accumTimes = { frames: 0, @@ -80,6 +89,7 @@ class World { World._inputSystem = new InputSystem() World._dragModeSystem = new DragModeSystem() World._performanceMonitorSystem = new PerformanceMonitoringSystem() + try { World._analyticsSystem = new AnalyticsSystem() } catch (_) { diff --git a/fission/src/systems/multiplayer/MultiplayerSystem.ts b/fission/src/systems/multiplayer/MultiplayerSystem.ts index 10c828086e..5f863966a9 100644 --- a/fission/src/systems/multiplayer/MultiplayerSystem.ts +++ b/fission/src/systems/multiplayer/MultiplayerSystem.ts @@ -2,34 +2,43 @@ import Peer, { DataConnection } from "peerjs" import MirabufSceneObject from "@/mirabuf/MirabufSceneObject" import PhysicsSystem from "../physics/PhysicsSystem" import World from "../World" -import type { ClientInfo, InitMultiplayerObjectData, Message } from "./types" - -class PeerConnection { +import type { + ClientInfo, + CollisionData, + InitData, + InitObjectData, + Message, + UpdateMultiplayerObjectData as UpdateObjectData, +} from "./types" + +const COLLISION_TIMEOUT = 500 + +class MultiplayerSystem { readonly client: Peer readonly roomId: string readonly connections: DataConnection[] = [] readonly clientId: string - isHost: boolean = false + info: ClientInfo - readonly handlePeerMessage: (peer: string, data: Message) => void + lastSentCollisionTimestamp: number = Date.now() + connected: boolean = false + otherPeers: string[] = [] - public static async create( - handlePeerMessage: (peer: string, data: Message) => void, - roomId: string - ): Promise { + public static async create(roomId: string, isHost: boolean = false): Promise { const clientId = await generateId(roomId) - return new PeerConnection(handlePeerMessage, roomId, clientId) + return new MultiplayerSystem(roomId, clientId, isHost) } - public static async createHost(handlePeerMessage: (peer: string, data: Message) => void): Promise { + public static async createHost(): Promise { const room = Math.random().toString(10).substring(2, 9) - return this.create(handlePeerMessage, room) + return this.create(room, true) } - private constructor(handlePeerMessage: (peer: string, data: Message) => void, roomId: string, clientId: string) { + private constructor(roomId: string, clientId: string, isHost: boolean = false) { this.roomId = roomId this.clientId = clientId - this.info = { clientId: this.clientId, displayName: this.clientId } + this.info = { clientId: this.clientId, displayName: this.clientId, isHost } + this.client = new Peer(this.clientId, { host: window.location.hostname, port: 9000, @@ -39,7 +48,6 @@ class PeerConnection { this.client.on("disconnected", console.log) this.client.on("call", console.log) this.client.on("close", console.log) - this.handlePeerMessage = handlePeerMessage this.client.on("open", async (id: string) => { console.log(`Broker connection opened: ID - ${id}`) @@ -83,7 +91,7 @@ class PeerConnection { // Called by the host, initializes the world with some defined set of objects, robots can be spawned in later async initWorld(physicsSystem: PhysicsSystem) { - const sceneObjects: InitMultiplayerObjectData[] = [...World.sceneRenderer.sceneObjects.entries()] + const sceneObjects: InitObjectData[] = [...World.sceneRenderer.sceneObjects.entries()] .filter( (sceneObjectPair): sceneObjectPair is [number, MirabufSceneObject] => sceneObjectPair[1] instanceof MirabufSceneObject @@ -95,8 +103,8 @@ class PeerConnection { } }) await this.broadcast({ - type: "init", - data: { physicsSystem, objects: sceneObjects }, + type: "init", + data: { physicsSystem, objects: sceneObjects }, }) } @@ -108,15 +116,18 @@ class PeerConnection { conn.on("open", async () => { console.log("Connection opened") this.connections.push(conn) - await this.send(conn.peer, { type: "info", data: this.info }) + this.send(conn.peer, { type: "info", data: this.info }) }) conn.on("data", (data: unknown) => { - this.handlePeerMessage(conn.peer, data as Message) + this.handlePeerMessage(data as Message) }) conn.on("close", () => { - this.handlePeerMessage(conn.peer, { type: "robotLeft", data: { sceneObjectKey: 0 } }) // TODO Get actual sceneObjectKey + this.handlePeerMessage({ + type: "robotLeft", + data: { sceneObjectKey: 0 }, + }) // TODO Get actual sceneObjectKey this.connections.splice( this.connections.findIndex(c => c == conn), 1 @@ -130,14 +141,65 @@ class PeerConnection { }) } + handlePeerMessage(message: Message) { + switch (message.type) { + case "info": + this.handlePeerInfo(message.data) + break + case "init": + this.handleWorldInitialization(message.data) + break + case "update": + this.handlePeerUpdate(message.data) + break + case "collision": + this.handleCollision(message.data) + break + case "newObject": + this.handleNewObject(message.data) + break + } + } + + handlePeerInfo(data: ClientInfo) {} + handleWorldInitialization(data: InitData) {} + handlePeerUpdate(data: UpdateObjectData[]) { + data.forEach(({ sceneObjectKey, mechanism, instance }) => { + const sceneObject = World.sceneRenderer.sceneObjects.get(sceneObjectKey) + if (sceneObject == null) { + console.error( + `Multiplayer SceneObject: ${sceneObjectKey} not found in sceneObjects map. Multiplayer SceneObjects must be initialized before being updated.` + ) + return + } else if (!(sceneObject instanceof MirabufSceneObject)) { + console.error(`Multiplayer SceneObject: ${sceneObjectKey} not MirabufSceneObject`) + return + } + sceneObject.mechanism = mechanism + sceneObject.mirabufInstance = instance + }) + } + + handleCollision(data: CollisionData) { + if (this.lastSentCollisionTimestamp < COLLISION_TIMEOUT) return + + World.physicsSystem = data.physicsSystem + World.sceneRenderer.sceneObjects = data.sceneObject + } + + handleNewObject(data: InitObjectData) { + World.sceneRenderer.sceneObjects.set(data.key, data.sceneObject) + } + async send(peer: string, message: Message) { const conn = this.connections.find(c => c.peer == peer) if (!conn) { - console.warn("Can't find peer", peer) + console.warn("Couldn't find peer: ", peer) return } await conn.send(message) } + async broadcast(message: Message) { return await Promise.all(this.connections.map(connection => connection.send(message))) } @@ -173,4 +235,4 @@ async function createSha256Hash(msg: string) { .join("") } -export default PeerConnection +export default MultiplayerSystem diff --git a/fission/src/systems/multiplayer/types.ts b/fission/src/systems/multiplayer/types.ts index 6868150129..2ed92b6b06 100644 --- a/fission/src/systems/multiplayer/types.ts +++ b/fission/src/systems/multiplayer/types.ts @@ -1,65 +1,66 @@ -import MirabufInstance from "@/mirabuf/MirabufInstance"; -import Mechanism from "../physics/Mechanism"; -import PhysicsSystem from "../physics/PhysicsSystem"; -import MirabufSceneObject from "@/mirabuf/MirabufSceneObject"; +import MirabufInstance from "@/mirabuf/MirabufInstance" +import Mechanism from "../physics/Mechanism" +import PhysicsSystem from "../physics/PhysicsSystem" +import MirabufSceneObject from "@/mirabuf/MirabufSceneObject" export type Metrics = { - startTime: number; - totalFrames: number; - frameTimes: number[]; - inputsSent: number; - messagesReceived: number; - bytesReceived: number; - bytesSent: number; - connectionTime: number; - averageFPS: number; - networkStats: { - packetsLost: number; - roundTripTimes: number[]; - jitter: number; - }; -}; + startTime: number + totalFrames: number + frameTimes: number[] + inputsSent: number + messagesReceived: number + bytesReceived: number + bytesSent: number + connectionTime: number + averageFPS: number + networkStats: { + packetsLost: number + roundTripTimes: number[] + jitter: number + } +} export type Message = - // Represents the initial information given in thee lobby or smth - | { type: "info"; data: ClientInfo } - | { type: "init"; data: InitData } - | { type: "update"; data: UpdateMultiplayerObjectData[] } - | { type: "collision"; data: CollisionData } - | { type: "newObject"; data: InitMultiplayerObjectData } - | { type: "robotLeft"; data: RobotLeftData } - | { type: "ping"; data: PingData } - | { type: "pong"; data: PingData }; + // Represents the initial information given in thee lobby or smth + | { type: "info"; data: ClientInfo } + | { type: "init"; data: InitData } + | { type: "update"; data: UpdateMultiplayerObjectData[] } + | { type: "collision"; data: CollisionData } + | { type: "newObject"; data: InitObjectData } + | { type: "robotLeft"; data: RobotLeftData } + | { type: "ping"; data: PingData } + | { type: "pong"; data: PingData } export type ClientInfo = { - displayName: string; - clientId: string; -}; + displayName: string + clientId: string + isHost: boolean +} // TODO: Figure out if InitMultiplayerObjectData is still necessary export type InitData = { - physicsSystem: PhysicsSystem; - objects: InitMultiplayerObjectData[]; // We need to send the entire scene object with rendering data and configuration (for fields and such) -}; + physicsSystem: PhysicsSystem + objects: InitObjectData[] // We need to send the entire scene object with rendering data and configuration (for fields and such) +} export type UpdateMultiplayerObjectData = { - sceneObjectKey: number; - mechanism: Mechanism; - instance: MirabufInstance; -}; + sceneObjectKey: number + mechanism: Mechanism + instance: MirabufInstance +} -export type InitMultiplayerObjectData = { - key: number; // TODO Check if we actually have to sync up keys (i think it's best if we do) - sceneObject: MirabufSceneObject; -}; +export type InitObjectData = { + key: number // TODO Check if we actually have to sync up keys (i think it's best if we do) + sceneObject: MirabufSceneObject +} export type CollisionData = { - physicsSystem: PhysicsSystem; - sceneObject: Map; -}; + physicsSystem: PhysicsSystem + sceneObject: Map +} export type RobotLeftData = { - sceneObjectKey: number; -}; + sceneObjectKey: number +} -export type PingData = { timestamp: number }; +export type PingData = { timestamp: number } diff --git a/fission/src/systems/scene/SceneRenderer.ts b/fission/src/systems/scene/SceneRenderer.ts index 640ecb4d30..6889e49d26 100644 --- a/fission/src/systems/scene/SceneRenderer.ts +++ b/fission/src/systems/scene/SceneRenderer.ts @@ -53,6 +53,9 @@ class SceneRenderer extends WorldSystem { public get sceneObjects() { return this._sceneObjects } + public set sceneObjects(objects: Map) { + this._sceneObjects = objects + } public get mainCamera() { return this._mainCamera @@ -175,7 +178,9 @@ class SceneRenderer extends WorldSystem { this._composer.addPass(new RenderPass(this._scene, this._mainCamera)) if (PreferencesSystem.getGraphicsPreferences().antiAliasing) { - const antiAliasEffect = new SMAAEffect({ edgeDetectionMode: EdgeDetectionMode.COLOR }) + const antiAliasEffect = new SMAAEffect({ + edgeDetectionMode: EdgeDetectionMode.COLOR, + }) const antiAliasPass = new EffectPass(this._mainCamera, antiAliasEffect) this._composer.addPass(antiAliasPass) } diff --git a/fission/src/ui/modals/MainMenuModal.tsx b/fission/src/ui/modals/MainMenuModal.tsx index ad679ab2c6..143c700faf 100644 --- a/fission/src/ui/modals/MainMenuModal.tsx +++ b/fission/src/ui/modals/MainMenuModal.tsx @@ -1,18 +1,16 @@ import React from "react" import Button from "@/components/Button.tsx" -import Modal, {ModalPropsImpl} from "@/components/Modal" -import {SynthesisIcons} from "../components/StyledComponents" -import {useModalControlContext} from "../helpers/UseModalManager" +import Modal, { ModalPropsImpl } from "@/components/Modal" +import { SynthesisIcons } from "../components/StyledComponents" +import { useModalControlContext } from "../helpers/UseModalManager" -const MainMenuModal: React.FC void, - startMultiplayerCallback: () => void -}> = ({ - modalId, - startSingleplayerCallback, - startMultiplayerCallback - }) => { - const {closeModal} = useModalControlContext() +const MainMenuModal: React.FC< + ModalPropsImpl & { + startSingleplayerCallback: () => void + startMultiplayerCallback: () => void + } +> = ({ modalId, startSingleplayerCallback, startMultiplayerCallback }) => { + const { closeModal } = useModalControlContext() return ( Date: Wed, 30 Jul 2025 09:11:03 -0700 Subject: [PATCH 06/32] feat: handle peer info and world initialization --- fission/src/Synthesis.tsx | 12 +++++------- .../systems/multiplayer/MultiplayerSystem.ts | 17 +++++++++++++++-- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/fission/src/Synthesis.tsx b/fission/src/Synthesis.tsx index 64ea9720b5..71af014139 100644 --- a/fission/src/Synthesis.tsx +++ b/fission/src/Synthesis.tsx @@ -126,17 +126,15 @@ const Synthesis: React.FC = () => { } const startMultiplayer = async () => { const roomId = urlParams.get("roomId") - let client: MultiplayerSystem - if (roomId) { - client = await MultiplayerSystem.create(roomId) - } else { - client = await MultiplayerSystem.createHost() - } + const client: MultiplayerSystem = await (roomId + ? MultiplayerSystem.create(roomId) + : MultiplayerSystem.createHost()) + console.log({ room: client.roomId }) console.log(client) window.multiplayer = client } - void startMultiplayer() + startMultiplayer() openModal("main-menu") // Cleanup diff --git a/fission/src/systems/multiplayer/MultiplayerSystem.ts b/fission/src/systems/multiplayer/MultiplayerSystem.ts index 5f863966a9..00a16883ab 100644 --- a/fission/src/systems/multiplayer/MultiplayerSystem.ts +++ b/fission/src/systems/multiplayer/MultiplayerSystem.ts @@ -19,6 +19,8 @@ class MultiplayerSystem { readonly connections: DataConnection[] = [] readonly clientId: string + clientToRobotMap: Map = new Map() // clientId -> [displayName , sceneObjectKey] + info: ClientInfo lastSentCollisionTimestamp: number = Date.now() connected: boolean = false @@ -161,8 +163,18 @@ class MultiplayerSystem { } } - handlePeerInfo(data: ClientInfo) {} - handleWorldInitialization(data: InitData) {} + handlePeerInfo(data: ClientInfo) { + this.clientToRobotMap.set(data.clientId, [data.displayName, null]) + } + + handleWorldInitialization(data: InitData) { + World.physicsSystem = data.physicsSystem + World.sceneRenderer.sceneObjects = this.initObjectDataToSceneObjectMap(data.objects) + } + initObjectDataToSceneObjectMap(objects: InitObjectData[]): Map { + return new Map(objects.map(object => [object.key, object.sceneObject])) + } + handlePeerUpdate(data: UpdateObjectData[]) { data.forEach(({ sceneObjectKey, mechanism, instance }) => { const sceneObject = World.sceneRenderer.sceneObjects.get(sceneObjectKey) @@ -181,6 +193,7 @@ class MultiplayerSystem { } handleCollision(data: CollisionData) { + // TODO Expand on this logic if (this.lastSentCollisionTimestamp < COLLISION_TIMEOUT) return World.physicsSystem = data.physicsSystem From 4baad9640beee602a641d4fe81bc1ee5a9f915a9 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Wed, 30 Jul 2025 11:00:26 -0700 Subject: [PATCH 07/32] feat(wip): move multiplayer to world --- fission/src/Synthesis.tsx | 16 ++++++- fission/src/Window.d.ts | 1 - fission/src/systems/World.ts | 16 ++++++- .../systems/multiplayer/MultiplayerSystem.ts | 2 +- fission/src/systems/multiplayer/types.ts | 6 +-- fission/src/systems/physics/Mechanism.ts | 12 +++-- fission/src/systems/physics/PhysicsSystem.ts | 45 +++++++++++++++++++ 7 files changed, 84 insertions(+), 14 deletions(-) diff --git a/fission/src/Synthesis.tsx b/fission/src/Synthesis.tsx index 71af014139..78f28c8f85 100644 --- a/fission/src/Synthesis.tsx +++ b/fission/src/Synthesis.tsx @@ -112,7 +112,21 @@ const Synthesis: React.FC = () => { World.sceneRenderer.updateSkyboxColors(defaultTheme) }} - startMultiplayerCallback={async () => {}} + startMultiplayerCallback={async () => { + World.initWorld(true) + + if (!PreferencesSystem.getGlobalPreference("ReportAnalytics") && !import.meta.env.DEV) { + setConsentPopupDisable(false) + } + + const mainLoop = () => { + mainLoopHandle.current = requestAnimationFrame(mainLoop) + World.updateWorld() + } + mainLoop() + + World.sceneRenderer.updateSkyboxColors(defaultTheme) + }} /> ), }) diff --git a/fission/src/Window.d.ts b/fission/src/Window.d.ts index 6815c0552f..d8c67b5981 100644 --- a/fission/src/Window.d.ts +++ b/fission/src/Window.d.ts @@ -1,5 +1,4 @@ declare interface Window { convertAuthToken(code: string): void gtag: () => void - multiplayer: unknown // todo: remove } diff --git a/fission/src/systems/World.ts b/fission/src/systems/World.ts index 762b3d6ee6..ea43e7aed9 100644 --- a/fission/src/systems/World.ts +++ b/fission/src/systems/World.ts @@ -77,12 +77,25 @@ class World { } } - public static initWorld() { + public static async initWorld(isMultiplayer: boolean = false) { if (World._isAlive) return World._clock = new THREE.Clock() World._isAlive = true + if (isMultiplayer) { + const urlParams = new URLSearchParams(document.location.search) + if (urlParams.has("code")) { + window.opener.convertAuthToken(urlParams.get("code")) + window.close() + return + } + const roomId = urlParams.get("roomId") + World._multiplayerSystem = await (roomId + ? MultiplayerSystem.create(roomId) + : MultiplayerSystem.createHost()) + } + World._sceneRenderer = new SceneRenderer() World._physicsSystem = new PhysicsSystem() World._simulationSystem = new SimulationSystem() @@ -106,6 +119,7 @@ class World { World._sceneRenderer.destroy() World._simulationSystem.destroy() World._inputSystem.destroy() + // World._multiplayerSystem.destroy() World._dragModeSystem.destroy() World._performanceMonitorSystem.destroy() diff --git a/fission/src/systems/multiplayer/MultiplayerSystem.ts b/fission/src/systems/multiplayer/MultiplayerSystem.ts index 00a16883ab..9a9b64c105 100644 --- a/fission/src/systems/multiplayer/MultiplayerSystem.ts +++ b/fission/src/systems/multiplayer/MultiplayerSystem.ts @@ -8,7 +8,7 @@ import type { InitData, InitObjectData, Message, - UpdateMultiplayerObjectData as UpdateObjectData, + UpdateObjectData as UpdateObjectData, } from "./types" const COLLISION_TIMEOUT = 500 diff --git a/fission/src/systems/multiplayer/types.ts b/fission/src/systems/multiplayer/types.ts index 2ed92b6b06..7b6e9f7adf 100644 --- a/fission/src/systems/multiplayer/types.ts +++ b/fission/src/systems/multiplayer/types.ts @@ -24,7 +24,7 @@ export type Message = // Represents the initial information given in thee lobby or smth | { type: "info"; data: ClientInfo } | { type: "init"; data: InitData } - | { type: "update"; data: UpdateMultiplayerObjectData[] } + | { type: "update"; data: UpdateObjectData[] } | { type: "collision"; data: CollisionData } | { type: "newObject"; data: InitObjectData } | { type: "robotLeft"; data: RobotLeftData } @@ -43,7 +43,7 @@ export type InitData = { objects: InitObjectData[] // We need to send the entire scene object with rendering data and configuration (for fields and such) } -export type UpdateMultiplayerObjectData = { +export type UpdateObjectData = { sceneObjectKey: number mechanism: Mechanism instance: MirabufInstance @@ -56,7 +56,7 @@ export type InitObjectData = { export type CollisionData = { physicsSystem: PhysicsSystem - sceneObject: Map + sceneObjects: Map } export type RobotLeftData = { diff --git a/fission/src/systems/physics/Mechanism.ts b/fission/src/systems/physics/Mechanism.ts index e25a7e7efa..d90b7a3d7c 100644 --- a/fission/src/systems/physics/Mechanism.ts +++ b/fission/src/systems/physics/Mechanism.ts @@ -16,11 +16,12 @@ export interface MechanismConstraint { class Mechanism { public rootBody: string public nodeToBody: Map - public constraints: Array - public stepListeners: Array - public layerReserve: LayerReserve | undefined + public constraints: MechanismConstraint[] = [] + public stepListeners: Jolt.PhysicsStepListener[] = [] + public layerReserve?: LayerReserve public controllable: boolean - public ghostBodies: Array + public ghostBodies: Jolt.BodyID[] = [] + public touchedBodies: Jolt.BodyID[] = [] public constructor( rootBody: string, @@ -30,10 +31,7 @@ class Mechanism { ) { this.rootBody = rootBody this.nodeToBody = bodyMap - this.constraints = [] - this.stepListeners = [] this.controllable = controllable - this.ghostBodies = [] this.layerReserve = layerReserve } diff --git a/fission/src/systems/physics/PhysicsSystem.ts b/fission/src/systems/physics/PhysicsSystem.ts index efdc232766..2476c37232 100644 --- a/fission/src/systems/physics/PhysicsSystem.ts +++ b/fission/src/systems/physics/PhysicsSystem.ts @@ -27,6 +27,10 @@ import { PhysicsEvent, } from "./ContactEvents" import Mechanism from "./Mechanism" +import Synthesis from "@/Synthesis" +import World from "../World" +import MirabufSceneObject from "@/mirabuf/MirabufSceneObject" +import { Message } from "../multiplayer/types" export type JoltBodyIndexAndSequence = number @@ -1278,10 +1282,49 @@ class PhysicsSystem extends WorldSystem { this._joltInterface.Step(lastDeltaT, substeps) + if (World.multiplayerSystem != null) { + const interObjectCollisions = this._physicsEventQueue.filter( + x => x instanceof OnContactAddedEvent && this.onSameLayer(x.message.body1, x.message.body2) + ) + + const message: Message = + interObjectCollisions.length > 0 + ? { + type: "collision", + data: { + // TODO We might not need to send over the entire physicsSystem, we might be able to just send over a more complete list of scene objects + physicsSystem: this, + sceneObjects: new Map( + [...World.sceneRenderer.sceneObjects].filter( + (x): x is [number, MirabufSceneObject] => x[1] instanceof MirabufSceneObject + ) + ), + }, + } + : { + type: "update", + data: [...World.sceneRenderer.sceneObjects] + .filter((x): x is [number, MirabufSceneObject] => x[1] instanceof MirabufSceneObject) + .map(([sceneObjectKey, sceneObject]) => { + return { + sceneObjectKey, + mechanism: sceneObject.mechanism, + instance: sceneObject.mirabufInstance, + } + }), + } + + World.multiplayerSystem?.broadcast(message) + } + this._physicsEventQueue.forEach(x => x.dispatch()) this._physicsEventQueue = [] } + private onSameLayer(body1: Jolt.BodyID, body2: Jolt.BodyID): boolean { + return this.getBody(body1).GetObjectLayer() === this.getBody(body2).GetObjectLayer() + } + /* * Destroys PhysicsSystem and frees all objects */ @@ -1449,6 +1492,8 @@ class PhysicsSystem extends WorldSystem { manifold: JOLT.wrapPointer(manifoldPtr, JOLT.ContactManifold) as Jolt.ContactManifold, settings: JOLT.wrapPointer(settingsPtr, JOLT.ContactSettings) as Jolt.ContactSettings, } + if (body1.GetObjectLayer() === LAYER_GENERAL_DYNAMIC && ROBOT_LAYERS.includes(body2.GetObjectLayer())) { + } this._physicsEventQueue.push(new OnContactAddedEvent(message)) } From c99d6f06ec9b84b6a4d092976332c627fd98dbde Mon Sep 17 00:00:00 2001 From: Zach Rutman Date: Wed, 30 Jul 2025 11:38:41 -0700 Subject: [PATCH 08/32] feat: add multiplayer start menu --- fission/src/Synthesis.tsx | 76 ++++++++++--------- fission/src/mirabuf/MirabufSceneObject.ts | 3 +- fission/src/systems/World.ts | 15 +--- .../systems/multiplayer/MultiplayerSystem.ts | 22 ++---- fission/src/systems/physics/BodyAssociate.ts | 13 ++++ fission/src/systems/physics/PhysicsSystem.ts | 19 +---- .../src/ui/modals/MultiplayerStartModal.tsx | 52 +++++++++++++ 7 files changed, 119 insertions(+), 81 deletions(-) create mode 100644 fission/src/systems/physics/BodyAssociate.ts create mode 100644 fission/src/ui/modals/MultiplayerStartModal.tsx diff --git a/fission/src/Synthesis.tsx b/fission/src/Synthesis.tsx index 78f28c8f85..3a3e85edd9 100644 --- a/fission/src/Synthesis.tsx +++ b/fission/src/Synthesis.tsx @@ -72,6 +72,8 @@ import AutoTestPanel from "./ui/panels/simulation/AutoTestPanel.tsx" import WiringPanel from "./ui/panels/simulation/WiringPanel.tsx" import WSViewPanel from "./ui/panels/WSViewPanel.tsx" import MultiplayerSystem from "@/systems/multiplayer/MultiplayerSystem.ts" +import MultiplayerStartModal from "@/modals/MultiplayerStartModal.tsx"; +import {globalAddToast} from "@/components/GlobalUIControls.ts"; const Synthesis: React.FC = () => { const { openModal, closeModal, getActiveModalElement, registerModal, activeModalId } = @@ -91,41 +93,52 @@ const Synthesis: React.FC = () => { const modalElement = getActiveModalElement() const mainLoopHandle = useRef(0) + const startMainLoop = () => { + if (!PreferencesSystem.getGlobalPreference("ReportAnalytics") && !import.meta.env.DEV) { + setConsentPopupDisable(false) + } + + const mainLoop = () => { + mainLoopHandle.current = requestAnimationFrame(mainLoop) + World.updateWorld() + } + mainLoop() + + World.sceneRenderer.updateSkyboxColors(defaultTheme) + } + registerModal("multiplayer-lobby", { + id: "multiplayer-lobby", + component: ( + { + const isHost = room == null + if (room == null) { + room = Math.random().toString(10).substring(2, 8) + globalAddToast("info", "Room code", room) + } + const multiplayerSystem = await MultiplayerSystem.create(room, isHost) + await World.initWorld(multiplayerSystem) + startMainLoop() + }} + /> + ), + }) registerModal("main-menu", { id: "main-menu", component: ( { - World.initWorld() - - if (!PreferencesSystem.getGlobalPreference("ReportAnalytics") && !import.meta.env.DEV) { - setConsentPopupDisable(false) - } - - const mainLoop = () => { - mainLoopHandle.current = requestAnimationFrame(mainLoop) - World.updateWorld() - } - mainLoop() + startSingleplayerCallback={async () => { + await World.initWorld() + startMainLoop() - World.sceneRenderer.updateSkyboxColors(defaultTheme) }} startMultiplayerCallback={async () => { - World.initWorld(true) + openModal("multiplayer-lobby") - if (!PreferencesSystem.getGlobalPreference("ReportAnalytics") && !import.meta.env.DEV) { - setConsentPopupDisable(false) - } - - const mainLoop = () => { - mainLoopHandle.current = requestAnimationFrame(mainLoop) - World.updateWorld() - } - mainLoop() - - World.sceneRenderer.updateSkyboxColors(defaultTheme) }} /> ), @@ -138,18 +151,6 @@ const Synthesis: React.FC = () => { window.close() return } - const startMultiplayer = async () => { - const roomId = urlParams.get("roomId") - const client: MultiplayerSystem = await (roomId - ? MultiplayerSystem.create(roomId) - : MultiplayerSystem.createHost()) - - console.log({ room: client.roomId }) - console.log(client) - window.multiplayer = client - } - startMultiplayer() - openModal("main-menu") // Cleanup return () => { @@ -285,6 +286,7 @@ const initialPanels: ReactElement[] = [ , , , + ] export default Synthesis diff --git a/fission/src/mirabuf/MirabufSceneObject.ts b/fission/src/mirabuf/MirabufSceneObject.ts index 63b5d529ce..e19e830667 100644 --- a/fission/src/mirabuf/MirabufSceneObject.ts +++ b/fission/src/mirabuf/MirabufSceneObject.ts @@ -3,7 +3,7 @@ import * as THREE from "three" import { mirabuf } from "@/proto/mirabuf" import { OnContactAddedEvent } from "@/systems/physics/ContactEvents" import Mechanism from "@/systems/physics/Mechanism" -import { BodyAssociate, LayerReserve } from "@/systems/physics/PhysicsSystem" +import { LayerReserve } from "@/systems/physics/PhysicsSystem" import PreferencesSystem from "@/systems/preferences/PreferencesSystem" import { Alliance, @@ -44,6 +44,7 @@ import { MiraType } from "./MirabufLoader" import MirabufParser, { ParseErrorSeverity, RigidNodeId, RigidNodeReadOnly } from "./MirabufParser" import ProtectedZoneSceneObject from "./ProtectedZoneSceneObject" import ScoringZoneSceneObject from "./ScoringZoneSceneObject" +import {BodyAssociate} from "@/systems/physics/BodyAssociate.ts"; const DEBUG_BODIES = false diff --git a/fission/src/systems/World.ts b/fission/src/systems/World.ts index ea43e7aed9..bfda3e0b60 100644 --- a/fission/src/systems/World.ts +++ b/fission/src/systems/World.ts @@ -77,23 +77,14 @@ class World { } } - public static async initWorld(isMultiplayer: boolean = false) { + public static async initWorld(multiplayerSystem?: MultiplayerSystem) { if (World._isAlive) return World._clock = new THREE.Clock() World._isAlive = true - if (isMultiplayer) { - const urlParams = new URLSearchParams(document.location.search) - if (urlParams.has("code")) { - window.opener.convertAuthToken(urlParams.get("code")) - window.close() - return - } - const roomId = urlParams.get("roomId") - World._multiplayerSystem = await (roomId - ? MultiplayerSystem.create(roomId) - : MultiplayerSystem.createHost()) + if (multiplayerSystem) { + World._multiplayerSystem = multiplayerSystem } World._sceneRenderer = new SceneRenderer() diff --git a/fission/src/systems/multiplayer/MultiplayerSystem.ts b/fission/src/systems/multiplayer/MultiplayerSystem.ts index 9a9b64c105..54ef4286fd 100644 --- a/fission/src/systems/multiplayer/MultiplayerSystem.ts +++ b/fission/src/systems/multiplayer/MultiplayerSystem.ts @@ -23,19 +23,12 @@ class MultiplayerSystem { info: ClientInfo lastSentCollisionTimestamp: number = Date.now() - connected: boolean = false - otherPeers: string[] = [] - public static async create(roomId: string, isHost: boolean = false): Promise { + public static async create(roomId: string, isHost:boolean): Promise { const clientId = await generateId(roomId) return new MultiplayerSystem(roomId, clientId, isHost) } - public static async createHost(): Promise { - const room = Math.random().toString(10).substring(2, 9) - return this.create(room, true) - } - private constructor(roomId: string, clientId: string, isHost: boolean = false) { this.roomId = roomId this.clientId = clientId @@ -192,12 +185,12 @@ class MultiplayerSystem { }) } - handleCollision(data: CollisionData) { + handleCollision(_data: CollisionData) { // TODO Expand on this logic if (this.lastSentCollisionTimestamp < COLLISION_TIMEOUT) return - World.physicsSystem = data.physicsSystem - World.sceneRenderer.sceneObjects = data.sceneObject + // World.physicsSystem = data.physicsSystem + // World.sceneRenderer.sceneObjects = data.sceneObject } handleNewObject(data: InitObjectData) { @@ -224,7 +217,7 @@ class MultiplayerSystem { const localStorageKey = "multiplayer_clientid" -async function generateId(roomId?: string): Promise { +async function generateId(roomId: string): Promise { let id = (import.meta.env.DEV ? new URLSearchParams(window.location.search).get("uid") : undefined) ?? window.localStorage.getItem(localStorageKey) @@ -232,10 +225,7 @@ async function generateId(roomId?: string): Promise { id = `client_${Math.random().toString(36).substring(2, 9)}` window.localStorage.setItem(localStorageKey, id) } - if (roomId) { - return `${id}-${await createSha256Hash(roomId)}` - } - return id + return `${id}-${await createSha256Hash(roomId)}` } async function createSha256Hash(msg: string) { diff --git a/fission/src/systems/physics/BodyAssociate.ts b/fission/src/systems/physics/BodyAssociate.ts new file mode 100644 index 0000000000..d357c2aad5 --- /dev/null +++ b/fission/src/systems/physics/BodyAssociate.ts @@ -0,0 +1,13 @@ +import Jolt from "@azaleacolburn/jolt-physics" +import { JoltBodyIndexAndSequence } from "@/systems/physics/PhysicsSystem.ts" + +/** + * An interface to create an association between a body and anything. + */ +export class BodyAssociate { + readonly associatedBody: JoltBodyIndexAndSequence + + public constructor(bodyId: Jolt.BodyID) { + this.associatedBody = bodyId.GetIndexAndSequenceNumber() + } +} diff --git a/fission/src/systems/physics/PhysicsSystem.ts b/fission/src/systems/physics/PhysicsSystem.ts index 2476c37232..1f2df6dd3b 100644 --- a/fission/src/systems/physics/PhysicsSystem.ts +++ b/fission/src/systems/physics/PhysicsSystem.ts @@ -1,5 +1,7 @@ import Jolt from "@azaleacolburn/jolt-physics" import * as THREE from "three" +import MirabufSceneObject from "@/mirabuf/MirabufSceneObject" +import { BodyAssociate } from "@/systems/physics/BodyAssociate.ts" import JOLT from "@/util/loading/JoltSyncLoader" import MirabufParser, { GAMEPIECE_SUFFIX, GROUNDED_JOINT_ID, RigidNodeReadOnly } from "../../mirabuf/MirabufParser" import { mirabuf } from "../../proto/mirabuf" @@ -15,7 +17,9 @@ import { convertThreeVector3ToJoltRVec3, convertThreeVector3ToJoltVec3, } from "../../util/TypeConversions" +import { Message } from "../multiplayer/types" import PreferencesSystem from "../preferences/PreferencesSystem" +import World from "../World" import WorldSystem from "../WorldSystem" import { CurrentContactData, @@ -27,10 +31,6 @@ import { PhysicsEvent, } from "./ContactEvents" import Mechanism from "./Mechanism" -import Synthesis from "@/Synthesis" -import World from "../World" -import MirabufSceneObject from "@/mirabuf/MirabufSceneObject" -import { Message } from "../multiplayer/types" export type JoltBodyIndexAndSequence = number @@ -1648,15 +1648,4 @@ export type RayCastHit = { ray: Jolt.RRayCast } -/** - * An interface to create an association between a body and anything. - */ -export class BodyAssociate { - readonly associatedBody: JoltBodyIndexAndSequence - - public constructor(bodyId: Jolt.BodyID) { - this.associatedBody = bodyId.GetIndexAndSequenceNumber() - } -} - export default PhysicsSystem diff --git a/fission/src/ui/modals/MultiplayerStartModal.tsx b/fission/src/ui/modals/MultiplayerStartModal.tsx new file mode 100644 index 0000000000..846c03d118 --- /dev/null +++ b/fission/src/ui/modals/MultiplayerStartModal.tsx @@ -0,0 +1,52 @@ +import React, {useState} from "react" +import Button from "@/components/Button.tsx" +import Modal, { ModalPropsImpl } from "@/components/Modal" +import { SynthesisIcons } from "../components/StyledComponents" +import { useModalControlContext } from "../helpers/UseModalManager" +import { Stack } from "@mui/system" +import {Divider, TextField} from "@mui/material"; + +const MultiplayerStartModal: React.FC< + ModalPropsImpl & { + startWorldCallback: (roomId?:string) => void + } +> = ({ modalId, startWorldCallback }) => { + const { closeModal } = useModalControlContext() + const [room, setRoom] = useState("") + return ( + + + - - { - setRoom(e.currentTarget.value.replace(/\D/, "").slice(0, 6)) // 6-digit numbers - }, - }} - /> - + + { + setRoom(e.currentTarget.value.replace(/\D/, "").slice(0, 6)) // 6-digit numbers + }, + }} + /> + + ) } +async function withTimeout(promise: Promise, timeoutMessage: string, duration: number = 5000) { + let timeout: NodeJS.Timeout + return await Promise.race([ + promise, + new Promise(res => { + timeout = setTimeout(() => { + globalAddToast("warning", timeoutMessage) + res(false) + }, duration) + }), + ]).then(v => { + clearTimeout(timeout) + return v + }) +} + export default MultiplayerStartModal diff --git a/fission/src/ui/modals/configuring/SettingsModal.tsx b/fission/src/ui/modals/configuring/SettingsModal.tsx index 58635714e4..ea67d22c1c 100644 --- a/fission/src/ui/modals/configuring/SettingsModal.tsx +++ b/fission/src/ui/modals/configuring/SettingsModal.tsx @@ -18,7 +18,7 @@ const SettingsModal: React.FC> = ({ modal }) => { const { closeModal, openPanel, configureScreen } = useUIContext() const [_, refresh] = useReducer(x => !x, false) const save = useCallback(() => { - SoundPlayer.changeVolume() + SoundPlayer.getInstance().changeVolume() PreferencesSystem.savePreferences() globalAddToast("info", "Settings Saved") }, []) @@ -26,7 +26,7 @@ const SettingsModal: React.FC> = ({ modal }) => { useEffect(() => { const onCancel = () => { PreferencesSystem.revertPreferences() - SoundPlayer.changeVolume() + SoundPlayer.getInstance().changeVolume() } configureScreen(modal!, { title: "Settings", allowClickAway: false }, { onBeforeAccept: save, onCancel }) diff --git a/fission/src/ui/modals/mirabuf/ImportLocalMirabufModal.tsx b/fission/src/ui/modals/mirabuf/ImportLocalMirabufModal.tsx index a53077bd9b..eb42f2ef4f 100644 --- a/fission/src/ui/modals/mirabuf/ImportLocalMirabufModal.tsx +++ b/fission/src/ui/modals/mirabuf/ImportLocalMirabufModal.tsx @@ -81,7 +81,7 @@ const ImportLocalMirabufModal: React.FC> = ({ modal } value={miraType} exclusive onChange={(_, v) => v != null && setSelectedType(v)} - {...SoundPlayer.buttonSoundEffects()} + {...SoundPlayer.getInstance().buttonSoundEffects()} sx={{ alignSelf: "center", }} diff --git a/fission/src/ui/panels/configuring/CameraSelectionPanel.tsx b/fission/src/ui/panels/configuring/CameraSelectionPanel.tsx index 50a02c7d3b..91776d16d4 100644 --- a/fission/src/ui/panels/configuring/CameraSelectionPanel.tsx +++ b/fission/src/ui/panels/configuring/CameraSelectionPanel.tsx @@ -56,7 +56,7 @@ const CameraSelectionPanel: React.FC> = ({ panel }) = setCameraControls(v) }} - onMouseDown={() => SoundPlayer.play(buttonPressSound)} + onMouseDown={() => SoundPlayer.getInstance().play(buttonPressSound)} > Orbit diff --git a/fission/src/ui/panels/configuring/assembly-config/interfaces/BrainSelectionInterface.tsx b/fission/src/ui/panels/configuring/assembly-config/interfaces/BrainSelectionInterface.tsx index 0f3d4d7ed3..7e316b0097 100644 --- a/fission/src/ui/panels/configuring/assembly-config/interfaces/BrainSelectionInterface.tsx +++ b/fission/src/ui/panels/configuring/assembly-config/interfaces/BrainSelectionInterface.tsx @@ -32,7 +32,7 @@ export default function BrainSelectionInterface({ selectedAssembly }: BrainSelec } setRobotBrainType(brainType) }} - {...SoundPlayer.buttonSoundEffects()} + {...SoundPlayer.getInstance().buttonSoundEffects()} sx={{ alignSelf: "center", }} diff --git a/fission/src/ui/panels/mirabuf/ImportMirabufPanel.tsx b/fission/src/ui/panels/mirabuf/ImportMirabufPanel.tsx index 0566c055bf..ee0f377f5f 100644 --- a/fission/src/ui/panels/mirabuf/ImportMirabufPanel.tsx +++ b/fission/src/ui/panels/mirabuf/ImportMirabufPanel.tsx @@ -460,7 +460,7 @@ const ImportMirabufPanel: React.FC = ({ assembly, setPlaying }) => { value={countdown} exclusive onChange={(_, v) => setCountdown(v)} - {...SoundPlayer.buttonSoundEffects()} + {...SoundPlayer.getInstance().buttonSoundEffects()} className="self-center" > 5 @@ -271,7 +271,7 @@ const Staging: React.FC = ({ assembly, setPlaying }) => { value={station} exclusive onChange={(_, v) => setStation(v)} - {...SoundPlayer.buttonSoundEffects()} + {...SoundPlayer.getInstance().buttonSoundEffects()} className="self-center" > 1 From 8f1b74273118900ead515d5692779fac31645f55 Mon Sep 17 00:00:00 2001 From: Zach Rutman Date: Tue, 5 Aug 2025 14:12:48 -0700 Subject: [PATCH 28/32] fix: import correctly --- fission/src/systems/physics/BodyAssociate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fission/src/systems/physics/BodyAssociate.ts b/fission/src/systems/physics/BodyAssociate.ts index 0a91597fcd..f3adffabc1 100644 --- a/fission/src/systems/physics/BodyAssociate.ts +++ b/fission/src/systems/physics/BodyAssociate.ts @@ -1,5 +1,5 @@ import type Jolt from "@azaleacolburn/jolt-physics" -import type { JoltBodyIndexAndSequence } from "@/systems/physics/PhysicsSystem.ts" +import type { JoltBodyIndexAndSequence } from "@/systems/physics/PhysicsTypes" /** * An interface to create an association between a body and anything. From f7ce4729946561c6f1af170de3fa9f03f426544d Mon Sep 17 00:00:00 2001 From: Zach Rutman Date: Tue, 5 Aug 2025 16:55:03 -0700 Subject: [PATCH 29/32] feat: add multiplayer tests --- fission/bun.lock | 188 ++++++++++++++++-- fission/package.json | 2 +- .../systems/multiplayer/MultiplayerSystem.ts | 30 ++- fission/src/test/TestSetup.server.ts | 40 ++-- .../src/test/multiplayer/Multiplayer.test.ts | 87 ++++++++ fission/src/vite-env.d.ts | 10 + fission/vite.config.ts | 2 +- 7 files changed, 300 insertions(+), 59 deletions(-) create mode 100644 fission/src/test/multiplayer/Multiplayer.test.ts diff --git a/fission/bun.lock b/fission/bun.lock index a29b6b7f95..7ee7894809 100644 --- a/fission/bun.lock +++ b/fission/bun.lock @@ -59,11 +59,11 @@ "eslint-plugin-react-refresh": "^0.4.20", "jsdom": "^24.1.3", "pako": "^2.1.0", + "peer": "^1.0.2", "postcss": "^8.5.6", "protobufjs": "^7.5.3", "protobufjs-cli": "^1.1.3", "rollup": "^4.46.2", - "sirv": "^3.0.1", "tailwindcss": "^3.4.17", "tsconfig-paths": "^4.2.0", "typescript": "^5.9.2", @@ -441,8 +441,12 @@ "@types/babel__traverse": ["@types/babel__traverse@7.20.7", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng=="], + "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], + "@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="], + "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], + "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], "@types/d3-drag": ["@types/d3-drag@3.0.7", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ=="], @@ -461,6 +465,12 @@ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/express": ["@types/express@4.17.23", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "*" } }, "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ=="], + + "@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.6", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A=="], + + "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], + "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], "@types/linkify-it": ["@types/linkify-it@5.0.0", "", {}, "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="], @@ -469,6 +479,8 @@ "@types/mdurl": ["@types/mdurl@2.0.0", "", {}, "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="], + "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], + "@types/node": ["@types/node@20.19.9", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw=="], "@types/offscreencanvas": ["@types/offscreencanvas@2019.7.3", "", {}, "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A=="], @@ -479,6 +491,10 @@ "@types/prop-types": ["@types/prop-types@15.7.15", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="], + "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], + + "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], + "@types/react": ["@types/react@18.3.23", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w=="], "@types/react-dom": ["@types/react-dom@18.3.7", "", { "peerDependencies": { "@types/react": "^18.0.0" } }, "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ=="], @@ -487,12 +503,18 @@ "@types/react-transition-group": ["@types/react-transition-group@4.4.12", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w=="], + "@types/send": ["@types/send@0.17.5", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w=="], + + "@types/serve-static": ["@types/serve-static@1.15.8", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "*" } }, "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg=="], + "@types/stats.js": ["@types/stats.js@0.17.4", "", {}, "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA=="], "@types/three": ["@types/three@0.178.1", "", { "dependencies": { "@dimforge/rapier3d-compat": "~0.12.0", "@tweenjs/tween.js": "~23.1.3", "@types/stats.js": "*", "@types/webxr": "*", "@webgpu/types": "*", "fflate": "~0.8.2", "meshoptimizer": "~0.18.1" } }, "sha512-WSabew1mgWgRx2RfLfKY+9h4wyg6U94JfLbZEGU245j/WY2kXqU0MUfghS+3AYMV5ET1VlILAgpy77cB6a3Itw=="], "@types/webxr": ["@types/webxr@0.5.22", "", {}, "sha512-Vr6Stjv5jPRqH690f5I5GLjVk8GSsoQSYJ2FVd/3jJF7KaqfwPi3ehfBS96mlQ2kPCwZaX6U0rG2+NGHBKkA/A=="], + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@7.18.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "7.18.0", "@typescript-eslint/type-utils": "7.18.0", "@typescript-eslint/utils": "7.18.0", "@typescript-eslint/visitor-keys": "7.18.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^1.3.0" }, "peerDependencies": { "@typescript-eslint/parser": "^7.0.0", "eslint": "^8.56.0" } }, "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw=="], "@typescript-eslint/parser": ["@typescript-eslint/parser@7.18.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "7.18.0", "@typescript-eslint/types": "7.18.0", "@typescript-eslint/typescript-estree": "7.18.0", "@typescript-eslint/visitor-keys": "7.18.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.56.0" } }, "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg=="], @@ -545,6 +567,8 @@ "@xyflow/system": ["@xyflow/system@0.0.66", "", { "dependencies": { "@types/d3-drag": "^3.0.7", "@types/d3-interpolate": "^3.0.4", "@types/d3-selection": "^3.0.10", "@types/d3-transition": "^3.0.8", "@types/d3-zoom": "^3.0.8", "d3-drag": "^3.0.0", "d3-interpolate": "^3.0.1", "d3-selection": "^3.0.0", "d3-zoom": "^3.0.0" } }, "sha512-TTxESDwPsATnuDMUeYYtKe4wt9v8bRO29dgYBhR8HyhSCzipnAdIL/1CDfFd+WqS1srVreo24u6zZeVIDk4r3Q=="], + "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], @@ -569,6 +593,8 @@ "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], + "array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="], + "array-includes": ["array-includes@3.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "get-intrinsic": "^1.3.0", "is-string": "^1.1.1", "math-intrinsics": "^1.1.0" } }, "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ=="], "array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="], @@ -607,6 +633,8 @@ "bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="], + "body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="], + "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], @@ -617,6 +645,8 @@ "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], @@ -647,6 +677,8 @@ "classcat": ["classcat@5.0.5", "", {}, "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w=="], + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], @@ -661,8 +693,18 @@ "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + "convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="], + "cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="], + + "cookie-signature": ["cookie-signature@1.0.6", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="], + + "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], + "cosmiconfig": ["cosmiconfig@7.1.0", "", { "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.10.0" } }, "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA=="], "cross-env": ["cross-env@7.0.3", "", { "dependencies": { "cross-spawn": "^7.0.1" }, "bin": { "cross-env": "src/bin/cross-env.js", "cross-env-shell": "src/bin/cross-env-shell.js" } }, "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw=="], @@ -709,6 +751,8 @@ "d3-zoom": ["d3-zoom@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="], + "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], + "data-urls": ["data-urls@5.0.0", "", { "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0" } }, "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg=="], "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], @@ -731,8 +775,12 @@ "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], + "detect-gpu": ["detect-gpu@5.0.70", "", { "dependencies": { "webgl-constants": "^1.1.1" } }, "sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w=="], "didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="], @@ -761,9 +809,13 @@ "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + "electron-to-chromium": ["electron-to-chromium@1.5.170", "", {}, "sha512-GP+M7aeluQo9uAyiTCxgIj/j+PrWhMlY7LFVj8prlsPljd0Fdg9AprlfUi+OCSFWy9Y5/2D/Jrj9HS8Z4rpKWA=="], - "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], @@ -789,6 +841,8 @@ "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], "escodegen": ["escodegen@1.14.3", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^4.2.0", "esutils": "^2.0.2", "optionator": "^0.8.1" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "esgenerate": "bin/esgenerate.js", "escodegen": "bin/escodegen.js" } }, "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw=="], @@ -827,10 +881,14 @@ "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + "eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], "expect-type": ["expect-type@1.2.1", "", {}, "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw=="], + "express": ["express@4.21.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], @@ -843,12 +901,16 @@ "fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="], + "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], + "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], "file-entry-cache": ["file-entry-cache@6.0.1", "", { "dependencies": { "flat-cache": "^3.0.4" } }, "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg=="], "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + "finalhandler": ["finalhandler@1.3.1", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", "statuses": "2.0.1", "unpipe": "~1.0.0" } }, "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ=="], + "find-root": ["find-root@1.1.0", "", {}, "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="], "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], @@ -863,10 +925,16 @@ "form-data": ["form-data@4.0.3", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA=="], + "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], + + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + "fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="], "framer-motion": ["framer-motion@10.18.0", "", { "dependencies": { "tslib": "^2.4.0" }, "optionalDependencies": { "@emotion/is-prop-valid": "^0.8.2" }, "peerDependencies": { "react": "^18.0.0", "react-dom": "^18.0.0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-oGlDh1Q1XqYPksuTD/usb0I70hq95OUzmL9+6Zd+Hs4XV0oaISBa/UUMSjYiq6m8EUF32132mOJ8xVZS+I0S6w=="], + "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], @@ -879,6 +947,8 @@ "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], @@ -927,6 +997,8 @@ "html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], + "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], + "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], @@ -949,6 +1021,8 @@ "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], @@ -1107,14 +1181,22 @@ "mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="], + "media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], + + "merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], "meshline": ["meshline@3.3.1", "", { "peerDependencies": { "three": ">=0.137" } }, "sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ=="], "meshoptimizer": ["meshoptimizer@0.18.1", "", {}, "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw=="], + "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="], + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], @@ -1137,6 +1219,12 @@ "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], + + "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + "node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="], "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], @@ -1165,6 +1253,8 @@ "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], @@ -1185,6 +1275,8 @@ "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], @@ -1195,12 +1287,16 @@ "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + "path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="], + "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "pathval": ["pathval@2.0.0", "", {}, "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA=="], + "peer": ["peer@1.0.2", "", { "dependencies": { "@types/express": "^4.17.3", "@types/ws": "^7.2.3 || ^8.0.0", "cors": "^2.8.5", "express": "^4.17.1", "node-fetch": "^3.3.0", "ws": "^7.2.3 || ^8.0.0", "yargs": "^17.6.2" }, "bin": { "peerjs": "dist/bin/peerjs.js" } }, "sha512-ZObVEhAaoskd3KuSxr5DJLM8QuqQW4w3i0MqrI8H7Bzz8DjRC3DjUg2XtQQGfdc36+8Xk+wIPT/tL5wE+KnIqg=="], + "peerjs": ["peerjs@1.5.5", "", { "dependencies": { "@msgpack/msgpack": "^2.8.0", "eventemitter3": "^4.0.7", "peerjs-js-binarypack": "^2.1.0", "webrtc-adapter": "^9.0.0" } }, "sha512-viMUCPDL6CSfOu0ZqVcFqbWRXNHIbv2lPqNbrBIjbFYrflebOjItJ4hPfhjnuUCstqciHVu9vVJ7jFqqKi/EuQ=="], "peerjs-js-binarypack": ["peerjs-js-binarypack@2.1.0", "", {}, "sha512-YIwCC+pTzp3Bi8jPI9UFKO0t0SLo6xALnHkiNt/iUFmUUZG0fEEmEyFKvjsDKweiFitzHRyhuh6NvyJZ4nNxMg=="], @@ -1303,16 +1399,24 @@ "protobufjs-cli": ["protobufjs-cli@1.1.3", "", { "dependencies": { "chalk": "^4.0.0", "escodegen": "^1.13.0", "espree": "^9.0.0", "estraverse": "^5.1.0", "glob": "^8.0.0", "jsdoc": "^4.0.0", "minimist": "^1.2.0", "semver": "^7.1.2", "tmp": "^0.2.1", "uglify-js": "^3.7.7" }, "peerDependencies": { "protobufjs": "^7.0.0" }, "bin": { "pbjs": "bin/pbjs", "pbts": "bin/pbts" } }, "sha512-MqD10lqF+FMsOayFiNOdOGNlXc4iKDCf0ZQPkPR+gizYh9gqUeGTWulABUCdI+N67w5RfJ6xhgX4J8pa8qmMXQ=="], + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + "psl": ["psl@1.15.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w=="], "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="], + "qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="], + "querystringify": ["querystringify@2.2.0", "", {}, "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="], "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="], + "react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], "react-colorful": ["react-colorful@5.6.1", "", { "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw=="], @@ -1343,6 +1447,8 @@ "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], "requires-port": ["requires-port@1.0.0", "", {}, "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="], @@ -1365,6 +1471,8 @@ "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="], + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="], "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], @@ -1379,12 +1487,18 @@ "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="], + + "serve-static": ["serve-static@1.16.2", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.19.0" } }, "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw=="], + "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="], "set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="], + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], @@ -1415,11 +1529,13 @@ "stats.js": ["stats.js@0.17.0", "", {}, "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw=="], + "statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], + "std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="], "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], - "string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -1487,6 +1603,8 @@ "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], "tough-cookie": ["tough-cookie@4.1.4", "", { "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", "universalify": "^0.2.0", "url-parse": "^1.5.3" } }, "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag=="], @@ -1513,6 +1631,8 @@ "type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="], + "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], + "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], "typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="], @@ -1537,6 +1657,8 @@ "universalify": ["universalify@0.2.0", "", {}, "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg=="], + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + "update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="], "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], @@ -1549,8 +1671,12 @@ "utility-types": ["utility-types@3.11.0", "", {}, "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw=="], + "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], + "uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + "vite": ["vite@6.3.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="], "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], @@ -1565,6 +1691,8 @@ "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], + "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], + "webgl-constants": ["webgl-constants@1.1.1", "", {}, "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg=="], "webgl-sdf-generator": ["webgl-sdf-generator@1.1.1", "", {}, "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA=="], @@ -1593,7 +1721,7 @@ "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], - "wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], @@ -1607,10 +1735,16 @@ "xmlcreate": ["xmlcreate@2.0.4", "", {}, "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg=="], + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], "yaml": ["yaml@2.8.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ=="], + "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], "zustand": ["zustand@5.0.5", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-mILtRfKW9xM47hqxGIxCv12gXusoY/xTSHBYApXozR0HmQv299whhBeeAcRy+KrPPybzosvJBCOmVjq6x12fCg=="], @@ -1635,8 +1769,12 @@ "@babel/template/@babel/types": ["@babel/types@7.27.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q=="], + "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + "@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + "@mui/base/@mui/utils": ["@mui/utils@6.4.9", "", { "dependencies": { "@babel/runtime": "^7.26.0", "@mui/types": "~7.2.24", "@types/prop-types": "^15.7.14", "clsx": "^2.1.1", "prop-types": "^15.8.1", "react-is": "^19.0.0" }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-Y12Q9hbK9g+ZY0T3Rxrx9m2m10gaphDuUMgWxyV5kNJevVxXYCLclYUCC9vXaIk1/NdNDTcW2Yfr2OGvNFNmHg=="], "@react-three/fiber/zustand": ["zustand@3.7.2", "", { "peerDependencies": { "react": ">=16.8" }, "optionalPeers": ["react"] }, "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA=="], @@ -1669,6 +1807,10 @@ "ast-v8-to-istanbul/js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], + "body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + "chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -1697,8 +1839,12 @@ "eslint-plugin-import/tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="], + "express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + "framer-motion/@emotion/is-prop-valid": ["@emotion/is-prop-valid@0.8.8", "", { "dependencies": { "@emotion/memoize": "0.7.4" } }, "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA=="], "glob/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], @@ -1735,19 +1881,21 @@ "protobufjs-cli/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + "raw-body/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + "react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], "readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], - "stats-gl/@types/three": ["@types/three@0.160.0", "", { "dependencies": { "@types/stats.js": "*", "@types/webxr": "*", "fflate": "~0.6.10", "meshoptimizer": "~0.18.1" } }, "sha512-jWlbUBovicUKaOYxzgkLlhkiEQJkhCVvg4W2IYD2trqD2om3VK4DGLpHH5zQHNr7RweZK/5re/4IVhbhvxbV9w=="], + "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "stats-gl/three": ["three@0.170.0", "", {}, "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ=="], + "send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], - "string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + "stats-gl/@types/three": ["@types/three@0.160.0", "", { "dependencies": { "@types/stats.js": "*", "@types/webxr": "*", "fflate": "~0.6.10", "meshoptimizer": "~0.18.1" } }, "sha512-jWlbUBovicUKaOYxzgkLlhkiEQJkhCVvg4W2IYD2trqD2om3VK4DGLpHH5zQHNr7RweZK/5re/4IVhbhvxbV9w=="], - "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "stats-gl/three": ["three@0.170.0", "", {}, "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ=="], "strip-literal/js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], @@ -1765,14 +1913,10 @@ "vite/rollup": ["rollup@4.44.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.44.0", "@rollup/rollup-android-arm64": "4.44.0", "@rollup/rollup-darwin-arm64": "4.44.0", "@rollup/rollup-darwin-x64": "4.44.0", "@rollup/rollup-freebsd-arm64": "4.44.0", "@rollup/rollup-freebsd-x64": "4.44.0", "@rollup/rollup-linux-arm-gnueabihf": "4.44.0", "@rollup/rollup-linux-arm-musleabihf": "4.44.0", "@rollup/rollup-linux-arm64-gnu": "4.44.0", "@rollup/rollup-linux-arm64-musl": "4.44.0", "@rollup/rollup-linux-loongarch64-gnu": "4.44.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.44.0", "@rollup/rollup-linux-riscv64-gnu": "4.44.0", "@rollup/rollup-linux-riscv64-musl": "4.44.0", "@rollup/rollup-linux-s390x-gnu": "4.44.0", "@rollup/rollup-linux-x64-gnu": "4.44.0", "@rollup/rollup-linux-x64-musl": "4.44.0", "@rollup/rollup-win32-arm64-msvc": "4.44.0", "@rollup/rollup-win32-ia32-msvc": "4.44.0", "@rollup/rollup-win32-x64-msvc": "4.44.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-qHcdEzLCiktQIfwBq420pn2dP+30uzqYxv9ETm91wdt2R9AFcWfjNAmje4NWlnCIQ5RMTzVf0ZyisOKqHR6RwA=="], - "wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], - - "wrap-ansi/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "@babel/helper-module-imports/@babel/traverse/@babel/generator": ["@babel/generator@7.27.5", "", { "dependencies": { "@babel/parser": "^7.27.5", "@babel/types": "^7.27.3", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw=="], "@babel/helper-module-imports/@babel/traverse/@babel/parser": ["@babel/parser@7.27.5", "", { "dependencies": { "@babel/types": "^7.27.3" }, "bin": "./bin/babel-parser.js" }, "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg=="], @@ -1787,10 +1931,16 @@ "@babel/helper-module-transforms/@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="], + "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="], "escodegen/optionator/levn": ["levn@0.3.0", "", { "dependencies": { "prelude-ls": "~1.1.2", "type-check": "~0.3.2" } }, "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA=="], @@ -1801,15 +1951,19 @@ "eslint-plugin-import/tsconfig-paths/json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], + "express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "framer-motion/@emotion/is-prop-valid/@emotion/memoize": ["@emotion/memoize@0.7.4", "", {}, "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw=="], "glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "jsdoc/@babel/parser/@babel/types": ["@babel/types@7.27.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q=="], - "stats-gl/@types/three/fflate": ["fflate@0.6.10", "", {}, "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg=="], + "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + "stats-gl/@types/three/fflate": ["fflate@0.6.10", "", {}, "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg=="], "sucrase/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], @@ -1853,10 +2007,6 @@ "vite/rollup/@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.44.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Q2Mgwt+D8hd5FIPUuPDsvPR7Bguza6yTkJxspDGkZj7tBRn2y4KSWYuIXpftFSjBra76TbKerCV7rgFPQrn+wQ=="], - "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], - "sucrase/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], } } diff --git a/fission/package.json b/fission/package.json index b205346803..d9e4eea2ea 100644 --- a/fission/package.json +++ b/fission/package.json @@ -77,11 +77,11 @@ "eslint-plugin-react-refresh": "^0.4.20", "jsdom": "^24.1.3", "pako": "^2.1.0", + "peer": "^1.0.2", "postcss": "^8.5.6", "protobufjs": "^7.5.3", "protobufjs-cli": "^1.1.3", "rollup": "^4.46.2", - "sirv": "^3.0.1", "tailwindcss": "^3.4.17", "tsconfig-paths": "^4.2.0", "typescript": "^5.9.2", diff --git a/fission/src/systems/multiplayer/MultiplayerSystem.ts b/fission/src/systems/multiplayer/MultiplayerSystem.ts index e65ad66ddd..c7e9586a46 100644 --- a/fission/src/systems/multiplayer/MultiplayerSystem.ts +++ b/fission/src/systems/multiplayer/MultiplayerSystem.ts @@ -44,6 +44,10 @@ class MultiplayerSystem { return await system._initializationPromise } + getClient() { + return this._client + } + private constructor(roomId: string, clientId: string, displayName: string, isHost: boolean = false) { this.roomId = roomId this.clientId = clientId @@ -51,7 +55,7 @@ class MultiplayerSystem { this._client = new Peer(this.clientId, { host: window.location.hostname, - port: 9000, + port: parseInt(import.meta.env.VITE_MULTIPLAYER_PORT) ?? 9000, path: "/", }) @@ -196,16 +200,7 @@ class MultiplayerSystem { ).catch(console.error) // TODO Get actual sceneObjectKey this._connections.delete(conn.peer) - - if (this._host == null) { - const newHost = this._peers.reduce((prev, current) => - (this._clientToInfoMap.get(prev.peer)?.creationTime ?? Infinity) < - (this._clientToInfoMap.get(current.peer)?.creationTime ?? Infinity) - ? prev - : current - ) - this._clientToInfoMap.get(newHost.peer)!.isHost = true // TODO: enforce that everybody agrees - } + // TODO: handle host transition MultiplayerStateEvent.dispatch(MultiplayerStateEventType.PEER_CHANGE) console.log("Connection closed:", conn.peer) @@ -356,14 +351,17 @@ class MultiplayerSystem { if (fieldAssembly) { assembly = fieldAssembly } else { - this.send(peerId, { + await this.send(peerId, { type: "needAssembly", data: { assemblyName, sceneObjectKey: data.sceneObjectKey }, }) return } } else { - this.send(peerId, { type: "needAssembly", data: { assemblyName, sceneObjectKey: data.sceneObjectKey } }) + await this.send(peerId, { + type: "needAssembly", + data: { assemblyName, sceneObjectKey: data.sceneObjectKey }, + }) return } } @@ -412,7 +410,7 @@ class MultiplayerSystem { }, } - this.send(peerId, message) + await this.send(peerId, message) } handleDeleteObject(sceneObjectKey: number, peerId: string) { @@ -472,10 +470,6 @@ class MultiplayerSystem { return [...this._connections.values()] } - private get _host() { - return this._peers.find(conn => this._clientToInfoMap.get(conn.peer)?.isHost) - } - get peerInfo(): ClientInfo[] { return this.peerIDs.map( peerId => diff --git a/fission/src/test/TestSetup.server.ts b/fission/src/test/TestSetup.server.ts index 1f41f9b6c1..74981d24be 100644 --- a/fission/src/test/TestSetup.server.ts +++ b/fission/src/test/TestSetup.server.ts @@ -1,37 +1,37 @@ -import http from "node:http" +import type http from "node:http" import path from "node:path" -import sirv from "sirv" +import express from "express" +import { ExpressPeerServer } from "peer" let server: http.Server | undefined -const PORT = 3001 -const serveDirectory = path.join(process.cwd(), "public") +const ASSET_PORT = 3001 +const serveDirectory = path.join(process.cwd(), "public/Downloadables") export async function setup() { if (server) { return } - - console.log("Starting static file server...") - - const assets = sirv(serveDirectory) - - server = http.createServer((req, res) => { - res.setHeader("Access-Control-Allow-Origin", "*") - res.setHeader("Access-Control-Allow-Methods", "GET") - assets(req, res) - }) + console.log("Starting testing server...") + const expressServer = express() await new Promise((resolve, reject) => { - if (!server) { + if (!expressServer) { console.warn("no server") return } - server.listen(PORT, "127.0.0.1", () => { - console.log(`Serving files from ${serveDirectory} on port ${PORT} `) + + server = expressServer.listen(ASSET_PORT, "127.0.0.1", () => { + console.log(`Started testing server on port ${ASSET_PORT}`) resolve() }) + const peerjsServer = ExpressPeerServer(server, { + allow_discovery: true, + path: "/", + }) + expressServer.use("/Downloadables/", express.static(serveDirectory)) + expressServer.use("/", peerjsServer) server.once("error", err => { - console.error("Failed to start static file server:", err) + console.error("Failed to start testing server:", err) server = undefined reject(err) }) @@ -43,12 +43,12 @@ export async function teardown() { await new Promise((resolve, reject) => { server!.close(err => { if (err) { - console.error("Error stopping static file server:", err) + console.error("Error stopping testing server:", err) reject(err) return } - console.log("Static file server stopped.") + console.log("testing server stopped.") server = undefined resolve() }) diff --git a/fission/src/test/multiplayer/Multiplayer.test.ts b/fission/src/test/multiplayer/Multiplayer.test.ts new file mode 100644 index 0000000000..c442901166 --- /dev/null +++ b/fission/src/test/multiplayer/Multiplayer.test.ts @@ -0,0 +1,87 @@ +import { server } from "@vitest/browser/context" +import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest" +import MultiplayerSystem from "@/systems/multiplayer/MultiplayerSystem.ts" +import PreferencesSystem from "@/systems/preferences/PreferencesSystem.ts" +import World from "@/systems/World.ts" + +vi.spyOn(World, "initWorld").mockImplementation(async () => { + console.log("tried to init world") +}) +describe("Multiplayer Tests", () => { + let multiplayer: MultiplayerSystem | undefined + let roomId: string = "1000000" + let altRoomId: string = "1000001" + beforeAll(() => { + vi.spyOn(World, "setMultiplayerSystem").mockImplementation(system => { + multiplayer = system + }) + }) + beforeEach(() => { + vi.clearAllMocks() + roomId = (parseInt(roomId) - 2).toString(10) + altRoomId = (parseInt(altRoomId) - 2).toString(10) + }) + afterEach(() => { + multiplayer?.destroy() + multiplayer = undefined + PreferencesSystem.setGlobalPreference("MultiplayerClientID", "") + }) + + test("Multiplayer system connects to server", async () => { + const success = await MultiplayerSystem.setup(roomId, "User", true) + expect(success).toBe(true) + expect(multiplayer).toBeDefined() + expect(multiplayer?.roomId).toBe(roomId) + }) + test("Can't join empty room", async () => { + const success = await MultiplayerSystem.setup(roomId, "User", false) + expect(success).toBe(false) + expect(multiplayer).not.toBeDefined() + }) + + describe.skipIf(server.browser == "firefox")("P2P connections", async () => { + test("Multiplayer clients connect to each other", async () => { + await MultiplayerSystem.setup(roomId, "User1", true) + expect(multiplayer).toBeDefined() + const player1 = multiplayer! + + PreferencesSystem.setGlobalPreference("MultiplayerClientID", "") + await MultiplayerSystem.setup(roomId, "User2", false) + expect(multiplayer).toBeDefined() + const player2 = multiplayer! + + expect(player1.roomId).toBe(player2.roomId) + await vi.waitUntil(() => player1.peerIDs.length > 0 && player2.peerIDs.length > 0) + expect(player1.peerIDs).toStrictEqual([player2.clientId]) + expect(player2.peerIDs).toStrictEqual([player1.clientId]) + }) + + test("Multiplayer clients check authentication", async () => { + await MultiplayerSystem.setup(roomId, "User1", true) + expect(multiplayer).toBeDefined() + const player1 = multiplayer! + + PreferencesSystem.setGlobalPreference("MultiplayerClientID", "") + await MultiplayerSystem.setup(altRoomId, "User2", true) + expect(multiplayer).toBeDefined() + const player2 = multiplayer! + + const connectionSpy = vi.fn() + const acceptedConnectionSpy = vi.spyOn(player1, "setupConnectionHandlers") + player1.getClient().on("connection", connectionSpy) + + player2.getClient().connect(player1.clientId, { + metadata: { + authHash: "invalid", + }, + }) + + await vi.waitUntil(() => connectionSpy.mock.calls.length > 0) + + expect(acceptedConnectionSpy).not.toHaveBeenCalled() + + expect(player1.peerIDs).toStrictEqual([]) + expect(player2.peerIDs).toStrictEqual([]) + }) + }) +}) diff --git a/fission/src/vite-env.d.ts b/fission/src/vite-env.d.ts index 11f02fe2a0..b1fa0342a3 100644 --- a/fission/src/vite-env.d.ts +++ b/fission/src/vite-env.d.ts @@ -1 +1,11 @@ /// + +interface ImportMetaEnv { + // biome-ignore lint/style/useNamingConvention: environment variable + readonly VITE_MULTIPLAYER_PORT: string +} + +// biome-ignore lint/correctness/noUnusedVariables: funky env stuff it is used +interface ImportMeta { + readonly env: ImportMetaEnv +} diff --git a/fission/vite.config.ts b/fission/vite.config.ts index d7e2f991ad..60ffd1e1df 100644 --- a/fission/vite.config.ts +++ b/fission/vite.config.ts @@ -47,7 +47,7 @@ const localAssetsExist = await fs.access("./public/Downloadables/Mira",fs.consta // https://vitejs.dev/config/ export default defineConfig(({ mode }) => { process.env = {...process.env, ...loadEnv(mode, process.cwd())}; - + process.env.VITE_MULTIPLAYER_PORT = mode === "test" ? "3001" : "9000" const useLocalAssets = localAssetsExist && (mode === "test" || process.env.NODE_ENV=="development") if (!localAssetsExist && (mode === "test" || process.env.NODE_ENV=="development")) { From 08667b31631a6fd73afa4f33754127a8ab3c17f3 Mon Sep 17 00:00:00 2001 From: Zach Rutman Date: Wed, 6 Aug 2025 10:00:03 -0700 Subject: [PATCH 30/32] feat: remove printing in tests and make server exit gracefully --- .../systems/multiplayer/MultiplayerSystem.ts | 22 +++++++------- fission/src/test/TestSetup.server.ts | 25 ++++++++-------- .../src/test/multiplayer/Multiplayer.test.ts | 4 +++ multiplayer/server.ts | 29 ++----------------- 4 files changed, 30 insertions(+), 50 deletions(-) diff --git a/fission/src/systems/multiplayer/MultiplayerSystem.ts b/fission/src/systems/multiplayer/MultiplayerSystem.ts index c7e9586a46..ed311d0ca8 100644 --- a/fission/src/systems/multiplayer/MultiplayerSystem.ts +++ b/fission/src/systems/multiplayer/MultiplayerSystem.ts @@ -59,14 +59,14 @@ class MultiplayerSystem { path: "/", }) - this._client.on("call", e => console.log("peerjs call", e)) + this._client.on("call", e => console.debug("peerjs call", e)) this._client.on("close", () => { - console.log("peerjs close") + console.debug("peerjs close") }) this._initializationPromise = new Promise(resolve => { this._client.on("open", async (id: string) => { - console.log(`Broker connection opened: ID - ${id}`) + console.debug(`Broker connection opened: ID - ${id}`) const peerCount = await this.connectToRoom() if (peerCount == 0 && !isHost) { globalAddToast("warning", `Could not find room`, this.roomId) @@ -92,12 +92,12 @@ class MultiplayerSystem { resolve(false) }) this._client.on("disconnected", peer => { - console.info("PeerJS Disconnect:", peer, this._clientToInfoMap.get(peer)?.displayName ?? "") + console.log("PeerJS Disconnect:", peer, this._clientToInfoMap.get(peer)?.displayName ?? "") }) }) this._client.on("connection", async conn => { - console.log("Receiving Connection: ", conn.peer) + console.debug("Receiving Connection: ", conn.peer) if ( conn.metadata.authHash != (await createSha256Hash({ @@ -128,7 +128,7 @@ class MultiplayerSystem { const peersPromise = new Promise(resolve => this._client.listAllPeers(resolve)) const peers = await peersPromise - console.log(`Peers: ${peers}`) + console.debug(`Peers: ${peers}`) const peerCount = await Promise.all( peers @@ -152,7 +152,7 @@ class MultiplayerSystem { }) this.setupConnectionHandlers(conn) - console.log(`Initiating Connection: ${peer}`) + console.debug(`Initiating Connection: ${peer}`) return true }) ).then(res => res.filter(success => success).length) @@ -180,7 +180,7 @@ class MultiplayerSystem { return } conn.on("open", async () => { - console.log("Connection opened", conn.peer) + console.debug("Connection opened", conn.peer) this._connections.set(conn.peer, conn) MultiplayerStateEvent.dispatch(MultiplayerStateEventType.PEER_CHANGE) await this.send(conn.peer, { type: "info", data: this.info }) @@ -203,9 +203,9 @@ class MultiplayerSystem { // TODO: handle host transition MultiplayerStateEvent.dispatch(MultiplayerStateEventType.PEER_CHANGE) - console.log("Connection closed:", conn.peer) + console.debug("Connection closed:", conn.peer) }) - conn.on("iceStateChanged", console.log) + conn.on("iceStateChanged", e => console.debug("ice change", e)) conn.on("error", (err: Error) => { console.error("Connection error:", err) @@ -445,7 +445,7 @@ class MultiplayerSystem { } async broadcast(message: Message) { - console.log(`Sending Message: ${message.type}`) + console.debug(`Sending Message: ${message.type}`) return await Promise.all(this._peers.map(peer => peer.send(message))) } diff --git a/fission/src/test/TestSetup.server.ts b/fission/src/test/TestSetup.server.ts index 74981d24be..9ab8e4d98e 100644 --- a/fission/src/test/TestSetup.server.ts +++ b/fission/src/test/TestSetup.server.ts @@ -1,4 +1,4 @@ -import type http from "node:http" +import http from "node:http" import path from "node:path" import express from "express" import { ExpressPeerServer } from "peer" @@ -11,25 +11,24 @@ export async function setup() { return } console.log("Starting testing server...") - const expressServer = express() + const expressApp = express() + server = http.createServer(expressApp) + const peerjsServer = ExpressPeerServer(server, { + allow_discovery: true, + path: "/", + }) + expressApp.use("/Downloadables/", express.static(serveDirectory)) + expressApp.use("/", peerjsServer) await new Promise((resolve, reject) => { - if (!expressServer) { + if (!server) { console.warn("no server") return } - - server = expressServer.listen(ASSET_PORT, "127.0.0.1", () => { + server.listen(ASSET_PORT, "127.0.0.1", () => { console.log(`Started testing server on port ${ASSET_PORT}`) resolve() }) - const peerjsServer = ExpressPeerServer(server, { - allow_discovery: true, - path: "/", - }) - expressServer.use("/Downloadables/", express.static(serveDirectory)) - expressServer.use("/", peerjsServer) - server.once("error", err => { console.error("Failed to start testing server:", err) server = undefined @@ -47,10 +46,10 @@ export async function teardown() { reject(err) return } - console.log("testing server stopped.") server = undefined resolve() + process.exit(0) }) }) } diff --git a/fission/src/test/multiplayer/Multiplayer.test.ts b/fission/src/test/multiplayer/Multiplayer.test.ts index c442901166..157ff26113 100644 --- a/fission/src/test/multiplayer/Multiplayer.test.ts +++ b/fission/src/test/multiplayer/Multiplayer.test.ts @@ -15,6 +15,10 @@ describe("Multiplayer Tests", () => { vi.spyOn(World, "setMultiplayerSystem").mockImplementation(system => { multiplayer = system }) + vi.spyOn(console, "log").mockImplementation(() => {}) + vi.spyOn(console, "warn").mockImplementation(() => {}) + vi.spyOn(console, "info").mockImplementation(() => {}) + vi.spyOn(console, "debug").mockImplementation(() => {}) }) beforeEach(() => { vi.clearAllMocks() diff --git a/multiplayer/server.ts b/multiplayer/server.ts index a1d371b1ee..f3880058b1 100644 --- a/multiplayer/server.ts +++ b/multiplayer/server.ts @@ -1,28 +1,5 @@ -import { PeerServer } from "peer"; +import { setup, teardown } from "../fission/src/test/TestSetup.server.ts"; -export const PORT = 9000; +await setup() -const peerServer = PeerServer({ - path: "/", - port: PORT, - allow_discovery: true, -}); - -console.log(`WebRTC Connection Server Running on Port ${PORT}`); -const connected = new Set() -peerServer.on("connection", (client) => { - console.log(`Connection with client ${client.getId()}`); - connected.add(client.getId()) -}); -peerServer.on("disconnect", (client) => { - console.log(`Client ${client.getId()} disconnecting`) - - connected.delete(client.getId()) -}) - -setInterval(() => { - let string = "" - connected.forEach((id) => string+=id+" ") - console.log(string) -}, 15000) -// peerServer.on("") \ No newline at end of file +setTimeout(teardown, 5000) \ No newline at end of file From de50ba7113fb585c33e07cc3da716e4882b5aaec Mon Sep 17 00:00:00 2001 From: Zach Rutman Date: Thu, 7 Aug 2025 11:28:35 -0700 Subject: [PATCH 31/32] feat: add singleplayer button back, fix default scene order and filenames --- fission/src/mirabuf/MirabufLoader.ts | 7 ++-- .../systems/multiplayer/MultiplayerSystem.ts | 13 ++++--- fission/src/systems/physics/PhysicsSystem.ts | 18 ++++------ .../systems/preferences/PreferencesSystem.ts | 25 ++++++-------- fission/src/systems/scene/SceneRenderer.ts | 2 +- fission/src/ui/modals/MainMenuModal.tsx | 34 ++++++++++++------- .../ui/panels/mirabuf/ImportMirabufPanel.tsx | 24 ++++++------- 7 files changed, 63 insertions(+), 60 deletions(-) diff --git a/fission/src/mirabuf/MirabufLoader.ts b/fission/src/mirabuf/MirabufLoader.ts index 475d6b3b39..ffec7f1823 100644 --- a/fission/src/mirabuf/MirabufLoader.ts +++ b/fission/src/mirabuf/MirabufLoader.ts @@ -508,9 +508,10 @@ class MirabufCachingService { ): Promise { try { const backupID = Date.now().toString() - if (!miraType) { - console.debug("Double loading") - miraType = this.assemblyFromBuffer(miraBuff).dynamic ? MiraType.ROBOT : MiraType.FIELD + if (miraType == null || name == null) { + const assembly = this.assemblyFromBuffer(miraBuff) + miraType ??= assembly.dynamic ? MiraType.ROBOT : MiraType.FIELD + name ??= assembly.info?.name ?? undefined } // Local cache map diff --git a/fission/src/systems/multiplayer/MultiplayerSystem.ts b/fission/src/systems/multiplayer/MultiplayerSystem.ts index e65ad66ddd..498f591c3f 100644 --- a/fission/src/systems/multiplayer/MultiplayerSystem.ts +++ b/fission/src/systems/multiplayer/MultiplayerSystem.ts @@ -110,11 +110,9 @@ class MultiplayerSystem { }) ConfigurationSavedEvent.listen(() => { - ;[...World.sceneRenderer.sceneObjects.values()] - .filter(obj => obj instanceof MirabufSceneObject) - .forEach(obj => { - this.broadcast({ type: "metadataUpdate", data: obj.multiplayerInfo }).catch(console.error) - }) + World.sceneRenderer.mirabufSceneObjects.getAll().forEach(obj => { + this.broadcast({ type: "metadataUpdate", data: obj.multiplayerInfo }).catch(console.error) + }) }) } @@ -159,11 +157,12 @@ class MultiplayerSystem { // Called by the host, initializes the world with some defined set of objects, robots can be spawned in later async initWorld(physicsSystem: PhysicsSystem) { - const sceneObjects = [...World.sceneRenderer.sceneObjects.values()] - .filter((sceneObject): sceneObject is MirabufSceneObject => sceneObject instanceof MirabufSceneObject) + const sceneObjects = World.sceneRenderer.mirabufSceneObjects + .getAll() .map(sceneObject => mirabuf.Assembly.encode(sceneObject.mirabufInstance.parser.assembly).finish() ) as EncodedAssembly[] + await this.broadcast({ type: "init", data: { physicsSystem, objects: sceneObjects }, diff --git a/fission/src/systems/physics/PhysicsSystem.ts b/fission/src/systems/physics/PhysicsSystem.ts index 21e1f85e12..238e2a77fb 100644 --- a/fission/src/systems/physics/PhysicsSystem.ts +++ b/fission/src/systems/physics/PhysicsSystem.ts @@ -1,6 +1,6 @@ import type Jolt from "@azaleacolburn/jolt-physics" import * as THREE from "three" -import MirabufSceneObject from "@/mirabuf/MirabufSceneObject" +import type MirabufSceneObject from "@/mirabuf/MirabufSceneObject" import type { BodyAssociate } from "@/systems/physics/BodyAssociate.ts" import JOLT from "@/util/loading/JoltSyncLoader" import type MirabufParser from "../../mirabuf/MirabufParser" @@ -1306,10 +1306,9 @@ class PhysicsSystem extends WorldSystem { interObjectCollisions.length > 0 ? { type: "collision", - data: [...World.sceneRenderer.sceneObjects.values()] - .map(object => { - if (object instanceof MirabufSceneObject) return object.getUpdateData() - }) + data: World.sceneRenderer.mirabufSceneObjects + .getAll() + .map(object => object.getUpdateData()) .filter(n => n != null), } : { @@ -1486,12 +1485,9 @@ class PhysicsSystem extends WorldSystem { private bodyToMiraSceneObject(body: Jolt.Body): MirabufSceneObject | null { const id = body.GetID() return ( - [...World.sceneRenderer.sceneObjects] - .map(x => x[1]) - .find( - (x): x is MirabufSceneObject => - x instanceof MirabufSceneObject && [...x.mechanism.nodeToBody].map(n => n[1]).includes(id) - ) ?? null + World.sceneRenderer.mirabufSceneObjects.findWhere(obj => + [...obj.mechanism.nodeToBody].some(n => n[1] == id) + ) ?? null ) } diff --git a/fission/src/systems/preferences/PreferencesSystem.ts b/fission/src/systems/preferences/PreferencesSystem.ts index 208f69dee7..f49f3f0bad 100644 --- a/fission/src/systems/preferences/PreferencesSystem.ts +++ b/fission/src/systems/preferences/PreferencesSystem.ts @@ -1,5 +1,4 @@ import { MiraType } from "@/mirabuf/MirabufLoader" -import MirabufSceneObject from "@/mirabuf/MirabufSceneObject" import World from "../World" import { defaultFieldPreferences, @@ -112,22 +111,20 @@ class PreferencesSystem { return mergedPrefs } - private static sendPreferences(miraName: string, miraType: MiraType) { + private static async sendPreferences(miraName: string, miraType: MiraType) { if (!World.multiplayerSystem) return - const sceneObjectPair = [...World.sceneRenderer.sceneObjects] - .filter( - (objectPair): objectPair is [number, MirabufSceneObject] => - objectPair[1] instanceof MirabufSceneObject && objectPair[1].miraType === miraType - ) - .find(([_id, o]) => o.assemblyName === miraName) - if (!sceneObjectPair) return + const sceneObject = World.sceneRenderer.mirabufSceneObjects.findWhere( + obj => obj.miraType == miraType && obj.assemblyName == miraName + ) + + if (!sceneObject) return - World.multiplayerSystem.broadcast({ + await World.multiplayerSystem.broadcast({ type: "configureObject", data: { - sceneObjectKey: sceneObjectPair[0], - objectConfigurationData: sceneObjectPair[1].getPreferenceData(), + sceneObjectKey: sceneObject.id, + objectConfigurationData: sceneObject.getPreferenceData(), }, }) } @@ -137,7 +134,7 @@ class PreferencesSystem { const allRoboPrefs = this.getAllRobotPreferences() allRoboPrefs[miraName] = value - this.sendPreferences(miraName, MiraType.ROBOT) + this.sendPreferences(miraName, MiraType.ROBOT).catch(console.error) } /** Sets the FieldPreferences object for the field of a specific mira name */ @@ -145,7 +142,7 @@ class PreferencesSystem { const allFieldPrefs = this.getAllFieldPreferences() allFieldPrefs[miraName] = value - this.sendPreferences(miraName, MiraType.FIELD) + this.sendPreferences(miraName, MiraType.FIELD).catch(console.error) } /** Sets the MotorPreferences object for the motor of a specific mira name */ diff --git a/fission/src/systems/scene/SceneRenderer.ts b/fission/src/systems/scene/SceneRenderer.ts index 77837c12d1..bc39994b86 100644 --- a/fission/src/systems/scene/SceneRenderer.ts +++ b/fission/src/systems/scene/SceneRenderer.ts @@ -64,7 +64,7 @@ class SceneRenderer extends WorldSystem { public readonly mirabufSceneObjects = { getAll: () => this.filterSceneObjects(obj => obj instanceof MirabufSceneObject), - findWhere: (predicate: Parameters<(typeof Array)["prototype"]["find"]>[0]) => + findWhere: (predicate: (obj: MirabufSceneObject) => boolean) => this.mirabufSceneObjects.getAll().find(predicate), getField: () => this.mirabufSceneObjects.findWhere(obj => obj.miraType == MiraType.FIELD), getRobots: () => this.mirabufSceneObjects.getAll().filter(obj => obj.miraType == MiraType.ROBOT), diff --git a/fission/src/ui/modals/MainMenuModal.tsx b/fission/src/ui/modals/MainMenuModal.tsx index 43404e7189..1171cdf57e 100644 --- a/fission/src/ui/modals/MainMenuModal.tsx +++ b/fission/src/ui/modals/MainMenuModal.tsx @@ -32,21 +32,13 @@ const MainMenuModal: React.FC> = ({ mo onClick={() => { closeModal(CloseType.Accept) startSingleplayerCallback() - - Promise.all([ - MirabufCachingService.cacheRemote("/api/mira/fields/FRC Field 2023_v7.mira", MiraType.FIELD), - MirabufCachingService.cacheRemote("/api/mira/robots/Dozer_v9.mira", MiraType.ROBOT), - ]).then(([cachedField, cachedRobot]) => { - if (cachedField && cachedRobot) { - spawnCachedMira(cachedField, MiraType.FIELD) - spawnCachedMira(cachedRobot, MiraType.ROBOT) - } - }) }} - className="my-1" + fullWidth={true} + className="mt-1 mb-3" > - Load Default + Singleplayer + + + ) } diff --git a/fission/src/ui/panels/mirabuf/ImportMirabufPanel.tsx b/fission/src/ui/panels/mirabuf/ImportMirabufPanel.tsx index 6bcb47b19b..9587991e34 100644 --- a/fission/src/ui/panels/mirabuf/ImportMirabufPanel.tsx +++ b/fission/src/ui/panels/mirabuf/ImportMirabufPanel.tsx @@ -96,7 +96,7 @@ function getCacheInfo(miraType: MiraType): MirabufCacheInfo[] { ) } -export function spawnCachedMira(info: MirabufCacheInfo, type: MiraType, progressHandle?: ProgressHandle) { +export async function spawnCachedMira(info: MirabufCacheInfo, type: MiraType, progressHandle?: ProgressHandle) { // If spawning a field, then remove all other fields if (type === MiraType.FIELD) { World.sceneRenderer.removeAllFields() @@ -107,7 +107,7 @@ export function spawnCachedMira(info: MirabufCacheInfo, type: MiraType, progress } World.physicsSystem.holdPause(PAUSE_REF_ASSEMBLY_SPAWNING) - MirabufCachingService.get(info.id, type) + await MirabufCachingService.get(info.id, type) .then(async assembly => { if (assembly) { createMirabuf(assembly, progressHandle, info.id).then(x => { @@ -263,8 +263,8 @@ const ImportMirabufPanel: React.FC { - spawnCachedMira(info, type) + async (info: MirabufCacheInfo, type: MiraType) => { + await spawnCachedMira(info, type) if (panel) closePanel(panel.id, CloseType.Cancel) }, @@ -278,9 +278,9 @@ const ImportMirabufPanel: React.FC { + .then(async cacheInfo => { if (cacheInfo) { - spawnCachedMira(cacheInfo, type, status) + await spawnCachedMira(cacheInfo, type, status) } else { status.fail("Failed to cache") } @@ -314,9 +314,9 @@ const ImportMirabufPanel: React.FC { + .then(async cacheInfo => { if (cacheInfo) { - spawnCachedMira(cacheInfo, type, status) + await spawnCachedMira(cacheInfo, type, status) } else { status.fail("Failed to cache") } @@ -338,13 +338,13 @@ const ImportMirabufPanel: React.FC { + primaryOnClick: async () => { console.log(`Selecting cached robot: ${info.cacheKey}`) - selectCache(info, MiraType.ROBOT) + await selectCache(info, MiraType.ROBOT) }, - secondaryOnClick: () => { + secondaryOnClick: async () => { console.log(`Deleting cache of: ${info.cacheKey}`) - MirabufCachingService.remove(info.cacheKey, info.id, MiraType.ROBOT) + await MirabufCachingService.remove(info.cacheKey, info.id, MiraType.ROBOT) setCachedRobots(getCacheInfo(MiraType.ROBOT)) }, From f898e4ffdd6f886da893a7d4f57082529bd17dcf Mon Sep 17 00:00:00 2001 From: Zach Rutman Date: Thu, 7 Aug 2025 15:16:59 -0700 Subject: [PATCH 32/32] feat: add match mode timer support --- .../match_mode/DefaultMatchModeConfigs.ts | 6 +- fission/src/systems/match_mode/MatchMode.ts | 71 +++++++++++-------- .../src/systems/match_mode/MatchModeTypes.ts | 4 +- .../systems/multiplayer/MultiplayerSystem.ts | 68 +++++++++++------- fission/src/systems/multiplayer/types.ts | 29 +++----- fission/src/test/MatchMode.test.ts | 11 +++ multiplayer/server.ts | 29 +++++++- 7 files changed, 137 insertions(+), 81 deletions(-) create mode 100644 fission/src/test/MatchMode.test.ts diff --git a/fission/src/systems/match_mode/DefaultMatchModeConfigs.ts b/fission/src/systems/match_mode/DefaultMatchModeConfigs.ts index eb7dcd2403..7b829877b5 100644 --- a/fission/src/systems/match_mode/DefaultMatchModeConfigs.ts +++ b/fission/src/systems/match_mode/DefaultMatchModeConfigs.ts @@ -12,7 +12,7 @@ class DefaultMatchModeConfigs { teleopTime: 135, endgameTime: 20, ignoreRotation: true, - maxHeight: Infinity, + maxHeight: Number.MAX_SAFE_INTEGER, heightLimitPenalty: 0, sideMaxExtension: convertFeetToMeters(1.5), sideExtensionPenalty: 0, @@ -60,9 +60,9 @@ class DefaultMatchModeConfigs { teleopTime: 15, endgameTime: 5, ignoreRotation: true, - maxHeight: Infinity, + maxHeight: Number.MAX_SAFE_INTEGER, heightLimitPenalty: 0, - sideMaxExtension: Infinity, + sideMaxExtension: Number.MAX_SAFE_INTEGER, sideExtensionPenalty: 0, } } diff --git a/fission/src/systems/match_mode/MatchMode.ts b/fission/src/systems/match_mode/MatchMode.ts index de0d528d4a..49d002bd9f 100644 --- a/fission/src/systems/match_mode/MatchMode.ts +++ b/fission/src/systems/match_mode/MatchMode.ts @@ -2,6 +2,7 @@ import beep from "@/assets/sound-files/beep.wav" import MatchEnd from "@/assets/sound-files/MatchEnd.wav" import MatchResume from "@/assets/sound-files/MatchResume.wav" import MatchStart from "@/assets/sound-files/MatchStart.wav" +import World from "@/systems/World.ts" import { globalOpenModal } from "@/ui/components/GlobalUIControls" import MatchResultsModal from "@/ui/modals/MatchResultsModal" import type { MatchModeConfig } from "@/ui/panels/configuring/MatchModeConfigPanel" @@ -67,56 +68,74 @@ class MatchMode { ) } - startTimer(duration: number, functionCall: () => void, updateTimeLeft: boolean = true) { + get matchModeConfig() { + return this._matchModeConfig + } + + async runTimer(duration: number, updateTimeLeft: boolean = true) { this._initialTime = duration this._timeLeft = duration // Dispatch an event to update the time left in the UI if (updateTimeLeft) new UpdateTimeLeft(this._initialTime).dispatch() - - this._intervalId = window.setInterval(() => { - this._timeLeft-- - - if (this._timeLeft >= 0 && updateTimeLeft) { - new UpdateTimeLeft(this._timeLeft).dispatch() - } - - // Checks if endgame has started - if (this._matchModeType === MatchModeType.TELEOP && this._timeLeft == this._matchModeConfig.endgameTime) { - this.endgameStart() - } - - if (this._timeLeft <= 0) { - clearInterval(this._intervalId as number) - functionCall() - } - }, 1000) + return new Promise(res => { + this._intervalId = window.setInterval(() => { + this._timeLeft-- + + if (this._timeLeft >= 0 && updateTimeLeft) { + new UpdateTimeLeft(this._timeLeft).dispatch() + } + + // Checks if endgame has started + if ( + this._matchModeType === MatchModeType.TELEOP && + this._timeLeft == this._matchModeConfig.endgameTime + ) { + this.endgameStart() + } + + if (this._timeLeft <= 0) { + console.log("resolving") + res() + } + }, 1000) + }).finally(() => { + clearInterval(this._intervalId as number) + }) } autonomousModeStart() { void SoundPlayer.getInstance().play(MatchStart) this.setMatchModeType(MatchModeType.AUTONOMOUS) - this.startTimer(this._matchModeConfig.autonomousTime, () => this.autonomousModeEnd()) + this.runTimer(this._matchModeConfig.autonomousTime).then(() => this.autonomousModeEnd()) } autonomousModeEnd() { void SoundPlayer.getInstance().play(MatchEnd) - this.startTimer(3, () => this.teleopModeStart(), false) // Delay between autonomous and teleop modes + this.runTimer(3, false).then(() => this.teleopModeStart()) // Delay between autonomous and teleop modes } teleopModeStart() { void SoundPlayer.getInstance().play(MatchResume) this.setMatchModeType(MatchModeType.TELEOP) - this.startTimer(this._matchModeConfig.teleopTime, () => this.matchEnded()) + this.runTimer(this._matchModeConfig.teleopTime).then(() => this.matchEnded()) } endgameStart() { void SoundPlayer.getInstance().play(beep) this._matchModeType = MatchModeType.ENDGAME + console.log("endgame start") this._endgame = true } - start() { + async start(broadcast = true) { + if (broadcast) { + await World.multiplayerSystem?.broadcast({ + type: "matchModeState", + data: { event: "start", config: this._matchModeConfig, moveRobots: false }, + }) + console.log("sent multiplayer") + } this.autonomousModeStart() SimulationSystem.resetScores() RobotDimensionTracker.matchStart() @@ -126,11 +145,7 @@ class MatchMode { void SoundPlayer.getInstance().play(MatchEnd) clearInterval(this._intervalId as number) this.setMatchModeType(MatchModeType.MATCH_ENDED) - globalOpenModal?.(MatchResultsModal, undefined, undefined, { - allowClickAway: false, - hideCancel: true, - hideAccept: true, - }) + globalOpenModal(MatchResultsModal, undefined) } sandboxModeStart() { diff --git a/fission/src/systems/match_mode/MatchModeTypes.ts b/fission/src/systems/match_mode/MatchModeTypes.ts index dfaad2575b..420521713e 100644 --- a/fission/src/systems/match_mode/MatchModeTypes.ts +++ b/fission/src/systems/match_mode/MatchModeTypes.ts @@ -11,7 +11,7 @@ export const DEFAULT_AUTONOMOUS_TIME = 15 export const DEFAULT_TELEOP_TIME = 135 export const DEFAULT_ENDGAME_TIME = 20 export const DEFAULT_IGNORE_ROTATION = true -export const DEFAULT_MAX_HEIGHT = Infinity +export const DEFAULT_MAX_HEIGHT = Number.MAX_SAFE_INTEGER export const DEFAULT_HEIGHT_LIMIT_PENALTY = 2 -export const DEFAULT_SIDE_MAX_EXTENSION = Infinity +export const DEFAULT_SIDE_MAX_EXTENSION = Number.MAX_SAFE_INTEGER export const DEFAULT_SIDE_EXTENSION_PENALTY = 2 diff --git a/fission/src/systems/multiplayer/MultiplayerSystem.ts b/fission/src/systems/multiplayer/MultiplayerSystem.ts index ed311d0ca8..4566f4a167 100644 --- a/fission/src/systems/multiplayer/MultiplayerSystem.ts +++ b/fission/src/systems/multiplayer/MultiplayerSystem.ts @@ -5,6 +5,7 @@ import { ConfigurationSavedEvent } from "@/events/ConfigurationSavedEvent.ts" import MirabufCachingService, { MiraType } from "@/mirabuf/MirabufLoader" import MirabufSceneObject, { createMirabuf } from "@/mirabuf/MirabufSceneObject" import { mirabuf } from "@/proto/mirabuf" +import MatchMode from "@/systems/match_mode/MatchMode.ts" import PreferencesSystem from "@/systems/preferences/PreferencesSystem.ts" import JOLT from "@/util/loading/JoltSyncLoader" import type PhysicsSystem from "../physics/PhysicsSystem" @@ -15,7 +16,9 @@ import type { EncodedAssembly, InitData, InitObjectData, + MatchModeStateData, Message, + MessageType, MetadataUpdateData, ObjectPreferences, UpdateObjectData, @@ -212,34 +215,45 @@ class MultiplayerSystem { }) } + peerMessageHandlers = { + info: this.handlePeerInfo, + init: this.handleWorldInitialization, + update: this.handlePeerUpdate, + collision: this.handleCollision, + newObject: this.handleNewObject, + needAssembly: this.handleAssemblyRequest, + deleteObject: this.handleDeleteObject, + configureObject: this.handleObjectConfiguration, + metadataUpdate: this.handleMetadataUpdate, + matchModeState: this.handleMatchModeState, + robotLeft: () => { + console.warn("unhandled event") + }, + ping: () => { + console.warn("unhandled event") + }, + pong: () => { + console.warn("unhandled event") + }, + } as const satisfies { [K in keyof MessageType]: (data: MessageType[K], peerId: string) => Promise | void } + async handlePeerMessage(message: Message, peerId: string) { - switch (message.type) { - case "info": - this.handlePeerInfo(message.data) - break - case "init": - await this.handleWorldInitialization(message.data) - break - case "update": - this.handlePeerUpdate(message.data) - break - case "collision": - this.handleCollision(message.data) - break - case "newObject": - await this.handleNewObject(message.data, peerId) - break - case "needAssembly": - await this.handleAssemblyRequest(message.data, peerId) - break - case "deleteObject": - this.handleDeleteObject(message.data, peerId) - break - case "configureObject": - this.handleObjectConfiguration(message.data) - break - case "metadataUpdate": - this.handleMetadataUpdate(message.data) + const handler = this.peerMessageHandlers[message.type].bind(this) as ( + data: MessageType[typeof message.type], + peerId: string + ) => Promise | void + await handler(message.data, peerId) + } + + async handleMatchModeState(data: MatchModeStateData) { + console.log(data) + if (data.event == "start") { + MatchMode.getInstance().setMatchModeConfig(data.config) + await MatchMode.getInstance().start(false) + } + if (data.event == "cancel") { + MatchMode.getInstance().sandboxModeStart() + globalAddToast("info", "Match Mode Cancelled") } } diff --git a/fission/src/systems/multiplayer/types.ts b/fission/src/systems/multiplayer/types.ts index 09c48c7abb..2e06b3330f 100644 --- a/fission/src/systems/multiplayer/types.ts +++ b/fission/src/systems/multiplayer/types.ts @@ -1,25 +1,9 @@ import type MirabufSceneObject from "@/mirabuf/MirabufSceneObject" +import type { MatchModeConfig } from "@/panels/configuring/MatchModeConfigPanel.tsx" import type { Alliance, Station } from "@/systems/preferences/PreferenceTypes.ts" import type PhysicsSystem from "../physics/PhysicsSystem" -export type Metrics = { - startTime: number - totalFrames: number - frameTimes: number[] - inputsSent: number - messagesReceived: number - bytesReceived: number - bytesSent: number - connectionTime: number - averageFPS: number - networkStats: { - packetsLost: number - roundTripTimes: number[] - jitter: number - } -} - -interface MessageType { +export interface MessageType { info: ClientInfo init: InitData update: UpdateObjectData[] @@ -32,8 +16,17 @@ interface MessageType { robotLeft: RobotLeftData ping: PingData pong: PingData + matchModeState: MatchModeStateData } +export type MatchModeStateData = + | { + event: "start" + config: MatchModeConfig + moveRobots: boolean + } + | { event: "cancel" } + export type Message = { [K in keyof MessageType]: { type: K; data: MessageType[K] } }[keyof MessageType] // biome-ignore lint: We're using this for type safety diff --git a/fission/src/test/MatchMode.test.ts b/fission/src/test/MatchMode.test.ts new file mode 100644 index 0000000000..17adfb78c6 --- /dev/null +++ b/fission/src/test/MatchMode.test.ts @@ -0,0 +1,11 @@ +import { describe, expect, test } from "vitest" +import DefaultMatchModeConfigs from "@/systems/match_mode/DefaultMatchModeConfigs.ts" + +describe("Match Mode Config Checks", () => { + test("Default Configs are Serializable", () => { + const configs = DefaultMatchModeConfigs.defaultMatchModeConfigCopies + configs.forEach(config => { + expect(JSON.parse(JSON.stringify(config))).toEqual(config) + }) + }) +}) diff --git a/multiplayer/server.ts b/multiplayer/server.ts index f3880058b1..a1d371b1ee 100644 --- a/multiplayer/server.ts +++ b/multiplayer/server.ts @@ -1,5 +1,28 @@ -import { setup, teardown } from "../fission/src/test/TestSetup.server.ts"; +import { PeerServer } from "peer"; -await setup() +export const PORT = 9000; -setTimeout(teardown, 5000) \ No newline at end of file +const peerServer = PeerServer({ + path: "/", + port: PORT, + allow_discovery: true, +}); + +console.log(`WebRTC Connection Server Running on Port ${PORT}`); +const connected = new Set() +peerServer.on("connection", (client) => { + console.log(`Connection with client ${client.getId()}`); + connected.add(client.getId()) +}); +peerServer.on("disconnect", (client) => { + console.log(`Client ${client.getId()} disconnecting`) + + connected.delete(client.getId()) +}) + +setInterval(() => { + let string = "" + connected.forEach((id) => string+=id+" ") + console.log(string) +}, 15000) +// peerServer.on("") \ No newline at end of file