using System;
using System.Collections.Generic;
using UnityEngine;
namespace Unity.Multiplayer.Samples.BossRoom
{
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
{
get
{
if (s_Instance == null)
{
s_Instance = new SessionManager();
}
return s_Instance;
}
}
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))
{
var playerData = GetPlayerData(playerId);
if (playerData != null && playerData.Value.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);
var playerData = GetPlayerData(playerId);
if (playerData != null && playerData.Value.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];
var playerData = GetPlayerData(playerId);
if (playerData != null && playerData.Value.ClientID == id)
{
m_ClientData.Remove(playerId);
}
m_ClientIDToPlayerId.Remove(id);
}
}
}
}