basic lobbies

This commit is contained in:
Daniel Flanagan 2021-11-17 13:57:45 -06:00
parent 6d608b6f0e
commit 160d0dbebe
Signed by: lytedev
GPG Key ID: 5B2020A0F9921EF4
10 changed files with 391 additions and 97 deletions

2
deps.ts Normal file
View File

@ -0,0 +1,2 @@
export const randomInt = (low: number, high: number) =>
Math.floor(Math.random() * (high - low + 1) + low);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

268
server.ts
View File

@ -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<ID, Lobby>();
const allClients = new Map<ID, Client>();
interface DataMessage {
type: string;
}
const lobbies = new Set<Lobby>();
const clients = new Set<Client>();
type ServerData = Record<string, string | number | symbol> | 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<ID, Client>;
hostClientId: ID;
constructor(host: Client, name?: string) {
this.id = crypto.randomUUID();
this.hostClientId = host.id;
this.clients = new Map<ID, Client>();
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" },
}),

View File

@ -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():