diff --git a/game.gd b/game.gd new file mode 100644 index 0000000..5efc9c1 --- /dev/null +++ b/game.gd @@ -0,0 +1,20 @@ +extends Node2D + + +# Declare member variables here. Examples: +# var a = 2 +# var b = "text" + + +# Called when the node enters the scene tree for the first time. +func _ready(): + pass # Replace with function body. + + +# Called every frame. 'delta' is the elapsed time since the previous frame. +#func _process(delta): +# pass + + +func _on_Button_pressed(): + Global.leave_game() diff --git a/game.tscn b/game.tscn new file mode 100644 index 0000000..f74d35f --- /dev/null +++ b/game.tscn @@ -0,0 +1,28 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://game.gd" type="Script" id=1] + +[node name="Node2D" type="Node2D"] +script = ExtResource( 1 ) + +[node name="Label" type="Label" parent="."] +margin_left = 93.0 +margin_top = 109.0 +margin_right = 253.0 +margin_bottom = 123.0 +text = "You are playing the game" +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Button" type="Button" parent="Label"] +margin_left = 29.0 +margin_top = 177.0 +margin_right = 120.0 +margin_bottom = 197.0 +text = "Leave Game" +__meta__ = { +"_edit_use_anchors_": false +} + +[connection signal="pressed" from="Label/Button" to="." method="_on_Button_pressed"] diff --git a/gen.ts b/gen.ts new file mode 100644 index 0000000..97a0c5e --- /dev/null +++ b/gen.ts @@ -0,0 +1,20 @@ +const ALFNUM = "0123456789"; +const SECRET_LENGTH = 8; + +function randomInt(low: number, high: number) { + return Math.floor(Math.random() * (high - low + 1) + low); +} + +export function randomId() { + const arr = new Int32Array(1); + crypto.getRandomValues(arr); + return Math.abs(arr[0]); +} + +export function randomSecret() { + let out = ""; + for (let i = 0; i < SECRET_LENGTH; i++) { + out += ALFNUM[randomInt(0, ALFNUM.length - 1)]; + } + return out; +} diff --git a/global.gd b/global.gd new file mode 100644 index 0000000..933a878 --- /dev/null +++ b/global.gd @@ -0,0 +1,22 @@ +extends Node + + +# Declare member variables here. Examples: +# var a = 2 +# var b = "text" + + +# Called when the node enters the scene tree for the first time. +func _ready(): + pass # Replace with function body. + + +# Called every frame. 'delta' is the elapsed time since the previous frame. +#func _process(delta): +# pass + +func start_singleplayer_game(): + print("Starting singleplayer game...") + +func leave_game(): + print("Leaving game...") diff --git a/main.gd b/main.gd index 250cfab..c97e825 100644 --- a/main.gd +++ b/main.gd @@ -1,14 +1,4 @@ -extends Control -func _ready(): - if OS.get_name() == "HTML5": - $VBoxContainer/Signaling.hide() -func _on_listen_toggled(button_pressed): - if button_pressed: - $Server.listen(int($VBoxContainer/Signaling/Port.value)) - else: - $Server.stop() - -func _on_LinkButton_pressed(): - OS.shell_open("https://github.com/godotengine/webrtc-native/releases") +func _on_Singleplayer_pressed(): + Global.start_singleplayer_game() diff --git a/main.tscn b/main.tscn index f92cccd..c1a04c8 100644 --- a/main.tscn +++ b/main.tscn @@ -1,7 +1,6 @@ -[gd_scene load_steps=3 format=2] +[gd_scene load_steps=2 format=2] [ext_resource path="res://main.gd" type="Script" id=1] -[ext_resource path="res://client_ui.tscn" type="PackedScene" id=2] [node name="Control" type="Control"] anchor_left = 0.0136719 @@ -23,76 +22,29 @@ __meta__ = { "_edit_use_anchors_": true } -[node name="Signaling" type="HBoxContainer" parent="VBoxContainer"] +[node name="Singleplayer" type="Button" parent="VBoxContainer"] margin_right = 995.0 -margin_bottom = 24.0 +margin_bottom = 20.0 +text = "Start Singleplayer" +__meta__ = { +"_edit_use_anchors_": false +} -[node name="Label" type="Label" parent="VBoxContainer/Signaling"] -margin_top = 5.0 -margin_right = 104.0 -margin_bottom = 19.0 -text = "Signaling server:" - -[node name="Port" type="SpinBox" parent="VBoxContainer/Signaling"] -margin_left = 108.0 -margin_right = 182.0 -margin_bottom = 24.0 -min_value = 1025.0 -max_value = 65535.0 -value = 9080.0 - -[node name="ListenButton" type="Button" parent="VBoxContainer/Signaling"] -margin_left = 186.0 -margin_right = 237.0 -margin_bottom = 24.0 -toggle_mode = true -text = "Listen" - -[node name="CenterContainer" type="CenterContainer" parent="VBoxContainer/Signaling"] -margin_left = 241.0 +[node name="CreateLobbyButton" type="Button" parent="VBoxContainer"] +margin_top = 70.0 margin_right = 995.0 -margin_bottom = 24.0 -size_flags_horizontal = 3 -size_flags_vertical = 3 +margin_bottom = 90.0 +text = "Create Lobby" -[node name="LinkButton" type="LinkButton" parent="VBoxContainer/Signaling/CenterContainer"] -margin_left = 104.0 -margin_top = 5.0 -margin_right = 650.0 -margin_bottom = 19.0 -text = "Make sure to download the GDNative WebRTC Plugin and place it in the project folder" - -[node name="Clients" type="GridContainer" parent="VBoxContainer"] -margin_top = 74.0 +[node name="JoinLobbyButton" type="Button" parent="VBoxContainer"] +margin_top = 140.0 margin_right = 995.0 -margin_bottom = 579.0 -size_flags_horizontal = 3 -size_flags_vertical = 3 -custom_constants/vseparation = 15 -custom_constants/hseparation = 15 -columns = 2 - -[node name="ClientUI" parent="VBoxContainer/Clients" instance=ExtResource( 2 )] -margin_right = 490.0 -margin_bottom = 245.0 - -[node name="ClientUI2" parent="VBoxContainer/Clients" instance=ExtResource( 2 )] -margin_left = 505.0 -margin_right = 995.0 -margin_bottom = 245.0 - -[node name="ClientUI3" parent="VBoxContainer/Clients" instance=ExtResource( 2 )] -margin_top = 260.0 -margin_right = 490.0 -margin_bottom = 505.0 - -[node name="ClientUI4" parent="VBoxContainer/Clients" instance=ExtResource( 2 )] -margin_left = 505.0 -margin_top = 260.0 -margin_right = 995.0 -margin_bottom = 505.0 +margin_bottom = 160.0 +text = "Join Lobby" +__meta__ = { +"_edit_use_anchors_": false +} [node name="Server" type="Node" parent="."] -[connection signal="toggled" from="VBoxContainer/Signaling/ListenButton" to="." method="_on_listen_toggled"] -[connection signal="pressed" from="VBoxContainer/Signaling/CenterContainer/LinkButton" to="." method="_on_LinkButton_pressed"] +[connection signal="pressed" from="VBoxContainer/Singleplayer" to="." method="_on_Singleplayer_pressed"] diff --git a/multiplayer_client.gd b/multiplayer_client.gd index 7a01486..09f1db3 100644 --- a/multiplayer_client.gd +++ b/multiplayer_client.gd @@ -39,6 +39,7 @@ func _create_peer(id): return peer func _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) func _offer_created(type, data, id): diff --git a/project.godot b/project.godot index b42a0a1..ec2a44e 100644 --- a/project.godot +++ b/project.godot @@ -14,6 +14,10 @@ config/name="kdt" run/main_scene="res://main.tscn" config/icon="res://icon.png" +[autoload] + +Global="*res://global.gd" + [gdnative] singletons=[ "res://webrtc/webrtc.tres" ] diff --git a/server.ts b/server.ts index f339351..6801201 100644 --- a/server.ts +++ b/server.ts @@ -1,11 +1,12 @@ +import { randomId, randomSecret } from "./gen.ts"; + const MAX_PEERS = 4096; const MAX_LOBBIES = 1024; const PORT = 9080; -const ALFNUM = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; -const NO_LOBBY_TIMEOUT = 1000; +const NO_LOBBY_TIMEOUT = 10000; const SEAL_CLOSE_TIMEOUT = 10000; -const PING_INTERVAL = 10000; +// const PING_INTERVAL = 10000; const STR_NO_LOBBY = "Have not joined lobby yet"; const STR_HOST_DISCONNECTED = "Room host has disconnected"; @@ -22,23 +23,11 @@ const STR_INVALID_DEST = "Invalid destination"; const STR_INVALID_CMD = "Invalid command"; const STR_TOO_MANY_PEERS = "Too many peers connected"; -function randomInt(low: number, high: number) { - return Math.floor(Math.random() * (high - low + 1) + low); -} +// TODO: setup regular pings? -function randomId() { - const arr = new Int32Array(1); - crypto.getRandomValues(arr); - return Math.abs(arr[0]); -} - -function randomSecret() { - let out = ""; - for (let i = 0; i < 16; i++) { - out += ALFNUM[randomInt(0, ALFNUM.length - 1)]; - } - return out; -} +// state +const lobbies = new Map(); +let peersCount = 0; class ProtoError extends Error { code: number; @@ -59,11 +48,38 @@ class Peer { this.id = id; this.ws = ws; this.lobby = ""; - // Close connection after 1 sec if client has not joined a lobby + + // close connection after 10 sec if client has not joined a lobby this.timeout = setTimeout(() => { if (!this.lobby) ws.close(4000, STR_NO_LOBBY); }, NO_LOBBY_TIMEOUT); } + + joinLobby(lobbyName: string) { + if (lobbyName === "") { + if (lobbies.size >= MAX_LOBBIES) { + throw new ProtoError(4000, STR_TOO_MANY_LOBBIES); + } + // Peer must not already be in a lobby + if (this.lobby !== "") { + throw new ProtoError(4000, STR_ALREADY_IN_LOBBY); + } + lobbyName = randomSecret(); + lobbies.set(lobbyName, new Lobby(lobbyName, this.id)); + console.log(`Peer ${this.id} created lobby ${lobbyName}`); + console.log(`Open lobbies: ${lobbies.size}`); + } + const lobby = lobbies.get(lobbyName); + if (!lobby) throw new ProtoError(4000, STR_LOBBY_DOES_NOT_EXISTS); + if (lobby.sealed) throw new ProtoError(4000, STR_LOBBY_IS_SEALED); + this.lobby = lobbyName; + console.log( + `Peer ${this.id} joining lobby ${lobbyName} ` + + `with ${lobby.peers.length} peers`, + ); + lobby.join(this); + this.ws.send(`J: ${lobbyName}\n`); + } } class Lobby { @@ -95,6 +111,7 @@ class Lobby { }); this.peers.push(peer); } + leave(peer: Peer) { const idx = this.peers.findIndex((p) => peer === p); if (idx === -1) return false; @@ -102,9 +119,9 @@ class Lobby { const close = assigned === 1; this.peers.forEach((p) => { try { - // Room host disconnected, must close. + // room host disconnected if (close) p.ws.close(4000, STR_HOST_DISCONNECTED); - // Notify peer disconnect. + // notify peers else p.ws.send(`D: ${assigned}\n`); } catch (e) { console.error(`Error when leaving: ${e}`); @@ -112,14 +129,15 @@ class Lobby { }); this.peers.splice(idx, 1); if (close && this.closeTimer >= 0) { - // We are closing already. + // 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) { throw new ProtoError(4000, STR_ONLY_HOST_CAN_SEAL); } @@ -132,7 +150,7 @@ class Lobby { `with ${this.peers.length} peers`, ); this.closeTimer = setTimeout(() => { - // Close peer connection to host (and thus the lobby) + // close peer connection to host (and thus the lobby) this.peers.forEach((p) => { p.ws.close(1000, STR_SEAL_COMPLETE); }); @@ -140,35 +158,6 @@ class Lobby { } } -const lobbies = new Map(); -let peersCount = 0; - -function joinLobby(peer: Peer, lobbyName: string) { - if (lobbyName === "") { - if (lobbies.size >= MAX_LOBBIES) { - throw new ProtoError(4000, STR_TOO_MANY_LOBBIES); - } - // Peer must not already be in a lobby - if (peer.lobby !== "") { - throw new ProtoError(4000, STR_ALREADY_IN_LOBBY); - } - lobbyName = randomSecret(); - lobbies.set(lobbyName, new Lobby(lobbyName, peer.id)); - console.log(`Peer ${peer.id} created lobby ${lobbyName}`); - console.log(`Open lobbies: ${lobbies.size}`); - } - const lobby = lobbies.get(lobbyName); - if (!lobby) throw new ProtoError(4000, STR_LOBBY_DOES_NOT_EXISTS); - if (lobby.sealed) throw new ProtoError(4000, STR_LOBBY_IS_SEALED); - peer.lobby = lobbyName; - console.log( - `Peer ${peer.id} joining lobby ${lobbyName} ` + - `with ${lobby.peers.length} peers`, - ); - lobby.join(peer); - peer.ws.send(`J: ${lobbyName}\n`); -} - function parseMsg(peer: Peer, msg: string) { const sep = msg.indexOf("\n"); if (sep < 0) throw new ProtoError(4000, STR_INVALID_FORMAT); @@ -178,9 +167,9 @@ function parseMsg(peer: Peer, msg: string) { const data = msg.slice(sep); - // Lobby joining. + // join if (cmd.startsWith("J: ")) { - joinLobby(peer, cmd.substr(3).trim()); + peer.joinLobby(cmd.substr(3).trim()); return; } @@ -188,7 +177,7 @@ function parseMsg(peer: Peer, msg: string) { const lobby = lobbies.get(peer.lobby); if (!lobby) throw new ProtoError(4000, STR_SERVER_ERROR); - // Lobby sealing. + // seal if (cmd.startsWith("S: ")) { lobby.seal(peer); return;