initial commit

This commit is contained in:
Daniel Bulant 2022-07-27 12:17:46 +02:00
parent 27c1df79d2
commit 7246ddea7b
26 changed files with 1946 additions and 0 deletions

12
.editorconfig Normal file
View 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
View file

@ -0,0 +1 @@
node_modules

View file

@ -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

Binary file not shown.

BIN
assets/Squareo.ttf Normal file

Binary file not shown.

BIN
assets/fall.wav Normal file

Binary file not shown.

BIN
assets/jump.wav Normal file

Binary file not shown.

8
client/.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example

1
client/.npmrc Normal file
View file

@ -0,0 +1 @@
engine-strict=true

13
client/.prettierignore Normal file
View 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
View file

@ -0,0 +1,6 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100
}

38
client/README.md Normal file
View 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
View 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

File diff suppressed because it is too large Load diff

11
client/src/app.d.ts vendored Normal file
View 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
View 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
View 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);

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

15
client/svelte.config.js Normal file
View 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
View 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
View 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
View 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
View file

@ -0,0 +1,3 @@
packages:
- client
- server

15
server/package.json Normal file
View 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
View 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}`);
});