This commit is contained in:
Daniel Flanagan 2021-11-17 08:18:12 -06:00
parent aad8b99dc1
commit a1636ff2f5
Signed by: lytedev
GPG key ID: 5B2020A0F9921EF4
11 changed files with 294 additions and 328 deletions

View file

@ -17,4 +17,4 @@ func _ready():
func _on_Button_pressed(): func _on_Button_pressed():
Global.leave_game() Global.main_menu()

View file

@ -4,35 +4,20 @@ const MultiplayerClient = preload("multiplayer_client.gd")
onready var client = MultiplayerClient.new() onready var client = MultiplayerClient.new()
const MULTIPLAYER_URL = "wss://webrtc-signaller.deno.dev/"
func _ready(): func _ready():
client.connect("lobby_joined", self, "_lobby_joined")
client.connect("connected", self, "_connected")
add_child(client) add_child(client)
func goto_scene(scene_resource_name):
var _result = get_tree().change_scene("res://%s.tscn" % scene_resource_name)
func main_menu():
goto_scene("main")
func start_singleplayer_game(): func start_singleplayer_game():
print("Starting singleplayer game...") goto_scene("game")
get_tree().change_scene("res://game.tscn")
func join_lobby(): func multiplayer():
get_tree().change_scene("res://join_lobby.tscn") goto_scene("multiplayer")
func create_lobby(): func quit():
join_lobby_with_code("") get_tree().quit()
func _lobby_joined(_lobby):
print("Joined!")
goto_lobby()
func goto_lobby():
print("Going to lobby...")
get_tree().change_scene("res://lobby.tscn")
func leave_game():
client.stop()
print("Leaving game...")
get_tree().change_scene("res://main.tscn")
func join_lobby_with_code(code):
client.start(MULTIPLAYER_URL, code)

View file

@ -5,8 +5,8 @@ func _on_Singleplayer_pressed():
func _on_CreateLobbyButton_pressed(): func _on_CreateLobbyButton_pressed():
Global.create_lobby() Global.multiplayer()
func _on_JoinLobbyButton_pressed(): func _on_JoinLobbyButton_pressed():
Global.join_lobby() Global.quit()

View file

@ -25,7 +25,7 @@ __meta__ = {
[node name="Singleplayer" type="Button" parent="VBoxContainer"] [node name="Singleplayer" type="Button" parent="VBoxContainer"]
margin_right = 995.0 margin_right = 995.0
margin_bottom = 20.0 margin_bottom = 20.0
text = "Start Singleplayer" text = "Start Singleplayer Game"
__meta__ = { __meta__ = {
"_edit_use_anchors_": false "_edit_use_anchors_": false
} }
@ -34,13 +34,13 @@ __meta__ = {
margin_top = 70.0 margin_top = 70.0
margin_right = 995.0 margin_right = 995.0
margin_bottom = 90.0 margin_bottom = 90.0
text = "Create Lobby" text = "Multiplayer"
[node name="JoinLobbyButton" type="Button" parent="VBoxContainer"] [node name="JoinLobbyButton" type="Button" parent="VBoxContainer"]
margin_top = 140.0 margin_top = 140.0
margin_right = 995.0 margin_right = 995.0
margin_bottom = 160.0 margin_bottom = 160.0
text = "Join Lobby" text = "Quit"
__meta__ = { __meta__ = {
"_edit_use_anchors_": false "_edit_use_anchors_": false
} }

9
multiplayer.gd Normal file
View file

@ -0,0 +1,9 @@
extends Control
onready var is_loaded = false
func _ready():
Global.client.connect_to_signaller()
func _on_back_pressed():
pass

40
multiplayer.tscn Normal file
View file

@ -0,0 +1,40 @@
[gd_scene load_steps=2 format=2]
[ext_resource path="res://multiplayer.gd" type="Script" id=1]
[node name="Control" type="Control"]
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource( 1 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="back" type="Button" parent="."]
margin_left = 27.0
margin_top = 357.0
margin_right = 286.0
margin_bottom = 432.0
text = "Back"
__meta__ = {
"_edit_use_anchors_": false
}
[node name="create_lobby" type="Button" parent="."]
margin_right = 259.0
margin_bottom = 75.0
text = "Create Lobby"
__meta__ = {
"_edit_use_anchors_": false
}
[node name="lobbies" type="Label" parent="."]
margin_left = 25.0
margin_top = 91.0
margin_right = 591.0
margin_bottom = 325.0
__meta__ = {
"_edit_use_anchors_": false
}
[connection signal="pressed" from="back" to="." method="_on_back_pressed"]

View file

@ -1,87 +1,90 @@
extends "ws_webrtc_client.gd" extends Node
var rtc_mp: WebRTCMultiplayer = WebRTCMultiplayer.new() """
var sealed = false This module sets up WebRTC peer connections.
"""
func _init(): var multiplayer_url = "ws://localhost:8888"
connect("connected", self, "connected") var webrtc_ice_servers = [
connect("disconnected", self, "disconnected") { "urls": ["stun:stun.l.google.com:19302"] }
]
connect("offer_received", self, "offer_received") const SignallerClient = preload("signaller_client.gd")
connect("answer_received", self, "answer_received")
connect("candidate_received", self, "candidate_received")
connect("lobby_joined", self, "lobby_joined") onready var mp = WebRTCMultiplayer.new()
connect("lobby_sealed", self, "lobby_sealed") onready var sc = SignallerClient.new()
connect("peer_connected", self, "peer_connected")
connect("peer_disconnected", self, "peer_disconnected")
func start(url, lobby = ""): func _ready():
stop() # connect("connected", self, "connected")
sealed = false # connect("disconnected", self, "disconnected")
self.lobby = lobby
connect_to_url(url)
func stop(): # connect("offer_received", self, "offer_received")
rtc_mp.close() # connect("answer_received", self, "answer_received")
# connect("candidate_received", self, "candidate_received")
# connect("lobby_joined", self, "lobby_joined")
# connect("lobby_sealed", self, "lobby_sealed")
# connect("peer_connected", self, "peer_connected")
# connect("peer_disconnected", self, "peer_disconnected")
add_child(sc)
func close():
mp.close()
sc.close()
func connect_to_signaller():
close() close()
sc.connect_to_websocket_signaller(multiplayer_url)
func _create_peer(id): func _create_peer(id):
var peer: WebRTCPeerConnection = WebRTCPeerConnection.new() var peer: WebRTCPeerConnection = WebRTCPeerConnection.new()
peer.initialize({ peer.initialize({"iceServers": webrtc_ice_servers})
"iceServers": [ { "urls": ["stun:stun.l.google.com:19302"] } ]
})
peer.connect("session_description_created", self, "_offer_created", [id]) peer.connect("session_description_created", self, "_offer_created", [id])
peer.connect("ice_candidate_created", self, "_new_ice_candidate", [id]) peer.connect("ice_candidate_created", self, "_new_ice_candidate", [id])
rtc_mp.add_peer(peer, id) mp.add_peer(peer, id)
if id > rtc_mp.get_unique_id(): if id > mp.get_unique_id():
peer.create_offer() # TODO: peer.create_offer()
pass
return peer return peer
func _new_ice_candidate(mid_name, index_name, sdp_name, id): func _new_ice_candidate(mid_name, index_name, sdp_name, id):
print("New ICE Candidate: ", mid_name, index_name, sdp_name, id) print("New ICE Candidate: ", mid_name, index_name, sdp_name, id)
send_candidate(id, mid_name, index_name, sdp_name) # TODO: send_candidate(id, mid_name, index_name, sdp_name)
func _offer_created(type, data, id): func _offer_created(type, data, id):
if not rtc_mp.has_peer(id): if not mp.has_peer(id):
return return
print("created", type) print("created", type)
rtc_mp.get_peer(id).connection.set_local_description(type, data) mp.get_peer(id).connection.set_local_description(type, data)
if type == "offer": send_offer(id, data) if type == "offer":
else: send_answer(id, data) # TODO: send_offer(id, data)
pass
else:
# TODO: send_answer(id, data)
pass
func connected(id): func connected(id):
print("Connected %d" % id) print("Connected %d" % id)
rtc_mp.initialize(id, true) mp.initialize(id, true)
func lobby_joined(lobby):
self.lobby = lobby
func lobby_sealed():
sealed = true
func disconnected():
print("Disconnected: %d: %s" % [code, reason])
if not sealed:
stop() # Unexpected disconnect
func peer_connected(id): func peer_connected(id):
print("Peer connected %d" % id) print("Peer connected %d" % id)
_create_peer(id) _create_peer(id)
func peer_disconnected(id): func peer_disconnected(id):
if rtc_mp.has_peer(id): rtc_mp.remove_peer(id) if mp.has_peer(id):
mp.remove_peer(id)
func offer_received(id, offer): func offer_received(id, offer):
print("Got offer: %d" % id) print("Got offer: %d" % id)
if rtc_mp.has_peer(id): if mp.has_peer(id):
rtc_mp.get_peer(id).connection.set_remote_description("offer", offer) mp.get_peer(id).connection.set_remote_description("offer", offer)
func answer_received(id, answer): func answer_received(id, answer):
print("Got answer: %d" % id) print("Got answer: %d" % id)
if rtc_mp.has_peer(id): if mp.has_peer(id):
rtc_mp.get_peer(id).connection.set_remote_description("answer", answer) mp.get_peer(id).connection.set_remote_description("answer", answer)
func candidate_received(id, mid, index, sdp): func candidate_received(id, mid, index, sdp):
if rtc_mp.has_peer(id): if mp.has_peer(id):
rtc_mp.get_peer(id).connection.add_ice_candidate(mid, index, sdp) mp.get_peer(id).connection.add_ice_candidate(mid, index, sdp)

View file

@ -4,4 +4,10 @@
# Signalling Server # Signalling Server
deno run -A server.ts The signaling server is written in TypeScript to run on Deno. It can run in the
cloud via Deno deploy.
PORT=8888 deno run --allow-env --allow-net server.ts
- https://dash.deno.com/projects/webrtc-signaller
- https://webrtc-signaller.deno.dev

295
server.ts
View file

@ -1,69 +1,66 @@
import { randomId, randomSecret } from "./gen.ts"; const PORT = parseInt(Deno.env.get("PORT") || "80");
const MAX_PEERS = 4096; const randomInt = (low: number, high: number) =>
const MAX_LOBBIES = 1024; Math.floor(Math.random() * (high - low + 1) + low);
const PORT = 80;
const NO_LOBBY_TIMEOUT = 10000; const randomSecret = () => new Array(8).map(() => randomInt(0, 10)).join("");
const SEAL_CLOSE_TIMEOUT = 10000;
// const PING_INTERVAL = 10000;
const STR_NO_LOBBY = "Have not joined lobby yet"; interface Client {
const STR_HOST_DISCONNECTED = "Room host has disconnected"; socket: WebSocket;
const STR_ONLY_HOST_CAN_SEAL = "Only host can seal the lobby";
const STR_SEAL_COMPLETE = "Seal complete";
const STR_TOO_MANY_LOBBIES = "Too many lobbies open, disconnecting";
const STR_ALREADY_IN_LOBBY = "Already in a lobby";
const STR_LOBBY_DOES_NOT_EXIST = "Lobby does not exist";
const STR_LOBBY_IS_SEALED = "Lobby is sealed";
const STR_INVALID_FORMAT = "Invalid message format";
const STR_NEED_LOBBY = "Invalid message when not in a lobby";
const STR_SERVER_ERROR = "Server error, lobby not found";
const STR_INVALID_DEST = "Invalid destination";
const STR_INVALID_CMD = "Invalid command";
const STR_TOO_MANY_PEERS = "Too many peers connected";
// TODO: setup regular pings?
// state
const lobbies = new Map();
let peersCount = 0;
class ProtoError extends Error {
code: number;
constructor(code: number, message: string) {
super(message);
this.code = code;
}
} }
class Peer { // TODO: version comparison
id: number;
ws: WebSocket;
lobby: string;
timeout: number;
constructor(id: number, ws: WebSocket) { interface Lobby {
this.id = id; name: string;
this.ws = ws; clients: Client[];
this.lobby = ""; // TODO: private vs public lobbies?
}
// close connection after 10 sec if client has not joined a lobby const lobbies = new Set<Lobby>();
this.timeout = setTimeout(() => { const clients = new Set<Client>();
if (!this.lobby) ws.close(4000, STR_NO_LOBBY);
}, NO_LOBBY_TIMEOUT);
}
joinLobby(lobbyName: string) { console.log("Listening on port", PORT);
if (lobbyName === "") { for await (const conn of Deno.listen({ port: PORT })) {
if (lobbies.size >= MAX_LOBBIES) { (async () => {
throw new ProtoError(4000, STR_TOO_MANY_LOBBIES); for await (const { respondWith, request } of Deno.serveHttp(conn)) {
} const { socket, response } = Deno.upgradeWebSocket(request);
// Peer must not already be in a lobby const client: Client = { socket };
if (this.lobby !== "") { socket.onmessage = (ev) => {
throw new ProtoError(4000, STR_ALREADY_IN_LOBBY); console.log("Client Message Received", ev);
} };
socket.onopen = (ev) => {
console.log("New Client:", ev);
clients.add(client);
socket.send("lobbies_start");
lobbies.forEach(({ name, clients }) => {
socket.send(JSON.stringify({ name, numClients: clients.length }));
});
socket.send("lobbies_end");
};
socket.onclose = (ev) => {
console.log("Client Socket Close:", ev);
clients.delete(client);
};
socket.onerror = (ev) => {
console.log("Client Socket Error:", ev);
clients.delete(client);
};
respondWith(response);
}
})();
}
/*
function peerCreatesLobby(peer: Peer, lobbyName: string) {
// TODO: ensure we can create a lobby
lobbies.add({
})
}
*/
/*
function peerJoinsLobby(peer: Peer, lobby: Lobby) {
lobbyName = randomSecret(); lobbyName = randomSecret();
lobbies.set(lobbyName, new Lobby(lobbyName, this.id)); lobbies.set(lobbyName, new Lobby(lobbyName, this.id));
console.log(`Peer ${this.id} created lobby ${lobbyName}`); console.log(`Peer ${this.id} created lobby ${lobbyName}`);
@ -80,63 +77,45 @@ class Peer {
lobby.join(this); lobby.join(this);
this.ws.send(`J: ${lobbyName}\n`); this.ws.send(`J: ${lobbyName}\n`);
} }
// TODO: ensure peer not already in a lobby
const assigned = this.getPeerId(peer);
peer.ws.send(`I: ${assigned}\n`);
this.peers.forEach((p) => {
p.ws.send(`N: ${assigned}\n`);
peer.ws.send(`N: ${this.getPeerId(p)}\n`);
});
this.peers.push(peer);
} }
*/
class Lobby { /*
name: string; function peerLeavesLobby(peer: Peer) {
host: number; const idx = this.peers.findIndex((p) => peer === p);
peers: Peer[]; if (idx === -1) return false;
sealed: boolean; const assigned = this.getPeerId(peer);
closeTimer: number; const close = assigned === 1;
this.peers.forEach((p) => {
constructor(name: string, host: number) { try {
this.name = name; // room host disconnected
this.host = host; if (close) p.ws.close(4000, STR_HOST_DISCONNECTED);
this.peers = []; // notify peers
this.sealed = false; else p.ws.send(`D: ${assigned}\n`);
} catch (e) {
console.error(`Error when leaving: ${e}`);
}
});
this.peers.splice(idx, 1);
if (close && this.closeTimer >= 0) {
// we are closing already.
clearTimeout(this.closeTimer);
this.closeTimer = -1; this.closeTimer = -1;
} }
return close;
}
*/
getPeerId(peer: Peer) { /*
if (this.host === peer.id) return 1; function graduateLobby(lobby: Lobby) {
return peer.id;
}
join(peer: Peer) {
const assigned = this.getPeerId(peer);
peer.ws.send(`I: ${assigned}\n`);
this.peers.forEach((p) => {
p.ws.send(`N: ${assigned}\n`);
peer.ws.send(`N: ${this.getPeerId(p)}\n`);
});
this.peers.push(peer);
}
leave(peer: Peer) {
const idx = this.peers.findIndex((p) => peer === p);
if (idx === -1) return false;
const assigned = this.getPeerId(peer);
const close = assigned === 1;
this.peers.forEach((p) => {
try {
// room host disconnected
if (close) p.ws.close(4000, STR_HOST_DISCONNECTED);
// notify peers
else p.ws.send(`D: ${assigned}\n`);
} catch (e) {
console.error(`Error when leaving: ${e}`);
}
});
this.peers.splice(idx, 1);
if (close && this.closeTimer >= 0) {
// we are closing already.
clearTimeout(this.closeTimer);
this.closeTimer = -1;
}
return close;
}
seal(peer: Peer) {
// only host can seal // only host can seal
if (peer.id !== this.host) { if (peer.id !== this.host) {
throw new ProtoError(4000, STR_ONLY_HOST_CAN_SEAL); throw new ProtoError(4000, STR_ONLY_HOST_CAN_SEAL);
@ -155,39 +134,14 @@ class Lobby {
p.ws.close(1000, STR_SEAL_COMPLETE); p.ws.close(1000, STR_SEAL_COMPLETE);
}); });
}, SEAL_CLOSE_TIMEOUT); }, SEAL_CLOSE_TIMEOUT);
}
} }
}
*/
/*
function parseMsg(peer: Peer, msg: string) { function parseMsg(peer: Peer, msg: string) {
const sep = msg.indexOf("\n"); // TODO: modify this?
if (sep < 0) throw new ProtoError(4000, STR_INVALID_FORMAT);
const cmd = msg.slice(0, sep);
if (cmd.length < 3) throw new ProtoError(4000, STR_INVALID_FORMAT);
const data = msg.slice(sep);
// join
if (cmd.startsWith("J: ")) {
peer.joinLobby(cmd.substr(3).trim());
return;
}
if (!peer.lobby) throw new ProtoError(4000, STR_NEED_LOBBY);
const lobby = lobbies.get(peer.lobby);
if (!lobby) throw new ProtoError(4000, STR_SERVER_ERROR);
// seal
if (cmd.startsWith("S: ")) {
lobby.seal(peer);
return;
}
// Message relaying format:
//
// [O|A|C]: DEST_ID\n
// PAYLOAD
//
// O: Client is sending an offer. // O: Client is sending an offer.
// A: Client is sending an answer. // A: Client is sending an answer.
// C: Client is sending a candidate. // C: Client is sending a candidate.
@ -208,63 +162,4 @@ function parseMsg(peer: Peer, msg: string) {
} }
throw new ProtoError(4000, STR_INVALID_CMD); throw new ProtoError(4000, STR_INVALID_CMD);
} }
*/
console.log(`Server running on port ${PORT}`);
const server = Deno.listen({ port: PORT });
for await (const conn of server) {
(async () => {
const httpConn = Deno.serveHttp(conn);
for await (const requestEvent of httpConn) {
if (requestEvent) {
try {
const { socket, response } = Deno.upgradeWebSocket(
requestEvent.request,
);
const id = randomId();
const peer = new Peer(id, socket);
socket.onopen = (_ev) => {
if (peersCount >= MAX_PEERS) {
socket.close(4000, STR_TOO_MANY_PEERS);
return;
}
peersCount++;
};
socket.onmessage = (ev) => {
try {
parseMsg(peer, ev.data);
} catch (e) {
const code = e.code || 4000;
console.log(`Error parsing message from ${id}:\n` + ev.data);
socket.close(code, e.message);
}
};
socket.onclose = (ev) => {
peersCount--;
console.log(
`Connection with peer ${peer.id} closed ` +
`with reason: ${ev.reason}`,
);
if (
peer.lobby &&
lobbies.has(peer.lobby) &&
lobbies.get(peer.lobby).leave(peer)
) {
lobbies.delete(peer.lobby);
console.log(`Deleted lobby ${peer.lobby}`);
console.log(`Open lobbies: ${lobbies.size}`);
peer.lobby = "";
}
if (peer.timeout >= 0) {
clearTimeout(peer.timeout);
peer.timeout = -1;
}
};
socket.onerror = (e) => console.error("WebSocket error:", e);
requestEvent.respondWith(response);
} catch (e) {
console.log(`Error during connection:`, e);
}
}
}
})();
}

View file

@ -1,52 +1,75 @@
extends Node extends Node
export var autojoin = true """
export var lobby = "" # Will create a new lobby if empty. This module is responsible for making a WebSocket connection to the signaller
in order to enable establishish WebRTC P2P connections. Another module is
expected to fully setup the peer connections.
"""
var client: WebSocketClient = WebSocketClient.new() onready var ws: WebSocketClient = WebSocketClient.new()
var code = 1000
var reason = "Unknown"
signal lobby_joined(lobby) func _ready():
signal connected(id) var _result = ws.connect("data_received", self, "_parse_msg")
signal disconnected() _result = ws.connect("connection_established", self, "_connected")
signal peer_connected(id) _result = ws.connect("connection_closed", self, "_closed")
signal peer_disconnected(id) _result = ws.connect("connection_error", self, "_closed")
signal offer_received(id, offer) _result = ws.connect("server_close_request", self, "_close_request")
signal answer_received(id, answer)
signal candidate_received(id, mid, index, sdp)
signal lobby_sealed()
func _init():
client.connect("data_received", self, "_parse_msg")
client.connect("connection_established", self, "_connected")
client.connect("connection_closed", self, "_closed")
client.connect("connection_error", self, "_closed")
client.connect("server_close_request", self, "_close_request")
func connect_to_url(url):
close()
code = 1000
reason = "Unknown"
client.connect_to_url(url)
func close(): func close():
client.disconnect_from_host() ws.disconnect_from_host()
func _closed(was_clean = false): func connect_to_websocket_signaller(url: String):
emit_signal("disconnected") print(ws)
close()
print("Attempting to connect to WebSocket signalling server at ", url)
var _result = ws.connect_to_url(url)
func _close_request(code, reason): func _closed():
self.code = code # emit_signal("disconnected")
self.reason = reason pass
func _close_request(code: int, reason: String):
print("Received WebSocket close request from signalling server ", code, reason)
func _connected(protocol = ""): func _connected(protocol = ""):
client.get_peer(1).set_write_mode(WebSocketPeer.WRITE_MODE_TEXT) print("WebSocket signaller connected via protocol ", protocol)
if autojoin: ws.get_peer(1).set_write_mode(WebSocketPeer.WRITE_MODE_TEXT)
join_lobby(lobby)
func _process(_delta: float):
var status: int = ws.get_connection_status()
if status == WebSocketClient.CONNECTION_CONNECTING or status == WebSocketClient.CONNECTION_CONNECTED:
ws.poll()
func _parse_msg(): func _parse_msg():
var pkt_str: String = client.get_peer(1).get_packet().get_string_from_utf8() var pkt_str: String = ws.get_peer(1).get_packet().get_string_from_utf8()
print("Signaller sent: ", pkt_str)
"""
func _parse_msg():
var pkt_str: String = ws.get_peer(1).get_packet().get_string_from_utf8()
var req: PoolStringArray = pkt_str.split("\n", true, 1) var req: PoolStringArray = pkt_str.split("\n", true, 1)
if req.size() != 2: # Invalid request size if req.size() != 2: # Invalid request size
@ -99,10 +122,10 @@ func _parse_msg():
emit_signal("candidate_received", src_id, candidate[0], int(candidate[1]), candidate[2]) emit_signal("candidate_received", src_id, candidate[0], int(candidate[1]), candidate[2])
func join_lobby(joined_lobby): func join_lobby(joined_lobby):
return client.get_peer(1).put_packet(("J: %s\n" % joined_lobby).to_utf8()) return ws.get_peer(1).put_packet(("J: %s\n" % joined_lobby).to_utf8())
func seal_lobby(): func seal_lobby():
return client.get_peer(1).put_packet("S: \n".to_utf8()) return ws.get_peer(1).put_packet("S: \n".to_utf8())
func send_candidate(id, mid, index, sdp) -> int: func send_candidate(id, mid, index, sdp) -> int:
return _send_msg("C", id, "\n%s\n%d\n%s" % [mid, index, sdp]) return _send_msg("C", id, "\n%s\n%d\n%s" % [mid, index, sdp])
@ -114,9 +137,5 @@ func send_answer(id, answer) -> int:
return _send_msg("A", id, answer) return _send_msg("A", id, answer)
func _send_msg(type, id, data) -> int: func _send_msg(type, id, data) -> int:
return client.get_peer(1).put_packet(("%s: %d\n%s" % [type, id, data]).to_utf8()) return ws.get_peer(1).put_packet(("%s: %d\n%s" % [type, id, data]).to_utf8())
"""
func _process(_delta):
var status: int = client.get_connection_status()
if status == WebSocketClient.CONNECTION_CONNECTING or status == WebSocketClient.CONNECTION_CONNECTED:
client.poll()

9
types.ts Normal file
View file

@ -0,0 +1,9 @@
export interface Client {
socket: WebSocket;
}
export interface Lobby {
name: string;
secret: string;
clients: Client[];
}