mirror of
https://github.com/danbulant/slightlyComplicatedTicTacToe
synced 2026-05-19 04:08:52 +00:00
initial commit
This commit is contained in:
parent
27c1df79d2
commit
7246ddea7b
26 changed files with 1946 additions and 0 deletions
12
.editorconfig
Normal file
12
.editorconfig
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
# EditorConfig is awesome: https://EditorConfig.org
|
||||||
|
|
||||||
|
# top-most EditorConfig file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
insert_final_newline = false
|
||||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
node_modules
|
||||||
|
|
@ -7,3 +7,6 @@ A multiplayer fight between dices.
|
||||||
### Audio
|
### Audio
|
||||||
|
|
||||||
reflectable【音楽素材MusMus】- Music by MusMus <https://musmus.main.jp/>
|
reflectable【音楽素材MusMus】- Music by MusMus <https://musmus.main.jp/>
|
||||||
|
<https://freesound.org/people/soundnimja/sounds/173326/>
|
||||||
|
<https://freesound.org/people/LittleRobotSoundFactory/sounds/270325/>
|
||||||
|
<https://www.dafont.com/squarefont.font>
|
||||||
|
|
|
||||||
BIN
assets/Square.ttf
Normal file
BIN
assets/Square.ttf
Normal file
Binary file not shown.
BIN
assets/Squareo.ttf
Normal file
BIN
assets/Squareo.ttf
Normal file
Binary file not shown.
BIN
assets/fall.wav
Normal file
BIN
assets/fall.wav
Normal file
Binary file not shown.
BIN
assets/jump.wav
Normal file
BIN
assets/jump.wav
Normal file
Binary file not shown.
8
client/.gitignore
vendored
Normal file
8
client/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/build
|
||||||
|
/.svelte-kit
|
||||||
|
/package
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
1
client/.npmrc
Normal file
1
client/.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
engine-strict=true
|
||||||
13
client/.prettierignore
Normal file
13
client/.prettierignore
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/build
|
||||||
|
/.svelte-kit
|
||||||
|
/package
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Ignore files for PNPM, NPM and YARN
|
||||||
|
pnpm-lock.yaml
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
6
client/.prettierrc
Normal file
6
client/.prettierrc
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"useTabs": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"printWidth": 100
|
||||||
|
}
|
||||||
38
client/README.md
Normal file
38
client/README.md
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
# create-svelte
|
||||||
|
|
||||||
|
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
|
||||||
|
|
||||||
|
## Creating a project
|
||||||
|
|
||||||
|
If you're seeing this, you've probably already done this step. Congrats!
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# create a new project in the current directory
|
||||||
|
npm create svelte@latest
|
||||||
|
|
||||||
|
# create a new project in my-app
|
||||||
|
npm create svelte@latest my-app
|
||||||
|
```
|
||||||
|
|
||||||
|
## Developing
|
||||||
|
|
||||||
|
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# or start the server and open the app in a new browser tab
|
||||||
|
npm run dev -- --open
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
To create a production version of your app:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
You can preview the production build with `npm run preview`.
|
||||||
|
|
||||||
|
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
|
||||||
28
client/package.json
Normal file
28
client/package.json
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"name": "client",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite dev",
|
||||||
|
"build": "vite build",
|
||||||
|
"package": "svelte-kit package",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"prepare": "svelte-kit sync",
|
||||||
|
"check": "svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
|
"lint": "prettier --check --plugin-search-dir=. .",
|
||||||
|
"format": "prettier --write --plugin-search-dir=. ."
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@sveltejs/adapter-auto": "next",
|
||||||
|
"@sveltejs/kit": "next",
|
||||||
|
"prettier": "^2.6.2",
|
||||||
|
"prettier-plugin-svelte": "^2.7.0",
|
||||||
|
"svelte": "^3.44.0",
|
||||||
|
"svelte-check": "^2.7.1",
|
||||||
|
"svelte-preprocess": "^4.10.6",
|
||||||
|
"tslib": "^2.3.1",
|
||||||
|
"typescript": "^4.7.4",
|
||||||
|
"vite": "^3.0.0"
|
||||||
|
},
|
||||||
|
"type": "module"
|
||||||
|
}
|
||||||
1342
client/pnpm-lock.yaml
Normal file
1342
client/pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load diff
11
client/src/app.d.ts
vendored
Normal file
11
client/src/app.d.ts
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
/// <reference types="@sveltejs/kit" />
|
||||||
|
|
||||||
|
// See https://kit.svelte.dev/docs/types#app
|
||||||
|
// for information about these interfaces
|
||||||
|
// and what to do when importing types
|
||||||
|
declare namespace App {
|
||||||
|
// interface Locals {}
|
||||||
|
// interface Platform {}
|
||||||
|
// interface Session {}
|
||||||
|
// interface Stuff {}
|
||||||
|
}
|
||||||
12
client/src/app.html
Normal file
12
client/src/app.html
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
%sveltekit.head%
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div>%sveltekit.body%</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
230
client/src/lib/Websocket.ts
Normal file
230
client/src/lib/Websocket.ts
Normal file
|
|
@ -0,0 +1,230 @@
|
||||||
|
import { writable, type Writable } from "svelte/store";
|
||||||
|
|
||||||
|
class FastEvent extends Event {
|
||||||
|
data: any;
|
||||||
|
constructor(name: string, data: any) {
|
||||||
|
super(name);
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hosts: { urls: string, credential?: string, username?: string }[] = ("stun.ipfire.org:3478\n" +
|
||||||
|
"stun.rolmail.net:3478\n" +
|
||||||
|
"stun.steinbeis-smi.de:3478\n" +
|
||||||
|
"stun.marcelproust.it:3478\n" +
|
||||||
|
"stun3.3cx.com:3478\n" +
|
||||||
|
"stun.voipraider.com:3478\n" +
|
||||||
|
"stun.kore.com:3478\n" +
|
||||||
|
"stun.voipstunt.com:3478\n" +
|
||||||
|
"stun.fairytel.at:3478\n" +
|
||||||
|
"stun.h4v.eu:3478\n" +
|
||||||
|
"stun.peethultra.be:3478\n" +
|
||||||
|
"stun.ortopediacoam.it:3478\n" +
|
||||||
|
"stun.infra.net:3478\n" +
|
||||||
|
"stun.vavadating.com:3478\n" +
|
||||||
|
"stun.mixvoip.com:3478\n" +
|
||||||
|
"stun.tele2.net:3478\n" +
|
||||||
|
"stun2.3cx.com:3478\n" +
|
||||||
|
"stun.myhowto.org:3478\n" +
|
||||||
|
"stun.cellmail.com:3478\n" +
|
||||||
|
"stun.poetamatusel.org:3478\n" +
|
||||||
|
"stun.textz.com:3478\n" +
|
||||||
|
"stun.romancecompass.com:3478\n" +
|
||||||
|
"stun.ixc.ua:3478\n" +
|
||||||
|
"stun.actionvoip.com:3478\n" +
|
||||||
|
"stun.bethesda.net:3478\n" +
|
||||||
|
"stun.parcodeinebrodi.it:3478\n" +
|
||||||
|
"stun.jay.net:3478\n" +
|
||||||
|
"stun.demos.ru:3478\n" +
|
||||||
|
"stun.cloopen.com:3478\n" +
|
||||||
|
"stun.crimeastar.net:3478\n" +
|
||||||
|
"stun.vivox.com:3478\n" +
|
||||||
|
"stun.openjobs.hu:3478\n" +
|
||||||
|
"stun.kaznpu.kz:3478\n" +
|
||||||
|
"stun.linphone.org:3478\n" +
|
||||||
|
"stun.l.google.com:19302\n" +
|
||||||
|
"stun.sonetel.net:3478").split("\n").map(t => ({ urls: "stun:" + t }));
|
||||||
|
|
||||||
|
hosts.push({
|
||||||
|
urls: 'turn:relay.backups.cz',
|
||||||
|
credential: 'webrtc',
|
||||||
|
username: 'webrtc'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
urls: 'turn:relay.backups.cz?transport=tcp',
|
||||||
|
credential: 'webrtc',
|
||||||
|
username: 'webrtc'
|
||||||
|
});
|
||||||
|
|
||||||
|
class ConnectedClient extends EventTarget {
|
||||||
|
conn: RTCPeerConnection;
|
||||||
|
sendChannel: RTCDataChannel;
|
||||||
|
candidates: any[] = [];
|
||||||
|
state: RTCDataChannelState | null = null;
|
||||||
|
|
||||||
|
constructor(public ws: WebsocketConnection, public name: string) {
|
||||||
|
super();
|
||||||
|
// @ts-ignore Initialized in the next function call
|
||||||
|
this.conn = null;
|
||||||
|
// @ts-ignore
|
||||||
|
this.sendChannel = null;
|
||||||
|
this.initializeConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeConnection() {
|
||||||
|
console.log("Initializing connection");
|
||||||
|
this.conn = new RTCPeerConnection({
|
||||||
|
iceServers: hosts
|
||||||
|
});
|
||||||
|
|
||||||
|
this.conn.onicecandidate = e => {
|
||||||
|
console.log(e);
|
||||||
|
if (!e.candidate) return;
|
||||||
|
this.candidates.push(e.candidate);
|
||||||
|
this.ws.send(JSON.stringify({ t: "cand", target: this.name, d: e.candidate }));
|
||||||
|
};
|
||||||
|
this.conn.onicecandidateerror = (e) => console.error(e);
|
||||||
|
this.conn.ondatachannel = e => {
|
||||||
|
this.sendChannel = e.channel;
|
||||||
|
let timer: any;
|
||||||
|
this.sendChannel.onclose = (e) => {
|
||||||
|
clearInterval(timer);
|
||||||
|
this.statusChanged();
|
||||||
|
}
|
||||||
|
this.sendChannel.onopen = (e) => {
|
||||||
|
timer = setInterval(() => {
|
||||||
|
this.send({ t: "p", d: Date.now() });
|
||||||
|
}, 300);
|
||||||
|
this.statusChanged();
|
||||||
|
}
|
||||||
|
this.statusChanged();
|
||||||
|
this.sendChannel.onmessage = (e) => {
|
||||||
|
const msg = JSON.parse(e.data);
|
||||||
|
switch (msg.t) {
|
||||||
|
default:
|
||||||
|
console.log("MSG", msg);
|
||||||
|
this.dispatchEvent(new FastEvent(msg.t, msg.d));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
send(data: any) {
|
||||||
|
this.sendChannel.send(JSON.stringify(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
statusChanged() {
|
||||||
|
if (this.sendChannel) {
|
||||||
|
if (this.state !== this.sendChannel.readyState) {
|
||||||
|
if (this.state === "open" && ["closing", "closed"].includes(this.sendChannel.readyState)) {
|
||||||
|
this.initializeConnection();
|
||||||
|
}
|
||||||
|
this.state = this.sendChannel.readyState;
|
||||||
|
console.log("state", this.state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class WebsocketConnection extends EventTarget {
|
||||||
|
ws: WebSocket;
|
||||||
|
fast: Map<string, ConnectedClient> = new Map();
|
||||||
|
roomName: string | null = null;
|
||||||
|
roomId: string | null = null;
|
||||||
|
|
||||||
|
constructor(public name: string) {
|
||||||
|
super();
|
||||||
|
// @ts-ignore Initialized in the next function call
|
||||||
|
this.ws = null;
|
||||||
|
this.connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
this.ws = new WebSocket("ws://" + location.hostname + ":8080");
|
||||||
|
this.ws.addEventListener("open", () => {
|
||||||
|
console.log("WS ready");
|
||||||
|
});
|
||||||
|
this.ws.addEventListener("message", (e) => {
|
||||||
|
const msg = JSON.parse(e.data);
|
||||||
|
console.log(msg);
|
||||||
|
switch (msg.t) {
|
||||||
|
case "cand": {
|
||||||
|
const fast = this.fast.get(msg.source);
|
||||||
|
if (!fast) return;
|
||||||
|
if (fast.state === "open") return console.log("Already open");
|
||||||
|
for (const candidate of msg.d) {
|
||||||
|
fast.conn.addIceCandidate(candidate).then();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "desc": {
|
||||||
|
const fast = this.fast.get(msg.source);
|
||||||
|
if (!fast) return;
|
||||||
|
if (fast.state === "open") return console.log("Already open");
|
||||||
|
fast.conn.setRemoteDescription(msg.d)
|
||||||
|
.then(() => fast.conn.createAnswer())
|
||||||
|
.then(answer => fast.conn.setLocalDescription(answer))
|
||||||
|
.then(() =>
|
||||||
|
this.ws.send(JSON.stringify({ t: "desc", target: fast.name, d: fast.conn.localDescription }))
|
||||||
|
)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "join": {
|
||||||
|
const fast = new ConnectedClient(this, msg.name);
|
||||||
|
this.fast.set(msg.name, fast);
|
||||||
|
if (fast.conn.localDescription) {
|
||||||
|
this.ws.send(JSON.stringify({ t: "desc", target: msg.name, d: fast.conn.localDescription }))
|
||||||
|
}
|
||||||
|
if (fast.candidates) {
|
||||||
|
this.ws.send(JSON.stringify({ t: "cand", target: msg.name, d: fast.candidates }));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "joined": {
|
||||||
|
const clients = msg.clients;
|
||||||
|
this.fast = new Map();
|
||||||
|
for (const client of clients) {
|
||||||
|
const fast = new ConnectedClient(this, client);
|
||||||
|
if (fast.conn.localDescription) {
|
||||||
|
this.ws.send(JSON.stringify({ t: "desc", target: msg.name, d: fast.conn.localDescription }))
|
||||||
|
}
|
||||||
|
if (fast.candidates) {
|
||||||
|
this.ws.send(JSON.stringify({ t: "cand", target: msg.name, d: fast.candidates }));
|
||||||
|
}
|
||||||
|
this.fast.set(client, fast);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "leave": {
|
||||||
|
const fast = this.fast.get(msg.name);
|
||||||
|
if (!fast) return;
|
||||||
|
fast.conn.close();
|
||||||
|
this.fast.delete(msg.name);
|
||||||
|
}
|
||||||
|
case "left": {
|
||||||
|
console.log("Left room successfully");
|
||||||
|
this.roomName = null;
|
||||||
|
this.roomId = null;
|
||||||
|
this.fast.forEach(connection => connection.conn.close());
|
||||||
|
this.fast = new Map();
|
||||||
|
}
|
||||||
|
case "list": {
|
||||||
|
list.set(msg.rooms);
|
||||||
|
}
|
||||||
|
case "error": {
|
||||||
|
console.error(msg.e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshList() {
|
||||||
|
this.ws.send(JSON.stringify({ t: "list" }));
|
||||||
|
}
|
||||||
|
|
||||||
|
send(data: any) {
|
||||||
|
this.ws.send(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const connection: Writable<WebsocketConnection|null> = writable(null);
|
||||||
|
export const list: Writable<{ id: string, name: string, count: number }[]|null> = writable(null);
|
||||||
2
client/src/routes/index.svelte
Normal file
2
client/src/routes/index.svelte
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
<h1>Welcome to SvelteKit</h1>
|
||||||
|
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>
|
||||||
BIN
client/static/favicon.png
Normal file
BIN
client/static/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
15
client/svelte.config.js
Normal file
15
client/svelte.config.js
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import adapter from '@sveltejs/adapter-auto';
|
||||||
|
import preprocess from 'svelte-preprocess';
|
||||||
|
|
||||||
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
|
const config = {
|
||||||
|
// Consult https://github.com/sveltejs/svelte-preprocess
|
||||||
|
// for more information about preprocessors
|
||||||
|
preprocess: preprocess(),
|
||||||
|
|
||||||
|
kit: {
|
||||||
|
adapter: adapter()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
13
client/tsconfig.json
Normal file
13
client/tsconfig.json
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"extends": "./.svelte-kit/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true
|
||||||
|
}
|
||||||
|
}
|
||||||
8
client/vite.config.js
Normal file
8
client/vite.config.js
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
|
|
||||||
|
/** @type {import('vite').UserConfig} */
|
||||||
|
const config = {
|
||||||
|
plugins: [sveltekit()]
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
17
pnpm-lock.yaml
Normal file
17
pnpm-lock.yaml
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
lockfileVersion: 5.4
|
||||||
|
|
||||||
|
importers:
|
||||||
|
|
||||||
|
server:
|
||||||
|
specifiers:
|
||||||
|
uWebSockets.js: github:uNetworking/uWebSockets.js#v20.10.0
|
||||||
|
dependencies:
|
||||||
|
uWebSockets.js: github.com/uNetworking/uWebSockets.js/806df48c9da86af7b3341f3e443388c7cd15c3de
|
||||||
|
|
||||||
|
packages:
|
||||||
|
|
||||||
|
github.com/uNetworking/uWebSockets.js/806df48c9da86af7b3341f3e443388c7cd15c3de:
|
||||||
|
resolution: {tarball: https://codeload.github.com/uNetworking/uWebSockets.js/tar.gz/806df48c9da86af7b3341f3e443388c7cd15c3de}
|
||||||
|
name: uWebSockets.js
|
||||||
|
version: 20.10.0
|
||||||
|
dev: false
|
||||||
3
pnpm-workspace.yaml
Normal file
3
pnpm-workspace.yaml
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
packages:
|
||||||
|
- client
|
||||||
|
- server
|
||||||
15
server/package.json
Normal file
15
server/package.json
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"name": "server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.10.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
168
server/src/index.js
Normal file
168
server/src/index.js
Normal file
|
|
@ -0,0 +1,168 @@
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const PORT = 8080;
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
function uuid() {
|
||||||
|
return (++i).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef Room
|
||||||
|
* @property {string} id
|
||||||
|
* @property {string} name
|
||||||
|
* @property {Client} host
|
||||||
|
* @property {Client[]} clients
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* @typedef Client
|
||||||
|
* @property {string} name
|
||||||
|
* @property {WebSocket} connection
|
||||||
|
* @property {Room?} room
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Map<string, Room>}
|
||||||
|
*/
|
||||||
|
const rooms = new Map();
|
||||||
|
/**
|
||||||
|
* @type {Map<WebSocket, Client>}
|
||||||
|
*/
|
||||||
|
const clients = new Map();
|
||||||
|
|
||||||
|
require("uWebSockets.js")
|
||||||
|
.App({})
|
||||||
|
.ws("/", {
|
||||||
|
idleTimeout: 30,
|
||||||
|
maxPayloadLength: 16 * 1024 * 1024,
|
||||||
|
upgrade: (res, req, context) => {
|
||||||
|
console.log(
|
||||||
|
`An Http connection wants to become WebSocket, URL: ${req.getUrl()}!`
|
||||||
|
);
|
||||||
|
|
||||||
|
const url = req.getUrl();
|
||||||
|
const parsed = new URL(url);
|
||||||
|
let name = parsed.searchParams.get("name");
|
||||||
|
if (!name || typeof name !== "string" || name.length < 2 || name.length > 64 || !name.trim()) return res.end("invalid_name");
|
||||||
|
name = name.trim();
|
||||||
|
if (clients.forEach(client => client.name === name)) return res.end("name_used");
|
||||||
|
/* This immediately calls open handler, you must not use res after this call */
|
||||||
|
res.upgrade(
|
||||||
|
{
|
||||||
|
url,
|
||||||
|
name
|
||||||
|
},
|
||||||
|
/* Spell these correctly */
|
||||||
|
req.getHeader("sec-websocket-key"),
|
||||||
|
req.getHeader("sec-websocket-protocol"),
|
||||||
|
req.getHeader("sec-websocket-extensions"),
|
||||||
|
context
|
||||||
|
);
|
||||||
|
},
|
||||||
|
open: (ws) => {
|
||||||
|
console.log(
|
||||||
|
"CON",
|
||||||
|
ws.url,
|
||||||
|
ws.name,
|
||||||
|
decoder.decode(ws.getRemoteAddressAsText())
|
||||||
|
);
|
||||||
|
clients.set(ws, {
|
||||||
|
connection: ws,
|
||||||
|
name: ws.name,
|
||||||
|
room: null
|
||||||
|
});
|
||||||
|
},
|
||||||
|
message: (ws, message, isBinary) => {
|
||||||
|
if (isBinary) return ws.end();
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(decoder.decode(message));
|
||||||
|
if(data.t === "ping") return ws.ping();
|
||||||
|
switch(data.t) {
|
||||||
|
case "ping": {
|
||||||
|
return ws.ping();
|
||||||
|
}
|
||||||
|
case "create": {
|
||||||
|
const client = clients.get(ws);
|
||||||
|
if(client.room) return ws.send(JSON.stringify({t: "error", m: "already_in_room"}));
|
||||||
|
const name = data.name.trim();
|
||||||
|
if (!name || typeof name !== "string" || name.length < 2 || name.length > 64 || !name.trim()) return res.send(JSON.stringify({t: "error", m: "invalid_room_name"}));
|
||||||
|
const room = {
|
||||||
|
name: name,
|
||||||
|
host: ws,
|
||||||
|
clients: [client],
|
||||||
|
id: uuid()
|
||||||
|
};
|
||||||
|
rooms.set(room.id, room);
|
||||||
|
client.room = room;
|
||||||
|
return ws.send(JSON.stringify({ t: "create", id: room.id, name: name }));
|
||||||
|
}
|
||||||
|
case "leave": {
|
||||||
|
const client = clients.get(ws);
|
||||||
|
const room = client.room;
|
||||||
|
if (!room) return ws.send(JSON.stringify({ t: "error", m: "room_not_found" }));
|
||||||
|
if (!room.clients.includes(client)) return ws.send(JSON.stringify({ t: "error", m: "not_in_room" }));
|
||||||
|
room.clients.splice(room.clients.indexOf(client), 1);
|
||||||
|
if (room.clients.length === 0) {
|
||||||
|
rooms.delete(room.id);
|
||||||
|
} else if(room.host == ws) {
|
||||||
|
room.host = room.clients[0];
|
||||||
|
room.clients.forEach(client => client.connection.send(JSON.stringify({ t: "host", host: client.name })));
|
||||||
|
}
|
||||||
|
client.room = null;
|
||||||
|
room.clients.forEach(client => client.connection.send(JSON.stringify({ t: "leave", id: room.id, name: client.name })));
|
||||||
|
ws.send(JSON.stringify({ t: "left", id: room.id, name: client.name }));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "join": {
|
||||||
|
const client = clients.get(ws);
|
||||||
|
if (!client) return ws.end();
|
||||||
|
const room = client.room;
|
||||||
|
if (!room) return ws.send(JSON.stringify({ t: "error", e: "room_not_found" }));
|
||||||
|
if (room.clients.includes(client)) return ws.send(JSON.stringify({ t: "error", e: "already_in_room" }));
|
||||||
|
room.clients.push(ws);
|
||||||
|
ws.room = room;
|
||||||
|
room.clients.slice(0, -2).forEach(client => client.connection.send(JSON.stringify({ t: "join", id: room.id, client: client.name })));
|
||||||
|
ws.send(JSON.stringify({ t: "joined", id: room.id, client: client.name, clients: room.clients.map(t => t.name) }));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "cand":
|
||||||
|
case "desc": {
|
||||||
|
const client = clients.get(ws);
|
||||||
|
if (!client) return ws.end();
|
||||||
|
const room = client.room;
|
||||||
|
if (!room) return ws.send(JSON.stringify({ t: "error", e: "room_not_found" }));
|
||||||
|
if (!room.clients.includes(client)) return ws.send(JSON.stringify({ t: "error", e: "not_in_room" }));
|
||||||
|
const targetClient = room.clients.find(t => t.name === msg.target);
|
||||||
|
if(!targetClient) return ws.send(JSON.stringify({ t: "error", e: "target_not_found" }));
|
||||||
|
if(!room.clients.includes(targetClient)) return ws.send(JSON.stringify({ t: "error", e: "target_not_in_room" }));
|
||||||
|
targetClient.connection.send(JSON.stringify({ t: msg.t, id: room.id, source: client.name, d: msg.d }));
|
||||||
|
}
|
||||||
|
case "list": {
|
||||||
|
ws.send(JSON.stringify({ t: "list", rooms: [...rooms.values()].map(t => ({ id: t.id, name: t.name, count: t.clients.length }))}));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return ws.end();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
close: (ws, code, message) => {
|
||||||
|
console.log("DIS", decoder.decode(ws.getRemoteAddressAsText()));
|
||||||
|
if (clients.get(ws)) {
|
||||||
|
let room = rooms.get(clients.get(ws));
|
||||||
|
if (room) {
|
||||||
|
room.clients.splice(room.clients.indexOf(ws), 1);
|
||||||
|
if (room.clients.length === 0) {
|
||||||
|
rooms.delete(room.id);
|
||||||
|
} else if(room.host == ws) {
|
||||||
|
room.host = room.clients[0];
|
||||||
|
room.clients.forEach(client => client.connection.send(JSON.stringify({ t: "host", host: client.name })));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
clients.delete(ws);
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.listen(PORT, () => {
|
||||||
|
console.log(`Listening on port ${PORT}`);
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue