extends Node ## FriendManager - Nakama friend list, DM channels, and lobby invites ## Autoload: FriendManager signal friends_updated(friends: Array) signal lobby_invite_received(from_user_id: String, from_name: String, match_id: String) signal dm_message_received(from_user_id: String, from_name: String, message: String) ## Notification codes (must match server-side RPC) const NOTIF_LOBBY_INVITE := 1001 ## Friend state codes from Nakama const STATE_FRIEND := 0 const STATE_INVITE_OUT := 1 const STATE_INVITE_IN := 2 const STATE_BLOCKED := 3 var friends: Array = [] # [{user_id, username, state}] var _friend_ids: Dictionary = {} # user_id -> true (mutual friends only) var _dm_channels: Dictionary = {} # user_id -> NakamaChannel func _ready() -> void: NakamaManager.connected_to_nakama.connect(_on_nakama_connected) if NakamaManager.socket and NakamaManager.socket.is_connected_to_host(): _connect_socket_signals() func _on_nakama_connected() -> void: _connect_socket_signals() load_friends() func _connect_socket_signals() -> void: var socket = NakamaManager.socket if not socket: return if not socket.received_notification.is_connected(_on_notification_received): socket.received_notification.connect(_on_notification_received) if not socket.received_channel_message.is_connected(_on_channel_message): socket.received_channel_message.connect(_on_channel_message) # ============================================================================= # Friend List # ============================================================================= func load_friends() -> void: if not NakamaManager.session: return var result = await NakamaManager.client.list_friends_async(NakamaManager.session, 100, null, null) if result.is_exception(): push_warning("[FriendManager] Failed to load friends: " + result.get_exception().message) return friends.clear() _friend_ids.clear() for f in result.friends: var u = f.user var entry := { "user_id": u.id, "username": u.display_name if u.display_name != "" else u.username, "state": f.state, } friends.append(entry) if f.state == STATE_FRIEND: _friend_ids[u.id] = true emit_signal("friends_updated", friends) func get_mutual_friends() -> Array: return friends.filter(func(f): return f.state == STATE_FRIEND) func is_friend(user_id: String) -> bool: return _friend_ids.has(user_id) func add_friend_by_id(user_id: String) -> bool: if not NakamaManager.session: return false var result = await NakamaManager.client.add_friends_async( NakamaManager.session, PackedStringArray([user_id]), null) if result.is_exception(): push_warning("[FriendManager] add_friend failed: " + result.get_exception().message) return false load_friends() return true func add_friend_by_username(username: String) -> bool: if not NakamaManager.session: return false var result = await NakamaManager.client.add_friends_async( NakamaManager.session, null, PackedStringArray([username])) if result.is_exception(): push_warning("[FriendManager] add_friend_by_username failed: " + result.get_exception().message) return false load_friends() return true func remove_friend(user_id: String) -> bool: if not NakamaManager.session: return false var result = await NakamaManager.client.delete_friends_async( NakamaManager.session, PackedStringArray([user_id]), null) if result.is_exception(): return false load_friends() return true # ============================================================================= # Lobby Invites # ============================================================================= func send_lobby_invite(to_user_id: String, match_id: String) -> void: if not NakamaManager.session: return var payload = JSON.stringify({"to_user_id": to_user_id, "match_id": match_id}) var result = await NakamaManager.client.rpc_async( NakamaManager.session, "send_lobby_invite", payload) if result.is_exception(): push_warning("[FriendManager] send_lobby_invite failed: " + result.get_exception().message) func _on_notification_received(notification) -> void: if notification.code == NOTIF_LOBBY_INVITE: var content = JSON.parse_string(notification.content) if content: var from_name: String = content.get("from_name", "Someone") var match_id: String = content.get("match_id", "") emit_signal("lobby_invite_received", notification.sender_id, from_name, match_id) # ============================================================================= # Direct Messages # ============================================================================= func open_dm(user_id: String) -> Object: if _dm_channels.has(user_id): return _dm_channels[user_id] var socket = NakamaManager.socket if not socket: return null var channel = await socket.join_chat_async( user_id, NakamaSocket.ChannelType.DirectMessage, true, false) if channel.is_exception(): push_warning("[FriendManager] Failed to open DM with " + user_id) return null _dm_channels[user_id] = channel return channel func send_dm(user_id: String, message: String) -> bool: var channel = await open_dm(user_id) if not channel: return false var socket = NakamaManager.socket if not socket: return false var result = await socket.write_chat_message_async(channel.id, {"msg": message}) return not result.is_exception() func get_dm_channel_id(user_id: String) -> String: var ch = _dm_channels.get(user_id, null) return ch.id if ch else "" func _on_channel_message(message) -> void: # Route to DM signal if this message is from a DM channel for user_id in _dm_channels: var ch = _dm_channels[user_id] if ch.id == message.channel_id: var text: String = "" var parsed = JSON.parse_string(message.content) if typeof(parsed) == TYPE_DICTIONARY: text = parsed.get("msg", message.content) else: text = message.content emit_signal("dm_message_received", message.sender_id, message.username, text) return func close_all_dm_channels() -> void: var socket = NakamaManager.socket for user_id in _dm_channels: var ch = _dm_channels[user_id] if socket: socket.leave_chat_async(ch.id) _dm_channels.clear()