From 160d0dbebe90f12af03b73dcb1fc190f4c0e8905 Mon Sep 17 00:00:00 2001 From: Daniel Flanagan Date: Wed, 17 Nov 2021 13:57:45 -0600 Subject: [PATCH] basic lobbies --- deps.ts | 2 + global.gd | 9 +- lobby.gd | 27 ++++- lobby.tscn | 13 +- main.gd | 4 +- multiplayer.gd | 41 ++++++- multiplayer.tscn | 39 ++++-- multiplayer_client.gd | 8 +- server.ts | 268 ++++++++++++++++++++++++++++++++++-------- signaller_client.gd | 77 ++++++++---- 10 files changed, 391 insertions(+), 97 deletions(-) create mode 100644 deps.ts diff --git a/deps.ts b/deps.ts new file mode 100644 index 0000000..75db3ae --- /dev/null +++ b/deps.ts @@ -0,0 +1,2 @@ +export const randomInt = (low: number, high: number) => + Math.floor(Math.random() * (high - low + 1) + low); diff --git a/global.gd b/global.gd index a0996a6..e5e2eb6 100644 --- a/global.gd +++ b/global.gd @@ -11,13 +11,20 @@ func goto_scene(scene_resource_name): var _result = get_tree().change_scene("res://%s.tscn" % scene_resource_name) func main_menu(): + client.close() goto_scene("main") func start_singleplayer_game(): + client.close() goto_scene("game") -func multiplayer(): +func lobby_browser(): + client.close() goto_scene("multiplayer") +func lobby(): + goto_scene("lobby") + func quit(): + client.close() get_tree().quit() diff --git a/lobby.gd b/lobby.gd index 6f7bc8c..f414887 100644 --- a/lobby.gd +++ b/lobby.gd @@ -1,8 +1,13 @@ extends Node2D +onready var peers = $MarginContainer/peers + func _ready(): - $MarginContainer/Label.text = Global.client.lobby - pass + Global.client.signaller.connect("peer_joined", self, "_peer_joined") + Global.client.signaller.connect("peer_left", self, "_peer_left") + Global.client.signaller.connect("lobby_left", self, "_lobby_left") + $MarginContainer/Label.text = Global.client.signaller.lobby_id + Global.client.signaller.request_peer_list() func _draw(): pass @@ -10,6 +15,22 @@ func _draw(): func _process(_delta): pass +func _peer_joined(joined_peers): + for i in range(len(joined_peers)): + var id = joined_peers[i]["id"] + var name = joined_peers[i]["name"] + print("New Peer ", id, name) + peers.add_item("%s" % name) + peers.set_item_metadata(peers.get_item_count() - 1, { "id": id }) + +func _peer_left(id): + for i in range(peers.get_item_count()): + if id == peers.get_item_metadata(i)["id"]: + peers.remove_item(i) + return func _on_Button_pressed(): - Global.leave_game() + Global.lobby_browser() + +func _lobby_left(_id): + Global.lobby_browser() diff --git a/lobby.tscn b/lobby.tscn index 22d9043..6927f01 100644 --- a/lobby.tscn +++ b/lobby.tscn @@ -6,8 +6,13 @@ script = ExtResource( 1 ) [node name="MarginContainer" type="VBoxContainer" parent="."] +anchor_right = 1.0 +anchor_bottom = 1.0 margin_right = 669.0 margin_bottom = 366.0 +__meta__ = { +"_edit_use_anchors_": false +} [node name="Label" type="Label" parent="MarginContainer"] margin_right = 669.0 @@ -21,6 +26,12 @@ __meta__ = { margin_top = 18.0 margin_right = 669.0 margin_bottom = 38.0 -text = "Close Lobby" +text = "Leave" + +[node name="peers" type="ItemList" parent="MarginContainer"] +margin_top = 42.0 +margin_right = 669.0 +margin_bottom = 51.0 +auto_height = true [connection signal="pressed" from="MarginContainer/Button" to="." method="_on_Button_pressed"] diff --git a/main.gd b/main.gd index ce29459..a5adb91 100644 --- a/main.gd +++ b/main.gd @@ -3,10 +3,8 @@ extends Node func _on_Singleplayer_pressed(): Global.start_singleplayer_game() - func _on_CreateLobbyButton_pressed(): - Global.multiplayer() - + Global.lobby_browser() func _on_JoinLobbyButton_pressed(): Global.quit() diff --git a/multiplayer.gd b/multiplayer.gd index 663fea2..150adc3 100644 --- a/multiplayer.gd +++ b/multiplayer.gd @@ -1,9 +1,48 @@ extends Control +# TODO: rename to server browser + onready var is_loaded = false func _ready(): + Global.client.signaller.connect("lobby_new", self, "_lobby_new") + Global.client.signaller.connect("lobby_delete", self, "_lobby_delete") + Global.client.signaller.connect("lobby_joined", self, "_lobby_joined") + Global.client.signaller.connect("lobby_left", self, "_lobby_left") + Global.client.signaller.connect("websocket_connected", self, "_signaller_connected") Global.client.connect_to_signaller() +func _lobby_joined(id): + Global.lobby() + +func _lobby_left(_id): + Global.lobby_browser() + +func _signaller_connected(): + Global.client.signaller.request_lobby_list() + func _on_back_pressed(): - pass + Global.main_menu() + +func _on_create_lobby_pressed(): + Global.client.signaller.create_lobby() + +func _on_join_pressed(): + var items = $lobbies.get_selected_items() + if len(items) > 0: + Global.client.signaller.join_lobby($lobbies.get_item_metadata(items[0])["id"]) + +func _lobby_new(lobbies): + for i in range(len(lobbies)): + var id = lobbies[i]["id"] + var name = lobbies[i]["name"] + print("New Lobby ", id, name) + # TODO: could keep an index of IDs and indexes + $lobbies.add_item("%s" % name) + $lobbies.set_item_metadata($lobbies.get_item_count() - 1, { "id": id }) + +func _lobby_delete(id): + for i in range($lobbies.get_item_count()): + if id == $lobbies.get_item_metadata(i)["id"]: + $lobbies.remove_item(i) + return diff --git a/multiplayer.tscn b/multiplayer.tscn index d243226..78bb90d 100644 --- a/multiplayer.tscn +++ b/multiplayer.tscn @@ -11,30 +11,47 @@ __meta__ = { } [node name="back" type="Button" parent="."] -margin_left = 27.0 -margin_top = 357.0 -margin_right = 286.0 -margin_bottom = 432.0 +margin_left = 565.0 +margin_top = 214.0 +margin_right = 824.0 +margin_bottom = 289.0 text = "Back" __meta__ = { "_edit_use_anchors_": false } [node name="create_lobby" type="Button" parent="."] -margin_right = 259.0 -margin_bottom = 75.0 +margin_left = 12.0 +margin_top = 11.0 +margin_right = 550.0 +margin_bottom = 86.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 +[node name="join" type="Button" parent="."] +margin_left = 10.0 +margin_top = 414.0 +margin_right = 548.0 +margin_bottom = 489.0 +text = "Join" +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="lobbies" type="ItemList" parent="."] +anchor_right = 0.121 +anchor_bottom = 0.217 +margin_left = 18.0 +margin_top = 105.0 +margin_right = 416.096 +margin_bottom = 267.8 +same_column_width = true __meta__ = { "_edit_use_anchors_": false } [connection signal="pressed" from="back" to="." method="_on_back_pressed"] +[connection signal="pressed" from="create_lobby" to="." method="_on_create_lobby_pressed"] +[connection signal="pressed" from="join" to="." method="_on_join_pressed"] diff --git a/multiplayer_client.gd b/multiplayer_client.gd index 6bacd99..1197195 100644 --- a/multiplayer_client.gd +++ b/multiplayer_client.gd @@ -12,7 +12,7 @@ var webrtc_ice_servers = [ const SignallerClient = preload("signaller_client.gd") onready var mp = WebRTCMultiplayer.new() -onready var sc = SignallerClient.new() +onready var signaller = SignallerClient.new() func _ready(): # connect("connected", self, "connected") @@ -26,15 +26,15 @@ func _ready(): # connect("lobby_sealed", self, "lobby_sealed") # connect("peer_connected", self, "peer_connected") # connect("peer_disconnected", self, "peer_disconnected") - add_child(sc) + add_child(signaller) func close(): mp.close() - sc.close() + signaller.close() func connect_to_signaller(): close() - sc.connect_to_websocket_signaller(multiplayer_url) + signaller.connect_to_websocket_signaller(multiplayer_url) func _create_peer(id): var peer: WebRTCPeerConnection = WebRTCPeerConnection.new() diff --git a/server.ts b/server.ts index 93cbeba..9fb7369 100644 --- a/server.ts +++ b/server.ts @@ -1,67 +1,241 @@ -const PORT = parseInt(Deno.env.get("PORT") || "80"); - -const randomInt = (low: number, high: number) => - Math.floor(Math.random() * (high - low + 1) + low); - -const randomSecret = () => new Array(8).map(() => randomInt(0, 10)).join(""); - -interface Client { - socket: WebSocket; -} +import { randomInt } from "./deps.ts"; +const SERVER_VERSION = "0.1.0"; // TODO: version comparison -interface Lobby { - name: string; - clients: Client[]; - // TODO: private vs public lobbies? +type ID = string; + +// app state +const allLobbies = new Map(); +const allClients = new Map(); + +interface DataMessage { + type: string; } -const lobbies = new Set(); -const clients = new Set(); +type ServerData = Record | string; +type Message = string | DataMessage; +const broadcast = (message: Message) => + allClients.forEach((client) => client.send(message)); + +const buildMessage = ( + type: string, + data: ServerData | ServerData[], +) => + Object.assign( + { type }, + Array.isArray(data) + ? { data } + : (typeof data === "object" ? data : { data }), + ); + +class Client { + id: ID; + name: string; + socket: WebSocket; + lobby: Lobby | null; + + constructor(socket: WebSocket) { + this.id = crypto.randomUUID(); + this.socket = socket; + this.name = "Client"; + this.lobby = null; + + this.send(buildMessage("your-id", this.id)); + + console.log(this); + allClients.set(this.id, this); + } + + isConnected() { + return this.socket.readyState == WebSocket.OPEN; + } + + remove() { + this.lobbyLeave(); + if (this.isConnected()) { + this.socket.close(); + } + allClients.delete(this.id); + } + + send(message: Message) { + try { + this.socket.send( + typeof message === "object" + ? ("json:" + JSON.stringify(message)) + : message, + ); + } catch (e) { + console.error( + `Failed to send on socket ${this.socket} to client ${this.id}. Disconnecting and removing...`, + ); + } + } + + clientList() { + if (!this.lobby) return; + const netClients: { id: ID; name: string }[] = []; + this.lobby.clients.forEach(({ id, name }) => netClients.push({ id, name })); + // TODO: chunk async? + this.send(buildMessage("peer_list", netClients)); + } + + lobbyList() { + const netLobbies: { id: ID; name: string }[] = []; + allLobbies.forEach(({ id, name }) => netLobbies.push({ id, name })); + // TODO: chunk async? + this.send(buildMessage("lobby_list", netLobbies)); + } + + lobbyNew({ id, name }: Lobby) { + // if the client is already in a lobby, we don't care about new lobbies + if (this.lobby) return; + this.send(buildMessage("lobby_new", { id, name })); + } + + lobbyDelete({ id }: Lobby) { + if (this.lobby) return; + this.send(buildMessage("lobby_delete", { id })); + } + + lobbyCreate() { + if (this.lobby) { + this.send( + `[info] cannot create lobby (already in lobby ${this.lobby.id})`, + ); + return; + } + new Lobby(this); + } + + lobbyJoin(lobby: Lobby) { + if (this.lobby) { + this.send(`[info] cannot join lobby (already in lobby ${this.lobby.id})`); + return; + } + this.lobby = lobby; + this.send(buildMessage("lobby_joined", { id: lobby.id })); + lobby.addClient(this); + } + + lobbyLeave() { + const leavingLobby = this.lobby; + if (!leavingLobby) { + this.send(`[info] cannot leave lobby (not in a lobby)`); + return; + } + this.lobby = null; + if (this.isConnected()) { + this.send(buildMessage("lobby_left", { id: leavingLobby.id })); + } + leavingLobby.removeClient(this); + } +} + +class Lobby { + id: ID; + name: string; + clients: Map; + hostClientId: ID; + + constructor(host: Client, name?: string) { + this.id = crypto.randomUUID(); + this.hostClientId = host.id; + this.clients = new Map(); + this.name = name || this.id; + + allLobbies.set(this.id, this); + host.lobbyJoin(this); + allClients.forEach((client) => client.lobbyNew(this)); + } + + remove() { + allClients.forEach((client) => client.lobbyDelete(this)); + this.clients.forEach((client) => { + client.lobbyLeave(); + }); + allLobbies.delete(this.id); + } + + broadcast(message: Message) { + this.clients.forEach((client) => client.send(message)); + } + + addClient(client: Client) { + this.clients.set(client.id, client); + this.broadcast( + buildMessage("peer_joined", { id: client.id, name: client.name }), + ); + } + + removeClient({ id }: Client) { + this.clients.delete(id); + this.broadcast(buildMessage("peer_left", { id })); + if (id === this.hostClientId) { + console.warn("Host left!"); + this.remove(); + } + } +} + +// events +function onMessage(client: Client, ev: MessageEvent) { + // TODO: log who from? + console.log("Client Message Received", ev.data); + if (ev.data === "lobby_create") client.lobbyCreate(); + if (ev.data === "lobby_leave") client.lobbyLeave(); + if (ev.data === "request_lobby_list") client.lobbyList(); + if (ev.data === "request_peer_list") client.clientList(); + if (ev.data.startsWith("lobby_join:")) { + const id = ev.data.substr(11); + const lobby = allLobbies.get(id); + if (lobby) client.lobbyJoin(lobby); + else client.send(`[info] could not find lobby ${id}`); + } +} + +function onSocketOpen(_client: Client, _ev: Event) { + console.log("New Client"); +} + +function onClientLeave(client: Client) { + client.remove(); +} + +function onSocketClose(client: Client, _ev: Event) { + console.log("Client Close"); + onClientLeave(client); +} + +function onSocketError(client: Client, _ev: Event) { + console.log("Client Error"); + onClientLeave(client); +} + +const PORT = parseInt(Deno.env.get("PORT") || "80"); console.log("Listening on port", PORT); const listener = Deno.listen({ port: PORT }); for await (const conn of listener) { - console.debug("Connection received:", conn); + // console.debug("Connection received:", conn); (async () => { const server = Deno.serveHttp(conn); for await (const { respondWith, request } of server) { - console.debug("HTTP Request Received", request); + // console.debug("HTTP Request Received", request); try { + console.warn(JSON.stringify([allClients, allLobbies])); const { socket, response } = Deno.upgradeWebSocket(request); - const client: Client = { socket }; - socket.onmessage = (ev) => { - console.log("Client Message Received", ev); - }; - socket.onopen = (ev) => { - console.log("New Client:", ev); - if (!clients.has(client)) 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); - if (clients.has(client)) clients.delete(client); - }; - socket.onerror = (ev) => { - console.log("Client Socket Error:", ev); - if (clients.has(client)) clients.delete(client); - }; + const client = new Client(socket); + socket.onmessage = (ev) => onMessage(client, ev); + socket.onopen = (ev) => onSocketOpen(client, ev); + socket.onclose = (ev) => onSocketClose(client, ev); + socket.onerror = (ev) => onSocketError(client, ev); respondWith(response); } catch (e) { - let body = "400 Bad Request"; - if (e instanceof TypeError) { - body += " - Expected to be able to upgrade to WebSocket connection"; - console.log("Could not add client:", e); - } else { - console.log("Could not add client for unhandled reason:", e); - } + console.log("Could not add client for unhandled reason:", e); respondWith( - new Response(body, { + new Response("400 Bad Request", { status: 400, headers: { "content-type": "text/html" }, }), diff --git a/signaller_client.gd b/signaller_client.gd index 0af67dc..3fc6716 100644 --- a/signaller_client.gd +++ b/signaller_client.gd @@ -6,8 +6,19 @@ in order to enable establishish WebRTC P2P connections. Another module is expected to fully setup the peer connections. """ +signal lobby_new(lobbiesList) +signal lobby_delete(id) +signal lobby_joined(id) +signal lobby_left(id) +signal peer_joined(id) +signal peer_left(id) +signal websocket_connected +signal websocket_disconnected + onready var ws: WebSocketClient = WebSocketClient.new() +onready var lobby_id = null + func _ready(): var _result = ws.connect("data_received", self, "_parse_msg") _result = ws.connect("connection_established", self, "_connected") @@ -25,8 +36,7 @@ func connect_to_websocket_signaller(url: String): var _result = ws.connect_to_url(url) func _closed(): - # emit_signal("disconnected") - pass + emit_signal("websocket_disconnected") func _close_request(code: int, reason: String): print("Received WebSocket close request from signalling server ", code, reason) @@ -34,38 +44,53 @@ func _close_request(code: int, reason: String): func _connected(protocol = ""): print("WebSocket signaller connected via protocol ", protocol) ws.get_peer(1).set_write_mode(WebSocketPeer.WRITE_MODE_TEXT) + emit_signal("websocket_connected") func _process(_delta: float): var status: int = ws.get_connection_status() if status == WebSocketClient.CONNECTION_CONNECTING or status == WebSocketClient.CONNECTION_CONNECTED: ws.poll() +func join_lobby(id: String): + return _send("lobby_join:%s" % id) + +func create_lobby(): + return _send("lobby_create") + +func request_lobby_list(): + return _send("request_lobby_list") + +func request_peer_list(): + return _send("request_peer_list") + +func _send(s: String): + return ws.get_peer(1).put_packet(s.to_utf8()) + func _parse_msg(): - var pkt_str: String = ws.get_peer(1).get_packet().get_string_from_utf8() - print("Signaller sent: ", pkt_str) - - - - - - - - - - - - - - - - - - - - - - + var msg: String = ws.get_peer(1).get_packet().get_string_from_utf8() + print("Signaller sent: ", msg) + if msg.begins_with("json:"): + var data = JSON.parse(msg.substr(5)) + if data.error == OK: + handle_message(data.result) + else: + print("Unhandled Message: ", msg) +func handle_message(data: Dictionary): + match data["type"]: + "lobby_new": emit_signal("lobby_new", [{"id": data["id"], "name": data["name"]}]) + "lobby_delete": emit_signal("lobby_delete", data["id"]) + "lobby_joined": + lobby_id = data["id"] + emit_signal("lobby_joined", data["id"]) + "lobby_left": + lobby_id = null + emit_signal("lobby_left", data["id"]) + "lobby_list": emit_signal("lobby_new", data["data"]) + "peer_list": emit_signal("peer_joined", data["data"]) + "peer_joined": emit_signal("peer_joined", [{"id": data["id"], "name": data["name"]}]) + "peer_left": emit_signal("peer_left", data["id"]) + _: print("Unhandled Data Message: ", JSON.print(data)) """ func _parse_msg():