177 lines
6.0 KiB
GDScript
177 lines
6.0 KiB
GDScript
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()
|