feat: update 2.1.9
This commit is contained in:
@@ -10,7 +10,7 @@ signal session_restored
|
||||
signal logged_out
|
||||
|
||||
# Auth modes
|
||||
enum AuthMode {GUEST, EMAIL, GOOGLE, APPLE, FACEBOOK, CUSTOM}
|
||||
enum AuthMode {GUEST, EMAIL, GOOGLE, APPLE, FACEBOOK, STEAM, CUSTOM}
|
||||
|
||||
# User data
|
||||
var current_user: Dictionary = {}
|
||||
@@ -325,6 +325,74 @@ func login_with_facebook(access_token: String) -> bool:
|
||||
emit_signal("auth_completed", true, current_user)
|
||||
return true
|
||||
|
||||
func login_with_steam() -> bool:
|
||||
emit_signal("auth_started")
|
||||
auth_mode = AuthMode.STEAM
|
||||
is_guest = false
|
||||
|
||||
print("[AuthManager] Steam login...")
|
||||
|
||||
# Get Steamworks manager from BackendService
|
||||
var backend = get_node_or_null("/root/BackendService")
|
||||
if not backend:
|
||||
emit_signal("auth_failed", "BackendService not available")
|
||||
return false
|
||||
|
||||
var steamworks = backend.get_steamworks_manager()
|
||||
if not steamworks or not steamworks.is_initialized():
|
||||
emit_signal("auth_failed", "Steamworks not available. Please launch through Steam.")
|
||||
return false
|
||||
|
||||
# Try full Steam ticket auth first, fall back to Steam ID custom auth for dev/testing
|
||||
var session: NakamaSession = await _authenticate_steam_with_fallback(steamworks)
|
||||
|
||||
if session == null or session.is_exception():
|
||||
var error: String = "Steam authentication failed"
|
||||
if session != null:
|
||||
error = session.get_exception().message
|
||||
emit_signal("auth_failed", error)
|
||||
return false
|
||||
|
||||
NakamaManager.session = session
|
||||
_save_session(session, AuthMode.STEAM)
|
||||
|
||||
var socket_success := await _connect_socket()
|
||||
if not socket_success:
|
||||
emit_signal("auth_failed", "Failed to connect to game server")
|
||||
return false
|
||||
|
||||
await _load_user_profile()
|
||||
is_authenticated = true
|
||||
|
||||
emit_signal("auth_completed", true, current_user)
|
||||
return true
|
||||
|
||||
func _authenticate_steam_with_fallback(steamworks: Node) -> NakamaSession:
|
||||
# Try proper Steam ticket auth first
|
||||
var auth_ticket = steamworks.get_auth_session_ticket()
|
||||
if not auth_ticket.is_empty():
|
||||
print("[AuthManager] Got Steam auth ticket, authenticating with Nakama...")
|
||||
var session: NakamaSession = await NakamaManager.client.authenticate_steam_async(auth_ticket, null, true)
|
||||
if not session.is_exception():
|
||||
return session
|
||||
print("[AuthManager] Steam ticket auth failed: %s" % session.get_exception().message)
|
||||
print("[AuthManager] Falling back to Steam ID custom auth (dev mode)...")
|
||||
|
||||
# Fallback: use Steam ID + username to create an email-style account (works without publisher key)
|
||||
var steam_id = str(steamworks.get_steam_user_id())
|
||||
var steam_name = steamworks.get_steam_user_name()
|
||||
if steam_id == "0" or steam_id.is_empty():
|
||||
return null
|
||||
|
||||
# Derive email and password from Steam credentials
|
||||
var email = steam_name.to_lower().replace(" ", "_") + "@steam.local"
|
||||
var password = steam_name # Default password = Steam username
|
||||
var username = steam_name
|
||||
|
||||
print("[AuthManager] Using Steam email auth: %s (%s)" % [email, username])
|
||||
var fallback_session: NakamaSession = await NakamaManager.client.authenticate_email_async(email, password, username, true)
|
||||
return fallback_session
|
||||
|
||||
# =============================================================================
|
||||
# Account Linking (Convert Guest to Full Account)
|
||||
# =============================================================================
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
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()
|
||||
@@ -0,0 +1 @@
|
||||
uid://deq26r61rqq51
|
||||
@@ -745,7 +745,8 @@ func _on_match_joined(match_id: String) -> void:
|
||||
"id": my_id,
|
||||
"name": local_player_name,
|
||||
"is_ready": false,
|
||||
"character": available_characters[local_character_index]
|
||||
"character": available_characters[local_character_index],
|
||||
"nakama_id": NakamaManager.session.user_id if NakamaManager.session else ""
|
||||
}
|
||||
players_in_room.append(my_data)
|
||||
|
||||
@@ -755,16 +756,17 @@ func _on_match_joined(match_id: String) -> void:
|
||||
# Client will request room info when peer connection is established
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func request_room_info(requester_id: int, requester_name: String, requester_character: String) -> void:
|
||||
"""Client requests room info from host, sending their name and character."""
|
||||
func request_room_info(requester_id: int, requester_name: String, requester_character: String, requester_nakama_id: String = "") -> void:
|
||||
"""Client requests room info from host, sending their name, character and nakama_id."""
|
||||
if not multiplayer.is_server():
|
||||
return
|
||||
|
||||
# Update the player's name and character in the list
|
||||
# Update the player's name, character and nakama_id in the list
|
||||
for player in players_in_room:
|
||||
if player["id"] == requester_id:
|
||||
player["name"] = requester_name
|
||||
player["character"] = requester_character
|
||||
player["nakama_id"] = requester_nakama_id
|
||||
break
|
||||
|
||||
# Send room data to requester
|
||||
@@ -805,7 +807,8 @@ func _on_peer_connected(peer_id: int) -> void:
|
||||
"id": peer_id,
|
||||
"name": "Player %d" % peer_id,
|
||||
"is_ready": false,
|
||||
"character": available_characters[0]
|
||||
"character": available_characters[0],
|
||||
"nakama_id": ""
|
||||
}
|
||||
players_in_room.append(new_player)
|
||||
|
||||
@@ -818,8 +821,9 @@ func _on_peer_connected(peer_id: int) -> void:
|
||||
if peer_id == 1 and not is_host:
|
||||
# Wait a frame to ensure connection is stable
|
||||
await get_tree().process_frame
|
||||
# Send our actual name and character to the host
|
||||
rpc_id(1, "request_room_info", multiplayer.get_unique_id(), local_player_name, available_characters[local_character_index])
|
||||
# Send our actual name, character, and nakama_id to the host
|
||||
var my_nakama_id: String = NakamaManager.session.user_id if NakamaManager.session else ""
|
||||
rpc_id(1, "request_room_info", multiplayer.get_unique_id(), local_player_name, available_characters[local_character_index], my_nakama_id)
|
||||
|
||||
func _on_peer_disconnected(peer_id: int) -> void:
|
||||
"""Called when peer disconnects."""
|
||||
|
||||
@@ -20,7 +20,7 @@ signal avatar_changed(url: String)
|
||||
var profile: Dictionary = {}
|
||||
var stats: Dictionary = {}
|
||||
var wallet: Dictionary = {"gold": 0, "star": 0}
|
||||
var fragments: Dictionary = {} # frag_common, frag_uncommon, frag_rare
|
||||
var fragments: Dictionary = {} # frag_common, frag_uncommon, frag_rare
|
||||
var inventory: Array = []
|
||||
var loadout: Dictionary = {"head": "", "costume": "", "glove": "", "accessory": ""}
|
||||
var shop_catalog: Dictionary = {}
|
||||
|
||||
Reference in New Issue
Block a user