extends RefCounted class_name NakamaMultiplayerBridge enum MatchState { DISCONNECTED, JOINING, CONNECTED, SOCKET_CLOSED, } enum MetaMessageType { CLAIM_HOST, ASSIGN_PEER_ID, } # Read-only variables. var _nakama_socket: NakamaSocket var nakama_socket: NakamaSocket: get: return _nakama_socket set(_v): pass var _match_state: int = MatchState.DISCONNECTED var match_state: int: get: return _match_state set(_v): pass var _match_id := '' var match_id: String: get: return _match_id set(_v): pass var _multiplayer_peer: NakamaMultiplayerPeer = NakamaMultiplayerPeer.new() var multiplayer_peer: NakamaMultiplayerPeer: get: return _multiplayer_peer set(_v): pass # Configuration that can be set by the developer. var meta_op_code: int = 9001 var rpc_op_code: int = 9002 # Internal variables. var _my_session_id: String var _my_peer_id: int = 0 var _id_map := {} var _users := {} var _matchmaker_ticket := '' class User extends RefCounted: var presence var peer_id: int = 0 func _init(p_presence) -> void: presence = p_presence signal match_join_error (exception) signal match_joined () func _set_readonly(_value) -> void: pass func _init(p_nakama_socket: NakamaSocket) -> void: _nakama_socket = p_nakama_socket _nakama_socket.received_match_presence.connect(self._on_nakama_socket_received_match_presence) _nakama_socket.received_matchmaker_matched.connect(self._on_nakama_socket_received_matchmaker_matched) _nakama_socket.received_match_state.connect(self._on_nakama_socket_received_match_state) _nakama_socket.closed.connect(self._on_nakama_socket_closed) _multiplayer_peer.packet_generated.connect(self._on_multiplayer_peer_packet_generated) _multiplayer_peer.set_connection_status(MultiplayerPeer.CONNECTION_CONNECTING) func create_match() -> void: if _match_state != MatchState.DISCONNECTED: push_error("Cannot create match when state is %s" % MatchState.keys()[_match_state]) return _match_state = MatchState.JOINING multiplayer_peer.set_connection_status(MultiplayerPeer.CONNECTION_CONNECTING) var res = await _nakama_socket.create_match_async() if res.is_exception(): match_join_error.emit(res.get_exception()) leave() return _setup_match(res) _setup_host() func join_match(p_match_id: String) -> void: if _match_state != MatchState.DISCONNECTED: push_error("Cannot join match when state is %s" % MatchState.keys()[_match_state]) return _match_state = MatchState.JOINING multiplayer_peer.set_connection_status(MultiplayerPeer.CONNECTION_CONNECTING) var res = await _nakama_socket.join_match_async(p_match_id) if res.is_exception(): match_join_error.emit(res.get_exception()) leave() return _setup_match(res) func join_named_match(_match_name: String) -> void: if _match_state != MatchState.DISCONNECTED: push_error("Cannot join match when state is %s" % MatchState.keys()[_match_state]) return _match_state = MatchState.JOINING multiplayer_peer.set_connection_status(MultiplayerPeer.CONNECTION_CONNECTING) var res = await _nakama_socket.create_match_async(_match_name) if res.is_exception(): match_join_error.emit(res.get_exception()) leave() return _setup_match(res) if res.size == 0 or (res.size == 1 and res.presences.size() == 0): _setup_host() func start_matchmaking(ticket) -> void: if _match_state != MatchState.DISCONNECTED: push_error("Cannot start matchmaking when state is %s" % MatchState.keys()[_match_state]) return if ticket.is_exception(): push_error("Ticket with exception passed into start_matchmaking()") return _match_state = MatchState.JOINING multiplayer_peer.set_connection_status(MultiplayerPeer.CONNECTION_CONNECTING) _matchmaker_ticket = ticket.ticket func _on_nakama_socket_received_matchmaker_matched(matchmaker_matched) -> void: if _matchmaker_ticket != matchmaker_matched.ticket: return # Get a list of sorted session ids. var session_ids := [] for matchmaker_user in matchmaker_matched.users: session_ids.append(matchmaker_user.presence.session_id) session_ids.sort() var res = await _nakama_socket.join_matched_async(matchmaker_matched) if res.is_exception(): match_join_error.emit(res.get_exception()) leave() return _setup_match(res) # If our session is the first alphabetically, then we'll be the host. if _my_session_id == session_ids[0]: _setup_host() # Add all of the existing peers. for presence in res.presences: if presence.session_id != _my_session_id: _host_add_peer(presence) func _on_nakama_socket_closed() -> void: match_state = MatchState.SOCKET_CLOSED _cleanup() func get_user_presence_for_peer(peer_id: int) -> NakamaRTAPI.UserPresence: var session_id = _id_map.get(peer_id) if session_id == null: return null var user = _users.get(session_id) if user == null: return null return user.presence func leave() -> void: if _match_state == MatchState.DISCONNECTED: return _match_state = MatchState.DISCONNECTED if _match_id: await _nakama_socket.leave_match_async(_match_id) if _matchmaker_ticket: await _nakama_socket.remove_matchmaker_async(_matchmaker_ticket) _cleanup() func _cleanup() -> void: for peer_id in _id_map: multiplayer_peer.peer_disconnected.emit(peer_id) _match_id = '' _matchmaker_ticket = '' _my_session_id = '' _my_peer_id = 0 _id_map.clear() _users.clear() _multiplayer_peer.set_connection_status(MultiplayerPeer.CONNECTION_DISCONNECTED) func _setup_match(res) -> void: _match_id = res.match_id _my_session_id = res.self_user.session_id _users[_my_session_id] = User.new(res.self_user) for presence in res.presences: if not _users.has(presence.session_id): _users[presence.session_id] = User.new(presence) func _setup_host() -> void: # Claim id 1 and start the match. _my_peer_id = 1 _map_id_to_session(1, _my_session_id) _match_state = MatchState.CONNECTED _multiplayer_peer.initialize(_my_peer_id) match_joined.emit() func _generate_id(session_id: String) -> int: # Peer ids can only be positive 32-bit signed integers. var peer_id: int = session_id.hash() & 0x7FFFFFFF # If this peer id is already taken, try to find another. while peer_id <= 1 or _id_map.has(peer_id): peer_id += 1 if peer_id > 0x7FFFFFFF or peer_id <= 0: peer_id = randi() & 0x7FFFFFFF return peer_id func _map_id_to_session(peer_id: int, session_id: String) -> void: _id_map[peer_id] = session_id _users[session_id].peer_id = peer_id func _host_add_peer(presence) -> void: var peer_id = _generate_id(presence.session_id) _map_id_to_session(peer_id, presence.session_id) # Tell them we are the host. _nakama_socket.send_match_state_async(_match_id, meta_op_code, JSON.stringify({ type = MetaMessageType.CLAIM_HOST, }), [presence]) # Tell them about all the other connected peers. for other_peer_id in _id_map: var other_session_id = _id_map[other_peer_id] if other_session_id == presence.session_id or other_session_id == _my_session_id: continue _nakama_socket.send_match_state_async(_match_id, meta_op_code, JSON.stringify({ type = MetaMessageType.ASSIGN_PEER_ID, session_id = other_session_id, peer_id = other_peer_id, }), [presence]) # Assign them a peer_id (tell everyone about it). _nakama_socket.send_match_state_async(_match_id, meta_op_code, JSON.stringify({ type = MetaMessageType.ASSIGN_PEER_ID, session_id = presence.session_id, peer_id = peer_id, })) _multiplayer_peer.peer_connected.emit(peer_id) func _on_nakama_socket_received_match_presence(event) -> void: if _match_state == MatchState.DISCONNECTED: return if event.match_id != _match_id: return for presence in event.joins: if not _users.has(presence.session_id): _users[presence.session_id] = User.new(presence) # If we are the host, and they don't yet have a peer id, then let's # generate a new id for them and send all the necessary messages. if _my_peer_id == 1 and _users[presence.session_id].peer_id == 0: _host_add_peer(presence) for presence in event.leaves: if not _users.has(presence.session_id): continue var peer_id = _users[presence.session_id].peer_id _multiplayer_peer.peer_disconnected.emit(peer_id) _users.erase(presence.session_id) _id_map.erase(peer_id) func _parse_json(data: String): var json = JSON.new() if json.parse(data) != OK: return null var content = json.get_data() if not content is Dictionary: return null return content func _on_nakama_socket_received_match_state(data) -> void: if _match_state == MatchState.DISCONNECTED: return if data.match_id != _match_id: return if data.op_code == meta_op_code: var content = _parse_json(data.data) if content == null: return var type = content['type'] #print ("RECEIVED: ", content) if type == MetaMessageType.CLAIM_HOST: if _id_map.has(1): # @todo Can we mediate this dispute? push_error("User %s claiming to be host, when user %s has already claimed it" % [data.presence.session_id, _id_map[1]]) else: _map_id_to_session(1, data.presence.session_id) return # Ensure that any meta messages are coming from the host! if data.presence.session_id != _id_map[1]: push_error("Received meta message from user %s who isn't the host: %s" % [data.presence.session_id, content]) return if type == MetaMessageType.ASSIGN_PEER_ID: var session_id = content['session_id'] var peer_id = content['peer_id'] if _users.has(session_id) and _users[session_id].peer_id != 0: push_error("Attempting to assign peer id %s to %s which already has id %s" % [ peer_id, session_id, _users[session_id].peer_id, ]) return _map_id_to_session(peer_id, session_id) if _my_session_id == session_id: _match_state = MatchState.CONNECTED _multiplayer_peer.initialize(peer_id) _multiplayer_peer.set_connection_status(MultiplayerPeer.CONNECTION_CONNECTED) match_joined.emit() _multiplayer_peer.peer_connected.emit(1) else: _multiplayer_peer.peer_connected.emit(peer_id) else: _nakama_socket.logger.error("Received meta message with unknown type: %s" % type) elif data.op_code == rpc_op_code: var from_session_id: String = data.presence.session_id if not _users.has(from_session_id) or _users[from_session_id].peer_id == 0: push_error("Received RPC from %s which isn't assigned a peer id" % data.presence.session_id) return var from_peer_id = _users[from_session_id].peer_id _multiplayer_peer.deliver_packet(data.binary_data, from_peer_id) func _on_multiplayer_peer_packet_generated(peer_id: int, buffer: PackedByteArray) -> void: if match_state == MatchState.CONNECTED: var target_presences = null if peer_id > 0: if not _id_map.has(peer_id): push_error("Attempting to send RPC to unknown peer id: %s" % peer_id) return target_presences = [ _users[_id_map[peer_id]].presence ] _nakama_socket.send_match_state_raw_async(_match_id, rpc_op_code, buffer, target_presences) else: push_error("RPC sent while the NakamaMultiplayerBridge isn't connected!")