using IW4MAdmin.Application.EventParsers; using IW4MAdmin.Application.IO; using IW4MAdmin.Application.RconParsers; using SharedLibraryCore; using SharedLibraryCore.Configuration; using SharedLibraryCore.Database.Models; using SharedLibraryCore.Dtos; using SharedLibraryCore.Exceptions; using SharedLibraryCore.Interfaces; using SharedLibraryCore.Localization; using SharedLibraryCore.Objects; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; namespace IW4MAdmin { public class IW4MServer : Server { private static readonly Index loc = Utilities.CurrentLocalization.LocalizationIndex; private GameLogEventDetection LogEvent; public int Id { get; private set; } public IW4MServer(IManager mgr, ServerConfiguration cfg) : base(mgr, cfg) { } public override int GetHashCode() { if ($"{IP}:{Port.ToString()}" == "66.150.121.184:28965") { return 886229536; } if ($"{IP}:{Port.ToString()}" == "66.150.121.184:28960") { return 1645744423; } if ($"{IP}:{Port.ToString()}" == "66.150.121.184:28970") { return 1645809959; } if (Id == 0) { Id = HashCode.Combine(IP, Port); Id = Id < 0 ? Math.Abs(Id) : Id; } return Id; } public async Task OnClientJoined(EFClient polledClient) { var existingClient = Clients[polledClient.ClientNumber]; if (existingClient != null) { await existingClient.OnJoin(polledClient.IPAddress); } } override public async Task OnClientConnected(EFClient clientFromLog) { Logger.WriteDebug($"Client slot #{clientFromLog.ClientNumber} now reserved"); try { EFClient client = await Manager.GetClientService().GetUnique(clientFromLog.NetworkId); // first time client is connecting to server if (client == null) { Logger.WriteDebug($"Client {clientFromLog} first time connecting"); client = await Manager.GetClientService().Create(clientFromLog); } // client has connected in the past else { var existingAlias = client.AliasLink.Children .FirstOrDefault(a => a.Name == clientFromLog.Name); if (existingAlias == null) { Logger.WriteDebug($"Client {clientFromLog} has connected previously under a different ip/name"); client.CurrentAlias = new EFAlias() { // this gets updated on client join IPAddress = clientFromLog.IPAddress, Name = clientFromLog.Name, }; } else { client.CurrentAlias = existingAlias; client.CurrentAliasId = existingAlias.AliasId; } await Manager.GetClientService().Update(client); } Logger.WriteInfo($"Client {client} connected..."); // Do the player specific stuff client.ClientNumber = clientFromLog.ClientNumber; client.IsBot = clientFromLog.IsBot; client.Score = clientFromLog.Score; client.Ping = clientFromLog.Ping; client.CurrentServer = this; Clients[client.ClientNumber] = client; client.OnConnect(); client.State = EFClient.ClientState.Connected; } catch (Exception ex) { Logger.WriteError($"{loc["SERVER_ERROR_ADDPLAYER"]} {clientFromLog.Name}::{clientFromLog.NetworkId}"); Logger.WriteDebug(ex.Message); Logger.WriteDebug(ex.StackTrace); } } //Remove player by CLIENT NUMBER override public async Task RemoveClient(int cNum) { if (cNum >= 0 && Clients[cNum] != null) { EFClient Leaving = Clients[cNum]; // occurs when the player disconnects via log before being authenticated by RCon if (Leaving.State != EFClient.ClientState.Connected) { Clients[cNum] = null; } else { Logger.WriteInfo($"Client {Leaving} [{Leaving.State.ToString().ToLower()}] disconnecting..."); Leaving.State = EFClient.ClientState.Disconnecting; Leaving.TotalConnectionTime += Leaving.ConnectionLength; Leaving.LastConnection = DateTime.UtcNow; await Manager.GetClientService().Update(Leaving); Clients[cNum] = null; } } } public override async Task ExecuteEvent(GameEvent E) { bool canExecuteCommand = true; if (!await ProcessEvent(E)) { return; } Command C = null; if (E.Type == GameEvent.EventType.Command) { try { C = await SharedLibraryCore.Commands.CommandProcessing.ValidateCommand(E); } catch (CommandException e) { Logger.WriteInfo(e.Message); } if (C != null) { E.Extra = C; } } foreach (var plugin in SharedLibraryCore.Plugins.PluginImporter.ActivePlugins) { try { await plugin.OnEventAsync(E, this); } catch (AuthorizationException e) { E.Origin.Tell($"{loc["COMMAND_NOTAUTHORIZED"]} - {e.Message}"); canExecuteCommand = false; } catch (Exception Except) { Logger.WriteError($"{loc["SERVER_PLUGIN_ERROR"]} [{plugin.Name}]"); Logger.WriteDebug(Except.GetExceptionInfo()); } } // hack: this prevents commands from getting executing that 'shouldn't' be if (E.Type == GameEvent.EventType.Command && E.Extra != null && (canExecuteCommand || E.Origin?.Level == EFClient.Permission.Console)) { await (((Command)E.Extra).ExecuteAsync(E)); } } /// /// Perform the server specific tasks when an event occurs /// /// /// override protected async Task ProcessEvent(GameEvent E) { if (E.Type == GameEvent.EventType.Connect) { E.Origin.State = EFClient.ClientState.Authenticated; await OnClientConnected(E.Origin); ChatHistory.Add(new ChatInfo() { Name = E.Origin.Name, Message = "CONNECTED", Time = DateTime.UtcNow }); if (E.Origin.Level > EFClient.Permission.Moderator) { E.Origin.Tell(string.Format(loc["SERVER_REPORT_COUNT"], E.Owner.Reports.Count)); } } else if (E.Type == GameEvent.EventType.Join) { await OnClientJoined(E.Origin); } else if (E.Type == GameEvent.EventType.Flag) { Penalty newPenalty = new Penalty() { Type = Penalty.PenaltyType.Flag, Expires = DateTime.UtcNow, Offender = E.Target, Offense = E.Data, Punisher = E.Origin, Active = true, When = DateTime.UtcNow, Link = E.Target.AliasLink }; var addedPenalty = await Manager.GetPenaltyService().Create(newPenalty); E.Target.ReceivedPenalties.Add(addedPenalty); await Manager.GetClientService().Update(E.Target); } else if (E.Type == GameEvent.EventType.Unflag) { await Manager.GetClientService().Update(E.Target); } else if (E.Type == GameEvent.EventType.Report) { this.Reports.Add(new Report() { Origin = E.Origin, Target = E.Target, Reason = E.Data }); } else if (E.Type == GameEvent.EventType.TempBan) { await TempBan(E.Data, (TimeSpan)E.Extra, E.Target, E.Origin); ; } else if (E.Type == GameEvent.EventType.Ban) { await Ban(E.Data, E.Target, E.Origin); } else if (E.Type == GameEvent.EventType.Unban) { await Unban(E.Data, E.Target, E.Origin); } else if (E.Type == GameEvent.EventType.Kick) { await Kick(E.Data, E.Target, E.Origin); } else if (E.Type == GameEvent.EventType.Warn) { await Warn(E.Data, E.Target, E.Origin); } else if (E.Type == GameEvent.EventType.Quit) { var origin = Clients.FirstOrDefault(p => p != null && p.NetworkId == E.Origin.NetworkId); if (origin != null && // we only want to forward the event if they are connected. origin.State == EFClient.ClientState.Connected && // make sure we don't get the disconnect event from every time the game ends origin.ConnectionLength < Manager.GetApplicationSettings().Configuration().RConPollRate) { var e = new GameEvent() { Type = GameEvent.EventType.Disconnect, Origin = origin, Owner = this }; Manager.GetEventHandler().AddEvent(e); } else if (origin != null && origin.State != EFClient.ClientState.Connected) { await RemoveClient(origin.ClientNumber); } } else if (E.Type == GameEvent.EventType.Disconnect) { ChatHistory.Add(new ChatInfo() { Name = E.Origin.Name, Message = "DISCONNECTED", Time = DateTime.UtcNow }); var currentState = E.Origin.State; await RemoveClient(E.Origin.ClientNumber); if (currentState != EFClient.ClientState.Connected) { throw new ServerException("Disconnecting player was not in a connected state"); } } if (E.Type == GameEvent.EventType.Say) { E.Data = E.Data.StripColors(); if (E.Data.Length > 0) { // this may be a fix for a hard to reproduce null exception error lock (ChatHistory) { ChatHistory.Add(new ChatInfo() { Name = E.Origin.Name, Message = E.Data ?? "NULL", Time = DateTime.UtcNow }); } } } if (E.Type == GameEvent.EventType.MapChange) { Logger.WriteInfo($"New map loaded - {ClientNum} active players"); // iw4 doesn't log the game info if (E.Extra == null) { var dict = await this.GetInfoAsync(); if (dict == null) { Logger.WriteWarning("Map change event response doesn't have any data"); } else { Gametype = dict["gametype"].StripColors(); Hostname = dict["hostname"]?.StripColors(); string mapname = dict["mapname"]?.StripColors() ?? CurrentMap.Name; CurrentMap = Maps.Find(m => m.Name == mapname) ?? new Map() { Alias = mapname, Name = mapname }; } } else { var dict = (Dictionary)E.Extra; Gametype = dict["g_gametype"].StripColors(); Hostname = dict["sv_hostname"].StripColors(); string mapname = dict["mapname"].StripColors(); CurrentMap = Maps.Find(m => m.Name == mapname) ?? new Map() { Alias = mapname, Name = mapname }; } } if (E.Type == GameEvent.EventType.MapEnd) { Logger.WriteInfo("Game ending..."); } if (E.Type == GameEvent.EventType.Tell) { await Tell(E.Message, E.Target); } if (E.Type == GameEvent.EventType.Broadcast) { #if DEBUG == false // this is a little ugly but I don't want to change the abstract class if (E.Data != null) { await E.Owner.ExecuteCommandAsync(E.Data); } #endif } while (ChatHistory.Count > Math.Ceiling((double)ClientNum / 2)) { ChatHistory.RemoveAt(0); } // the last client hasn't fully disconnected yet // so there will still be at least 1 client left if (ClientNum < 2) { ChatHistory.Clear(); } return true; } /// /// lists the connecting and disconnecting clients via RCon response /// array index 0 = connecting clients /// array index 1 = disconnecting clients /// /// async Task[]> PollPlayersAsync() { #if DEBUG var now = DateTime.Now; #endif var currentClients = GetClientsAsList(); var polledClients = (await this.GetStatusAsync()).AsEnumerable(); if (this.Manager.GetApplicationSettings().Configuration().IgnoreBots) { polledClients = polledClients.Where(c => !c.IsBot); } #if DEBUG Logger.WriteInfo($"Polling players took {(DateTime.Now - now).TotalMilliseconds}ms"); #endif Throttled = false; foreach (var client in polledClients) { // todo: move out somehwere var existingClient = Clients[client.ClientNumber] ?? client; existingClient.Ping = client.Ping; existingClient.Score = client.Score; } var disconnectingClients = currentClients.Except(polledClients); var connectingClients = polledClients.Except(currentClients.Where(c => c.State == EFClient.ClientState.Connected)); return new List[] { connectingClients.ToList(), disconnectingClients.ToList() }; } DateTime start = DateTime.Now; DateTime playerCountStart = DateTime.Now; DateTime lastCount = DateTime.Now; override public async Task ProcessUpdatesAsync(CancellationToken cts) { try { if (Manager.ShutdownRequested()) { // todo: fix up disconnect //for (int i = 0; i < EFClients.Count; i++) // await RemoveClient(i); foreach (var plugin in SharedLibraryCore.Plugins.PluginImporter.ActivePlugins) { await plugin.OnUnloadAsync(); } } // only check every 2 minutes if the server doesn't seem to be responding /* if ((DateTime.Now - LastPoll).TotalMinutes < 0.5 && ConnectionErrors >= 1) return true;*/ try { var polledClients = await PollPlayersAsync(); var waiterList = new List(); foreach (var disconnectingClient in polledClients[1]) { if (disconnectingClient.State == EFClient.ClientState.Disconnecting) { continue; } var e = new GameEvent() { Type = GameEvent.EventType.Disconnect, Origin = disconnectingClient, Owner = this }; Manager.GetEventHandler().AddEvent(e); // wait until the disconnect event is complete // because we don't want to try to fill up a slot that's not empty yet waiterList.Add(e); } // wait for all the disconnect tasks to finish await Task.WhenAll(waiterList.Select(e => e.WaitAsync(10 * 1000))); waiterList.Clear(); // this are our new connecting clients foreach (var client in polledClients[0]) { // this prevents duplicate events from being sent to the event api if (GetClientsAsList().Count(c => c.NetworkId == client.NetworkId && c.State == EFClient.ClientState.Connected) != 0) { continue; } var e = new GameEvent() { Type = GameEvent.EventType.Connect, Origin = client, Owner = this }; Manager.GetEventHandler().AddEvent(e); waiterList.Add(e); } // wait for all the connect tasks to finish await Task.WhenAll(waiterList.Select(e => e.WaitAsync(10 * 1000))); if (ConnectionErrors > 0) { Logger.WriteVerbose($"{loc["MANAGER_CONNECTION_REST"]} {IP}:{Port}"); Throttled = false; } ConnectionErrors = 0; LastPoll = DateTime.Now; } catch (NetworkException e) { ConnectionErrors++; if (ConnectionErrors == 3) { Logger.WriteError($"{e.Message} {IP}:{Port}, {loc["SERVER_ERROR_POLLING"]}"); Logger.WriteDebug($"Internal Exception: {e.Data["internal_exception"]}"); Throttled = true; } return true; } LastMessage = DateTime.Now - start; lastCount = DateTime.Now; // todo: re-enable on tick /* if ((DateTime.Now - tickTime).TotalMilliseconds >= 1000) { foreach (var Plugin in SharedLibraryCore.Plugins.PluginImporter.ActivePlugins) { if (cts.IsCancellationRequested) break; await Plugin.OnTickAsync(this); } tickTime = DateTime.Now; }*/ // update the player history if ((lastCount - playerCountStart).TotalMinutes >= SharedLibraryCore.Helpers.PlayerHistory.UpdateInterval) { while (ClientHistory.Count > ((60 / SharedLibraryCore.Helpers.PlayerHistory.UpdateInterval) * 12)) // 12 times a hour for 12 hours { ClientHistory.Dequeue(); } ClientHistory.Enqueue(new SharedLibraryCore.Helpers.PlayerHistory(ClientNum)); playerCountStart = DateTime.Now; } // send out broadcast messages if (LastMessage.TotalSeconds > Manager.GetApplicationSettings().Configuration().AutoMessagePeriod && BroadcastMessages.Count > 0 && ClientNum > 0) { string[] messages = this.ProcessMessageToken(Manager.GetMessageTokens(), BroadcastMessages[NextMessage]).Split(Environment.NewLine); foreach (string message in messages) { Broadcast(message); } NextMessage = NextMessage == (BroadcastMessages.Count - 1) ? 0 : NextMessage + 1; start = DateTime.Now; } return true; } // this one is ok catch (ServerException e) { if (e is NetworkException) { Logger.WriteError($"{loc["SERVER_ERROR_COMMUNICATION"]} {IP}:{Port}"); Logger.WriteDebug(e.GetExceptionInfo()); } return false; } catch (Exception E) { Logger.WriteError($"{loc["SERVER_ERROR_EXCEPTION"]} {IP}:{Port}"); Logger.WriteDebug(E.GetExceptionInfo()); return false; } } public async Task Initialize() { RconParser = ServerConfig.UseT6MParser ? (IRConParser)new T6MRConParser() : new IW3RConParser(); if (ServerConfig.UseIW5MParser) { RconParser = new IW5MRConParser(); } var version = await this.GetDvarAsync("version"); GameName = Utilities.GetGame(version.Value); if (GameName == Game.IW4) { EventParser = new IW4EventParser(); RconParser = new IW4RConParser(); } else if (GameName == Game.IW5) { EventParser = new IW5EventParser(); } else if (GameName == Game.T5M) { EventParser = new T5MEventParser(); } else if (GameName == Game.T6M) { EventParser = new T6MEventParser(); } else { EventParser = new IW3EventParser(); // this uses the 'main' folder for log paths } if (GameName == Game.UKN) { Logger.WriteWarning($"Game name not recognized: {version}"); } var infoResponse = await this.GetInfoAsync(); // this is normally slow, but I'm only doing it because different games have different prefixes var hostname = infoResponse == null ? (await this.GetDvarAsync("sv_hostname")).Value : infoResponse.Where(kvp => kvp.Key.Contains("hostname")).Select(kvp => kvp.Value).First(); var mapname = infoResponse == null ? (await this.GetDvarAsync("mapname")).Value : infoResponse["mapname"]; int maxplayers = (GameName == Game.IW4) ? // gotta love IW4 idiosyncrasies (await this.GetDvarAsync("party_maxplayers")).Value : infoResponse == null ? (await this.GetDvarAsync("sv_maxclients")).Value : Convert.ToInt32(infoResponse["sv_maxclients"]); var gametype = infoResponse == null ? (await this.GetDvarAsync("g_gametype")).Value : infoResponse.Where(kvp => kvp.Key.Contains("gametype")).Select(kvp => kvp.Value).First(); var basepath = await this.GetDvarAsync("fs_basepath"); var game = infoResponse == null || !infoResponse.ContainsKey("fs_game") ? (await this.GetDvarAsync("fs_game")).Value : infoResponse["fs_game"]; var logfile = await this.GetDvarAsync("g_log"); var logsync = await this.GetDvarAsync("g_logsync"); WorkingDirectory = basepath.Value; try { var website = await this.GetDvarAsync("_website"); Website = website.Value; } catch (DvarException) { Website = loc["SERVER_WEBSITE_GENERIC"]; } InitializeMaps(); this.Hostname = hostname.StripColors(); this.CurrentMap = Maps.Find(m => m.Name == mapname) ?? new Map() { Alias = mapname, Name = mapname }; this.MaxClients = maxplayers; this.FSGame = game; this.Gametype = gametype; if (logsync.Value == 0 || logfile.Value == string.Empty) { // this DVAR isn't set until the a map is loaded await this.SetDvarAsync("logfile", 2); await this.SetDvarAsync("g_logsync", 2); // set to 2 for continous in other games, clamps to 1 for IW4 await this.SetDvarAsync("g_log", "games_mp.log"); Logger.WriteWarning("Game log file not properly initialized, restarting map..."); await this.ExecuteCommandAsync("map_restart"); logfile = await this.GetDvarAsync("g_log"); } CustomCallback = await ScriptLoaded(); string mainPath = EventParser.GetGameDir(); #if DEBUG // basepath.Value = @"D:\"; #endif string logPath = string.Empty; LogPath = game == string.Empty ? $"{basepath.Value.Replace('\\', Path.DirectorySeparatorChar)}{Path.DirectorySeparatorChar}{mainPath}{Path.DirectorySeparatorChar}{logfile.Value}" : $"{basepath.Value.Replace('\\', Path.DirectorySeparatorChar)}{Path.DirectorySeparatorChar}{game.Replace('/', Path.DirectorySeparatorChar)}{Path.DirectorySeparatorChar}{logfile.Value}"; if (GameName == Game.IW5 || ServerConfig.ManualLogPath?.Length > 0) { logPath = ServerConfig.ManualLogPath; } else { logPath = LogPath; } // hopefully fix wine drive name mangling if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { logPath = Regex.Replace($"{Path.DirectorySeparatorChar}{LogPath}", @"[A-Z]:/", ""); } if (!File.Exists(logPath) && !logPath.StartsWith("http")) { Logger.WriteError($"{logPath} {loc["SERVER_ERROR_DNE"]}"); #if !DEBUG throw new ServerException($"{loc["SERVER_ERROR_LOG"]} {logPath}"); #else LogEvent = new GameLogEventDetection(this, logPath, logfile.Value); #endif } else { LogEvent = new GameLogEventDetection(this, logPath, logfile.Value); } Logger.WriteInfo($"Log file is {logPath}"); _ = Task.Run(() => LogEvent.PollForChanges()); #if !DEBUG Broadcast(loc["BROADCAST_ONLINE"]); #endif } protected override async Task Warn(String Reason, EFClient Target, EFClient Origin) { // ensure player gets warned if command not performed on them in game if (Target.ClientNumber < 0) { var ingameClient = Manager.GetActiveClients() .FirstOrDefault(c => c.ClientId == Target.ClientId); if (ingameClient != null) { await Warn(Reason, ingameClient, Origin); return; } } else { if (Target.Warnings >= 4) { Target.Kick(loc["SERVER_WARNLIMT_REACHED"], Utilities.IW4MAdminClient(this)); return; } String message = $"^1{loc["SERVER_WARNING"]} ^7[^3{Target.Warnings}^7]: ^3{Target.Name}^7, {Reason}"; Target.CurrentServer.Broadcast(message); } Penalty newPenalty = new Penalty() { Type = Penalty.PenaltyType.Warning, Expires = DateTime.UtcNow, Offender = Target, Offense = Reason, Punisher = Origin, Active = true, When = DateTime.UtcNow, Link = Target.AliasLink }; await Manager.GetPenaltyService().Create(newPenalty); } protected override async Task Kick(String Reason, EFClient Target, EFClient Origin) { // ensure player gets kicked if command not performed on them in game if (Target.ClientNumber < 0) { var ingameClient = Manager.GetActiveClients() .FirstOrDefault(c => c.ClientId == Target.ClientId); if (ingameClient != null) { await Kick(Reason, ingameClient, Origin); return; } } #if !DEBUG else { string formattedKick = String.Format(RconParser.GetCommandPrefixes().Kick, Target.ClientNumber, $"{loc["SERVER_KICK_TEXT"]} - ^5{Reason}^7"); await Target.CurrentServer.ExecuteCommandAsync(formattedKick); } #endif #if DEBUG await Target.CurrentServer.RemoveClient(Target.ClientNumber); #endif var newPenalty = new Penalty() { Type = Penalty.PenaltyType.Kick, Expires = DateTime.UtcNow, Offender = Target, Offense = Reason, Punisher = Origin, When = DateTime.UtcNow, Link = Target.AliasLink }; await Manager.GetPenaltyService().Create(newPenalty); } protected override async Task TempBan(String Reason, TimeSpan length, EFClient Target, EFClient Origin) { // ensure player gets banned if command not performed on them in game if (Target.ClientNumber < 0) { var ingameClient = Manager.GetActiveClients() .FirstOrDefault(c => c.ClientId == Target.ClientId); if (ingameClient != null) { await TempBan(Reason, length, ingameClient, Origin); return; } } #if !DEBUG else { string formattedKick = String.Format(RconParser.GetCommandPrefixes().Kick, Target.ClientNumber, $"^7{loc["SERVER_TB_TEXT"]}- ^5{Reason}"); await Target.CurrentServer.ExecuteCommandAsync(formattedKick); } #else await Target.CurrentServer.RemoveClient(Target.ClientNumber); #endif Penalty newPenalty = new Penalty() { Type = Penalty.PenaltyType.TempBan, Expires = DateTime.UtcNow + length, Offender = Target, Offense = Reason, Punisher = Origin, Active = true, When = DateTime.UtcNow, Link = Target.AliasLink }; await Manager.GetPenaltyService().Create(newPenalty); } override protected async Task Ban(String Message, EFClient Target, EFClient Origin) { // ensure player gets banned if command not performed on them in game if (Target.ClientNumber < 0) { EFClient ingameClient = null; ingameClient = Manager.GetServers() .Select(s => s.GetClientsAsList()) .FirstOrDefault(l => l.FirstOrDefault(c => c.ClientId == Target.ClientId) != null) ?.First(c => c.ClientId == Target.ClientId); if (ingameClient != null) { await Ban(Message, ingameClient, Origin); return; } } else { // this is set only because they're still in the server. Target.Level = EFClient.Permission.Banned; #if !DEBUG string formattedString = String.Format(RconParser.GetCommandPrefixes().Kick, Target.ClientNumber, $"{loc["SERVER_BAN_TEXT"]} - ^5{Message} ^7({loc["SERVER_BAN_APPEAL"]} {Website})^7"); await Target.CurrentServer.ExecuteCommandAsync(formattedString); #else await Target.CurrentServer.RemoveClient(Target.ClientNumber); #endif } Penalty newPenalty = new Penalty() { Type = Penalty.PenaltyType.Ban, Expires = null, Offender = Target, Offense = Message, Punisher = Origin, Active = true, When = DateTime.UtcNow, Link = Target.AliasLink, AutomatedOffense = Origin.AdministeredPenalties.FirstOrDefault()?.AutomatedOffense }; await Manager.GetPenaltyService().Create(newPenalty); // prevent them from logging in again Manager.GetPrivilegedClients().Remove(Target.ClientId); } override public async Task Unban(string reason, EFClient Target, EFClient Origin) { var unbanPenalty = new Penalty() { Type = Penalty.PenaltyType.Unban, Expires = null, Offender = Target, Offense = reason, Punisher = Origin, When = DateTime.UtcNow, Active = true, Link = Target.AliasLink }; await Manager.GetPenaltyService().RemoveActivePenalties(Target.AliasLink.AliasLinkId); await Manager.GetPenaltyService().Create(unbanPenalty); } override public void InitializeTokens() { Manager.GetMessageTokens().Add(new SharedLibraryCore.Helpers.MessageToken("TOTALPLAYERS", (Server s) => Manager.GetClientService().GetTotalClientsAsync().Result.ToString())); Manager.GetMessageTokens().Add(new SharedLibraryCore.Helpers.MessageToken("VERSION", (Server s) => Application.Program.Version.ToString())); Manager.GetMessageTokens().Add(new SharedLibraryCore.Helpers.MessageToken("NEXTMAP", (Server s) => SharedLibraryCore.Commands.CNextMap.GetNextMap(s).Result)); Manager.GetMessageTokens().Add(new SharedLibraryCore.Helpers.MessageToken("ADMINS", (Server s) => SharedLibraryCore.Commands.CListAdmins.OnlineAdmins(s))); } } }