using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using System.Threading.Tasks; using Prometheus; using Robust.Shared.Configuration; using Robust.Shared.Enums; using Robust.Shared.GameObjects; using Robust.Shared.GameStates; using Robust.Shared.Input; using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Network; using Robust.Shared.Network.Messages; using Robust.Shared.Player; using Robust.Shared.Players; using Robust.Shared.Reflection; using Robust.Shared.Timing; using Robust.Shared.Utility; using Robust.Shared.ViewVariables; namespace Robust.Server.Player { /// /// This class will manage connected player sessions. /// public sealed class PlayerManager : IPlayerManager { private static readonly Gauge PlayerCountMetric = Metrics .CreateGauge("robust_player_count", "Number of players on the server."); [Dependency] private readonly IBaseServer _baseServer = default!; [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IServerNetManager _network = default!; [Dependency] private readonly IReflectionManager _reflectionManager = default!; [Dependency] private readonly IEntityManager _entityManager = default!; public BoundKeyMap KeyMap { get; private set; } = default!; private GameTick _lastStateUpdate; private readonly ReaderWriterLockSlim _sessionsLock = new(); /// /// Active sessions of connected clients to the server. /// [ViewVariables] private readonly Dictionary _sessions = new(); [ViewVariables] private readonly Dictionary _playerData = new(); [ViewVariables] private readonly Dictionary _userIdMap = new(); /// public IEnumerable NetworkedSessions => Sessions; /// public IEnumerable Sessions { get { _sessionsLock.EnterReadLock(); try { return _sessions.Values; } finally { _sessionsLock.ExitReadLock(); } } } public IEnumerable ServerSessions => Sessions.Cast(); /// [ViewVariables] public int PlayerCount { get { _sessionsLock.EnterReadLock(); try { return _sessions.Count; } finally { _sessionsLock.ExitReadLock(); } } } /// [ViewVariables] public int MaxPlayers { get; private set; } = 32; /// public event EventHandler? PlayerStatusChanged; /// public void Initialize(int maxPlayers) { KeyMap = new BoundKeyMap(_reflectionManager); KeyMap.PopulateKeyFunctionsMap(); MaxPlayers = maxPlayers; _network.RegisterNetMessage(HandlePlayerListReq); _network.RegisterNetMessage(); _network.Connecting += OnConnecting; _network.Connected += NewSession; _network.Disconnect += EndSession; } public void Shutdown() { KeyMap = default!; _network.Connecting -= OnConnecting; _network.Connected -= NewSession; _network.Disconnect -= EndSession; } public bool TryGetSessionByUsername(string username, [NotNullWhen(true)] out IPlayerSession? session) { if (!_userIdMap.TryGetValue(username, out var userId)) { session = null; return false; } _sessionsLock.EnterReadLock(); try { if (_sessions.TryGetValue(userId, out var iSession)) { session = iSession; return true; } } finally { _sessionsLock.ExitReadLock(); } session = null; return false; } IPlayerSession IPlayerManager.GetSessionByChannel(INetChannel channel) => GetSessionByChannel(channel); public bool TryGetSessionByChannel(INetChannel channel, [NotNullWhen(true)] out IPlayerSession? session) { _sessionsLock.EnterReadLock(); try { // Should only be one session per client. Returns that session, in theory. if (_sessions.TryGetValue(channel.UserId, out var concrete)) { session = concrete; return true; } session = null; return false; } finally { _sessionsLock.ExitReadLock(); } } private PlayerSession GetSessionByChannel(INetChannel channel) { _sessionsLock.EnterReadLock(); try { // Should only be one session per client. Returns that session, in theory. return _sessions[channel.UserId]; } finally { _sessionsLock.ExitReadLock(); } } /// public IPlayerSession GetSessionByUserId(NetUserId index) { _sessionsLock.EnterReadLock(); try { return _sessions[index]; } finally { _sessionsLock.ExitReadLock(); } } public bool ValidSessionId(NetUserId index) { _sessionsLock.EnterReadLock(); try { return _sessions.ContainsKey(index); } finally { _sessionsLock.ExitReadLock(); } } public bool TryGetSessionById(NetUserId userId, [NotNullWhen(true)] out IPlayerSession? session) { _sessionsLock.EnterReadLock(); try { if (_sessions.TryGetValue(userId, out var playerSession)) { session = playerSession; return true; } } finally { _sessionsLock.ExitReadLock(); } session = default; return false; } /// /// Causes all sessions to switch from the lobby to the the game. /// [Obsolete] public void SendJoinGameToAll() { _sessionsLock.EnterReadLock(); try { foreach (var s in _sessions.Values) { s.JoinGame(); } } finally { _sessionsLock.ExitReadLock(); } } public bool TryGetUserId(string userName, out NetUserId userId) { return _userIdMap.TryGetValue(userName, out userId); } public IEnumerable GetAllPlayerData() { return _playerData.Values; } /// /// Causes all sessions to detach from their entity. /// [Obsolete] public void DetachAll() { _sessionsLock.EnterReadLock(); try { foreach (var s in _sessions.Values) { s.DetachFromEntity(); } } finally { _sessionsLock.ExitReadLock(); } } /// /// Gets all players inside of a circle. /// /// Position of the circle in world-space. /// Radius of the circle in world units. /// [Obsolete("Use player Filter or Inline me!")] public List GetPlayersInRange(MapCoordinates worldPos, int range) { return Filter.Empty() .AddInRange(worldPos, range) .Recipients .Cast() .ToList(); } /// /// Gets all players inside of a circle. /// /// Position of the circle in world-space. /// Radius of the circle in world units. /// [Obsolete("Use player Filter or Inline me!")] public List GetPlayersInRange(EntityCoordinates worldPos, int range) { return Filter.Empty() .AddInRange(worldPos.ToMap(_entityManager), range) .Recipients .Cast() .ToList(); } [Obsolete("Use player Filter or Inline me!")] public List GetPlayersBy(Func predicate) { return Filter.Empty() .AddWhere((session => predicate((IPlayerSession)session))) .Recipients .Cast() .ToList(); } /// /// Gets all players in the server. /// /// [Obsolete("Use player Filter or Inline me!")] public List GetAllPlayers() { return ServerSessions.ToList(); } /// /// Gets all player states in the server. /// /// /// public List? GetPlayerStates(GameTick fromTick) { if (_lastStateUpdate < fromTick) { return null; } _sessionsLock.EnterReadLock(); try { return _sessions.Values .Select(s => s.PlayerState) .ToList(); } finally { _sessionsLock.ExitReadLock(); } } private Task OnConnecting(NetConnectingArgs args) { if (PlayerCount >= _baseServer.MaxPlayers) { args.Deny("The server is full."); } return Task.CompletedTask; } /// /// Creates a new session for a client. /// /// /// private void NewSession(object? sender, NetChannelArgs args) { if (!_playerData.TryGetValue(args.Channel.UserId, out var data)) { data = new PlayerData(args.Channel.UserId); _playerData.Add(args.Channel.UserId, data); } _userIdMap[args.Channel.UserName] = args.Channel.UserId; var session = new PlayerSession(this, args.Channel, data); session.PlayerStatusChanged += (_, sessionArgs) => OnPlayerStatusChanged(session, sessionArgs.OldStatus, sessionArgs.NewStatus); _sessionsLock.EnterWriteLock(); try { _sessions.Add(args.Channel.UserId, session); } finally { _sessionsLock.ExitWriteLock(); } PlayerCountMetric.Set(PlayerCount); IoCManager.Resolve().SyncConnectingClient(args.Channel); } private void OnPlayerStatusChanged(IPlayerSession session, SessionStatus oldStatus, SessionStatus newStatus) { PlayerStatusChanged?.Invoke(this, new SessionStatusEventArgs(session, oldStatus, newStatus)); } /// /// Ends a clients session, and disconnects them. /// private void EndSession(object? sender, NetChannelArgs args) { if (!TryGetSessionByChannel(args.Channel, out var session)) { return; } // make sure nothing got messed up during the life of the session DebugTools.Assert(session.ConnectedClient == args.Channel); //Detach the entity and (don't)delete it. session.OnDisconnect(); _sessionsLock.EnterWriteLock(); try { _sessions.Remove(session.UserId); } finally { _sessionsLock.ExitWriteLock(); } PlayerCountMetric.Set(PlayerCount); Dirty(); } private void HandlePlayerListReq(MsgPlayerListReq message) { var channel = message.MsgChannel; var players = Sessions; var netMsg = channel.CreateNetMessage(); // client session is complete, set their status accordingly. // This is done before the packet is built, so that the client // can see themselves Connected. var session = GetSessionByChannel(channel); session.Status = SessionStatus.Connected; var list = new List(); foreach (var client in players) { var info = new PlayerState { UserId = client.UserId, Name = client.Name, Status = client.Status, Ping = client.ConnectedClient.Ping }; list.Add(info); } netMsg.Plyrs = list; netMsg.PlyCount = (byte)list.Count; channel.SendMessage(netMsg); } public void Dirty() { _lastStateUpdate = _timing.CurTick; } public IPlayerData GetPlayerData(NetUserId userId) { return _playerData[userId]; } public bool TryGetPlayerData(NetUserId userId, [NotNullWhen(true)] out IPlayerData? data) { if (_playerData.TryGetValue(userId, out var playerData)) { data = playerData; return true; } data = default; return false; } public bool TryGetPlayerDataByUsername(string userName, [NotNullWhen(true)] out IPlayerData? data) { if (!_userIdMap.TryGetValue(userName, out var userId)) { data = null; return false; } // PlayerData is initialized together with the _userIdMap so we can trust that it'll be present. data = _playerData[userId]; return true; } public bool HasPlayerData(NetUserId userId) { return _playerData.ContainsKey(userId); } } public sealed class SessionStatusEventArgs : EventArgs { public SessionStatusEventArgs(IPlayerSession session, SessionStatus oldStatus, SessionStatus newStatus) { Session = session; OldStatus = oldStatus; NewStatus = newStatus; } public IPlayerSession Session { get; } public SessionStatus OldStatus { get; } public SessionStatus NewStatus { get; } } }