mirror of
https://github.com/danbulant/slightlyComplicatedTicTacToe
synced 2026-06-14 12:01:21 +00:00
connection + chat
This commit is contained in:
parent
2206e2774f
commit
3d3aa884fe
7 changed files with 554 additions and 201 deletions
|
|
@ -8,59 +8,19 @@ class FastEvent extends Event {
|
|||
}
|
||||
}
|
||||
|
||||
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'
|
||||
},
|
||||
const hosts: { urls: string, credential?: string, username?: string }[] = [
|
||||
{
|
||||
urls: 'turn:relay.backups.cz?transport=tcp',
|
||||
credential: 'webrtc',
|
||||
username: 'webrtc'
|
||||
});
|
||||
urls: "stun:openrelay.metered.ca:80",
|
||||
}
|
||||
]
|
||||
|
||||
class ConnectedClient extends EventTarget {
|
||||
conn: RTCPeerConnection;
|
||||
sendChannel: RTCDataChannel;
|
||||
candidates: any[] = [];
|
||||
state: RTCDataChannelState | null = null;
|
||||
readyState: number = 0;
|
||||
pings: number[] = [];
|
||||
|
||||
constructor(public ws: WebsocketConnection, public name: string) {
|
||||
super();
|
||||
|
|
@ -72,41 +32,62 @@ class ConnectedClient extends EventTarget {
|
|||
}
|
||||
|
||||
initializeConnection() {
|
||||
this.pings = [];
|
||||
console.log("Initializing connection");
|
||||
this.conn = new RTCPeerConnection({
|
||||
iceServers: hosts
|
||||
});
|
||||
|
||||
this.conn.onicecandidate = e => {
|
||||
console.log(e);
|
||||
console.log("candidate", e, e.candidate);
|
||||
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.conn.ondatachannel = e => this.onDataChannel(e.channel);
|
||||
}
|
||||
|
||||
onDataChannel(channel: RTCDataChannel) {
|
||||
console.log("on data channel");
|
||||
this.sendChannel = channel;
|
||||
let timer: any;
|
||||
this.sendChannel.onclose = (e) => {
|
||||
clearInterval(timer);
|
||||
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));
|
||||
}
|
||||
};
|
||||
}
|
||||
this.sendChannel.onopen = (e) => {
|
||||
timer = setInterval(() => {
|
||||
this.send({ t: "p", d: Date.now() });
|
||||
}, 100);
|
||||
this.statusChanged();
|
||||
}
|
||||
this.statusChanged();
|
||||
this.sendChannel.onmessage = (e) => {
|
||||
const msg = JSON.parse(e.data);
|
||||
switch (msg.t) {
|
||||
case "p":
|
||||
this.send({
|
||||
t: "pr",
|
||||
d: msg.d,
|
||||
y: Date.now()
|
||||
})
|
||||
break;
|
||||
case "pr":
|
||||
this.pings.push(Date.now() - msg.d);
|
||||
if(this.pings.length > 15) this.pings = this.pings.slice(-15);
|
||||
players.update(t => t);
|
||||
break;
|
||||
case "msg":
|
||||
console.log("message", msg.d);
|
||||
this.dispatchEvent(new FastEvent("message", msg.d));
|
||||
messages.update(t => { t.push({ author: this.name, content: msg.d });return t})
|
||||
break;
|
||||
default:
|
||||
console.log("MSG", msg);
|
||||
this.dispatchEvent(new FastEvent(msg.t, msg.d));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
send(data: any) {
|
||||
|
|
@ -120,7 +101,10 @@ class ConnectedClient extends EventTarget {
|
|||
this.initializeConnection();
|
||||
}
|
||||
this.state = this.sendChannel.readyState;
|
||||
if (this.state === "open") this.readyState = 3;
|
||||
if (["closing", "closed"].includes(this.state)) this.readyState = 4;
|
||||
console.log("state", this.state);
|
||||
players.update(t => t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -130,13 +114,14 @@ export class WebsocketConnection extends EventTarget {
|
|||
ws: WebSocket;
|
||||
fast: Map<string, ConnectedClient> = new Map();
|
||||
roomName: string | null = null;
|
||||
roomId: string | null = null;
|
||||
roomHost: string | null = null;
|
||||
|
||||
constructor(public name: string) {
|
||||
super();
|
||||
// @ts-ignore Initialized in the next function call
|
||||
this.ws = null;
|
||||
this.connect();
|
||||
players.set(this.fast);
|
||||
}
|
||||
|
||||
connect() {
|
||||
|
|
@ -149,11 +134,15 @@ export class WebsocketConnection extends EventTarget {
|
|||
console.log("WS closed");
|
||||
lastError.set(e.reason || "Connection closed");
|
||||
connection.set(null);
|
||||
room.set(null);
|
||||
list.set(null);
|
||||
});
|
||||
this.ws.addEventListener("error", (e) => {
|
||||
console.error("WS error");
|
||||
lastError.set("Connection error");
|
||||
connection.set(null);
|
||||
room.set(null);
|
||||
list.set(null);
|
||||
});
|
||||
this.ws.addEventListener("message", (e) => {
|
||||
const msg = JSON.parse(e.data);
|
||||
|
|
@ -161,58 +150,79 @@ export class WebsocketConnection extends EventTarget {
|
|||
switch (msg.t) {
|
||||
case "cand": {
|
||||
const fast = this.fast.get(msg.source);
|
||||
if (!fast) return;
|
||||
if (!fast) return console.log("No fast connection");
|
||||
if (fast.readyState < 1) fast.readyState == 1;
|
||||
players.set(this.fast);
|
||||
console.log("Received candidates");
|
||||
if (fast.state === "open") return console.log("Already open");
|
||||
for (const candidate of msg.d) {
|
||||
fast.conn.addIceCandidate(candidate).then();
|
||||
}
|
||||
fast.conn.addIceCandidate(msg.d).then();
|
||||
break;
|
||||
}
|
||||
case "desc": {
|
||||
const fast = this.fast.get(msg.source);
|
||||
if (!fast) return;
|
||||
if (!fast) return console.log("No fast connection");
|
||||
if (fast.readyState < 2) fast.readyState == 2;
|
||||
players.set(this.fast);
|
||||
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 }))
|
||||
)
|
||||
if (msg.d.type === "answer") {
|
||||
fast.conn.setRemoteDescription(msg.d);
|
||||
} else if (msg.d.type === "offer") {
|
||||
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 }));
|
||||
const fast = new ConnectedClient(this, msg.client);
|
||||
players.set(this.fast);
|
||||
this.fast.set(msg.client, fast);
|
||||
if (fast.candidates && fast.candidates.length) {
|
||||
for (const candidate of fast.candidates) {
|
||||
this.ws.send(JSON.stringify({ t: "cand", target: msg.client, d: candidate }));
|
||||
}
|
||||
}
|
||||
messages.update(t => { t.push({ author: " SYS ", content: `${msg.client} joined`});return t})
|
||||
break;
|
||||
}
|
||||
case "joined": {
|
||||
const clients = msg.clients;
|
||||
this.fast = new Map();
|
||||
for (const client of clients) {
|
||||
if (client === this.name) continue;
|
||||
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 }));
|
||||
}
|
||||
fast.conn.createOffer()
|
||||
.then(offer => fast.conn.setLocalDescription(offer))
|
||||
.then(() =>
|
||||
this.ws.send(JSON.stringify({ t: "desc", target: client, d: fast.conn.localDescription }))
|
||||
);
|
||||
fast.sendChannel = fast.conn.createDataChannel("sendChannel");
|
||||
fast.onDataChannel(fast.sendChannel);
|
||||
this.fast.set(client, fast);
|
||||
}
|
||||
// missing break on purpose
|
||||
players.set(this.fast);
|
||||
messages.set([{
|
||||
author: " SYS ", content: `${msg.client} joined`
|
||||
}]);
|
||||
this.roomName = msg.name;
|
||||
this.roomHost = msg.host;
|
||||
room.set({
|
||||
name: msg.name,
|
||||
host: msg.host
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "create": {
|
||||
this.roomName = msg.name;
|
||||
this.roomId = msg.id;
|
||||
this.roomHost = this.name;
|
||||
room.set({
|
||||
name: msg.name,
|
||||
id: msg.id
|
||||
host: this.name
|
||||
});
|
||||
messages.update(t => { t.push({ author: " SYS ", content: `${msg.name} created the room`});return t})
|
||||
break;
|
||||
}
|
||||
case "leave": {
|
||||
|
|
@ -220,15 +230,24 @@ export class WebsocketConnection extends EventTarget {
|
|||
if (!fast) return;
|
||||
fast.conn.close();
|
||||
this.fast.delete(msg.name);
|
||||
players.set(this.fast);
|
||||
messages.update(t => { t.push({ author: " SYS ", content: `${msg.client} left`});return t})
|
||||
break;
|
||||
}
|
||||
case "host": {
|
||||
this.roomHost = msg.host;
|
||||
room.update(t => { t!.host = this.roomHost!; return t });
|
||||
messages.update(t => { t.push({ author: " SYS ", content: `${msg.host} is now host`});return t})
|
||||
break;
|
||||
}
|
||||
case "left": {
|
||||
console.log("Left room successfully");
|
||||
this.roomName = null;
|
||||
this.roomId = null;
|
||||
room.set(null);
|
||||
this.fast.forEach(connection => connection.conn.close());
|
||||
this.fast = new Map();
|
||||
players.set(this.fast);
|
||||
messages.set([]);
|
||||
break;
|
||||
}
|
||||
case "list": {
|
||||
|
|
@ -245,10 +264,22 @@ export class WebsocketConnection extends EventTarget {
|
|||
});
|
||||
}
|
||||
|
||||
sendMessage(msg: string) {
|
||||
if (!this.roomName) return console.log("Not in a room");
|
||||
for(const [, client] of this.fast) {
|
||||
client.send({ t: "msg", d: msg });
|
||||
}
|
||||
messages.update(t => { t.push({ author: this.name, content: msg }); return t });
|
||||
}
|
||||
|
||||
createGame(name: string) {
|
||||
this.ws.send(JSON.stringify({ t: "create", name: name }));
|
||||
}
|
||||
|
||||
join(name: string) {
|
||||
this.ws.send(JSON.stringify({ t: "join", name: name }));
|
||||
}
|
||||
|
||||
refreshList() {
|
||||
this.ws.send(JSON.stringify({ t: "list" }));
|
||||
listLoading.set(true);
|
||||
|
|
@ -259,8 +290,10 @@ export class WebsocketConnection extends EventTarget {
|
|||
}
|
||||
}
|
||||
|
||||
export const connection: Writable<WebsocketConnection|null> = writable(null);
|
||||
export const list: Writable<{ id: string, name: string, count: number }[]|null> = writable(null);
|
||||
export const connection: Writable<WebsocketConnection | null> = writable(null);
|
||||
export const list: Writable<{ name: string, count: number }[] | null> = writable(null);
|
||||
export const listLoading = writable(true);
|
||||
export const lastError: Writable<string> = writable("");
|
||||
export const room: Writable<{ name: string, id: string }|null> = writable(null);
|
||||
export const room: Writable<{ name: string, host: string } | null> = writable(null);
|
||||
export const players: Writable<Map<string, ConnectedClient>> = writable(new Map);
|
||||
export const messages: Writable<{ author: string, content: string }[]> = writable([]);
|
||||
21
client/src/lib/view/components/button.svelte
Normal file
21
client/src/lib/view/components/button.svelte
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
|
||||
<button on:click><slot /></button>
|
||||
|
||||
<style>
|
||||
button {
|
||||
width: 10em;
|
||||
height: 3em;
|
||||
border: 10px solid #bd5ce6;
|
||||
background-color: #85E65C;
|
||||
color: #bd5ce6;
|
||||
border-radius: 0;
|
||||
padding: 0.5em;
|
||||
font-size: 1.5em;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:active {
|
||||
background-color: #bd5ce6;
|
||||
color: #85E65C;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<script>
|
||||
import Waiting from "./waiting.svelte";
|
||||
</script>
|
||||
|
||||
<Waiting />
|
||||
168
client/src/lib/view/game/waiting.svelte
Normal file
168
client/src/lib/view/game/waiting.svelte
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
<script lang="ts">
|
||||
import { connection, messages, players, room } from '$lib/Websocket';
|
||||
import Button from '../components/button.svelte';
|
||||
|
||||
function startGame() {}
|
||||
let content = "";
|
||||
function sendMessage() {
|
||||
if(!content.length || content.length > 512) return;
|
||||
$connection!.sendMessage(content);
|
||||
content = "";
|
||||
}
|
||||
$: if($messages.length > 512) {
|
||||
$messages = $messages.slice(-512);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<div class="playerlist">
|
||||
<ul>
|
||||
<li>
|
||||
{$connection?.name}
|
||||
{#if $room?.host === $connection?.name}
|
||||
<span class="host">Host</span>
|
||||
{/if}
|
||||
<span class="state">YOU</span>
|
||||
</li>
|
||||
{#each [...$players.values()] as player (player.name)}
|
||||
<li>
|
||||
{player.name}
|
||||
{#if $room?.host === player.name}
|
||||
<span class="host">Host</span>
|
||||
{/if}
|
||||
<span class="state state-{player.readyState}"
|
||||
>{['joined', 'connecting', 'connecting...', 'ready', 'reconnecting'][
|
||||
player.readyState
|
||||
]}</span
|
||||
>
|
||||
{#if player.pings && player.pings.length > 0}
|
||||
<span class="pings">{Math.floor(player.pings.reduce((a, b) => a + b, 0) / player.pings.length * 10) / 10}ms</span>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
<main>
|
||||
<div class="text">
|
||||
{#each $messages as message}
|
||||
<div class="message">
|
||||
{#if message.author !== " SYS "}
|
||||
<span class="name">{message.author}</span>:
|
||||
{/if}
|
||||
<span class="content">{message.content}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="bottom">
|
||||
<form action="/" on:submit|preventDefault={sendMessage}>
|
||||
<input type="text" placeholder="Chat.." bind:value={content} on:submit|preventDefault={sendMessage}>
|
||||
</form>
|
||||
<Button on:click={() => sendMessage()}>Send</Button>
|
||||
{#if $room?.host === $connection?.name}
|
||||
<Button on:click={() => startGame()}>START GAME</Button>
|
||||
{:else}
|
||||
<Button>Ready</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.pings {
|
||||
width: 3em;
|
||||
text-align: right;
|
||||
display: inline-block;
|
||||
}
|
||||
.container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: #85e65c;
|
||||
color: #bd5ce6;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.playerlist {
|
||||
width: 20em;
|
||||
height: 100%;
|
||||
background-color: #85e65c;
|
||||
color: #bd5ce6;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-right: 10px solid #bd5ce6;
|
||||
}
|
||||
.state {
|
||||
font-size: 0.8em;
|
||||
margin-left: 0.5em;
|
||||
background-color: #bd5ce6;
|
||||
color: #85e65c;
|
||||
border-radius: 0.25em;
|
||||
padding: 0.3em;
|
||||
}
|
||||
.state.state-0 {
|
||||
background-color: red;
|
||||
}
|
||||
.state.state-3 {
|
||||
background-color: transparent;
|
||||
color: #000;
|
||||
}
|
||||
.state.state-4 {
|
||||
background-color: #ff0000;
|
||||
color: #000;
|
||||
}
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-grow: 1;
|
||||
height: calc(100vh - 40px);
|
||||
}
|
||||
.text {
|
||||
flex-grow: 1;
|
||||
width: calc(100% - 2rem);
|
||||
margin: 1em;
|
||||
}
|
||||
.bottom {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-grow: 0;
|
||||
height: 2em;
|
||||
width: 100%;
|
||||
}
|
||||
form {
|
||||
flex-grow: 1;
|
||||
}
|
||||
input {
|
||||
width: 100%;
|
||||
background-color: #85e65c;
|
||||
color: #bd5ce6;
|
||||
font-size: 1.5em;
|
||||
font-family: inherit;
|
||||
padding: 0.5em;
|
||||
border: 10px solid #bd5ce6;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
}
|
||||
.text {
|
||||
overflow: auto;
|
||||
}
|
||||
.text .message {
|
||||
height: 1.5em;
|
||||
}
|
||||
.text .name {
|
||||
font-weight: bold;
|
||||
background-color: #bd5ce6;
|
||||
color: #85e65c;
|
||||
padding: 0.2em;
|
||||
border-radius: 0.25em;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { connection, lastError, list, listLoading } from "$lib/Websocket";
|
||||
import { onMount } from "svelte";
|
||||
import Button from "../components/button.svelte";
|
||||
|
||||
onMount(() => {
|
||||
let i = setInterval(() => {
|
||||
|
|
@ -35,6 +36,10 @@
|
|||
error = "Creating game...";
|
||||
$connection!.createGame(newGameName);
|
||||
}
|
||||
|
||||
function connect(game: { name: string, count: number }) {
|
||||
$connection!.join(game.name);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if creatingGame}
|
||||
|
|
@ -44,8 +49,8 @@
|
|||
{error}
|
||||
</div>
|
||||
<div class="controls">
|
||||
<button on:click={() => creatingGame = false}>CANCEL</button>
|
||||
<button on:click={submit}>SUBMIT</button>
|
||||
<Button on:click={() => creatingGame = false}>CANCEL</Button>
|
||||
<Button on:click={submit}>SUBMIT</Button>
|
||||
</div>
|
||||
</dialog>
|
||||
{/if}
|
||||
|
|
@ -54,20 +59,20 @@
|
|||
<main>
|
||||
<div class="flex">
|
||||
<h1>Games - {$connection?.name}</h1>
|
||||
<button on:click={() => creatingGame = true}>CREATE</button>
|
||||
<Button on:click={() => creatingGame = true}>CREATE</Button>
|
||||
</div>
|
||||
<div class="status">
|
||||
{#if $listLoading} Loading... {/if}
|
||||
</div>
|
||||
<ul>
|
||||
{#if $list}
|
||||
{#each $list as $game}
|
||||
<li class="flex">
|
||||
{#each $list as game}
|
||||
<li class="flex" on:click={() => connect(game)}>
|
||||
<span>
|
||||
{$game.name}
|
||||
{game.name}
|
||||
</span>
|
||||
<span>
|
||||
{$game.count}
|
||||
{game.count}
|
||||
</span>
|
||||
</li>
|
||||
{/each}
|
||||
|
|
@ -133,19 +138,7 @@
|
|||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
button {
|
||||
width: 10em;
|
||||
height: 3em;
|
||||
border: 1px solid #bd5ce6;
|
||||
border-radius: 0;
|
||||
padding: 0.5em;
|
||||
font-size: 1.5rem;
|
||||
border: 10px solid #bd5ce6;
|
||||
background-color: #85E65C;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:active {
|
||||
background-color: #bd5ce6;
|
||||
color: #85E65C;
|
||||
input {
|
||||
font-family: inherit;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import { connection, lastError, WebsocketConnection } from "$lib/Websocket";
|
||||
import Button from "../components/button.svelte";
|
||||
|
||||
var name = "";
|
||||
var error = "";
|
||||
|
|
@ -25,7 +26,7 @@
|
|||
<div class="error">
|
||||
{error || $lastError}
|
||||
</div>
|
||||
<button on:click={submit}>SUBMIT</button>
|
||||
<Button on:click={submit}>SUBMIT</Button>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
|
|
@ -57,20 +58,4 @@
|
|||
color: #bd5ce6;
|
||||
font-family: inherit;
|
||||
}
|
||||
button {
|
||||
width: 10em;
|
||||
height: 3em;
|
||||
border: 10px solid #bd5ce6;
|
||||
background-color: #85E65C;
|
||||
color: #bd5ce6;
|
||||
border-radius: 0;
|
||||
padding: 0.5em;
|
||||
font-size: 1.5em;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:active {
|
||||
background-color: #bd5ce6;
|
||||
color: #85E65C;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,15 +1,8 @@
|
|||
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
|
||||
|
|
@ -37,16 +30,24 @@ require("uWebSockets.js")
|
|||
maxPayloadLength: 16 * 1024 * 1024,
|
||||
upgrade: (res, req, context) => {
|
||||
console.log(
|
||||
`An Http connection wants to become WebSocket, URL: ${req.getUrl()}!`
|
||||
`An Http connection wants to become WebSocket, URL: ${req.getUrl()}?${req.getQuery()}!`
|
||||
);
|
||||
let name = req.getQuery("name").trim().toLocaleLowerCase();
|
||||
if (!name || typeof name !== "string" || name.length < 2 || name.length > 64 || !name.trim()) return res.end("invalid_name");
|
||||
if (
|
||||
!name ||
|
||||
typeof name !== "string" ||
|
||||
name.length < 2 ||
|
||||
name.length > 64 ||
|
||||
!name.trim()
|
||||
)
|
||||
return res.end("invalid_name");
|
||||
name = name.trim();
|
||||
if ([...clients.values()].find(client => client.name === name)) return res.end("name_used");
|
||||
if ([...clients.values()].find((client) => client.name === name))
|
||||
return res.end("name_used");
|
||||
/* This immediately calls open handler, you must not use res after this call */
|
||||
res.upgrade(
|
||||
{
|
||||
name
|
||||
name,
|
||||
},
|
||||
/* Spell these correctly */
|
||||
req.getHeader("sec-websocket-key"),
|
||||
|
|
@ -65,101 +66,248 @@ require("uWebSockets.js")
|
|||
connection: ws,
|
||||
name: ws.name,
|
||||
ipa: decoder.decode(ws.getRemoteAddressAsText()),
|
||||
room: null
|
||||
room: null,
|
||||
});
|
||||
},
|
||||
message: (ws, message, isBinary) => {
|
||||
if (isBinary) return ws.end();
|
||||
if (isBinary) return ws.end(0, "invalid_message");
|
||||
try {
|
||||
const data = JSON.parse(decoder.decode(message));
|
||||
if(data.t === "ping") return ws.ping();
|
||||
switch(data.t) {
|
||||
const client = clients.get(ws);
|
||||
console.log(client.name, data);
|
||||
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", e: "already_in_room"}));
|
||||
if (client.room)
|
||||
return ws.send(
|
||||
JSON.stringify({
|
||||
t: "error",
|
||||
e: "already_in_room",
|
||||
})
|
||||
);
|
||||
const name = data.name.trim().toLocaleLowerCase();
|
||||
if (!name || typeof name !== "string" || name.length < 2 || name.length > 64 || !name.trim()) return res.send(JSON.stringify({t: "error", e: "invalid_room_name"}));
|
||||
if ([...rooms.values()].find(room => room.name === name)) return ws.send(JSON.stringify({t: "error", e: "room_name_used"}));
|
||||
if (
|
||||
!name ||
|
||||
typeof name !== "string" ||
|
||||
name.length < 2 ||
|
||||
name.length > 64 ||
|
||||
!name.trim()
|
||||
)
|
||||
return res.send(
|
||||
JSON.stringify({
|
||||
t: "error",
|
||||
e: "invalid_room_name",
|
||||
})
|
||||
);
|
||||
if (
|
||||
[...rooms.values()].find(
|
||||
(room) => room.name === name
|
||||
)
|
||||
)
|
||||
return ws.send(
|
||||
JSON.stringify({
|
||||
t: "error",
|
||||
e: "room_name_used",
|
||||
})
|
||||
);
|
||||
const room = {
|
||||
name: name,
|
||||
host: ws,
|
||||
clients: [client],
|
||||
id: uuid()
|
||||
};
|
||||
rooms.set(room.id, room);
|
||||
rooms.set(room.name, room);
|
||||
client.room = room;
|
||||
return ws.send(JSON.stringify({ t: "create", id: room.id, name: name }));
|
||||
return ws.send(
|
||||
JSON.stringify({ t: "create", name: name })
|
||||
);
|
||||
}
|
||||
case "leave": {
|
||||
const client = clients.get(ws);
|
||||
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" }));
|
||||
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" })
|
||||
);
|
||||
room.clients.splice(room.clients.indexOf(client), 1);
|
||||
if (room.clients.length === 0) {
|
||||
rooms.delete(room.id);
|
||||
} else if(room.host == ws) {
|
||||
rooms.delete(room.name);
|
||||
} else if (room.host == ws) {
|
||||
room.host = room.clients[0];
|
||||
room.clients.forEach(client => client.connection.send(JSON.stringify({ t: "host", host: client.name })));
|
||||
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 }));
|
||||
room.clients.forEach((client) =>
|
||||
client.connection.send(
|
||||
JSON.stringify({
|
||||
t: "leave",
|
||||
name: client.name,
|
||||
})
|
||||
)
|
||||
);
|
||||
ws.send(
|
||||
JSON.stringify({ t: "left", name: client.name })
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "join": {
|
||||
const client = clients.get(ws);
|
||||
if (!client) return ws.end();
|
||||
const room = rooms.get(msg.room);
|
||||
if (!room) return ws.send(JSON.stringify({ t: "error", e: "room_not_found" }));
|
||||
if(client.room) return ws.send(JSON.stringify({ t: "error", e: "already_in_other_room" }));
|
||||
if (room.clients.includes(client)) return ws.send(JSON.stringify({ t: "error", e: "already_in_room" }));
|
||||
if(room.clients.length > 5) return ws.send(JSON.stringify({ t: "error", e: "room_full" }));
|
||||
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, name: room.name, client: client.name, clients: room.clients.map(t => t.name) }));
|
||||
if (!client) return ws.end(0, "missing_client");
|
||||
const room = rooms.get(data.name);
|
||||
if (!room)
|
||||
return ws.send(
|
||||
JSON.stringify({
|
||||
t: "error",
|
||||
e: "room_not_found",
|
||||
})
|
||||
);
|
||||
if (client.room)
|
||||
return ws.send(
|
||||
JSON.stringify({
|
||||
t: "error",
|
||||
e: "already_in_other_room",
|
||||
})
|
||||
);
|
||||
if (room.clients.includes(client))
|
||||
return ws.send(
|
||||
JSON.stringify({
|
||||
t: "error",
|
||||
e: "already_in_room",
|
||||
})
|
||||
);
|
||||
if (room.clients.length > 5)
|
||||
return ws.send(
|
||||
JSON.stringify({ t: "error", e: "room_full" })
|
||||
);
|
||||
room.clients.push(client);
|
||||
client.room = room;
|
||||
const srcclient = client;
|
||||
room.clients
|
||||
.filter((t) => t !== client)
|
||||
.forEach((client) =>
|
||||
client.connection.send(
|
||||
JSON.stringify({
|
||||
t: "join",
|
||||
client: srcclient.name,
|
||||
})
|
||||
)
|
||||
);
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
t: "joined",
|
||||
name: room.name,
|
||||
client: client.name,
|
||||
host: room.host.name,
|
||||
clients: room.clients.map((t) => t.name),
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "cand":
|
||||
case "desc": {
|
||||
const client = clients.get(ws);
|
||||
if (!client) return ws.end();
|
||||
if (!client) return ws.end(0, "missing_client");
|
||||
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 }));
|
||||
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 === data.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: data.t,
|
||||
source: client.name,
|
||||
d: data.d,
|
||||
})
|
||||
);
|
||||
}
|
||||
case "list": {
|
||||
ws.send(JSON.stringify({ t: "list", rooms: [...rooms.values()].filter(t => t.clients.length < 5).map(t => ({ id: t.id, name: t.name, count: t.clients.length }))}));
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
t: "list",
|
||||
rooms: [...rooms.values()]
|
||||
.filter((t) => t.clients.length < 5)
|
||||
.map((t) => ({
|
||||
name: t.name,
|
||||
host: t.host.name,
|
||||
count: t.clients.length,
|
||||
})),
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
return ws.end();
|
||||
console.warn(e);
|
||||
return ws.end(0, "internal");
|
||||
}
|
||||
},
|
||||
close: (ws, code, message) => {
|
||||
console.log("DIS1", ws);
|
||||
if (clients.get(ws)) {
|
||||
const client = clients.get(ws);
|
||||
console.log("DIS", client.name, client.ipa);
|
||||
let room = client.room;
|
||||
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 })));
|
||||
console.log("DIS1", ws, code, message);
|
||||
try {
|
||||
if (clients.get(ws)) {
|
||||
const client = clients.get(ws);
|
||||
console.log("DIS", client.name, client.ipa);
|
||||
let room = client.room;
|
||||
if (room) {
|
||||
room.clients.splice(room.clients.indexOf(client), 1);
|
||||
if (room.clients.length === 0) {
|
||||
rooms.delete(room.name);
|
||||
} else if (room.host == ws) {
|
||||
room.host = room.clients[0];
|
||||
room.clients.forEach((client) =>
|
||||
client.connection.send(
|
||||
JSON.stringify({
|
||||
t: "host",
|
||||
host: client.name,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
const srcclient = client;
|
||||
room.clients.forEach((client) =>
|
||||
client.connection.send(JSON.stringify({ t: "leave", client: srcclient.name }))
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Error during closing", e);
|
||||
}
|
||||
clients.delete(ws);
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue