extends Node const DISPLAY_NAME_FILE = "user://display_name.txt" const DEFAULT_DISPLAY_NAME = "UnnamedPlayer" const DEFAULT_SIGNALLER_URL = "ws://localhost:8888" # const DEFAULT_SIGNALLER_URL = "wss://webrtc-signaller.deno.dev:443" const DEFAULT_ICE_SERVERS = [ # first element in this array is for STUN { "urls": ["stun:localhost:3478", "stun:stun.l.google.com:19302"] }, # { "urls": ["stun:stun.l.google.com:19302"] }, # just google # { "urls": ["stun:localhost:3478"] }, # just localhost # {} # TURN servers ] signal lobby_data(lobbies) signal lobby_begin_join(uuid) signal lobby_joined(lobby_and_peer) signal lobby_left(uuid) signal lobby_delete(uuid) signal peer_data(peers) signal peer_created(peer) signal peer_left(id) signal peer_init(id) signal candidate_received(cand) signal offer_received(data) signal answer_received(data) # webrtc signal webrtc_peer_connected(id) signal webrtc_peer_disconnected(id) signal webrtc_connection_succeeded() # websocket signal signaller_connected signal signaller_connection_failure signal signaller_disconnected onready var webrtc = null onready var websocket = WebSocketClient.new() onready var display_name = DEFAULT_DISPLAY_NAME setget set_display_name onready var current_lobby = null onready var lobbies = {} onready var peers = {} onready var signaller_url = DEFAULT_SIGNALLER_URL func _init(): pass func _ready(): display_name = _load_display_name() websocket.connect("connection_established", self, "_signaller_connected") websocket.connect("data_received", self, "_signaller_data") websocket.connect("server_close_request", self, "_signaller_close_request") websocket.connect("connection_closed", self, "_signaller_closed") websocket.connect("connection_error", self, "_signaller_error", [false]) websocket.connect("connection_failed", self, "_signaller_error", [false]) func is_signaller_connected(): return websocket.get_connection_status() in [ websocket.CONNECTION_CONNECTED, websocket.CONNECTION_CONNECTED, ] func is_in_lobby(): return current_lobby != null func connect_to_signaller(url = signaller_url): if url == signaller_url and is_signaller_connected(): return signaller_url = url close() print("Attempting to connect to WebSocket signalling server at %s" % signaller_url) var result = websocket.connect_to_url(signaller_url) if result != OK: print("Failed to connect to WebSocket signalling server at %s: %s" % [signaller_url, result]) func singleplayer(): close() webrtc = WebRTCMultiplayer.new() webrtc.initialize(1, false) get_tree().network_peer = webrtc peers[1] = { connected = false, ready = false, name = display_name, } func close(): if webrtc != null: webrtc.close() websocket.disconnect_from_host() get_tree().network_peer = null func set_display_name(new_display_name: String): display_name = new_display_name _send("update_display_name:%s" % display_name) func request_lobby_list(): return _send("request_lobby_list") func join_lobby(id: String): emit_signal("lobby_begin_join", id) call_deferred("_send", "lobby_join:%s" % id) func leave_lobby(): return _send("lobby_leave") func get_lobby_name(): return current_lobby.name if current_lobby.has("name") else null func create_lobby(): _send("lobby_create") func is_host(): return is_signaller_connected() and is_in_lobby() and get_tree().get_network_unique_id() == 1 func set_lobby_name(s: String): if is_host(): _send_json({"name": s}, "update_lobby") func lock_lobby(): if is_host(): _send_json({"locked": true}, "update_lobby") func set_lobby_max_players(n: int): if is_host(): _send_json({"maxPlayers": n}, "update_lobby") func request_peer_list(): _send("request_peer_list") func send_candidate(id, mid, index, sdp) -> int: return _send_json({"id": id, "mid": mid, "index": index, "sdp": sdp}, "candidate") func send_offer(id, offer) -> int: return _send_json({"id": id, "offer": offer }, "offer") func send_answer(id, answer) -> int: return _send_json({"id": id, "answer": answer }, "answer") func _webrtc_peer_connected(id): peers[id].connected = true emit_signal("webrtc_peer_connected", id) func _webrtc_peer_disconnected(id): peers[id].connected = false emit_signal("webrtc_peer_disconnected", id) func _webrtc_connection_succeeded(): emit_signal("webrtc_connection_succeeded") func _signaller_error(err): print("WebSocket error: %s" % err) emit_signal("signaller_connection_failure") _signaller_closed(err) func _signaller_closed(code): print("WebSocket closed: %s: " % code) emit_signal("signaller_disconnected") func _signaller_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 _signaller_connected(protocol = ""): print("Signaller connected via WebSocket using protocol %s" % protocol) websocket.get_peer(1).set_write_mode(WebSocketPeer.WRITE_MODE_TEXT) emit_signal("signaller_connected") _send_json({name = display_name}, "init") func _process(_delta: float): if webrtc: webrtc.poll() while webrtc.get_available_packet_count() > 0: print("WebRTC Packet: %s" % webrtc.get_packet().get_string_from_utf8()) var status: int = websocket.get_connection_status() if status in [WebSocketClient.CONNECTION_CONNECTED, WebSocketClient.CONNECTION_CONNECTING]: websocket.poll() func _input(event): if event is InputEventKey: if event.pressed and event.scancode == KEY_D: print("## DEBUG NET INFO") for p in get_tree().get_network_connected_peers(): var d = webrtc.get_peer(p) print("get_peer(%s): %s" % [p, d]) for c in d.channels: print("Channel: %s" % c) if c: print("Label: %s, Protocol: %s, Negotiated: %s" % [c.get_label(), c.get_protocol(), "true" if c.is_negotiated() else "false"]) else: print("Channel is null?") func _signaller_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: _process_signaller_message(data.result) var _result = websocket.get_peer(1).put_packet("".to_utf8()) else: var d = msg.split(":", false, 2) _process_signaller_message({type = d[0], data = d[1]}) func _init_webrtc_peer(data): webrtc = WebRTCMultiplayer.new() webrtc.connect("connection_succeeded", self, "_webrtc_connection_succeeded") webrtc.connect("peer_connected", self, "_webrtc_peer_connected") webrtc.connect("peer_disconnected", self, "_webrtc_peer_disconnected") webrtc.initialize(int(data), true) get_tree().network_peer = webrtc func _deinit_webrtc_peer(): webrtc.close() get_tree().network_peer = null webrtc = null func _process_signaller_message(data: Dictionary): match data["type"]: # "init": "init_peer": _init_webrtc_peer(data.data) "lobby_data": emit_signal("lobby_data", data.data) "lobby_delete": emit_signal("lobby_delete", data.uuid) "lobby_joined": _lobby_joined(data) "lobby_left": _lobby_left(data.uuid) "peer_data": _signaller_peer_data([data] if data.has("id") else data.data) "peer_left": _signaller_peer_left(data.id) "candidate": _webrtc_candidate_received(data) "offer": _webrtc_offer_received(data) "answer": _webrtc_answer_received(data) "ping": _send("pong") _: print("Unhandled Message - Data: %s" % JSON.print(data)) func _lobby_joined(lobby_data): current_lobby = lobby_data peers[int(lobby_data.id)] = { connected = true, ready = int(lobby_data.id) == 1, name = display_name, id = int(lobby_data.id), } print("Lobby Joined: %s" % lobby_data) emit_signal("lobby_joined", lobby_data) func _lobby_left(uuid): current_lobby = null peers.erase(get_tree().get_network_unique_id()) print("Lobby Left: %s" % uuid) emit_signal("lobby_left", uuid) _deinit_webrtc_peer() func _create_peer(data: Dictionary): var id = data.id if id == webrtc.get_unique_id(): return var peer: WebRTCPeerConnection = WebRTCPeerConnection.new() print("Creating WebRTC Peer: %s" % [data]) peer.connect("session_description_created", self, "_webrtc_offer_created", [id]) peer.connect("ice_candidate_created", self, "_new_ice_candidate", [id]) peer.initialize({"iceServers": DEFAULT_ICE_SERVERS}) webrtc.add_peer(peer, int(id)) # this guarantees only one peer sends the offer and that offers are never # sent to ourselves? if int(id) > webrtc.get_unique_id(): peer.create_offer() peers[int(id)] = { connected = false, ready = false, name = data.name if data.has(name) else DEFAULT_DISPLAY_NAME, } emit_signal("peer_created", data) func _delete_peer(id): if webrtc.has_peer(id): webrtc.remove_peer(id) if peers.has(id): peers.erase(id) func _webrtc_offer_created(type, data, id): print("WebRTC %s created for peer %s" % [type, id]) if not webrtc.has_peer(int(id)): return print("WebRTC local description set for peer %s" % id) webrtc.get_peer(id).connection.set_local_description(type, data) print("--> Local Description: %s" % JSON.print(data)) if type == "offer": send_offer(id, data) else: send_answer(id, data) func _new_ice_candidate(mid, index, sdp, id): print("New ICE candidate for peer %s" % id) send_candidate(id, mid, index, sdp) func _signaller_peer_left(id): _delete_peer(id) func _signaller_peer_data(peers): for peer in peers: if peer.has("id") and not webrtc.has_peer(int(peer.id)): _create_peer(peer) emit_signal("peer_data", peers) func _webrtc_offer_received(data): if webrtc.has_peer(int(data.id)): print("Setting offer remote description for peer %s" % data.id) print("--> Offer: %s" % JSON.print(data.offer)) webrtc.get_peer(data.id).connection.set_remote_description("offer", data.offer) else: print("Received an offer for a peer with ID %s that hasn't been added" % data.id) func _webrtc_answer_received(data): if webrtc.has_peer(data.id): print("Setting answer remote description for peer %s" % data.id) print("--> Answer: %s" % JSON.print(data.answer)) webrtc.get_peer(data.id).connection.set_remote_description("answer", data.answer) func _webrtc_candidate_received(data): if webrtc.has_peer(data.id): print("Adding ice candidate for peer %s" % data.id) print("--> Candidate: %s" % JSON.print(data)) webrtc.get_peer(data.id).connection.add_ice_candidate(data.mid, data.index, data.sdp) else: print("Received candidate for non-existant peer %s" % data.id) 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 _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(s: String): return websocket.get_peer(1).put_packet(s.to_utf8()) func _send_json(data: Dictionary, type=null): if type != null: data["type"] = type _send("json:%s" % JSON.print(data))