summaryrefslogtreecommitdiff
path: root/game/addons/com.heroiclabs.nakama/utils/NakamaMultiplayerBridge.gd
diff options
context:
space:
mode:
Diffstat (limited to 'game/addons/com.heroiclabs.nakama/utils/NakamaMultiplayerBridge.gd')
-rw-r--r--game/addons/com.heroiclabs.nakama/utils/NakamaMultiplayerBridge.gd363
1 files changed, 363 insertions, 0 deletions
diff --git a/game/addons/com.heroiclabs.nakama/utils/NakamaMultiplayerBridge.gd b/game/addons/com.heroiclabs.nakama/utils/NakamaMultiplayerBridge.gd
new file mode 100644
index 0000000..66c15bb
--- /dev/null
+++ b/game/addons/com.heroiclabs.nakama/utils/NakamaMultiplayerBridge.gd
@@ -0,0 +1,363 @@
+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!")