using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; namespace Barotrauma.Networking { partial class Client : IDisposable { public bool VoiceEnabled = true; public VoipServerDecoder VoipServerDecoder; public UInt16 LastRecvClientListUpdate = NetIdUtils.GetIdOlderThan(GameMain.Server.LastClientListUpdateID); public UInt16 LastSentServerSettingsUpdate = NetIdUtils.GetIdOlderThan(GameMain.Server.ServerSettings.LastUpdateIdForFlag[ServerSettings.NetFlags.Properties]); public UInt16 LastRecvServerSettingsUpdate = NetIdUtils.GetIdOlderThan(GameMain.Server.ServerSettings.LastUpdateIdForFlag[ServerSettings.NetFlags.Properties]); public UInt16 LastRecvLobbyUpdate = NetIdUtils.GetIdOlderThan(GameMain.NetLobbyScreen.LastUpdateID); public bool InitialLobbyUpdateSent; public UInt16 LastSentChatMsgID = 0; //last msg this client said public UInt16 LastRecvChatMsgID = 0; //last msg this client knows about public UInt16 LastSentEntityEventID = 0; public UInt16 LastRecvEntityEventID = 0; public readonly Dictionary LastRecvCampaignUpdate = new Dictionary(); public UInt16 LastRecvCampaignSave = 0; public (UInt16 saveId, float time) LastCampaignSaveSendTime; public readonly List ChatMsgQueue = new List(); public UInt16 LastChatMsgQueueID; //latest chat messages sent by this client public readonly List LastSentChatMessages = new List(); public float ChatSpamSpeed; public float ChatSpamTimer; public int ChatSpamCount; public string RejectedName; public float KickAFKTimer; public double MidRoundSyncTimeOut; public bool NeedsMidRoundSync; //how many unique events the client missed before joining the server public UInt16 UnreceivedEntityEventCount; public UInt16 FirstNewEventID; //when was a specific entity event last sent to the client // key = event id, value = NetTime.Now when sending public readonly Dictionary EntityEventLastSent = new Dictionary(); //when was a position update for a given entity last sent to the client // key = entity, value = NetTime.Now when sending public readonly Dictionary PositionUpdateLastSent = new Dictionary(); public readonly Queue PendingPositionUpdates = new Queue(); public bool ReadyToStart; public List JobPreferences { get; set; } public JobVariant AssignedJob; public float DeleteDisconnectedTimer; public DateTime JoinTime; public static readonly TimeSpan NameChangeCoolDown = new TimeSpan(hours: 0, minutes: 0, seconds: 30); public DateTime LastNameChangeTime; private CharacterInfo characterInfo; public CharacterInfo CharacterInfo { get { return characterInfo; } set { if (characterInfo == value) { return; } if (characterInfo is { Character: null }) { //if a character hasn't spawned for this characterInfo, we can remove the info and free the sprites and such characterInfo.Remove(); } characterInfo = value; } } public string PendingName; public NetworkConnection Connection { get; set; } public bool SpectateOnly; public bool AFK; public bool? WaitForNextRoundRespawn; public int KarmaKickCount; private float syncedKarma = 100.0f; private float karma = 100.0f; public float Karma { get { if (GameMain.Server == null || !GameMain.Server.ServerSettings.KarmaEnabled || GameMain.GameSession?.GameMode is PvPMode) { return 100.0f; } if (HasPermission(ClientPermissions.KarmaImmunity)) { return 100.0f; } return karma; } set { if (GameMain.Server == null || !GameMain.Server.ServerSettings.KarmaEnabled || GameMain.GameSession?.GameMode is PvPMode) { return; } karma = Math.Min(Math.Max(value, 0.0f), 100.0f); if (!MathUtils.NearlyEqual(karma, syncedKarma, 10.0f)) { syncedKarma = karma; GameMain.NetworkMember.LastClientListUpdateID++; } } } private List kickVoters; public int KickVoteCount { get { return kickVoters.Count; } } public WeakReference PreviousCharacter; partial void InitProjSpecific() { kickVoters = new List(); JobPreferences = new List(); VoipQueue = new VoipQueue(SessionId, true, true); VoipServerDecoder = new VoipServerDecoder(VoipQueue, this); GameMain.Server.VoipServer.RegisterQueue(VoipQueue); //initialize to infinity, gets set to a proper value when initializing midround syncing MidRoundSyncTimeOut = double.PositiveInfinity; JoinTime = DateTime.Now; } partial void DisposeProjSpecific() { GameMain.Server.VoipServer.UnregisterQueue(VoipQueue); VoipQueue.Dispose(); if (characterInfo != null) { if (characterInfo.Character == null || characterInfo.Character.Removed) { characterInfo?.Remove(); characterInfo = null; } } } public void InitClientSync() { LastSentChatMsgID = 0; LastRecvChatMsgID = ChatMessage.LastID; LastRecvLobbyUpdate = NetIdUtils.GetIdOlderThan(GameMain.NetLobbyScreen.LastUpdateID); InitialLobbyUpdateSent = false; LastRecvEntityEventID = 0; UnreceivedEntityEventCount = 0; NeedsMidRoundSync = false; } public static bool IsValidName(string name, ServerSettings serverSettings) { if (string.IsNullOrWhiteSpace(name)) { return false; } char[] disallowedChars = { //',', //previously disallowed because of the ban list format ';', '<', '>', '/', //disallowed because of server messages using forward slash as a delimiter (TODO: implement escaping) '\\', '[', ']', '"', '?' }; if (name.Any(c => disallowedChars.Contains(c))) { return false; } foreach (char character in name) { if (!serverSettings.AllowedClientNameChars.Any(charRange => (int)character >= charRange.Start && (int)character <= charRange.End)) { return false; } } return true; } public bool AddressMatches(Address address) { return Connection.Endpoint.Address.Equals(address); } public void AddKickVote(Client voter) { if (voter != null && !kickVoters.Contains(voter)) { kickVoters.Add(voter); } } public void RemoveKickVote(Client voter) { kickVoters.Remove(voter); } public bool HasKickVoteFrom(Client voter) { return kickVoters.Contains(voter); } public bool HasKickVoteFromSessionId(int id) { return kickVoters.Any(k => k.SessionId == id); } public static void UpdateKickVotes(IReadOnlyList connectedClients) { foreach (Client client in connectedClients) { client.kickVoters.RemoveAll(voter => !connectedClients.Contains(voter)); } } /// /// Reset what this client has voted for and the kick votes given to this client /// public void ResetVotes(bool resetKickVotes) { for (int i = 0; i < votes.Length; i++) { votes[i] = null; } if (resetKickVotes) { kickVoters.Clear(); } } public void SetPermissions(ClientPermissions permissions, IEnumerable permittedConsoleCommands) { Permissions = permissions; PermittedConsoleCommands.Clear(); PermittedConsoleCommands.UnionWith(permittedConsoleCommands); if (Permissions.HasFlag(ClientPermissions.ManageSettings)) { //ensure the client has the up-to-date server settings GameMain.Server?.ServerSettings?.ForcePropertyUpdate(); } } public void GivePermission(ClientPermissions permission) { if (!Permissions.HasFlag(permission)) { Permissions |= permission; if (permission.HasFlag(ClientPermissions.ManageSettings)) { //ensure the client has the up-to-date server settings GameMain.Server?.ServerSettings?.ForcePropertyUpdate(); } } } public void RemovePermission(ClientPermissions permission) { Permissions &= ~permission; } public bool HasPermission(ClientPermissions permission) { return Permissions.HasFlag(permission); } public bool TryTakeOverBot(Character botCharacter) { if (GameMain.Server == null) { DebugConsole.ThrowError($"TryTakeOverBot: Client {Name} requested to take over a bot but GameMain.Server is null!"); return false; } if (GameMain.NetworkMember is not { ServerSettings.RespawnMode: RespawnMode.Permadeath }) { DebugConsole.ThrowError($"Client {Name} requested to take over a bot but Permadeath is not enabled!"); GameMain.Server.SendConsoleMessage($"Permadeath mode is not enabled, cannot take over a bot.", this, Color.Red); return false; } if (CharacterInfo == null) { DebugConsole.ThrowError($"Permadeath: Client {Name} requested to take over a bot, but they don't seem to have a character at all yet."); GameMain.Server.SendConsoleMessage($"Permadeath: Taking over a bot requires having a character that died first.", this, Color.Red); return false; } if (CharacterInfo is not { PermanentlyDead: true }) { DebugConsole.ThrowError($"Permadeath: Client {Name} requested to take over a bot, but their character has not been permanently killed."); GameMain.Server.SendConsoleMessage($"Permadeath: Could not take over the bot, previous character not permanently killed.", this, Color.Red); return false; } if (!botCharacter.IsBot) { DebugConsole.ThrowError($"Permadeath: {Name} requested to take over a bot character, but the target character is not a bot!"); GameMain.Server.SendConsoleMessage($"Permadeath: Could not take over the target character because it is not a bot.", this, Color.Red); return false; } if (botCharacter.Info != null) { botCharacter.Info.RenamingEnabled = true; // Grant one opportunity to rename a taken over bot } // Now that the old permanently killed character will be replaced, we can fully discard it var mpCampaign = GameMain.GameSession?.Campaign as MultiPlayerCampaign; mpCampaign?.DiscardClientCharacterData(this); GameMain.Server.SetClientCharacter(this, botCharacter); if (mpCampaign?.SetClientCharacterData(this) is CharacterCampaignData characterData) { //the bot has spawned, but the new CharacterCampaignData technically hasn't, because we just created it characterData.HasSpawned = true; mpCampaign.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.CharacterInfo); } SpectateOnly = false; return true; } public void ResetSync() { NeedsMidRoundSync = false; PendingPositionUpdates.Clear(); EntityEventLastSent.Clear(); LastSentEntityEventID = 0; LastRecvEntityEventID = 0; UnreceivedEntityEventCount = 0; } } }