From a0ce4a0d6d074d95347adbd84b68f466a4feed7a Mon Sep 17 00:00:00 2001 From: FreehuntX Date: Fri, 29 Dec 2023 23:31:10 +0100 Subject: [PATCH] fix: Fix asserts causing indebuggable errors --- README.md | 8 +- addons/matcha/MatchaPeer.gd | 158 ++++++++++++++------------- addons/matcha/MatchaRoom.gd | 129 ++++++++++++---------- addons/matcha/lib/WebSocketClient.gd | 31 ++++-- addons/matcha/nostr/Secp256k1.gd | 5 +- examples/bobble/bobble.gd | 17 +-- project.godot | 4 + 7 files changed, 196 insertions(+), 156 deletions(-) diff --git a/README.md b/README.md index 1d2c66a..d3ee78c 100644 --- a/README.md +++ b/README.md @@ -64,13 +64,13 @@ func _init(): # Changelog ### 29. Dec. 2023 +- Replaced asserts with push_error + Error return value + - This fixed some nasty invisible bugs - Added example for server/client implementation - Improved MatchaPeer class - - Made it extend from WebRTCPeerConnectionExtension - - This allows you to use it like an WebRTCPeerConnection + - Made it extend from WebRTCPeerConnection - Improved MatchaRoom class - - Made it extend from MultiplayerPeerExtension - - This allows you to use it like an MultiplayerPeer + - Made it extend from MultiplayerPeer - Added peer_joined/peer_left signals for direct access to the peer - Changed naming from info_hash to room_id - Added client/server/mesh functionality diff --git a/addons/matcha/MatchaPeer.gd b/addons/matcha/MatchaPeer.gd index 4a7167f..f628cb4 100644 --- a/addons/matcha/MatchaPeer.gd +++ b/addons/matcha/MatchaPeer.gd @@ -1,6 +1,6 @@ # TODO: DOCUMENT, DOCUMENT, DOCUMENT! -class_name MatchaPeer extends WebRTCPeerConnectionExtension +class_name MatchaPeer extends WebRTCPeerConnection const Utils := preload("./lib/Utils.gd") enum State { NEW, GATHERING, CONNECTING, CONNECTED, CLOSED } @@ -15,7 +15,6 @@ signal sdp_created(sdp: String) # Members var _announced := false -var _peer := WebRTCPeerConnection.new() var _peer_id: String var _offer_id: String var _state := State.NEW @@ -45,117 +44,126 @@ var peer_id: static func create_offer_peer(offer_id := Utils.gen_id()) -> MatchaPeer: return MatchaPeer.new("offer", offer_id) -static func create_answer_peer(offer_id: String, offer_sdp: String) -> MatchaPeer: - return MatchaPeer.new("answer", offer_id, offer_sdp) +static func create_answer_peer(offer_id: String, remote_sdp: String) -> MatchaPeer: + return MatchaPeer.new("answer", offer_id, remote_sdp) # Constructor -func _init(type: String, offer_id: String, sdp=""): - assert(type == "offer" or type == "answer", "Invalid type: %s" % [type]) +func _init(type: String, offer_id: String, remote_sdp=""): _type = type _offer_id = offer_id + _remote_sdp = remote_sdp - _peer.session_description_created.connect(self._on_session_description_created) - _peer.ice_candidate_created.connect(self._on_ice_candidate_created) - _peer.data_channel_received.connect(self._on_data_channel_received) - - initialize({"iceServers":[{"urls":["stun:stun.l.google.com:19302"]}]}) - - # Initialize deferred so the peer can be added to multiplayer first - Engine.get_main_loop().create_timer(0).timeout.connect(func(): - _state = State.GATHERING - if type == "offer": - create_offer() - elif type == "answer": - assert(sdp != "", "Missing sdp") - _remote_sdp = sdp - set_remote_description("offer", sdp) + session_description_created.connect(self._on_session_description_created) + ice_candidate_created.connect(self._on_ice_candidate_created) - Engine.get_main_loop().process_frame.connect(self.poll) # Start the poll loop - ) + var err := initialize({"iceServers":[{"urls":["stun:stun.l.google.com:19302"]}]}) + if err != OK: + push_error("Initializing failed") + _state = State.CLOSED # Public methods +func start() -> Error: + if _state != State.NEW: + push_error("Peer state is not new") + return Error.ERR_ALREADY_IN_USE + + _state = State.GATHERING + + if _type == "offer": + var err := create_offer() + if err != OK: + push_error("Creating offer failed") + return err + elif _type == "answer": + if _remote_sdp == "": + push_error("Missing sdp") + return Error.ERR_INVALID_DATA + + var err := set_remote_description("offer", _remote_sdp) + if err != OK: + push_error("Creating answer failed") + return err + else: + push_error("Unknown type: ", _type) + return Error.ERR_INVALID_DATA + + Engine.get_main_loop().process_frame.connect(self.__poll) # Start the poll loop + return Error.OK + func set_peer_id(new_peer_id: String) -> void: _peer_id = new_peer_id func set_offer_id(new_offer_id: String) -> void: _offer_id = new_offer_id -func set_answer(remote_sdp: String): - assert(_type == "offer", "The peer is not an offer") - assert(not _answered, "The offer was already answered") +func set_answer(remote_sdp: String) -> Error: + if _type != "offer": + push_error("The peer is not an offer") + return Error.ERR_INVALID_DATA + if _answered: + push_error("The offer was already answered") + return Error.ERR_ALREADY_IN_USE + _answered = true _remote_sdp = remote_sdp - set_remote_description("answer", remote_sdp) + return set_remote_description("answer", remote_sdp) -func mark_as_announced(): - assert(_announced == false, "Already announced") +func mark_as_announced() -> Error: + if _announced: + push_error("The offer was already answered") + return Error.ERR_ALREADY_IN_USE + _announced = true + return Error.OK # Private methods -func _close(): # Virtual - if _state == State.CLOSED: - return - - _peer.close() - - if _state == State.CONNECTING: - connecting_failed.emit() - elif _state == State.CONNECTED: - disconnected.emit() - - _state = State.CLOSED - closed.emit() +func __poll(): + if _state == State.NEW or _state == State.CLOSED: return + poll() -func _poll(): # Virtual if _state == State.GATHERING: - get_gathering_state() - elif _state == State.CONNECTING or _state == State.CONNECTED: - get_connection_state() - return _peer.poll() + var gathering_state := get_gathering_state() + if gathering_state != WebRTCPeerConnection.GATHERING_STATE_COMPLETE: + return -func _get_gathering_state(): # Virtual - var gathering_state := _peer.get_gathering_state() - - if _state == State.GATHERING and gathering_state == WebRTCPeerConnection.GATHERING_STATE_COMPLETE: _state = State.CONNECTING sdp_created.emit(_local_sdp) connecting.emit() - return gathering_state - -func _get_connection_state(): # Virtual - var connection_state := _peer.get_connection_state() - - if _state == State.CONNECTING and connection_state != WebRTCPeerConnection.STATE_CONNECTING: + var connection_state := get_connection_state() + if _state == State.CONNECTING: + if connection_state == WebRTCPeerConnection.STATE_CONNECTING: + return if connection_state != WebRTCPeerConnection.STATE_CONNECTED: - close() - else: - _state = State.CONNECTED - connected.emit() + __close() + return + + _state = State.CONNECTED + connected.emit() if _state == State.CONNECTED: if connection_state != WebRTCPeerConnection.STATE_CONNECTED: - close() + __close() + return + +func __close(): + if _state == State.CLOSED: + return - return connection_state + close() -func _add_ice_candidate(p_sdp_mid_name: String, p_sdp_mline_index: int, p_sdp_name: String): return _peer.add_ice_candidate(p_sdp_mid_name, p_sdp_mline_index, p_sdp_name) -func _create_data_channel(p_label: String, p_config: Dictionary): return _peer.create_data_channel(p_label, p_config) -func _create_offer(): return _peer.create_offer() -func _get_signaling_state(): return _peer.get_signaling_state() -func _initialize(p_config: Dictionary): return _peer.initialize(p_config) -func _set_local_description(p_type: String, p_sdp: String): return _peer.set_local_description(p_type, p_sdp) -func _set_remote_description(p_type: String, p_sdp: String): return _peer.set_remote_description(p_type, p_sdp) + if _state == State.CONNECTING: + connecting_failed.emit() + elif _state == State.CONNECTED: + disconnected.emit() + + _state = State.CLOSED + closed.emit() # Callbacks func _on_session_description_created(type: String, sdp: String): _local_sdp = sdp - session_description_created.emit(type, sdp) set_local_description(type, sdp) func _on_ice_candidate_created(media: String, index: int, name: String): _local_sdp += "a=%s\r\n" % [name] - ice_candidate_created.emit(media, index, name) - -func _on_data_channel_received(channel: WebRTCDataChannel): - data_channel_received.emit(channel) diff --git a/addons/matcha/MatchaRoom.gd b/addons/matcha/MatchaRoom.gd index 6c0bf79..4b0f5d5 100644 --- a/addons/matcha/MatchaRoom.gd +++ b/addons/matcha/MatchaRoom.gd @@ -1,16 +1,20 @@ # TODO: DOCUMENT, DOCUMENT, DOCUMENT! -class_name MatchaRoom extends MultiplayerPeerExtension -const Utils := preload("res://addons/matcha/lib/Utils.gd") +class_name MatchaRoom extends WebRTCMultiplayerPeer +const Utils := preload("./lib/Utils.gd") const TrackerClient := preload("./tracker/TrackerClient.gd") const MatchaPeer := preload("./MatchaPeer.gd") +# Constants +enum State { NEW, STARTED } + # Signals signal peer_joined(rpc_id: int, peer: MatchaPeer) # Emitted when a peer joined the room signal peer_left(rpc_id: int, peer: MatchaPeer) # Emitted when a peer left the room # Members -var _mp := WebRTCMultiplayerPeer.new() # Our internal reference to the multiplayer peer +var _state := State.NEW # Internal state +var _tracker_urls := [] # A list of tracker urls var _tracker_clients: Array[TrackerClient] = [] # A list of tracker clients we use to share/get offers/answers var _room_id: String # An unique identifier var _peer_id := Utils.gen_id() @@ -29,7 +33,7 @@ var type: var room_id: get: return _room_id var _peers: - get: return _mp.get_peers().values().map(func(v): return v.connection) + get: return get_peers().values().map(func(v): return v.connection) # Static methods static func create_mesh_room(options:={}) -> MatchaRoom: @@ -46,42 +50,65 @@ static func create_client_room(room_id: String, options:={}) -> MatchaRoom: return MatchaRoom.new(options) # Constructor -func _init(options:={}) -> void: +func _init(options:={}): if not "pool_size" in options: options.pool_size = _pool_size if not "offer_timeout" in options: options.offer_timeout = _offer_timeout if not "identifier" in options: options.identifier = "com.matcha.default" if not "tracker_urls" in options: options.tracker_urls = ["wss://tracker.webtorrent.dev"] if not "room_id" in options: options.room_id = options.identifier.sha1_text().substr(0, 20) if not "type" in options: options.type = "mesh" + if not "autostart" in options: options.autostart = true + _tracker_urls = options.tracker_urls _pool_size = options.pool_size _offer_timeout = options.offer_timeout _room_id = options.room_id _type = options.type - - _mp.peer_connected.connect(self._on_peer_connected) - _mp.peer_disconnected.connect(self._on_peer_disconnected) + + peer_connected.connect(self._on_peer_connected) + peer_disconnected.connect(self._on_peer_disconnected) + + if options.autostart: + start.call() + +# Public methods +func start() -> Error: + if _state != State.NEW: + push_error("Already started") + return Error.ERR_ALREADY_IN_USE + + _state = State.STARTED if _type == "mesh": - assert(_mp.create_mesh(generate_unique_id()) == OK, "Creating mesh failed") + var err := create_mesh(generate_unique_id()) + if err != OK: + push_error("Creating mesh failed") + return err elif _type == "client": - assert(_mp.create_client(generate_unique_id()) == OK, "Creating client failed") + var err := create_client(generate_unique_id()) + if err != OK: + push_error("Creating client failed") + return err elif _type == "server": _room_id = _peer_id # Our room_id should be our peer_id to identify ourself as the server - assert(_mp.create_server() == OK, "Creating server failed") + var err := create_server() + if err != OK: + push_error("Creating server failed") + return err else: - assert(false, "Invalid type") + push_error("Invalid type") + return Error.ERR_INVALID_DATA # Create the tracker_clients based on the urls - for tracker_url in options.tracker_urls: + for tracker_url in _tracker_urls: var tracker_client := TrackerClient.new(tracker_url, _peer_id) tracker_client.got_offer.connect(self._on_got_offer.bind(tracker_client)) tracker_client.got_answer.connect(self._on_got_answer.bind(tracker_client)) tracker_client.failure.connect(self._on_failure.bind(tracker_client)) _tracker_clients.append(tracker_client) - Engine.get_main_loop().process_frame.connect(self.poll) + Engine.get_main_loop().process_frame.connect(self.__poll) + return Error.OK -# Public methods func find_peers(filter:={}) -> Array[MatchaPeer]: var result: Array[MatchaPeer] = [] for peer in _peers: @@ -101,8 +128,8 @@ func find_peer(filter:={}, allow_multiple_results:=false) -> MatchaPeer: return matches[0] # Private methods -func _poll(): # Virtual - _mp.poll() +func __poll(): + poll() _create_offers() _handle_offers_announcment() @@ -112,18 +139,22 @@ func _remove_unanswered_offer(offer_id: String) -> void: offer.close() func _create_offer() -> void: - if _type == "client" and _mp.has_peer(1): return # We already created the host offer. So lets ignore the offer creating + if _type == "client" and has_peer(1): return # We already created the host offer. So lets ignore the offer creating - var offer_peer = MatchaPeer.create_offer_peer() - _mp.add_peer(offer_peer, 1 if _type == "client" else generate_unique_id()) + var offer_peer := MatchaPeer.create_offer_peer() + var offer_rpc_id := 1 if _type == "client" else generate_unique_id() + add_peer(offer_peer, offer_rpc_id) - # Cleanup when the offer was not answered for long time - Engine.get_main_loop().create_timer(_offer_timeout).timeout.connect(self._remove_unanswered_offer.bind(offer_peer.offer_id)) + if offer_peer.start() == OK: + # Cleanup when the offer was not answered for long time + Engine.get_main_loop().create_timer(_offer_timeout).timeout.connect(self._remove_unanswered_offer.bind(offer_peer.offer_id)) + else: + remove_peer(offer_rpc_id) func _create_offers() -> void: var unanswered_offers := find_peers({ "type": "offer", "answered": false }) if unanswered_offers.size() > 0: return # There are ongoing offers. Dont refresh the pool. - if _type == "client" and _mp.has_peer(1): return # If we are already connected in client mode dont create further offers + if _type == "client" and has_peer(1): return # If we are already connected in client mode dont create further offers # Create as many offers as the pool_size for i in range(_pool_size): @@ -139,7 +170,9 @@ func _handle_offers_announcment(): if _type == "client": # As client lets announce the host peer multiple times. Since we cannot have multiple peers with id 1 setup - assert(unannounced_offers.size() == 1, "In client mode you should have just 1 offer") + if unannounced_offers.size() != 1: + push_error("In client mode you should have just 1 offer") + return for i in range(_pool_size): announce_offers.append({ "offer_id": Utils.gen_id(), "offer": { "type": "offer", "sdp": offer_peer.local_sdp } }) else: @@ -159,11 +192,15 @@ func _on_got_offer(offer: TrackerClient.Response, tracker_client: TrackerClient) if find_peer({ "peer_id": offer.peer_id }) != null: return # Ignore if the peer is already known if _type == "client" and offer.peer_id != room_id: return # Ignore offers from others than host (in client mode) - var peer := MatchaPeer.create_answer_peer(offer.offer_id, offer.sdp) - peer.set_peer_id(offer.peer_id) + var answer_peer := MatchaPeer.create_answer_peer(offer.offer_id, offer.sdp) + var answer_rpc_id := 1 if _type == "client" else generate_unique_id() + answer_peer.set_peer_id(offer.peer_id) + + answer_peer.sdp_created.connect(self._send_answer_sdp.bind(answer_peer, tracker_client)) + add_peer(answer_peer, answer_rpc_id) - peer.sdp_created.connect(self._send_answer_sdp.bind(peer, tracker_client)) - _mp.add_peer(peer, 1 if _type == "client" else generate_unique_id()) + if answer_peer.start() != OK: + remove_peer(answer_rpc_id) func _on_got_answer(answer: TrackerClient.Response, tracker_client: TrackerClient) -> void: if answer.info_hash != _room_id: return @@ -171,8 +208,8 @@ func _on_got_answer(answer: TrackerClient.Response, tracker_client: TrackerClien var offer_peer: MatchaPeer if _type == "client": - if _mp.has_peer(1): - offer_peer = _mp.get_peer(1).connection + if has_peer(1): + offer_peer = get_peer(1).connection offer_peer.set_offer_id(answer.offer_id) # Fix the offer_id since we gave the server alot of offers to choose from else: offer_peer = find_peer({ "offer_id": answer.offer_id }) @@ -184,38 +221,12 @@ func _on_got_answer(answer: TrackerClient.Response, tracker_client: TrackerClien func _on_failure(reason: String, tracker_client: TrackerClient) -> void: print("Tracker failure: ", reason, ", Tracker: ", tracker_client.tracker_url) -func _on_peer_connected(id: int) -> void: - peer_connected.emit(id) - - var peer: MatchaPeer = _mp.get_peer(id).connection +func _on_peer_connected(id: int): + var peer: MatchaPeer = get_peer(id).connection _connected_peers[id] = peer peer_joined.emit(id, peer) -func _on_peer_disconnected(id: int) -> void: - peer_disconnected.emit(id) - +func _on_peer_disconnected(id: int): var peer: MatchaPeer = _connected_peers[id] _connected_peers.erase(id) peer_left.emit(id, peer) - -# Virtuals -func _get_unique_id(): return _mp.get_unique_id() -func _get_available_packet_count(): return _mp.get_available_packet_count() -func _get_connection_status(): return _mp.get_connection_status() -func _close(): _mp.close() -func _disconnect_peer(p_peer, p_force): _mp.disconnect_peer(p_peer, p_force) -func _get_max_packet_size(): return _mp.get_max_packet_size() -func _get_packet_channel(): return _mp.get_packet_channel() -func _get_packet_mode(): return _mp.get_packet_mode() -func _get_packet_peer(): return _mp.get_packet_peer() -func _get_packet_script(): return _mp.get_packet() -func _get_transfer_channel(): return _mp.get_transfer_channel() -func _get_transfer_mode(): return _mp.get_transfer_mode() -func _is_refusing_new_connections(): return _mp.is_refusing_new_connections() -func _is_server(): return _mp.is_server() -func _is_server_relay_supported(): return _mp.is_server_relay_supported() -func _put_packet_script(p_buffer): return _mp.put_packet(p_buffer) -func _set_refuse_new_connections(p_enable): _mp.set_refuse_new_connections(p_enable) -func _set_target_peer(p_peer): _mp.set_target_peer(p_peer) -func _set_transfer_channel(p_channel): _mp.set_transfer_channel(p_channel) -func _set_transfer_mode(p_mode): _mp.set_transfer_mode(p_mode) diff --git a/addons/matcha/lib/WebSocketClient.gd b/addons/matcha/lib/WebSocketClient.gd index bf5120e..eb9b40f 100644 --- a/addons/matcha/lib/WebSocketClient.gd +++ b/addons/matcha/lib/WebSocketClient.gd @@ -43,21 +43,34 @@ func _init(url: String, options:={}) -> void: Engine.get_main_loop().process_frame.connect(self._start, CONNECT_ONE_SHOT) # Public methods -func send(data, mode=_options.mode) -> void: - assert(is_connected, "NOT_CONNECTED") +func send(data, mode=_options.mode) -> Error: + if not is_connected: + push_error("NOT_CONNECTED") + return Error.ERR_CONNECTION_ERROR if mode == Mode.BYTES and typeof(data) != TYPE_PACKED_BYTE_ARRAY: - if typeof(data) == TYPE_STRING: data = data.to_utf8_buffer() - else: assert(false, "UNKNOWN_TYPE") + if typeof(data) == TYPE_STRING: + data = data.to_utf8_buffer() + else: + push_error("UNKOWN_TYPE") + return Error.ERR_INVALID_DATA elif mode == Mode.TEXT and typeof(data) != TYPE_STRING: - if typeof(data) == TYPE_PACKED_BYTE_ARRAY: data = data.get_string_from_utf8() - else: assert(false, "UNKNOWN_TYPE") + if typeof(data) == TYPE_PACKED_BYTE_ARRAY: + data = data.get_string_from_utf8() + else: + push_error("UNKNOWN_TYPE") + return Error.ERR_INVALID_DATA elif mode == Mode.JSON: data = JSON.stringify(data) - if data == null: assert(false, "INVALID_JSON") + if data == null: + push_error("INVALID_JSON") + return Error.ERR_INVALID_DATA + + if typeof(data) != TYPE_STRING: + push_error("INVALID_DATA") + return Error.ERR_INVALID_DATA - assert(typeof(data) == TYPE_STRING, "INVALID_DATA") - _socket.send_text(data) + return _socket.send_text(data) func close(was_error=false) -> void: if _socket != null: diff --git a/addons/matcha/nostr/Secp256k1.gd b/addons/matcha/nostr/Secp256k1.gd index 010bfb1..d99bb4d 100644 --- a/addons/matcha/nostr/Secp256k1.gd +++ b/addons/matcha/nostr/Secp256k1.gd @@ -5,7 +5,10 @@ class_name Secp256k1 extends RefCounted func uint256(x, base:=-1): - assert(base == -1, "Base not implemented!") + if base != -1: + push_error("Base not implemented!") + return + return Big.new(x) func _init(): diff --git a/examples/bobble/bobble.gd b/examples/bobble/bobble.gd index c7d89f3..dbb820b 100644 --- a/examples/bobble/bobble.gd +++ b/examples/bobble/bobble.gd @@ -7,27 +7,28 @@ var local_player: if not $Players.has_node(matcha_room.peer_id): return null return $Players.get_node(matcha_room.peer_id) -func _ready(): +func _enter_tree(): multiplayer.multiplayer_peer = matcha_room - _join.rpc(matcha_room.peer_id) - matcha_room.peer_joined.connect(func(id: int, _peer: MatchaPeer): - _join.rpc_id(id, matcha_room.peer_id) # Tell the new peer about us +func _ready(): + _create_player(matcha_room.peer_id, multiplayer.get_unique_id()) + + matcha_room.peer_joined.connect(func(id: int, peer: MatchaPeer): + _create_player(peer.peer_id, id) ) matcha_room.peer_left.connect(func(_id: int, peer: MatchaPeer): if $Players.has_node(peer.peer_id): # Remove the player if it exists $Players.remove_child($Players.get_node(peer.peer_id)) ) -@rpc("any_peer", "call_local") -func _join(peer_id: String): +func _create_player(peer_id: String, authority_id: int): if $Players.has_node(peer_id): return # That peer is already known - + var node := PlayerComponent.instantiate() node.name = peer_id # The node must have the same name for every person. Otherwise syncing it will fail because path mismatch node.position = Vector2(100, 100) $Players.add_child(node) - node.set_multiplayer_authority(multiplayer.get_remote_sender_id()) + node.set_multiplayer_authority(authority_id) func _on_line_edit_text_submitted(new_text): $UI/LineEdit.text = "" diff --git a/project.godot b/project.godot index 67c990a..3b5cd2b 100644 --- a/project.godot +++ b/project.godot @@ -15,6 +15,10 @@ run/main_scene="res://root.tscn" config/features=PackedStringArray("4.2", "GL Compatibility") config/icon="res://icon.svg" +[audio] + +driver/mix_rate.web=44100 + [display] window/size/viewport_width=1200