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#
278 lines
10 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|
|
}
|