first commit
This commit is contained in:
359
NitroxServer/GameLogic/PlayerManager.cs
Normal file
359
NitroxServer/GameLogic/PlayerManager.cs
Normal file
@@ -0,0 +1,359 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using NitroxModel.DataStructures;
|
||||
using NitroxModel.DataStructures.GameLogic;
|
||||
using NitroxModel.DataStructures.Unity;
|
||||
using NitroxModel.DataStructures.Util;
|
||||
using NitroxModel.MultiplayerSession;
|
||||
using NitroxModel.Packets;
|
||||
using NitroxModel.Server;
|
||||
using NitroxModel.Serialization;
|
||||
using NitroxServer.Communication;
|
||||
|
||||
namespace NitroxServer.GameLogic
|
||||
{
|
||||
// TODO: These methods are a little chunky. Need to look at refactoring just to clean them up and get them around 30 lines a piece.
|
||||
public class PlayerManager
|
||||
{
|
||||
private readonly ThreadSafeDictionary<string, Player> allPlayersByName;
|
||||
private readonly ThreadSafeDictionary<ushort, Player> connectedPlayersById = [];
|
||||
private readonly ThreadSafeDictionary<INitroxConnection, ConnectionAssets> assetsByConnection = new();
|
||||
private readonly ThreadSafeDictionary<string, PlayerContext> reservations = new();
|
||||
private readonly ThreadSafeSet<string> reservedPlayerNames = new("Player"); // "Player" is often used to identify the local player and should not be used by any user
|
||||
|
||||
private ThreadSafeQueue<KeyValuePair<INitroxConnection, MultiplayerSessionReservationRequest>> JoinQueue { get; set; } = new();
|
||||
private bool PlayerCurrentlyJoining { get; set; }
|
||||
|
||||
private Timer initialSyncTimer;
|
||||
|
||||
private readonly SubnauticaServerConfig serverConfig;
|
||||
private ushort currentPlayerId;
|
||||
|
||||
public PlayerManager(List<Player> players, SubnauticaServerConfig serverConfig)
|
||||
{
|
||||
allPlayersByName = new ThreadSafeDictionary<string, Player>(players.ToDictionary(x => x.Name), false);
|
||||
currentPlayerId = players.Count == 0 ? (ushort)0 : players.Max(x => x.Id);
|
||||
|
||||
this.serverConfig = serverConfig;
|
||||
}
|
||||
|
||||
public List<Player> GetConnectedPlayers()
|
||||
{
|
||||
return ConnectedPlayers().ToList();
|
||||
}
|
||||
|
||||
public List<Player> GetConnectedPlayersExcept(Player excludePlayer)
|
||||
{
|
||||
return ConnectedPlayers().Where(player => player != excludePlayer).ToList();
|
||||
}
|
||||
|
||||
public IEnumerable<Player> GetAllPlayers()
|
||||
{
|
||||
return allPlayersByName.Values;
|
||||
}
|
||||
|
||||
public MultiplayerSessionReservation ReservePlayerContext(
|
||||
INitroxConnection connection,
|
||||
PlayerSettings playerSettings,
|
||||
AuthenticationContext authenticationContext,
|
||||
string correlationId)
|
||||
{
|
||||
if (reservedPlayerNames.Count >= serverConfig.MaxConnections)
|
||||
{
|
||||
MultiplayerSessionReservationState rejectedState = MultiplayerSessionReservationState.REJECTED | MultiplayerSessionReservationState.SERVER_PLAYER_CAPACITY_REACHED;
|
||||
return new MultiplayerSessionReservation(correlationId, rejectedState);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(serverConfig.ServerPassword) && (!authenticationContext.ServerPassword.HasValue || authenticationContext.ServerPassword.Value != serverConfig.ServerPassword))
|
||||
{
|
||||
MultiplayerSessionReservationState rejectedState = MultiplayerSessionReservationState.REJECTED | MultiplayerSessionReservationState.AUTHENTICATION_FAILED;
|
||||
return new MultiplayerSessionReservation(correlationId, rejectedState);
|
||||
}
|
||||
|
||||
//https://regex101.com/r/eTWiEs/2/
|
||||
if (!Regex.IsMatch(authenticationContext.Username, @"^[a-zA-Z0-9._-]{3,25}$"))
|
||||
{
|
||||
MultiplayerSessionReservationState rejectedState = MultiplayerSessionReservationState.REJECTED | MultiplayerSessionReservationState.INCORRECT_USERNAME;
|
||||
return new MultiplayerSessionReservation(correlationId, rejectedState);
|
||||
}
|
||||
|
||||
if (PlayerCurrentlyJoining)
|
||||
{
|
||||
if (JoinQueue.Any(pair => ReferenceEquals(pair.Key, connection)))
|
||||
{
|
||||
// Don't enqueue the request if there is already another enqueued request by the same user
|
||||
return new MultiplayerSessionReservation(correlationId, MultiplayerSessionReservationState.REJECTED);
|
||||
}
|
||||
|
||||
JoinQueue.Enqueue(new KeyValuePair<INitroxConnection, MultiplayerSessionReservationRequest>(
|
||||
connection,
|
||||
new MultiplayerSessionReservationRequest(correlationId, playerSettings, authenticationContext)));
|
||||
|
||||
return new MultiplayerSessionReservation(correlationId, MultiplayerSessionReservationState.ENQUEUED_IN_JOIN_QUEUE);
|
||||
}
|
||||
|
||||
string playerName = authenticationContext.Username;
|
||||
|
||||
allPlayersByName.TryGetValue(playerName, out Player player);
|
||||
if (player?.IsPermaDeath == true && serverConfig.IsHardcore())
|
||||
{
|
||||
MultiplayerSessionReservationState rejectedState = MultiplayerSessionReservationState.REJECTED | MultiplayerSessionReservationState.HARDCORE_PLAYER_DEAD;
|
||||
return new MultiplayerSessionReservation(correlationId, rejectedState);
|
||||
}
|
||||
|
||||
if (reservedPlayerNames.Contains(playerName))
|
||||
{
|
||||
MultiplayerSessionReservationState rejectedState = MultiplayerSessionReservationState.REJECTED | MultiplayerSessionReservationState.UNIQUE_PLAYER_NAME_CONSTRAINT_VIOLATED;
|
||||
return new MultiplayerSessionReservation(correlationId, rejectedState);
|
||||
}
|
||||
|
||||
assetsByConnection.TryGetValue(connection, out ConnectionAssets assetPackage);
|
||||
if (assetPackage == null)
|
||||
{
|
||||
assetPackage = new ConnectionAssets();
|
||||
assetsByConnection.Add(connection, assetPackage);
|
||||
reservedPlayerNames.Add(playerName);
|
||||
}
|
||||
|
||||
bool hasSeenPlayerBefore = player != null;
|
||||
ushort playerId = hasSeenPlayerBefore ? player.Id : ++currentPlayerId;
|
||||
NitroxId playerNitroxId = hasSeenPlayerBefore ? player.GameObjectId : new NitroxId();
|
||||
NitroxGameMode gameMode = hasSeenPlayerBefore ? player.GameMode : serverConfig.GameMode;
|
||||
IntroCinematicMode introCinematicMode = hasSeenPlayerBefore ? IntroCinematicMode.COMPLETED : IntroCinematicMode.LOADING;
|
||||
|
||||
// TODO: At some point, store the muted state of a player
|
||||
PlayerContext playerContext = new(playerName, playerId, playerNitroxId, !hasSeenPlayerBefore, playerSettings, false, gameMode, null, introCinematicMode);
|
||||
string reservationKey = Guid.NewGuid().ToString();
|
||||
|
||||
reservations.Add(reservationKey, playerContext);
|
||||
assetPackage.ReservationKey = reservationKey;
|
||||
|
||||
PlayerCurrentlyJoining = true;
|
||||
|
||||
InitialSyncTimerData timerData = new InitialSyncTimerData(connection, authenticationContext, serverConfig.InitialSyncTimeout);
|
||||
initialSyncTimer = new Timer(InitialSyncTimerElapsed, timerData, 0, 200);
|
||||
|
||||
|
||||
return new MultiplayerSessionReservation(correlationId, playerId, reservationKey);
|
||||
}
|
||||
|
||||
private void InitialSyncTimerElapsed(object state)
|
||||
{
|
||||
if (state is InitialSyncTimerData timerData && !timerData.Disposing)
|
||||
{
|
||||
allPlayersByName.TryGetValue(timerData.Context.Username, out Player player);
|
||||
|
||||
if (timerData.Connection.State < NitroxConnectionState.Connected)
|
||||
{
|
||||
if (player == null) // player can cancel the joining process before this timer elapses
|
||||
{
|
||||
Log.Error("Player was nulled while joining");
|
||||
PlayerDisconnected(timerData.Connection);
|
||||
}
|
||||
else
|
||||
{
|
||||
player.SendPacket(new PlayerKicked("An error occured while loading, Initial sync took too long to complete"));
|
||||
PlayerDisconnected(player.Connection);
|
||||
SendPacketToOtherPlayers(new Disconnect(player.Id), player);
|
||||
}
|
||||
timerData.Disposing = true;
|
||||
FinishProcessingReservation();
|
||||
}
|
||||
|
||||
if (timerData.Counter >= timerData.MaxCounter)
|
||||
{
|
||||
Log.Error("An unexpected Error occured during InitialSync");
|
||||
PlayerDisconnected(timerData.Connection);
|
||||
|
||||
timerData.Disposing = true;
|
||||
initialSyncTimer.Dispose(); // Looped long enough to require an override
|
||||
}
|
||||
|
||||
timerData.Counter++;
|
||||
}
|
||||
}
|
||||
|
||||
public void NonPlayerDisconnected(INitroxConnection connection)
|
||||
{
|
||||
// Remove any requests sent by the connection from the join queue
|
||||
JoinQueue = new(JoinQueue.Where(pair => !Equals(pair.Key, connection)));
|
||||
}
|
||||
|
||||
public Player PlayerConnected(INitroxConnection connection, string reservationKey, out bool wasBrandNewPlayer)
|
||||
{
|
||||
PlayerContext playerContext = reservations[reservationKey];
|
||||
Validate.NotNull(playerContext);
|
||||
ConnectionAssets assetPackage = assetsByConnection[connection];
|
||||
Validate.NotNull(assetPackage);
|
||||
|
||||
wasBrandNewPlayer = playerContext.WasBrandNewPlayer;
|
||||
|
||||
if (!allPlayersByName.TryGetValue(playerContext.PlayerName, out Player player))
|
||||
{
|
||||
player = new Player(playerContext.PlayerId,
|
||||
playerContext.PlayerName,
|
||||
false,
|
||||
playerContext,
|
||||
connection,
|
||||
NitroxVector3.Zero,
|
||||
NitroxQuaternion.Identity,
|
||||
playerContext.PlayerNitroxId,
|
||||
Optional.Empty,
|
||||
serverConfig.DefaultPlayerPerm,
|
||||
serverConfig.DefaultPlayerStats,
|
||||
serverConfig.GameMode,
|
||||
new List<NitroxTechType>(),
|
||||
Array.Empty<Optional<NitroxId>>(),
|
||||
new Dictionary<string, NitroxId>(),
|
||||
new Dictionary<string, float>(),
|
||||
new Dictionary<string, PingInstancePreference>(),
|
||||
new List<int>()
|
||||
);
|
||||
allPlayersByName[playerContext.PlayerName] = player;
|
||||
}
|
||||
|
||||
connectedPlayersById.Add(playerContext.PlayerId, player);
|
||||
|
||||
// TODO: make a ConnectedPlayer wrapper so this is not stateful
|
||||
player.PlayerContext = playerContext;
|
||||
player.Connection = connection;
|
||||
|
||||
// reconnecting players need to have their cell visibility refreshed
|
||||
player.ClearVisibleCells();
|
||||
|
||||
assetPackage.Player = player;
|
||||
assetPackage.ReservationKey = null;
|
||||
reservations.Remove(reservationKey);
|
||||
|
||||
return player;
|
||||
}
|
||||
|
||||
public void PlayerDisconnected(INitroxConnection connection)
|
||||
{
|
||||
assetsByConnection.TryGetValue(connection, out ConnectionAssets assetPackage);
|
||||
if (assetPackage == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (assetPackage.ReservationKey != null)
|
||||
{
|
||||
PlayerContext playerContext = reservations[assetPackage.ReservationKey];
|
||||
reservedPlayerNames.Remove(playerContext.PlayerName);
|
||||
reservations.Remove(assetPackage.ReservationKey);
|
||||
}
|
||||
|
||||
if (assetPackage.Player != null)
|
||||
{
|
||||
Player player = assetPackage.Player;
|
||||
reservedPlayerNames.Remove(player.Name);
|
||||
connectedPlayersById.Remove(player.Id);
|
||||
}
|
||||
|
||||
assetsByConnection.Remove(connection);
|
||||
|
||||
if (!ConnectedPlayers().Any())
|
||||
{
|
||||
Server.Instance.PauseServer();
|
||||
Server.Instance.Save();
|
||||
}
|
||||
}
|
||||
|
||||
public void FinishProcessingReservation(Player player = null)
|
||||
{
|
||||
initialSyncTimer.Dispose();
|
||||
PlayerCurrentlyJoining = false;
|
||||
if (player != null)
|
||||
{
|
||||
BroadcastPlayerJoined(player);
|
||||
}
|
||||
|
||||
Log.Info($"Finished processing reservation. Remaining requests: {JoinQueue.Count}");
|
||||
|
||||
// Tell next client that it can start joining.
|
||||
if (JoinQueue.Count > 0)
|
||||
{
|
||||
KeyValuePair<INitroxConnection, MultiplayerSessionReservationRequest> keyValuePair = JoinQueue.Dequeue();
|
||||
INitroxConnection requestConnection = keyValuePair.Key;
|
||||
MultiplayerSessionReservationRequest reservationRequest = keyValuePair.Value;
|
||||
|
||||
MultiplayerSessionReservation reservation = ReservePlayerContext(requestConnection,
|
||||
reservationRequest.PlayerSettings,
|
||||
reservationRequest.AuthenticationContext,
|
||||
reservationRequest.CorrelationId);
|
||||
|
||||
requestConnection.SendPacket(reservation);
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryGetPlayerByName(string playerName, out Player foundPlayer)
|
||||
{
|
||||
foundPlayer = null;
|
||||
foreach (Player player in ConnectedPlayers())
|
||||
{
|
||||
if (player.Name == playerName)
|
||||
{
|
||||
foundPlayer = player;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryGetPlayerById(ushort playerId, out Player player)
|
||||
{
|
||||
return connectedPlayersById.TryGetValue(playerId, out player);
|
||||
}
|
||||
|
||||
public Player GetPlayer(INitroxConnection connection)
|
||||
{
|
||||
if (!assetsByConnection.TryGetValue(connection, out ConnectionAssets assetPackage))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return assetPackage.Player;
|
||||
}
|
||||
|
||||
public Optional<Player> GetPlayer(string playerName)
|
||||
{
|
||||
allPlayersByName.TryGetValue(playerName, out Player player);
|
||||
return Optional.OfNullable(player);
|
||||
}
|
||||
|
||||
public void SendPacketToAllPlayers(Packet packet)
|
||||
{
|
||||
foreach (Player player in ConnectedPlayers())
|
||||
{
|
||||
player.SendPacket(packet);
|
||||
}
|
||||
}
|
||||
|
||||
public void SendPacketToOtherPlayers(Packet packet, Player sendingPlayer)
|
||||
{
|
||||
foreach (Player player in ConnectedPlayers())
|
||||
{
|
||||
if (player != sendingPlayer)
|
||||
{
|
||||
player.SendPacket(packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<Player> ConnectedPlayers()
|
||||
{
|
||||
return assetsByConnection.Values
|
||||
.Where(assetPackage => assetPackage.Player != null)
|
||||
.Select(assetPackage => assetPackage.Player);
|
||||
}
|
||||
|
||||
public void BroadcastPlayerJoined(Player player)
|
||||
{
|
||||
PlayerJoinedMultiplayerSession playerJoinedPacket = new(player.PlayerContext, player.SubRootId, player.Entity);
|
||||
SendPacketToOtherPlayers(playerJoinedPacket, player);
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user