315 lines
10 KiB
GDScript
315 lines
10 KiB
GDScript
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
|
|
_create_peer({id = 1})
|
|
|
|
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):
|
|
var status: int = websocket.get_connection_status()
|
|
if status in [WebSocketClient.CONNECTION_CONNECTED, WebSocketClient.CONNECTION_CONNECTING]:
|
|
websocket.poll()
|
|
|
|
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
|
|
print("Lobby Joined: %s" % lobby_data)
|
|
emit_signal("lobby_joined", lobby_data)
|
|
|
|
func _lobby_left(uuid):
|
|
current_lobby = null
|
|
print("Lobby Left: %s" % uuid)
|
|
emit_signal("lobby_left", uuid)
|
|
_deinit_webrtc_peer()
|
|
|
|
func _create_peer(data):
|
|
var id = data.id
|
|
var peer: WebRTCPeerConnection = WebRTCPeerConnection.new()
|
|
print("Creating WebRTC Peer %s" % [id])
|
|
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 = "",
|
|
}
|
|
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))
|