using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; using Data.Models; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Serilog.Context; using SharedLibraryCore.Configuration; using SharedLibraryCore.Database.Models; using SharedLibraryCore.Dtos; using SharedLibraryCore.Exceptions; using SharedLibraryCore.Helpers; using SharedLibraryCore.Interfaces; using ILogger = Microsoft.Extensions.Logging.ILogger; namespace SharedLibraryCore { public abstract class Server : IGameServer { public enum Game { COD = -1, UKN = 0, IW3 = 1, IW4 = 2, IW5 = 3, IW6 = 4, T4 = 5, T5 = 6, T6 = 7, T7 = 8, SHG1 = 9, CSGO = 10 } // only here for performance private readonly bool CustomSayEnabled; protected readonly string CustomSayName; protected readonly DefaultSettings DefaultSettings; protected readonly ILogger ServerLogger; protected List<string> BroadcastMessages; protected int ConnectionErrors; protected string FSGame; protected IGameLogReaderFactory gameLogReaderFactory; // Info private string hostname; protected TimeSpan LastMessage; protected DateTime LastPoll; protected int NextMessage; protected ManualResetEventSlim OnRemoteCommandResponse; protected IRConConnectionFactory RConConnectionFactory; #pragma warning disable CS0612 public Server(ILogger<Server> logger, Interfaces.ILogger deprecatedLogger, #pragma warning restore CS0612 ServerConfiguration config, IManager mgr, IRConConnectionFactory rconConnectionFactory, IGameLogReaderFactory gameLogReaderFactory, IServiceProvider serviceProvider) { Password = config.Password; IP = config.IPAddress; Port = config.Port; Manager = mgr; #pragma warning disable CS0612 Logger = deprecatedLogger ?? throw new ArgumentNullException(nameof(deprecatedLogger)); #pragma warning restore CS0612 ServerConfig = config; EventProcessing = new SemaphoreSlim(1, 1); Clients = new List<EFClient>(new EFClient[64]); Reports = new List<Report>(); ClientHistory = new ClientHistoryInfo(); ChatHistory = new List<ChatInfo>(); NextMessage = 0; CustomSayEnabled = Manager.GetApplicationSettings().Configuration().EnableCustomSayName; CustomSayName = Manager.GetApplicationSettings().Configuration().CustomSayName; this.gameLogReaderFactory = gameLogReaderFactory; RConConnectionFactory = rconConnectionFactory; ServerLogger = logger; DefaultSettings = serviceProvider.GetRequiredService<DefaultSettings>(); InitializeTokens(); InitializeAutoMessages(); } public long EndPoint => IPAddress.TryParse(IP, out _) ? Convert.ToInt64($"{IP.Replace(".", "")}{Port}") : $"{IP.Replace(".", "")}{Port}".GetStableHashCode(); // Objects public IManager Manager { get; protected set; } [Obsolete] public Interfaces.ILogger Logger { get; } public ServerConfiguration ServerConfig { get; } public List<Map> Maps { get; protected set; } = new List<Map>(); public List<Report> Reports { get; set; } public List<ChatInfo> ChatHistory { get; protected set; } public ClientHistoryInfo ClientHistory { get; } public Game GameName { get; set; } public string Hostname { get => ServerConfig.CustomHostname ?? hostname; protected set => hostname = value; } public string Website { get; protected set; } public string Gametype { get; set; } public string GametypeName => DefaultSettings.Gametypes.FirstOrDefault(gt => gt.Game == GameName)?.Gametypes ?.FirstOrDefault(gt => gt.Name == Gametype)?.Alias ?? Gametype; public string GamePassword { get; protected set; } public Map CurrentMap { get; set; } public int ClientNum { get { return Clients.ToArray().Count(p => p != null && !p.IsBot); } } public int MaxClients { get; protected set; } public List<EFClient> Clients { get; protected set; } public string Password { get; } public bool Throttled { get; protected set; } public bool CustomCallback { get; protected set; } public string WorkingDirectory { get; protected set; } public IRConConnection RemoteConnection { get; protected set; } public IRConParser RconParser { get; set; } public IEventParser EventParser { get; set; } public string LogPath { get; protected set; } public bool RestartRequested { get; set; } public SemaphoreSlim EventProcessing { get; } // Internal /// <summary> /// this is actually the hostname now /// </summary> public string IP { get; protected set; } public IPEndPoint ResolvedIpEndPoint { get; protected set; } public string Version { get; protected set; } public bool IsInitialized { get; set; } public int Port { get; } public abstract Task Kick(string reason, EFClient target, EFClient origin, EFPenalty originalPenalty); /// <summary> /// Returns list of all current players /// </summary> /// <returns></returns> public List<EFClient> GetClientsAsList() { return Clients.FindAll(x => x != null && x.NetworkId != 0); } /// <summary> /// Add a player to the server's player list /// </summary> /// <param name="P">EFClient pulled from memory reading</param> /// <returns>True if player added successfully, false otherwise</returns> public abstract Task<EFClient> OnClientConnected(EFClient P); /// <summary> /// Remove player by client number /// </summary> /// <returns>true if removal succeeded, false otherwise</returns> public abstract Task OnClientDisconnected(EFClient client); /// <summary> /// Get a player by name /// todo: make this an extension /// </summary> /// <param name="pName">EFClient name to search for</param> /// <returns>Matching player if found</returns> public List<EFClient> GetClientByName(string pName) { if (string.IsNullOrEmpty(pName)) { return new List<EFClient>(); } pName = pName.Trim().StripColors(); var QuoteSplit = pName.Split('"'); var literal = false; if (QuoteSplit.Length > 1) { pName = QuoteSplit[1]; literal = true; } if (literal) { return GetClientsAsList().Where(p => p.Name?.StripColors()?.ToLower() == pName.ToLower()).ToList(); } return GetClientsAsList().Where(p => (p.Name?.StripColors()?.ToLower() ?? "").Contains(pName.ToLower())) .ToList(); } public virtual Task<bool> ProcessUpdatesAsync(CancellationToken cts) { return (Task<bool>)Task.CompletedTask; } /// <summary> /// Process any server event /// </summary> /// <param name="E">Event</param> /// <returns>True on sucess</returns> protected abstract Task<bool> ProcessEvent(GameEvent E); public abstract Task ExecuteEvent(GameEvent E); /// <summary> /// Send a message to all players /// </summary> /// <param name="message">Message to be sent to all players</param> /// <param name="sender">Client that initiated the broadcast</param> public GameEvent Broadcast(string message, EFClient sender = null) { var formattedMessage = string.Format(RconParser.Configuration.CommandPrefixes.Say ?? "", $"{(CustomSayEnabled && GameName == Game.IW4 ? $"{CustomSayName}: " : "")}{message.FormatMessageForEngine(RconParser.Configuration.ColorCodeMapping)}"); ServerLogger.LogDebug("All-> {Message}", message.FormatMessageForEngine(RconParser.Configuration.ColorCodeMapping).StripColors()); var e = new GameEvent { Type = GameEvent.EventType.Broadcast, Data = formattedMessage, Owner = this, Origin = sender }; Manager.AddEvent(e); return e; } [Obsolete("Use BroadcastAsync")] public void Broadcast(IEnumerable<string> messages, EFClient sender = null) { foreach (var message in messages) { #pragma warning disable 4014 Broadcast(message, sender).WaitAsync(); #pragma warning restore 4014 } } public async Task BroadcastAsync(IEnumerable<string> messages, EFClient sender = null, CancellationToken token = default) { foreach (var message in messages) { if (Manager.CancellationToken.IsCancellationRequested) { return; } await Broadcast(message, sender).WaitAsync(Utilities.DefaultCommandTimeout, Manager.CancellationToken); } } /// <summary> /// Send a message to a particular players /// </summary> /// <param name="message">Message to send</param> /// <param name="targetClient">EFClient to send message to</param> protected async Task Tell(string message, EFClient targetClient) { var engineMessage = message.FormatMessageForEngine(RconParser.Configuration.ColorCodeMapping); if (!Utilities.IsDevelopment) { var temporalClientId = targetClient.GetAdditionalProperty<string>("ConnectionClientId"); var parsedClientId = string.IsNullOrEmpty(temporalClientId) ? (int?)null : int.Parse(temporalClientId); var clientNumber = parsedClientId ?? targetClient.ClientNumber; var formattedMessage = string.Format(RconParser.Configuration.CommandPrefixes.Tell, clientNumber, $"{(CustomSayEnabled && GameName == Game.IW4 ? $"{CustomSayName}: " : "")}{engineMessage}"); if (targetClient.ClientNumber > -1 && message.Length > 0 && targetClient.Level != Data.Models.Client.EFClient.Permission.Console) { await this.ExecuteCommandAsync(formattedMessage); } } else { ServerLogger.LogDebug("Tell[{ClientNumber}]->{Message}", targetClient.ClientNumber, message.FormatMessageForEngine(RconParser.Configuration.ColorCodeMapping).StripColors()); } if (targetClient.Level == Data.Models.Client.EFClient.Permission.Console) { Console.ForegroundColor = ConsoleColor.Green; using (LogContext.PushProperty("Server", ToString())) { ServerLogger.LogInformation("Command output received: {Message}", engineMessage.StripColors()); } Console.WriteLine(engineMessage.StripColors()); Console.ForegroundColor = ConsoleColor.Gray; } } /// <summary> /// Send a message to all admins on the server /// </summary> /// <param name="message">Message to send out</param> public void ToAdmins(string message) { foreach (var client in GetClientsAsList() .Where(c => c.Level > Data.Models.Client.EFClient.Permission.Flagged)) client.Tell(message); } /// <summary> /// Kick a player from the server /// </summary> /// <param name="reason">Reason for kicking</param> /// <param name="target">EFClient to kick</param> /// <param name="origin">Client initating the kick</param> public Task Kick(string reason, EFClient target, EFClient origin) { return Kick(reason, target, origin, null); } /// <summary> /// Temporarily ban a player ( default 1 hour ) from the server /// </summary> /// <param name="reason">Reason for banning the player</param> /// <param name="length">Duration of the ban</param> /// <param name="target">The client to ban</param> /// <param name="origin">The client performing the ban</param> public abstract Task TempBan(string reason, TimeSpan length, EFClient target, EFClient origin); /// <summary> /// Perm ban a player from the server /// </summary> /// <param name="reason">The reason for the ban</param> /// <param name="target">The person to ban</param> /// <param name="origin">The person who banned the target</param> /// <param name="isEvade">obsolete</param> public abstract Task Ban(string reason, EFClient target, EFClient origin, bool isEvade = false); public abstract Task Warn(string reason, EFClient target, EFClient origin); /// <summary> /// Unban a player by npID / GUID /// </summary> /// <param name="reason">reason for unban</param> /// <param name="targetClient">client being unbanned</param> /// <param name="originClient">client performing the unban</param> /// <returns></returns> public abstract Task Unban(string reason, EFClient targetClient, EFClient originClient); /// <summary> /// Change the current server map /// </summary> /// <param name="mapName">Non-localized map name</param> public async Task LoadMap(string mapName) { await this.ExecuteCommandAsync($"map {mapName}"); } /// <summary> /// Initalize the macro variables /// </summary> public abstract void InitializeTokens(); /// <summary> /// Initialize the messages to be broadcasted /// </summary> protected void InitializeAutoMessages() { BroadcastMessages = new List<string>(); if (ServerConfig.AutoMessages != null) { BroadcastMessages.AddRange(ServerConfig.AutoMessages); } BroadcastMessages.AddRange(Manager.GetApplicationSettings().Configuration().AutoMessages); } public override string ToString() { return $"{IP}:{Port}"; } protected async Task<bool> ScriptLoaded() { try { return (await this.GetDvarAsync("sv_customcallbacks", "0", Manager.CancellationToken)).Value == "1"; } catch (DvarException) { return false; } } public abstract Task<long> GetIdForServer(Server server = null); public string[] ExecuteServerCommand(string command, int timeoutMs = 1000) { var tokenSource = new CancellationTokenSource(); tokenSource.CancelAfter(TimeSpan.FromSeconds(timeoutMs)); try { return this.ExecuteCommandAsync(command, tokenSource.Token).GetAwaiter().GetResult(); } catch { return null; } } public string GetServerDvar(string dvarName, int timeoutMs = 1000) { var tokenSource = new CancellationTokenSource(); tokenSource.CancelAfter(TimeSpan.FromSeconds(timeoutMs)); try { return this.GetDvarAsync<string>(dvarName, token: tokenSource.Token).GetAwaiter().GetResult().Value; } catch { return null; } } public bool SetServerDvar(string dvarName, string dvarValue, int timeoutMs = 1000) { var tokenSource = new CancellationTokenSource(); tokenSource.CancelAfter(TimeSpan.FromSeconds(timeoutMs)); try { this.SetDvarAsync(dvarName, dvarValue, tokenSource.Token).GetAwaiter().GetResult(); return true; } catch { return false; } } public EFClient GetClientByNumber(int clientNumber) => GetClientsAsList().FirstOrDefault(client => client.ClientNumber == clientNumber); } }