feat: update 2.1.9

This commit is contained in:
2026-04-29 01:36:49 +08:00
parent 1585b91509
commit 8a2f865ad8
76 changed files with 2628 additions and 39 deletions
+69 -1
View File
@@ -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)
# =============================================================================
+176
View File
@@ -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()
+1
View File
@@ -0,0 +1 @@
uid://deq26r61rqq51
+11 -7
View File
@@ -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."""
+1 -1
View File
@@ -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 = {}