This commit is contained in:
Daniel Flanagan 2021-12-08 23:39:56 -06:00
parent 25b93a9f2b
commit d2abbb69f2
15 changed files with 464 additions and 465 deletions

View file

@ -1,29 +0,0 @@
[remap]
importer="font_data_dynamic"
type="FontData"
uid="uid://cklw0gsblqjmg"
path="res://.godot/imported/iosevkalyte-regular.ttf-a2a88ad4f5b4e12dda9997dfa2756a6e.fontdata"
[deps]
source_file="res://assets/fonts/iosevkalyte/iosevkalyte-regular.ttf"
dest_files=["res://.godot/imported/iosevkalyte-regular.ttf-a2a88ad4f5b4e12dda9997dfa2756a6e.fontdata"]
[params]
antialiased=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
force_autohinter=false
hinting=1
oversampling=0.0
compress=true
preload/char_ranges=PackedStringArray()
preload/glyph_ranges=PackedStringArray()
preload/configurations=PackedStringArray()
support_overrides/language_enabled=PackedStringArray()
support_overrides/language_disabled=PackedStringArray()
support_overrides/script_enabled=PackedStringArray()
support_overrides/script_disabled=PackedStringArray()

View file

@ -3,7 +3,7 @@
[ext_resource path="res://assets/fonts/iosevkalyte/iosevkalyte.tres" type="DynamicFont" id=1]
[ext_resource path="res://assets/img/panel.png" type="Texture" id=2]
[sub_resource type="Image" id=1]
[sub_resource type="Image" id=5]
data = {
"data": PoolByteArray( 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42, 37, 37, 42 ),
"format": "RGB8",
@ -15,7 +15,7 @@ data = {
[sub_resource type="ImageTexture" id=2]
flags = 4
flags = 4
image = SubResource( 1 )
image = SubResource( 5 )
size = Vector2( 8, 8 )
[sub_resource type="StyleBoxTexture" id=3]

View file

@ -36,7 +36,7 @@ Global="*res://scripts/global/global.gd"
[gdnative]
singletons=[ "res://webrtc/webrtc.tres" ]
singletons=[ "res://webrtc/webrtc_debug.tres" ]
[physics]

View file

@ -62,10 +62,10 @@ __meta__ = {
}
[node name="subhead" type="HBoxContainer" parent="v"]
visible = false
margin_top = 54.0
margin_right = 984.0
margin_bottom = 85.0
size_flags_horizontal = 3
[node name="label" type="Label" parent="v/subhead"]
margin_right = 830.0
@ -83,7 +83,7 @@ rect_min_size = Vector2( 150, 0 )
placeholder_text = "Your Name Here"
[node name="body" type="ScrollContainer" parent="v"]
margin_top = 54.0
margin_top = 89.0
margin_right = 984.0
margin_bottom = 560.0
size_flags_horizontal = 3
@ -91,7 +91,7 @@ size_flags_vertical = 3
[node name="p" type="PanelContainer" parent="v/body"]
margin_right = 984.0
margin_bottom = 506.0
margin_bottom = 471.0
size_flags_horizontal = 3
size_flags_vertical = 3
@ -99,21 +99,21 @@ size_flags_vertical = 3
margin_left = 7.0
margin_top = 7.0
margin_right = 977.0
margin_bottom = 499.0
margin_bottom = 464.0
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="zero_state" type="CenterContainer" parent="v/body/p/lobbies"]
margin_right = 970.0
margin_bottom = 492.0
margin_bottom = 457.0
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="Label" type="Label" parent="v/body/p/lobbies/zero_state"]
margin_left = 177.0
margin_top = 235.0
margin_top = 218.0
margin_right = 793.0
margin_bottom = 256.0
margin_bottom = 239.0
size_flags_horizontal = 3
size_flags_vertical = 3
text = "Looks like there are no active lobbies at the moment! Why don't you make one?"

View file

@ -107,6 +107,6 @@ hint_tooltip = "Open https://lyte.dev"
text = "lytedev"
[connection signal="pressed" from="VBoxContainer/HBoxContainer/quit" to="." method="_on_JoinLobbyButton_pressed"]
[connection signal="pressed" from="VBoxContainer/HBoxContainer/multiplayer" to="." method="_on_CreateLobbyButton_pressed"]
[connection signal="pressed" from="VBoxContainer/HBoxContainer/multiplayer" to="." method="_on_multiplayer_pressed"]
[connection signal="pressed" from="VBoxContainer/HBoxContainer/Singleplayer" to="." method="_on_Singleplayer_pressed"]
[connection signal="pressed" from="VBoxContainer/Container/VBoxContainer/HBoxContainer/LinkButton" to="." method="_on_LinkButton_pressed"]

View file

@ -185,6 +185,7 @@ text = "Send Message"
[connection signal="pressed" from="v/head/leave_button" to="." method="_on_leave_button_pressed"]
[connection signal="text_changed" from="v/head/lobby_info" to="." method="_on_lobby_info_text_changed"]
[connection signal="pressed" from="v/head/start" to="." method="_on_start_pressed"]
[connection signal="toggled" from="v/head/ready_up" to="." method="_on_ready_up_toggled"]
[connection signal="text_entered" from="v/body/v/h/chat" to="." method="_on_TextEdit_text_entered"]
[connection signal="pressed" from="v/body/v/h/Button" to="." method="_on_Button_pressed"]

View file

@ -1,52 +1,74 @@
extends Node
const MultiplayerClient = preload("multiplayer_client.gd")
onready var signaller_url = "ws://localhost:8888"
# onready var signaller_url = "wss://webrtc-signaller.deno.dev:443"
onready var client = MultiplayerClient.new()
onready var ice_servers = [
{ "urls": ["stun:[::1]:3478", "stun:stun.l.google.com:19302"] },
# { "urls": ["stun:stun.l.google.com:19302"] }, # just google
# { "urls": ["stun:[::1]:3478"] }, # just localhost
]
# for command line flags
onready var goto_multiplayer = false
onready var create_lobby = false
onready var join_first_available_lobby = false
const SignallerClient = preload("signaller_client.gd")
const WebRTCNegotiator = preload("webrtc_negotiator.gd")
onready var signaller_client = SignallerClient.new(signaller_url)
onready var negotiator = WebRTCNegotiator.new(ice_servers, signaller_client)
onready var onetime_cmd_flags = {
"--multiplayer": false,
"--create-lobby": false,
"--join-first-available-lobby": false,
}
func check_onetime_flag(flag):
var result = onetime_cmd_flags["--%s" % flag]
onetime_cmd_flags["--%s" % flag] = false
return result
func _ready():
# client.signaller.connect("websocket_connected", self, "signaller_disconnected")
add_child(client)
client.signaller.connect("websocket_connected", self, "_signaller_connected")
negotiator.signaller_client = signaller_client
negotiator.ice_servers = ice_servers
add_child(negotiator)
for arg in OS.get_cmdline_args():
match arg:
"--multiplayer": goto_multiplayer = true
"--create-lobby": create_lobby = true
"--join-first-available-lobby": join_first_available_lobby = true
var a: print("Unknown command line arg: %s" % a)
if goto_multiplayer:
goto_multiplayer = false
lobby_browser()
for flag in onetime_cmd_flags.keys():
if flag in OS.get_cmdline_args():
onetime_cmd_flags[flag] = true
func goto_scene(scene_resource_name):
var _result = get_tree().change_scene("res://screens/%s.tscn" % scene_resource_name)
func main_menu():
client.close()
negotiator.close()
goto_scene("main_menu")
func fake_singleplayer():
print("Faking network peer for singleplayer...")
negotiator.multiplayer.initialize(1, true)
get_tree().network_peer = negotiator.multiplayer
var peer: WebRTCPeerConnection = WebRTCPeerConnection.new()
peer.initialize()
negotiator.multiplayer.add_peer(peer, 1)
func start_singleplayer_game():
client.fake_singleplayer()
client.close()
fake_singleplayer()
negotiator.close()
goto_game()
func goto_game():
goto_scene("game")
func _signaller_connected():
goto_scene("lobby_browser")
func lobby_browser():
client.close()
Global.client.connect_to_signaller()
negotiator.close()
signaller_client.connect("connected", self, "_signaller_connected")
negotiator.connect_to_signaller()
func lobby():
goto_scene("multiplayer_lobby")
func quit():
client.close()
negotiator.close()
get_tree().quit()

View file

@ -1,117 +0,0 @@
extends Node
"""
This module sets up WebRTC peer connections.
"""
# var multiplayer_url = "wss://webrtc-signaller.deno.dev:443"
var multiplayer_url = "ws://localhost:8888"
# var multiplayer_url = "ws://echo.websocket.org"
var webrtc_ice_servers = [
{ "urls": ["stun:stun.l.google.com:19302"] },
# { "urls": ["stun:localhost:3478"] }
]
const SignallerClient = preload("signaller_client.gd")
onready var mp = WebRTCMultiplayer.new()
onready var signaller = SignallerClient.new()
func _ready():
signaller.connect("offer_received", self, "offer_received")
signaller.connect("answer_received", self, "answer_received")
signaller.connect("candidate_received", self, "candidate_received")
# signaller.connect("websocket_connected", self, "connected")
# signaller.connect("websocket_disconnected", self, "disconnected")
signaller.connect("peer_id_set", self, "_peer_id_set")
signaller.connect("peer_joined", self, "_peer_joined")
# mp.connect("peer_disconnected", self, "peer_disconnected")
# connect("lobby_sealed", self, "lobby_sealed")
add_child(signaller)
func fake_singleplayer():
print("Faking network peer for singleplayer...")
mp.initialize(1, true)
get_tree().network_peer = mp
var peer: WebRTCPeerConnection = WebRTCPeerConnection.new()
peer.initialize()
mp.add_peer(peer, 1)
func close():
mp.close()
signaller.close()
func connect_to_signaller():
close()
signaller.connect_to_websocket_signaller(multiplayer_url)
func _create_peer(peer_id):
# if peer_id == signaller.peer_id: return
var peer: WebRTCPeerConnection = WebRTCPeerConnection.new()
print("New Net Peer with peer_id %d" % [peer_id])
var _peer_init_results = peer.initialize({"iceServers": webrtc_ice_servers})
var _result = peer.connect("session_description_created", self, "_offer_created", [peer_id])
_result = peer.connect("ice_candidate_created", self, "_new_ice_candidate", [peer_id])
mp.add_peer(peer, peer_id)
var uid = mp.get_unique_id()
if peer_id > uid:
var offer_result = peer.create_offer()
print("Create Offer Result: %s" % offer_result)
else:
print("Did not create offer: %d <= %d" % [peer_id, uid])
return peer
func _new_ice_candidate(mid_name, index_name, sdp_name, peer_id):
print("New ICE Candidate - MID: %s, Index: %s, SDP: %s, PeerID: %s" % [mid_name, index_name, sdp_name, peer_id])
signaller.send_candidate(peer_id, mid_name, index_name, sdp_name)
func _offer_created(type, data, peer_id):
print("Offer created - Type: %s, PeerID: %s, Data: %s" % [type, peer_id, JSON.print(data)])
if not mp.has_peer(peer_id):
return
mp.get_peer(peer_id).connection.set_local_description(type, data)
if type == "offer":
signaller.send_offer(peer_id, data)
else:
signaller.send_answer(peer_id, data)
func _peer_id_set(peer_id):
print("Peer ID received %s - setting up to peer..." % [peer_id])
mp.initialize(peer_id, true)
get_tree().network_peer = mp
func _peer_joined(peers):
for i in range(len(peers)):
var peer = peers[i]
if peer.has("peerId"):
var peer_id = peer["peerId"]
if not mp.has_peer(peer_id):
_create_peer(peer_id)
func peer_disconnected(id):
print("Peer %s disconnected" % id)
if mp.has_peer(id):
mp.remove_peer(id)
func offer_received(data):
var id = data["peerId"]
if mp.has_peer(id):
print("Setting offer remote description: %s" % JSON.print(data))
mp.get_peer(id).connection.set_remote_description("offer", data["offer"])
else:
print("Received an offer for a peer with ID %s that hasn't been added" % id)
func answer_received(data):
var id = data["peerId"]
if mp.has_peer(id):
print("Setting answer remote description: %s" % JSON.print(data))
mp.get_peer(id).connection.set_remote_description("answer", data["answer"])
func candidate_received(data):
var id = data["peerId"]
if mp.has_peer(id):
print("Adding ice candidate: %s" % JSON.print(data))
mp.get_peer(id).connection.add_ice_candidate(data["mid"], data["index"], data["sdp"])
else:
print("Received candidate for non-existant peer %s" % id)

View file

@ -3,168 +3,154 @@ extends Node
"""
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. (./multiplayer_client.gd)
expected to fully setup the peer connections.
"""
signal lobby_new(lobbiesList)
signal lobby_delete(id)
signal lobby_joined(id, peer_id)
signal lobby_left(id)
const DISPLAY_NAME_FILE = "user://display_name.txt"
const DEFAULT_DISPLAY_NAME = "UnnamedPlayer"
signal peer_joined(peers)
# lobby data
signal lobby_data_received(lobbies)
signal lobby_joined(lobby_and_peer)
signal lobby_left(id)
signal lobby_delete(id)
# peer data
signal peer_data_received(peers)
signal peer_left(id)
signal candidate_received(data)
# WebRTC negotiations
signal candidate_received(cand)
signal offer_received(data)
signal answer_received(data)
signal client_id_set(client_id)
signal peer_id_set(peer_id)
signal websocket_connected
signal websocket_disconnected
onready var ws: WebSocketClient = WebSocketClient.new()
# underlying websocket
signal received_message
signal connected
signal connection_failure
signal disconnected
onready var websocket: WebSocketClient = WebSocketClient.new()
# TODO: setget these?
onready var client_id = null
onready var lobby_id = null
onready var display_name = DEFAULT_DISPLAY_NAME setget set_display_name
onready var peer_id = null
onready var display_name = "UnnamedPlayer"
onready var lobby_name = null
func is_host(): return peer_id == 1
var websocket_url = null
const DISPLAY_NAME_FILE = "user://display_name.txt"
func _init(url):
websocket_url = url
func _ready():
var dnf = File.new()
if dnf.open(DISPLAY_NAME_FILE, File.READ) == OK:
display_name = dnf.get_as_text()
dnf.close()
var _result = ws.connect("data_received", self, "_parse_msg")
_result = ws.connect("connection_established", self, "_connected")
_result = ws.connect("connection_closed", self, "_closed")
_result = ws.connect("connection_error", self, "_closed", [false])
_result = ws.connect("connection_failed", self, "_closed", [false])
_result = ws.connect("connection_succeeded", self, "_succ")
_result = ws.connect("server_close_request", self, "_close_request")
display_name = _load_display_name()
websocket.connect("data_received", self, "_handle_data")
websocket.connect("connection_established", self, "_connected")
# websocket.connect("connection_succeeded", self, "_connection_succeeded")
websocket.connect("connection_closed", self, "_closed")
websocket.connect("connection_error", self, "_connection_error", [false])
websocket.connect("connection_failed", self, "_connection_error", [false])
websocket.connect("server_close_request", self, "_close_request")
func _succ():
print("WebSocket Connection Succeeded")
func connect_websocket():
close()
print("Attempting to connect to WebSocket signalling server at %s" % websocket_url)
var result = websocket.connect_to_url(websocket_url)
if result != OK:
print("Failed to connect to WebSocket signalling server at %s: %s" % [websocket_url, result])
func close():
ws.disconnect_from_host()
websocket.disconnect_from_host()
func connect_to_websocket_signaller(url: String):
print("WebSocket: %s" % ws)
close()
print("Attempting to connect to WebSocket signalling server at %s" % url)
var result = ws.connect_to_url(url)
if result != OK:
print("FAILED TO CONNECT TO %s" % url)
print("WebSocket Connect Result: %s" % result)
func set_display_name(new_display_name: String):
display_name = new_display_name
_send("update_display_name:%s" % display_name)
func _closed(unknown):
print("WebSocket closed: %s: " % unknown)
emit_signal("websocket_disconnected")
func request_lobby_list():
return _send("request_lobby_list")
func _close_request(code: int, reason: String):
print("Received WebSocket close request from signalling server - Code: %s, Reason: %s" % [code, reason])
func _connected(protocol = ""):
print("WebSocket signaller connected via protocol %s" % protocol)
ws.get_peer(1).set_write_mode(WebSocketPeer.WRITE_MODE_TEXT)
emit_signal("websocket_connected")
_send("json:%s" % JSON.print({
"type": "init",
"data": {
"name": display_name
}
}))
func _process(_delta: float):
var status: int = ws.get_connection_status()
if status == WebSocketClient.CONNECTION_CONNECTED or status == WebSocketClient.CONNECTION_CONNECTING:
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 set_display_name(new_display_name: String):
display_name = new_display_name
_send("update_display_name:%s" % display_name)
var dnf = File.new()
if dnf.open(DISPLAY_NAME_FILE, File.WRITE) == OK:
dnf.store_string(display_name)
dnf.close()
func _send(s: String):
return ws.get_peer(1).put_packet(s.to_utf8())
func is_host(): return peer_id == 1
func set_lobby_name(s: String):
if is_host():
_send("json:%s" % JSON.print({"type": "update_lobby", "data": {"name": s}}))
if is_host(): _send_json({"name": s}, "update_lobby")
func lock_lobby():
if is_host():
_send("json:%s" % JSON.print({"type": "update_lobby", "data": {"locked": true}}))
if is_host(): _send_json({"locked": true}, "update_lobby")
func set_lobby_max_players(n: int):
if is_host():
_send("json:%s" % JSON.print({"type": "update_lobby", "data": {"maxPlayers": n}}))
if is_host(): _send_json({"maxPlayers": n}, "update_lobby")
func set_ready(n: bool):
if !is_host():
_send("set_ready:%s" % JSON.print(n))
func request_peer_list(): _send("request_peer_list")
func _parse_msg():
var msg: String = ws.get_peer(1).get_packet().get_string_from_utf8()
func send_candidate(peerId, mid, index, sdp) -> int:
return _send_json({"peerId": peerId, "mid": mid, "index": index, "sdp": sdp}, "candidate")
func send_offer(peerId, offer) -> int:
return _send_json({"peerId": peerId, "offer": offer }, "offer")
func send_answer(peerId, answer) -> int:
return _send_json({"peerId": peerId, "answer": answer }, "answer")
func _closed(code):
print("WebSocket closed: %s: " % code)
emit_signal("disconnected")
func _close_request(code: int, reason: String):
print("Received WebSocket close request from signalling server - Code: %s, Reason: %s" % [code, reason])
# TODO: does this fire _closed?
func _connected(protocol = ""):
print("Signaller connected via WebSocket using protocol %s" % protocol)
websocket.get_peer(1).set_write_mode(WebSocketPeer.WRITE_MODE_TEXT)
emit_signal("connected")
_send_json({name = display_name}, "init")
func _process(_delta: float):
var status: int = websocket.get_connection_status()
if status == WebSocketClient.CONNECTION_CONNECTED or status == WebSocketClient.CONNECTION_CONNECTING:
websocket.poll()
func _handle_data():
var msg: String = websocket.get_peer(1).get_packet().get_string_from_utf8()
if msg.begins_with("json:"):
var data = JSON.parse(msg.substr(5))
if data.error == OK:
handle_message(data.result)
var _result = ws.get_peer(1).put_packet("".to_utf8())
_process_message(data.result)
var _result = websocket.get_peer(1).put_packet("".to_utf8())
else:
print("Unhandled message: %s" % msg)
func handle_message(data: Dictionary):
func _process_message(data: Dictionary):
match data["type"]:
"your_id":
client_id = data["data"]["id"]
emit_signal("client_id_set", client_id)
"init":
client_id = data.id
print("Signaller Received init response: %s" % data)
"your_peer_id":
peer_id = int(data["data"])
peer_id = int(data.peerId)
emit_signal("peer_id_set", peer_id)
"lobby_list": emit_signal("lobby_new", data["data"])
"lobby_new": emit_signal("lobby_new", [data])
"lobby_delete": emit_signal("lobby_delete", data["id"])
"lobby_data": emit_signal("lobby_data_received", data.data)
"lobby_delete": emit_signal("lobby_delete", data.id)
"lobby_joined":
lobby_id = data["id"]
lobby_name = data["name"]
peer_id = data["peerId"]
emit_signal("lobby_joined", data["id"], data["peerId"])
peer_id = data.peerId
emit_signal("lobby_joined", data)
"lobby_left":
lobby_id = null
lobby_name = null
peer_id = null
emit_signal("lobby_left", data["id"])
"peer_joined":
"peer_data":
print(typeof(data), data)
if data.get("id") != null: emit_signal("peer_joined", [data])
else: emit_signal("peer_joined", data["data"])
"peer_left":
print("Peer Left: %s" % data)
emit_signal("peer_left", data["data"])
"ready_change":
print("Peer Ready Change")
emit_signal("peer_joined", [data])
emit_signal("peer_left", data)
"candidate":
print("Candidate received - Data: %s" % JSON.print(data["data"]))
@ -176,29 +162,31 @@ func handle_message(data: Dictionary):
print("Answer received - Data: %s" % JSON.print(data["data"]))
emit_signal("answer_received", data["data"])
"ping":
# print("Signaller Ping")
_send("pong")
_: print("Unhandled Message - Data: %s" % JSON.print(data))
func send_candidate(peerId, mid, index, sdp) -> int:
return _send("json:%s" % JSON.print({
"type": "candidate",
"data": {
"peerId": peerId,
"mid": mid,
"index": index,
"sdp": sdp
}
}))
func _load_display_name():
var _display_name = DEFAULT_DISPLAY_NAME
var display_name_file = File.new()
if display_name_file.open(DISPLAY_NAME_FILE, File.READ) == OK:
_display_name = display_name_file.get_as_text()
else:
print("Failed to open %s for reading display_name, creating default" % DISPLAY_NAME_FILE)
_store_display_name()
display_name_file.close()
return _display_name
func send_offer(peerId, offer) -> int:
return _send("json:%s" % JSON.print({ "type": "offer", "data": {"peerId": peerId, "offer": offer }}))
func _store_display_name():
var display_name_file = File.new()
if display_name_file.open(DISPLAY_NAME_FILE, File.WRITE) == OK:
display_name_file.store_string(display_name)
else:
print("Failed to open %s for writing display_name" % DISPLAY_NAME_FILE)
display_name_file.close()
func send_answer(peerId, answer) -> int:
return _send("json:%s" % JSON.print({ "type": "answer", "data": {"peerId": peerId, "answer": answer }}))
func _send(s: String):
return websocket.get_peer(1).put_packet(s.to_utf8())
"""
elif type.begins_with("S: "):
emit_signal("lobby_sealed")
return
"""
func _send_json(data: Dictionary, type=null):
if type != null: data["type"] = type
_send("json:%s" % JSON.print(data))

View file

@ -0,0 +1,107 @@
extends Node
"""
This module sets up WebRTC peer connections.
"""
onready var webrtc = WebRTCMultiplayer.new()
var signaller_client = null
var ice_servers = null
func _init(_ice_servers, _signaller_client):
ice_servers = _ice_servers
signaller_client = _signaller_client
func _ready():
signaller_client.connect("offer_received", self, "_offer_received")
signaller_client.connect("answer_received", self, "_answer_received")
signaller_client.connect("candidate_received", self, "_candidate_received")
signaller_client.connect("connected", self, "_signaller_connected")
# signaller_client.connect("websocket_disconnected", self, "disconnected")
signaller_client.connect("peer_id_set", self, "_peer_id_set")
signaller_client.connect("peer_joined", self, "_peer_joined")
# webrtc.connect("peer_disconnected", self, "peer_disconnected")
# connect("lobby_sealed", self, "lobby_sealed")
add_child(self.signaller_client)
func _signaller_connected():
print("Connected to signaller. Will be using the following ICE servers: %s", ice_servers)
func close():
webrtc.close()
signaller_client.close()
func connect_to_signaller():
signaller_client.connect_websocket()
func _create_peer(peer_id):
# if peer_id == signaller_client.peer_id: return
var peer: WebRTCPeerConnection = WebRTCPeerConnection.new()
print("New Net Peer with peer_id %d" % [peer_id])
var _peer_init_results = peer.initialize({"iceServers": ice_servers})
var _result = peer.connect("session_description_created", self, "_offer_created", [peer_id])
_result = peer.connect("ice_candidate_created", self, "_new_ice_candidate", [peer_id])
webrtc.add_peer(peer, peer_id)
var uid = webrtc.get_unique_id()
if peer_id > uid:
var offer_result = peer.create_offer()
print("Create Offer Result: %s" % offer_result)
else:
print("Did not create offer: %d <= %d" % [peer_id, uid])
return peer
func _new_ice_candidate(mid_name, index_name, sdp_name, peer_id):
print("New ICE Candidate - MID: %s, Index: %s, SDP: %s, PeerID: %s" % [mid_name, index_name, sdp_name, peer_id])
signaller_client.send_candidate(peer_id, mid_name, index_name, sdp_name)
func _offer_created(type, data, peer_id):
print("Offer created - Type: %s, PeerID: %s, Data: %s" % [type, peer_id, JSON.print(data)])
if not webrtc.has_peer(peer_id):
return
webrtc.get_peer(peer_id).connection.set_local_description(type, data)
if type == "offer":
signaller_client.send_offer(peer_id, data)
else:
signaller_client.send_answer(peer_id, data)
func _peer_id_set(peer_id):
print("Peer ID received %s - setting up to peer..." % [peer_id])
webrtc.initialize(peer_id, true)
get_tree().network_peer = webrtc
func _peer_joined(peers):
for i in range(len(peers)):
var peer = peers[i]
if peer.has("peerId"):
var peer_id = peer["peerId"]
if not webrtc.has_peer(peer_id):
_create_peer(peer_id)
func peer_disconnected(id):
print("Peer %s disconnected" % id)
if webrtc.has_peer(id):
webrtc.remove_peer(id)
func offer_received(data):
var id = data["peerId"]
if webrtc.has_peer(id):
print("Setting offer remote description: %s" % JSON.print(data))
webrtc.get_peer(id).connection.set_remote_description("offer", data["offer"])
else:
print("Received an offer for a peer with ID %s that hasn't been added" % id)
func answer_received(data):
var id = data["peerId"]
if webrtc.has_peer(id):
print("Setting answer remote description: %s" % JSON.print(data))
webrtc.get_peer(id).connection.set_remote_description("answer", data["answer"])
func candidate_received(data):
var id = data["peerId"]
if webrtc.has_peer(id):
print("Adding ice candidate: %s" % JSON.print(data))
webrtc.get_peer(id).connection.add_ice_candidate(data["mid"], data["index"], data["sdp"])
else:
print("Received candidate for non-existant peer %s" % id)

View file

@ -12,6 +12,8 @@ remotesync func add_player(peer_id):
var new_player = player.instance()
new_player.set_network_master(peer_id)
add_child(new_player)
new_player.global_position = Vector2(100, 100)
print("Added player: %s" % new_player)
func _on_Button_pressed():
Global.main_menu()

View file

@ -14,17 +14,16 @@ onready var lobbies = {}
onready var lobby = preload("res://objects/lobby.tscn")
func _ready():
display_name_edit.text = Global.client.signaller.display_name
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_disconnected", self, "_signaller_disconnected")
Global.client.signaller.request_lobby_list()
display_name_edit.text = Global.signaller_client.display_name
Global.signaller_client.connect("lobby_new", self, "_lobby_new")
Global.signaller_client.connect("lobby_delete", self, "_lobby_delete")
Global.signaller_client.connect("lobby_joined", self, "_lobby_joined")
Global.signaller_client.connect("lobby_left", self, "_lobby_left")
Global.signaller_client.connect("websocket_disconnected", self, "_signaller_disconnected")
Global.signaller_client.request_lobby_list()
if Global.create_lobby:
Global.create_lobby = false
Global.client.signaller.create_lobby()
if Global.check_onetime_flag("create-lobby"):
Global.signaller_client.create_lobby()
"""
var ls = []
@ -35,13 +34,14 @@ func _ready():
# TODO: add search
func _input(ev):
if ev is InputEventKey and ev.pressed:
if ev.scancode == KEY_T:
var ls = []
for n in range(5):
var j = n + lobbies.size()
ls.push_back({id = j, name = j, currentPlayers = j, maxPlayers = j, locked = false})
call_deferred("_lobby_new", ls)
pass
# if ev is InputEventKey and ev.pressed:
# if ev.scancode == KEY_T:
# var ls = []
# for n in range(5):
# var j = n + lobbies.size()
# ls.push_back({id = j, name = j, currentPlayers = j, maxPlayers = j, locked = false})
# call_deferred("_lobby_new", ls)
func _signaller_disconnected():
Global.main_menu()
@ -56,12 +56,12 @@ func _on_back_pressed():
Global.main_menu()
func _on_create_lobby_pressed():
Global.client.signaller.create_lobby()
Global.signaller_client.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"])
Global.signaller_client.join_lobby(lobbies.get_item_metadata(items[0])["id"])
func _lobby_new(new_lobbies: Array):
# TODO: handle scrolling so that the user is never too jarred (like chat)
@ -75,14 +75,14 @@ func _lobby_new(new_lobbies: Array):
if Global.join_first_available_lobby:
Global.join_first_available_lobby = false
Global.client.signaller.join_lobby(id)
Global.signaller_client.join_lobby(id)
func _lobby_delete(id: String):
print("Lobby Deleted: %s" % id)
_delete_lobby(id)
func _add_lobby(id, lobby_data):
call_deferred("update_lobbies_text")
call_deferred("_update_lobbies_text")
print("New Lobby ", lobby_data)
# if lobby_data.currentPlayers > 0:
var new_lobby = lobby.instance()
@ -91,19 +91,19 @@ func _add_lobby(id, lobby_data):
lobbies[id] = new_lobby
func _update_lobby(id, lobby_data):
call_deferred("update_lobbies_text")
call_deferred("_update_lobbies_text")
print("Updated Lobby ", lobby_data)
lobbies[id].set_with_dict(lobby_data)
func _delete_lobby(id):
call_deferred("update_lobbies_text")
call_deferred("_update_lobbies_text")
if lobbies.has(id):
print("Removing lobby...")
lobbies_grid.remove_child(lobbies[id])
lobbies[id].queue_free()
lobbies.erase(id)
func update_lobbies_text():
func _update_lobbies_text():
var n = lobbies.size()
lobbies_label.text = "Active Lobbies: %d" % n
if n < 1:
@ -114,4 +114,4 @@ func update_lobbies_text():
lobbies_grid.remove_child(zero_state)
func _on_display_name_text_changed(new_text):
Global.client.signaller.set_display_name(new_text)
Global.signaller_client.display_name = new_text

View file

@ -1,14 +1,17 @@
extends Node
func _ready():
if Global.check_onetime_flag("multiplayer"):
_on_multiplayer_pressed()
func _on_Singleplayer_pressed():
Global.start_singleplayer_game()
func _on_CreateLobbyButton_pressed():
func _on_multiplayer_pressed():
Global.lobby_browser()
func _on_JoinLobbyButton_pressed():
Global.quit()
func _on_LinkButton_pressed():
OS.shell_open("https://lyte.dev")

View file

@ -29,7 +29,7 @@ func _ready():
call_deferred("add_chat", "# Connected to %s" % Global.client.signaller.lobby_name)
# hide/show controls depending on whether or not we're the host
var is_host = Global.client.signaller.peer_id == 1
is_host = Global.client.signaller.peer_id == 1
if is_host:
ready_up.queue_free()
else:
@ -37,6 +37,7 @@ func _ready():
max_players.editable = false
start.queue_free()
lock.queue_free()
print("Setting up lobby... %s %s" % [Global.client.signaller.peer_id, is_host])
func _lobby_update(l):
for u in l:
@ -48,7 +49,7 @@ func _lobby_update(l):
func _peer_joined(joined_peers):
call_deferred("update_player_count")
if Global.client.signaller.is_host(): call_deferred("update_can_start")
if is_host: call_deferred("update_can_start")
for peer_index in range(len(joined_peers)):
var peer_data = joined_peers[peer_index]
var id = peer_data["id"]
@ -112,6 +113,14 @@ func update_can_start():
if can_start: add_chat("! All players ready - game may now be started")
start.disabled = !can_start
func _on_start_pressed():
print("Start %s" % is_host)
if is_host: rpc("start_game")
remotesync func start_game():
print("Starting game!")
Global.goto_game()
func _lobby_left(_id): Global.lobby_browser()
func _on_leave_button_pressed(): Global.lobby_browser()
func _on_TextEdit_text_entered(_new_text): send_chat_message()

265
server.ts
View file

@ -15,7 +15,7 @@ interface DataMessage {
}
type ServerDataObject = Record<
string,
"peerId" | string,
string | number | symbol | null | boolean
>;
type ServerData = ServerDataObject[] | ServerDataObject | string | boolean;
@ -24,13 +24,11 @@ type Message = string | DataMessage;
const buildMessage = (
type: string,
data: ServerData | ServerData[],
) =>
Object.assign(
{ type },
Array.isArray(data)
? { data }
: (typeof data === "object" ? data : { data }),
);
) => {
if (Array.isArray(data)) return { type, data };
else if (typeof data === "object") return { type, ...data };
return `${type}:${data}`;
};
class Client {
id: ID;
@ -38,7 +36,6 @@ class Client {
name: string;
socket: WebSocket;
lobby: Lobby | null;
ready: boolean;
constructor(socket: WebSocket) {
this.id = crypto.randomUUID();
@ -46,7 +43,6 @@ class Client {
this.peerId = null;
this.name = "Client";
this.lobby = null;
this.ready = false;
allClients.set(this.id, this);
}
@ -55,13 +51,6 @@ class Client {
return this.socket.readyState == WebSocket.OPEN;
}
setReady(ready: boolean) {
this.ready = ready;
this.lobby?.broadcast(
buildMessage("ready_change", { id: this.id, ready: this.ready }),
);
}
remove() {
this.lobbyLeave();
if (this.isConnected()) {
@ -94,17 +83,24 @@ class Client {
}
peerList() {
if (!this.lobby) return;
if (!this.lobby) {
this.send(
buildMessage(
"info",
"you cannot request a list of peers unless you are in a lobby",
),
);
return;
}
const peers: {
id: ID;
name: string;
peerId: number | null;
ready: boolean;
}[] = [];
this.lobby.clients.forEach(({ id, name, peerId, ready }) =>
peers.push({ id, name, peerId, ready })
this.lobby.clients.forEach(({ id, name, peerId }) =>
peers.push({ id, name, peerId })
);
this.send(buildMessage("peer_joined", peers));
this.send(buildMessage("peer_data", peers));
// TODO: chunk async?
}
@ -116,32 +112,15 @@ class Client {
locked: boolean;
currentPlayers: number;
}[] = [];
allLobbies.forEach((lobby) => {
const { id, name, maxPlayers, locked } = lobby;
netLobbies.push({
id,
name,
maxPlayers,
locked,
currentPlayers: lobby.clients.size,
});
});
// TODO: chunk async?
this.send(buildMessage("lobby_list", netLobbies));
allLobbies.forEach((lobby) => netLobbies.push(lobby.toData()));
this.send(buildMessage("lobby_data", netLobbies));
}
lobbyNew(lobby: Lobby) {
// if the client is already in a lobby, only send messages about their lobby
if (this.lobby && this.lobby != lobby) return;
const { id, name, maxPlayers, locked } = lobby;
// if the client is already in a lobby, do not send them lobby updates
if (this.lobby) return;
this.send(
buildMessage("lobby_new", {
id,
name,
maxPlayers,
locked,
currentPlayers: lobby.clients.size,
}),
buildMessage("lobby_data", [lobby.toData()]),
);
}
@ -151,10 +130,12 @@ class Client {
}
lobbyCreate(opts?: Partial<Lobby>) {
this.ready = true;
if (this.lobby) {
this.send(
`[info] cannot create lobby (already in lobby ${this.lobby.id})`,
buildMessage(
"info",
`cannot create lobby: already in lobby ${this.lobby.id}`,
),
);
return;
}
@ -163,22 +144,25 @@ class Client {
lobbyJoin(lobby: Lobby) {
if (this.lobby) {
this.send(`[info] cannot join lobby (already in lobby ${this.lobby.id})`);
this.send(
buildMessage(
`info`,
`cannot join lobby: already in lobby ${this.lobby.id}`,
),
);
return;
}
this.lobby = lobby;
lobby.addClient(this);
this.send(
buildMessage("lobby_joined", {
id: lobby.id,
name: lobby.name,
...this.lobby.toData(),
peerId: this.peerId,
}),
);
}
lobbyLeave() {
this.ready = false;
const leavingLobby = this.lobby;
if (!leavingLobby) {
this.send(`[info] cannot leave lobby (not in a lobby)`);
@ -219,6 +203,16 @@ class Lobby {
this.notify();
}
toData() {
return {
id: this.id,
name: this.name,
maxPlayers: this.maxPlayers,
locked: this.locked,
currentPlayers: this.clients.size,
};
}
update(
requestor: Client,
newValues: { name?: string; maxPlayers?: number; locked?: boolean },
@ -273,18 +267,17 @@ class Lobby {
crypto.getRandomValues(arr);
client.peerId = Math.abs(arr[0]);
}
client.send(buildMessage("your_peer_id", client.peerId.toString()));
client.send(buildMessage("init_peer", client.peerId.toString()));
this.broadcast(
buildMessage("peer_joined", [{
buildMessage("peer_data", [{
id: client.id,
name: client.name,
peerId: client.peerId,
}]),
);
console.log("Sending peer_joined...");
client.send(
buildMessage(
"peer_joined",
"peer_data",
Array.from(this.clients.values()).map(
({ id, name, peerId }) => ({ id, name, peerId }),
),
@ -303,6 +296,13 @@ class Lobby {
}
this.notify();
}
getPeer(peerId: number): Client | null {
for (const [_id, client] of this.clients) {
if (client.peerId == peerId) return client;
}
return null;
}
}
interface ClientMessage {
@ -316,86 +316,102 @@ interface ClientMessage {
data: ServerDataObject;
}
function parseMessage(message: string): { type: string; data?: ServerData } {
const trimmedMessage = message.trim();
if (trimmedMessage.startsWith("json:")) {
const { type, ...data } = JSON.parse(trimmedMessage.substr(5));
return { type, data };
} else {
const splitAt = trimmedMessage.indexOf(":");
if (splitAt > 0) {
return {
type: trimmedMessage.substr(0, splitAt),
data: trimmedMessage.substr(splitAt + 1),
};
} else {
return { type: trimmedMessage };
}
}
}
// events
function onMessage(client: Client, ev: MessageEvent) {
// TODO: log who from?
const msg = ev.data.trim();
if (msg === "pong") return;
const msg = parseMessage(ev.data);
if (msg.type === "pong") return;
console.log("Client Message Received", msg);
if (msg === "init") {
client.send(
buildMessage("init", {
switch (msg.type) {
case "init":
if (msg.data && (msg.data as { name: string })["name"]) {
client.name = (msg.data as { name: string }).name.toString();
}
client.send(buildMessage("init", {
id: client.id,
peerId: client.peerId,
name: client.name,
serverVersion: SERVER_VERSION,
}),
);
}
if (msg === "lobby_create") client.lobbyCreate();
if (msg.startsWith("set_ready:")) {
client.setReady(JSON.parse(msg.split(":", 2)[1]));
}
if (msg === "lobby_leave") client.lobbyLeave();
if (msg === "request_lobby_list") client.lobbyList();
if (msg === "request_peer_list") {
if (client.lobby == null) {
client.send(`[info] not in a lobby`);
} else {
}));
break;
case "lobby_create":
client.lobbyCreate(msg.data as Partial<Lobby>);
break;
case "lobby_leave":
client.lobbyLeave();
break;
case "request_lobby_list":
client.lobbyList();
break;
case "request_peer_list":
client.peerList();
}
}
if (msg.startsWith("update_display_name:")) {
const displayName = msg.substr("update_display_name:".indexOf(":") + 1);
client.name = displayName;
}
if (msg.startsWith("lobby_join:")) {
const id = msg.substr(11);
const lobby = allLobbies.get(id);
if (lobby) {
client.lobbyJoin(lobby);
// client.peerList();
} else client.send(`[info] could not find lobby ${id}`);
}
if (msg.startsWith("json:")) {
const data: ClientMessage = JSON.parse(msg.substr(5));
if (data.type === "init") {
if (data.data.name) {
client.name = data.data.name.toString();
}
client.send(buildMessage("init", { id: client.id, name: client.name }));
} else if (data.type === "update_lobby") {
if (client.lobby) {
client.lobby.update(client, data["data"]);
} else {
client.send("[info] cannot update lobby (you're not even in one!)");
}
} else if (data.type === "lobby_create") {
client.lobbyCreate({ name: data.data?.name?.toString() });
if (data.data.name) {
client.name = data.data.name.toString();
}
client.send(buildMessage("init", { id: client.id, name: client.name }));
} else if (["candidate", "answer", "offer"].includes(data.type)) {
console.log("Received WebRTC Negotiation Message...");
if (typeof data.data === "object") {
const subdata = data.data;
if (typeof subdata["peerId"] === "number") {
const destPeerId: number = subdata["peerId"];
for (const iClient of client.lobby?.clients.values() || []) {
if (iClient.peerId == destPeerId) {
const payload = Object.assign({}, data);
const srcPeerId = client.peerId;
payload.data.peerId = srcPeerId;
console.log(
`Forwarding WebRTC Negotiation Message from peer ${srcPeerId} to peer ${destPeerId}...`,
);
iClient.send(payload);
break;
}
}
break;
case "update_display_name":
if (msg.data) client.name = msg.data.toString();
break;
case "lobby_join":
if (msg.data) {
const lobby = allLobbies.get(msg.data.toString());
if (lobby) {
client.lobbyJoin(lobby);
} else {
client.send(buildMessage("info", `count not find lobby ${msg.data}`));
}
}
break;
case "update_lobby":
if (client.lobby) {
if (typeof (msg.data) === "object") {
client.lobby.update(client, msg.data as Partial<Lobby>);
}
} else {
client.send(
buildMessage(
"info",
"failed to update lobby info: you are not in a lobby",
),
);
}
break;
case "candidate":
/* falls through */
case "answer":
/* falls through */
case "offer": {
if (!client.lobby) return;
console.log(`Received WebRTC Negotiation Message (type: ${msg.type})...`);
if (typeof msg.data !== "object") return;
const webrtcMessage = (msg.data as { peerId?: number });
if (!webrtcMessage.peerId || typeof webrtcMessage.peerId !== "number") {
return;
}
const destClient = client.lobby.getPeer(webrtcMessage.peerId);
if (!destClient || !destClient.peerId) return;
webrtcMessage.peerId = destClient.peerId;
destClient.send(buildMessage(msg.type, webrtcMessage));
break;
}
default:
console.debug("Unknown message: ", msg);
break;
}
}
@ -428,13 +444,10 @@ 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);
(async () => {
const server = Deno.serveHttp(conn);
for await (const { respondWith, request } of server) {
// console.debug("HTTP Request Received", request);
try {
// console.warn(JSON.stringify([allClients, allLobbies]));
const { socket, response } = Deno.upgradeWebSocket(request);
const client = new Client(socket);
socket.onmessage = (ev) => onMessage(client, ev);