You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

278 lines
10 KiB
C#

3 weeks ago
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();
}
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <typeparam name="T"></typeparam>
public class SessionManager<T> where T : struct, ISessionPlayerData
{
SessionManager()
{
m_ClientData = new Dictionary<string, T>();
m_ClientIDToPlayerId = new Dictionary<ulong, string>();
}
public static SessionManager<T> Instance
{
get
{
if (s_Instance == null)
{
s_Instance = new SessionManager<T>();
}
return s_Instance;
}
}
static SessionManager<T> s_Instance;
/// <summary>
/// Maps a given client player id to the data for a given client player.
/// </summary>
Dictionary<string, T> m_ClientData;
/// <summary>
/// Map to allow us to cheaply map from player id to player data.
/// </summary>
Dictionary<ulong, string> m_ClientIDToPlayerId;
bool m_HasSessionStarted;
/// <summary>
/// Handles client disconnect."
/// </summary>
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);
}
}
}
}
/// <summary>
///
/// </summary>
/// <param name="playerId">This is the playerId that is unique to this client and persists across multiple logins from the same client</param>
/// <returns>True if a player with this ID is already connected.</returns>
public bool IsDuplicateConnection(string playerId)
{
return m_ClientData.ContainsKey(playerId) && m_ClientData[playerId].IsConnected;
}
/// <summary>
/// Adds a connecting player's session data if it is a new connection, or updates their session data in case of a reconnection.
/// </summary>
/// <param name="clientId">This is the clientId that Netcode assigned us on login. It does not persist across multiple logins from the same client. </param>
/// <param name="playerId">This is the playerId that is unique to this client and persists across multiple logins from the same client</param>
/// <param name="sessionPlayerData">The player's initial data</param>
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;
}
/// <summary>
///
/// </summary>
/// <param name="clientId"> id of the client whose data is requested</param>
/// <returns>The Player ID matching the given client ID</returns>
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;
}
/// <summary>
///
/// </summary>
/// <param name="clientId"> id of the client whose data is requested</param>
/// <returns>Player data struct matching the given ID</returns>
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;
}
/// <summary>
///
/// </summary>
/// <param name="playerId"> Player ID of the client whose data is requested</param>
/// <returns>Player data struct matching the given ID</returns>
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;
}
/// <summary>
/// Updates player data
/// </summary>
/// <param name="clientId"> id of the client whose data will be updated </param>
/// <param name="sessionPlayerData"> new data to overwrite the old </param>
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}");
}
}
/// <summary>
/// Marks the current session as started, so from now on we keep the data of disconnected players.
/// </summary>
public void OnSessionStarted()
{
m_HasSessionStarted = true;
}
/// <summary>
/// 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
/// </summary>
public void OnSessionEnded()
{
ClearDisconnectedPlayersData();
ReinitializePlayersData();
m_HasSessionStarted = false;
}
/// <summary>
/// Resets all our runtime state, so it is ready to be reinitialized when starting a new server
/// </summary>
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<ulong> idsToClear = new List<ulong>();
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);
}
}
}
}