using System; using System.Collections.Generic; using UnityEngine; namespace Unity.Multiplayer.Samples.Utilities { public interface ISessionPlayerData { bool IsConnected { get; set; } ulong ClientID { get; set; } void Reinitialize(); } /// /// This class uses a unique player ID to bind a player to a session. Once that player connects to a host, the host /// associates the current ClientID to the player's unique ID. If the player disconnects and reconnects to the same /// host, the session is preserved. /// /// /// Using a client-generated player ID and sending it directly could be problematic, as a malicious user could /// intercept it and reuse it to impersonate the original user. We are currently investigating this to offer a /// solution that handles security better. /// /// public class SessionManager where T : struct, ISessionPlayerData { SessionManager() { m_ClientData = new Dictionary(); m_ClientIDToPlayerId = new Dictionary(); } public static SessionManager Instance => s_Instance ??= new SessionManager(); static SessionManager s_Instance; /// /// Maps a given client player id to the data for a given client player. /// Dictionary m_ClientData; /// /// Map to allow us to cheaply map from player id to player data. /// Dictionary m_ClientIDToPlayerId; bool m_HasSessionStarted; /// /// Handles client disconnect." /// public void DisconnectClient(ulong clientId) { if (m_HasSessionStarted) { // Mark client as disconnected, but keep their data so they can reconnect. if (m_ClientIDToPlayerId.TryGetValue(clientId, out var playerId)) { if (GetPlayerData(playerId)?.ClientID == clientId) { var clientData = m_ClientData[playerId]; clientData.IsConnected = false; m_ClientData[playerId] = clientData; } } } else { // Session has not started, no need to keep their data if (m_ClientIDToPlayerId.TryGetValue(clientId, out var playerId)) { m_ClientIDToPlayerId.Remove(clientId); if (GetPlayerData(playerId)?.ClientID == clientId) { m_ClientData.Remove(playerId); } } } } /// /// /// /// This is the playerId that is unique to this client and persists across multiple logins from the same client /// True if a player with this ID is already connected. public bool IsDuplicateConnection(string playerId) { return m_ClientData.ContainsKey(playerId) && m_ClientData[playerId].IsConnected; } /// /// Adds a connecting player's session data if it is a new connection, or updates their session data in case of a reconnection. /// /// This is the clientId that Netcode assigned us on login. It does not persist across multiple logins from the same client. /// This is the playerId that is unique to this client and persists across multiple logins from the same client /// The player's initial data public void SetupConnectingPlayerSessionData(ulong clientId, string playerId, T sessionPlayerData) { var isReconnecting = false; // Test for duplicate connection if (IsDuplicateConnection(playerId)) { Debug.LogError($"Player ID {playerId} already exists. This is a duplicate connection. Rejecting this session data."); return; } // If another client exists with the same playerId if (m_ClientData.ContainsKey(playerId)) { if (!m_ClientData[playerId].IsConnected) { // If this connecting client has the same player Id as a disconnected client, this is a reconnection. isReconnecting = true; } } // Reconnecting. Give data from old player to new player if (isReconnecting) { // Update player session data sessionPlayerData = m_ClientData[playerId]; sessionPlayerData.ClientID = clientId; sessionPlayerData.IsConnected = true; } //Populate our dictionaries with the SessionPlayerData m_ClientIDToPlayerId[clientId] = playerId; m_ClientData[playerId] = sessionPlayerData; } /// /// /// /// id of the client whose data is requested /// The Player ID matching the given client ID public string GetPlayerId(ulong clientId) { if (m_ClientIDToPlayerId.TryGetValue(clientId, out string playerId)) { return playerId; } Debug.Log($"No client player ID found mapped to the given client ID: {clientId}"); return null; } /// /// /// /// id of the client whose data is requested /// Player data struct matching the given ID public T? GetPlayerData(ulong clientId) { //First see if we have a playerId matching the clientID given. var playerId = GetPlayerId(clientId); if (playerId != null) { return GetPlayerData(playerId); } Debug.Log($"No client player ID found mapped to the given client ID: {clientId}"); return null; } /// /// /// /// Player ID of the client whose data is requested /// Player data struct matching the given ID public T? GetPlayerData(string playerId) { if (m_ClientData.TryGetValue(playerId, out T data)) { return data; } Debug.Log($"No PlayerData of matching player ID found: {playerId}"); return null; } /// /// Updates player data /// /// id of the client whose data will be updated /// new data to overwrite the old public void SetPlayerData(ulong clientId, T sessionPlayerData) { if (m_ClientIDToPlayerId.TryGetValue(clientId, out string playerId)) { m_ClientData[playerId] = sessionPlayerData; } else { Debug.LogError($"No client player ID found mapped to the given client ID: {clientId}"); } } /// /// Marks the current session as started, so from now on we keep the data of disconnected players. /// public void OnSessionStarted() { m_HasSessionStarted = true; } /// /// Reinitializes session data from connected players, and clears data from disconnected players, so that if they reconnect in the next game, they will be treated as new players /// public void OnSessionEnded() { ClearDisconnectedPlayersData(); ReinitializePlayersData(); m_HasSessionStarted = false; } /// /// Resets all our runtime state, so it is ready to be reinitialized when starting a new server /// public void OnServerEnded() { m_ClientData.Clear(); m_ClientIDToPlayerId.Clear(); m_HasSessionStarted = false; } void ReinitializePlayersData() { foreach (var id in m_ClientIDToPlayerId.Keys) { string playerId = m_ClientIDToPlayerId[id]; T sessionPlayerData = m_ClientData[playerId]; sessionPlayerData.Reinitialize(); m_ClientData[playerId] = sessionPlayerData; } } void ClearDisconnectedPlayersData() { List idsToClear = new List(); foreach (var id in m_ClientIDToPlayerId.Keys) { var data = GetPlayerData(id); if (data is { IsConnected: false }) { idsToClear.Add(id); } } foreach (var id in idsToClear) { string playerId = m_ClientIDToPlayerId[id]; if (GetPlayerData(playerId)?.ClientID == id) { m_ClientData.Remove(playerId); } m_ClientIDToPlayerId.Remove(id); } } } }