diff --git a/Admin/Manager.cs b/Admin/Manager.cs index 7a9cc8b9..d4b5d920 100644 --- a/Admin/Manager.cs +++ b/Admin/Manager.cs @@ -28,7 +28,7 @@ namespace IW4MAdmin List TaskStatuses; List Commands; List MessageTokens; - Kayak.IScheduler webServiceTask; + WebService WebSvc; Thread WebThread; ClientService ClientSvc; AliasService AliasSvc; @@ -70,14 +70,8 @@ namespace IW4MAdmin { #region WEBSERVICE SharedLibrary.WebService.Init(); - webServiceTask = WebService.GetScheduler(); - - WebThread = new Thread(webServiceTask.Start) - { - Name = "Web Thread" - }; - - WebThread.Start(); + WebSvc = new WebService(); + WebSvc.StartScheduler(); #endregion #region PLUGINS @@ -182,6 +176,7 @@ namespace IW4MAdmin Commands.Add(new CPlugins()); Commands.Add(new CIP()); Commands.Add(new CMask()); + Commands.Add(new CPruneAdmins()); foreach (Command C in SharedLibrary.Plugins.PluginImporter.ActiveCommands) Commands.Add(C); @@ -192,19 +187,22 @@ namespace IW4MAdmin public void Start() { - while (Running) + while (Running || TaskStatuses.Count > 0) { for (int i = 0; i < TaskStatuses.Count; i++) { var Status = TaskStatuses[i]; + + // remove the task when we want to quit and last run has finished + if (!Running) + { + TaskStatuses.RemoveAt(i); + continue; + } + + // task is read to be rerun if (Status.RequestedTask == null || Status.RequestedTask.Status == TaskStatus.RanToCompletion) { - if (Status.ElapsedMillisecondsTime() > 60000) - { - Logger.WriteWarning($"Task took longer than 60 seconds to complete, killing"); - //Status.RequestedTask. - } - Status.Update(new Task(() => { return (Status.Dependant as Server).ProcessUpdatesAsync(Status.GetToken()).Result; })); if (Status.RunAverage > 1000 + UPDATE_FREQUENCY) Logger.WriteWarning($"Update task average execution is longer than desired for {(Status.Dependant as Server)} [{Status.RunAverage}ms]"); @@ -218,13 +216,16 @@ namespace IW4MAdmin S.Broadcast("^1IW4MAdmin going offline!"); #endif _servers.Clear(); - WebThread.Abort(); - webServiceTask.Stop(); + WebSvc.WebScheduler.Stop(); + WebSvc.SchedulerThread.Join(); } public void Stop() { + // tell threads it's time to stop + foreach (var status in TaskStatuses) + status.TokenSrc.Cancel(); Running = false; } diff --git a/Admin/Server.cs b/Admin/Server.cs index 601050d0..015b138a 100644 --- a/Admin/Server.cs +++ b/Admin/Server.cs @@ -15,11 +15,13 @@ namespace IW4MAdmin { public class IW4MServer : Server { + private CancellationToken cts; + public IW4MServer(IManager mgr, ServerConfiguration cfg) : base(mgr, cfg) { } public override int GetHashCode() { - return Math.Abs(IP.GetHashCode() + Port); + return Math.Abs($"{IP}:{Port.ToString()}".GetHashCode()); } override public async Task AddPlayer(Player polledPlayer) { @@ -32,7 +34,7 @@ namespace IW4MAdmin if (Players[polledPlayer.ClientNumber] != null && Players[polledPlayer.ClientNumber].NetworkId == polledPlayer.NetworkId) { - // update their ping + // update their ping & score Players[polledPlayer.ClientNumber].Ping = polledPlayer.Ping; Players[polledPlayer.ClientNumber].Score = polledPlayer.Score; return true; @@ -117,6 +119,17 @@ namespace IW4MAdmin Players[player.ClientNumber] = player; Logger.WriteInfo($"Client {player} connecting..."); + // give trusted rank + if (Config.AllowTrustedRank && + player.TotalConnectionTime / 60.0 >= 2880 && + player.Level < Player.Permission.Trusted && + player.Level != Player.Permission.Flagged) + { + player.Level = Player.Permission.Trusted; + await player.Tell("Congratulations, you are now a ^5trusted ^7player! Type ^5!help ^7to view new commands."); + await player.Tell("You earned this by playing for ^53 ^7full days!"); + } + await ExecuteEvent(new Event(Event.GType.Connect, "", player, null, this)); // if (NewPlayer.Level > Player.Permission.Moderator) @@ -261,7 +274,9 @@ namespace IW4MAdmin E.Target = matchingPlayers.First(); E.Data = Regex.Replace(E.Data, $"\"{E.Target.Name}\"", "", RegexOptions.IgnoreCase).Trim(); - if (E.Data.ToLower().Trim() == E.Target.Name.ToLower().Trim()) + if ((E.Data.ToLower().Trim() == E.Target.Name.ToLower().Trim() || + E.Data == String.Empty) && + C.RequiresTarget) { await E.Origin.Tell($"Not enough arguments supplied!"); await E.Origin.Tell(C.Syntax); @@ -284,8 +299,11 @@ namespace IW4MAdmin { E.Target = matchingPlayers.First(); E.Data = Regex.Replace(E.Data, $"{E.Target.Name}", "", RegexOptions.IgnoreCase).Trim(); + E.Data = Regex.Replace(E.Data, $"{Args[0]}", "", RegexOptions.IgnoreCase).Trim(); - if (E.Data.Trim() == E.Target.Name.ToLower().Trim()) + if ((E.Data.Trim() == E.Target.Name.ToLower().Trim() || + E.Data == String.Empty) && + C.RequiresTarget) { await E.Origin.Tell($"Not enough arguments supplied!"); await E.Origin.Tell(C.Syntax); @@ -316,6 +334,9 @@ namespace IW4MAdmin try #endif { + if (cts.IsCancellationRequested) + break; + await P.OnEventAsync(E, this); } #if !DEBUG @@ -362,6 +383,7 @@ namespace IW4MAdmin override public async Task ProcessUpdatesAsync(CancellationToken cts) { + this.cts = cts; #if DEBUG == false try #endif @@ -400,7 +422,12 @@ namespace IW4MAdmin if ((DateTime.Now - tickTime).TotalMilliseconds >= 1000) { foreach (var Plugin in SharedLibrary.Plugins.PluginImporter.ActivePlugins) + { + if (cts.IsCancellationRequested) + break; + await Plugin.OnTickAsync(this); + } tickTime = DateTime.Now; } @@ -459,6 +486,11 @@ namespace IW4MAdmin } oldLines = lines; l_size = LogFile.Length(); + if (!((ApplicationManager)Manager).Running) + { + foreach (var plugin in SharedLibrary.Plugins.PluginImporter.ActivePlugins) + await plugin.OnUnloadAsync(); + } return true; } #if DEBUG == false diff --git a/Admin/ServerConfigurationGenerator.cs b/Admin/ServerConfigurationGenerator.cs index f5abeb25..c888bdc6 100644 --- a/Admin/ServerConfigurationGenerator.cs +++ b/Admin/ServerConfigurationGenerator.cs @@ -16,6 +16,7 @@ namespace IW4MAdmin int Port = 0; string Password; bool AllowMultipleOwners; + bool AllowTrustedRank; while (IP == String.Empty) { @@ -53,7 +54,18 @@ namespace IW4MAdmin Console.Write("Allow multiple owners? [y/n]: "); AllowMultipleOwners = (Console.ReadLine().ToLower().FirstOrDefault() as char?) == 'y'; - var config = new ServerConfiguration() { IP = IP, Password = Password, Port = Port, AllowMultipleOwners = AllowMultipleOwners }; + Console.Write("Allow trusted rank? [y/n]: "); + AllowTrustedRank = (Console.ReadLine().ToLower().FirstOrDefault() as char?) == 'y'; + + var config = new ServerConfiguration() + { + IP = IP, + Password = Password, + Port = Port, + AllowMultipleOwners = AllowMultipleOwners, + AllowTrustedRank = AllowTrustedRank + }; + config.Write(); Console.Write("Configuration saved, add another? [y/n]:"); diff --git a/Admin/WebService.cs b/Admin/WebService.cs index 036c0bfc..6d118939 100644 --- a/Admin/WebService.cs +++ b/Admin/WebService.cs @@ -19,12 +19,14 @@ namespace IW4MAdmin { public class WebService { - public static IServer webService; + public IServer WebServer { get; private set; } + public IScheduler WebScheduler { get; private set; } + public Thread SchedulerThread { get; private set; } - public static IScheduler GetScheduler() + public void StartScheduler() { - var webScheduler = Kayak.KayakScheduler.Factory.Create(new Scheduler()); - webService = KayakServer.Factory.CreateHttp(new Request(), webScheduler); + WebScheduler = KayakScheduler.Factory.Create(new Scheduler()); + WebServer = KayakServer.Factory.CreateHttp(new Request(), WebScheduler); SharedLibrary.WebService.PageList.Add(new Pages()); SharedLibrary.WebService.PageList.Add(new Homepage()); @@ -40,13 +42,14 @@ namespace IW4MAdmin SharedLibrary.WebService.PageList.Add(new AdminsJSON()); SharedLibrary.WebService.PageList.Add(new Admins()); - Thread scheduleThread = new Thread(() => { ScheduleThreadStart(webScheduler, webService); }) + SchedulerThread = new Thread(() => { + ScheduleThreadStart(WebScheduler, WebServer); + }) { Name = "Web Service Thread" }; - scheduleThread.Start(); - return webScheduler; + SchedulerThread.Start(); } private static void ScheduleThreadStart(IScheduler S, IServer ss) diff --git a/Admin/config/messages.cfg b/Admin/config/messages.cfg index eef2903e..c72ab1b7 100644 --- a/Admin/config/messages.cfg +++ b/Admin/config/messages.cfg @@ -1,7 +1,8 @@ 60 +Over ^5{{TOTALPLAYTIME}} ^7man hours have been played on this server! This server uses ^5IW4M Admin v{{VERSION}} ^7get it at ^5raidmax.org/IW4MAdmin ^5IW4M Admin ^7sees ^5YOU! This server has harvested the information of ^5{{TOTALPLAYERS}} ^7players! Cheaters are ^1unwelcome ^7 on this server Did you know 8/10 people agree with unverified statistics? -^5{{TOTALKILLS}} ^7innocent people having been murdered in this server! \ No newline at end of file +^5{{TOTALKILLS}} ^7innocent people have been murdered in this server! \ No newline at end of file diff --git a/Admin/lib/SharedLibrary.dll b/Admin/lib/SharedLibrary.dll index 0a35b3e5..0552f463 100644 Binary files a/Admin/lib/SharedLibrary.dll and b/Admin/lib/SharedLibrary.dll differ diff --git a/Plugins/SimpleStats/Chat/ChatDatabase.cs b/Plugins/SimpleStats/Chat/ChatDatabase.cs deleted file mode 100644 index e3089b5b..00000000 --- a/Plugins/SimpleStats/Chat/ChatDatabase.cs +++ /dev/null @@ -1,188 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -using SharedLibrary; -using System.IO; -using System.Data; - -namespace StatsPlugin -{ - public class ChatDatabase : _Database - { - private string[] CommonWords = new string[] { "for", -"with", -"from", -"about", -"your", -"just", -"into", -"over", -"after", -"that", -"not", -"you", -"this", -"but", -"his", -"they", -"then", -"her", -"she", -"will", -"one", -"all", -"would", -"there", -"their", -"have", -"say", -"get", -"make", -"know", -"take", -"see", -"come", -"think", -"look", -"want", -"can", -"was", -"give", -"use", -"find", -"tell", -"ask", -"work", -"seem", -"feel", -"try", -"leave", -"call", -"good", -"new", -"first", -"last", -"long", -"great", -"little", -"own", -"other", -"old", -"right", -"big", -"high", -"small", -"large", -"next", -"early", -"young", -"important", -"few", -"public", -"same", -"able", -"the", -"and", -"that", -"than", -"have", -"this", -"one", -"would", - "yeah", - "yah", - "why", - "who" , - "when", - "where", - }; - - public ChatDatabase(string FN, SharedLibrary.Interfaces.ILogger logger) : base(FN, logger) - { - } - - public override void Init() - { - if (!File.Exists(FileName)) - { - string createChatHistory = @"CREATE TABLE `CHATHISTORY` ( - `ClientID` INTEGER NOT NULL, - `Message` TEXT NOT NULL, - `ServerID` INTEGER NOT NULL, - `TimeSent` TEXT NOT NULL - );"; - - ExecuteNonQuery(createChatHistory); - - string createChatStats = @"CREATE TABLE `WORDSTATS` ( - `Word` TEXT NOT NULL, - `Count` INTEGER NOT NULL DEFAULT 1, - PRIMARY KEY(`Word`) - );"; - - ExecuteNonQuery(createChatStats); - } - } - - private List GetChatHistoryFromQuery(DataTable dt) - { - return dt.Select().Select(q => new ChatHistory() - { - ClientID = Convert.ToInt32(q["ClientID"].ToString()), - Message = q["Message"].ToString(), - ServerID = Convert.ToInt32(q["ServerID"].ToString()), - TimeSent = DateTime.Parse(q["TimeSent"].ToString()) - }) - .ToList(); - } - - public List GetChatForPlayer(int clientID) - { - var queryResult = GetDataTable("CHATHISTORY", new KeyValuePair("ClientID", clientID)); - return GetChatHistoryFromQuery(queryResult); - } - - public List GetChatForServer(int serverID) - { - var queryResult = GetDataTable("CHATHISTORY", new KeyValuePair("ServerID", serverID)); - return GetChatHistoryFromQuery(queryResult); - } - - public void AddChatHistory(int clientID, int serverID, string message) - { - if (message.Length < 3) - return; - - var chat = new Dictionary() - { - { "ClientID", clientID }, - { "ServerID", serverID }, - { "Message", message}, - { "TimeSent", DateTime.UtcNow } - }; - - Insert("CHATHISTORY", chat); - - var eachWord = message.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries) - .Where (word => word.Length >= 3) - .Where(word => CommonWords.FirstOrDefault(c => c == word.ToLower()) == null) - .ToList(); - - foreach (string _word in eachWord) - { - string word = _word.ToLower(); - Insert("WORDSTATS", new Dictionary() { { "Word", word } }, true); - UpdateIncrement("WORDSTATS", "Count", new Dictionary() { { "Count", 1 } }, new KeyValuePair("Word", word)); - } - } - - public KeyValuePair[] GetWords() - { - var result = GetDataTable("SELECT * FROM WORDSTATS ORDER BY Count desc LIMIT 200"); - return result.Select().Select(w => new KeyValuePair(w["Word"].ToString(), Convert.ToInt32(w["Count"].ToString()))).ToArray(); - } - } -} diff --git a/Plugins/SimpleStats/Chat/ChatHistory.cs b/Plugins/SimpleStats/Chat/ChatHistory.cs deleted file mode 100644 index 46563b00..00000000 --- a/Plugins/SimpleStats/Chat/ChatHistory.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace StatsPlugin -{ - public class ChatHistory - { - public int ClientID { get; set; } - public string Message { get; set; } - public int ServerID { get; set; } - public DateTime TimeSent { get; set; } - } -} diff --git a/Plugins/SimpleStats/Chat/ChatHistoryPage.cs b/Plugins/SimpleStats/Chat/ChatHistoryPage.cs deleted file mode 100644 index 6dd91c1f..00000000 --- a/Plugins/SimpleStats/Chat/ChatHistoryPage.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -using SharedLibrary; -using System.Collections.Specialized; - -namespace StatsPlugin.Chat -{ - public class ChatPage : HTMLPage - { - public override string GetContent(NameValueCollection querySet, IDictionary headers) - { - StringBuilder S = new StringBuilder(); - S.Append(LoadHeader()); - - IFile chat = new IFile("webfront\\chat.html"); - S.Append(chat.GetText()); - chat.Close(); - - S.Append(LoadFooter()); - - return S.ToString(); - } - - public override string GetName() => "Word Cloud"; - public override string GetPath() => "/chat"; - } - - public class WordCloudJSON : IPage - { - public string GetName() => "Word Cloud JSON"; - public string GetPath() => "/_words"; - public string GetContentType() => "application/json"; - public bool Visible() => false; - - public HttpResponse GetPage(NameValueCollection querySet, IDictionary headers) - { - - HttpResponse resp = new HttpResponse() - { - contentType = GetContentType(), - content = Stats.ChatDB.GetWords().Select(w => new - { - Word = w.Key, - Count = w.Value - }) - .OrderByDescending(x => x.Count) - .ToArray(), - - additionalHeaders = new Dictionary() - }; - - return resp; - } - } - - public class ClientChatJSON : IPage - { - public string GetName() => "Client Chat JSON"; - public string GetPath() => "/_clientchat"; - public string GetContentType() => "application/json"; - public bool Visible() => false; - - public HttpResponse GetPage(NameValueCollection querySet, IDictionary headers) - { - int clientID = Convert.ToInt32(querySet["clientid"]); - var name = Stats.ManagerInstance.GetDatabase().GetClient(clientID).Name; - - HttpResponse resp = new HttpResponse() - { - contentType = GetContentType(), - content = Stats.ChatDB.GetChatForPlayer(clientID).ToArray().Select(c => new - { - ClientID = c.ClientID, - ServerID = c.ServerID, - Message = c.Message, - TimeSent = c.TimeSent, - ClientName = name, - }), - additionalHeaders = new Dictionary() - }; - - return resp; - } - } -} diff --git a/Plugins/SimpleStats/Commands/ResetStats.cs b/Plugins/SimpleStats/Commands/ResetStats.cs new file mode 100644 index 00000000..8d41e6f1 --- /dev/null +++ b/Plugins/SimpleStats/Commands/ResetStats.cs @@ -0,0 +1,40 @@ +using SharedLibrary; +using SharedLibrary.Objects; +using StatsPlugin.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StatsPlugin.Commands +{ + + public class ResetStats : Command + { + public ResetStats() : base("resetstats", "reset your stats to factory-new", "rs", Player.Permission.User, false) { } + + public override async Task ExecuteAsync(Event E) + { + if (E.Origin.ClientNumber >= 0) + { + var svc = new SharedLibrary.Services.GenericRepository(); + var stats = svc.Find(s => s.ClientId == E.Origin.ClientId).First(); + + stats.Deaths = 0; + stats.Kills = 0; + stats.SPM = 0; + stats.Skill = 0; + + // fixme: this doesn't work properly when another context exists + await svc.SaveChangesAsync(); + await E.Origin.Tell("Your stats have been reset"); + } + + else + { + await E.Origin.Tell("You must be connected to a server to reset your stats"); + } + } + } +} diff --git a/Plugins/SimpleStats/Commands/TopStats.cs b/Plugins/SimpleStats/Commands/TopStats.cs new file mode 100644 index 00000000..70e44d3c --- /dev/null +++ b/Plugins/SimpleStats/Commands/TopStats.cs @@ -0,0 +1,43 @@ +using SharedLibrary; +using SharedLibrary.Objects; +using SharedLibrary.Services; +using StatsPlugin.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StatsPlugin.Commands +{ + class TopStats : Command + { + public TopStats() : base("topstats", "view the top 5 players on this server", "ts", Player.Permission.User, false) { } + + public override async Task ExecuteAsync(Event E) + { + var statsSvc = new GenericRepository(); + var iqStats = statsSvc.GetQuery(cs => cs.Active); + + var topStats = iqStats.Where(cs => cs.Skill > 100) + .OrderByDescending(cs => cs.Skill) + .Take(5) + .ToList(); + + if (!E.Message.IsBroadcastCommand()) + { + await E.Origin.Tell("^5--Top Players--"); + + foreach (var stat in topStats) + await E.Origin.Tell($"^3{stat.Client.Name}^7 - ^5{stat.KDR} ^7KDR | ^5{stat.Skill} ^7SKILL"); + } + else + { + await E.Owner.Broadcast("^5--Top Players--"); + + foreach (var stat in topStats) + await E.Owner.Broadcast($"^3{stat.Client.Name}^7 - ^5{stat.KDR} ^7KDR | ^5{stat.Skill} ^7SKILL"); + } + } + } +} diff --git a/Plugins/SimpleStats/Commands/ViewStats.cs b/Plugins/SimpleStats/Commands/ViewStats.cs new file mode 100644 index 00000000..fe52f3be --- /dev/null +++ b/Plugins/SimpleStats/Commands/ViewStats.cs @@ -0,0 +1,72 @@ +using SharedLibrary; +using SharedLibrary.Objects; +using SharedLibrary.Services; +using StatsPlugin.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StatsPlugin.Commands +{ + public class CViewStats : Command + { + public CViewStats() : base("stats", "view your stats", "xlrstats", Player.Permission.User, false, new CommandArgument[] + { + new CommandArgument() + { + Name = "player", + Required = false + } + }) + { } + + public override async Task ExecuteAsync(Event E) + { + + if (E.Origin.ClientNumber < 0) + { + await E.Origin.Tell("You must be ingame to view your stats"); + return; + } + + String statLine; + EFClientStatistics pStats; + + if (E.Data.Length > 0 && E.Target == null) + { + await E.Origin.Tell("Cannot find the player you specified"); + return; + } + + var clientStats = new GenericRepository(); + + if (E.Target != null) + { + pStats = clientStats.Find(c => c.ClientId == E.Target.ClientId).First(); + statLine = String.Format("^5{0} ^7KILLS | ^5{1} ^7DEATHS | ^5{2} ^7KDR | ^5{3} ^7SKILL", pStats.Kills, pStats.Deaths, pStats.KDR, pStats.Skill); + } + + else + { + pStats = pStats = clientStats.Find(c => c.ClientId == E.Origin.ClientId).First(); + statLine = String.Format("^5{0} ^7KILLS | ^5{1} ^7DEATHS | ^5{2} ^7KDR | ^5{3} ^7SKILL", pStats.Kills, pStats.Deaths, pStats.KDR, pStats.Skill); + } + + if (E.Message.IsBroadcastCommand()) + { + string name = E.Target == null ? E.Origin.Name : E.Target.Name; + await E.Owner.Broadcast($"Stats for ^5{name}^7"); + await E.Owner.Broadcast(statLine); + } + + else + { + if (E.Target != null) + await E.Origin.Tell($"Stats for ^5{E.Target.Name}^7"); + await E.Origin.Tell(statLine); + } + } + } +} diff --git a/Plugins/SimpleStats/Helpers/StatManager.cs b/Plugins/SimpleStats/Helpers/StatManager.cs index 1e6e63b0..fcc305bb 100644 --- a/Plugins/SimpleStats/Helpers/StatManager.cs +++ b/Plugins/SimpleStats/Helpers/StatManager.cs @@ -14,22 +14,16 @@ namespace StatsPlugin.Helpers public class StatManager { private Dictionary Servers; + private Dictionary ContextThreads; private ILogger Log; private IManager Manager; - private GenericRepository ClientStatSvc; - private GenericRepository ServerSvc; - private GenericRepository KillStatsSvc; - private GenericRepository ServerStatsSvc; public StatManager(IManager mgr) { Servers = new Dictionary(); + ContextThreads = new Dictionary(); Log = mgr.GetLogger(); Manager = mgr; - ClientStatSvc = new GenericRepository(); - ServerSvc = new GenericRepository(); - KillStatsSvc = new GenericRepository(); - ServerStatsSvc = new GenericRepository(); } ~StatManager() @@ -48,9 +42,11 @@ namespace StatsPlugin.Helpers try { int serverId = sv.GetHashCode(); + var statsSvc = new ThreadSafeStatsService(); + ContextThreads.Add(serverId, statsSvc); // get the server from the database if it exists, otherwise create and insert a new one - var server = ServerSvc.Find(c => c.ServerId == serverId).FirstOrDefault(); + var server = statsSvc.ServerSvc.Find(c => c.ServerId == serverId).FirstOrDefault(); if (server == null) { server = new EFServer() @@ -60,17 +56,16 @@ namespace StatsPlugin.Helpers ServerId = serverId }; - ServerSvc.Insert(server); + statsSvc.ServerSvc.Insert(server); } // this doesn't need to be async as it's during initialization - ServerSvc.SaveChanges(); - InitializeServerStats(sv); - ServerStatsSvc.SaveChanges(); - - var serverStats = ServerStatsSvc.Find(c => c.ServerId == serverId).FirstOrDefault(); + statsSvc.ServerSvc.SaveChanges(); // check to see if the stats have ever been initialized + InitializeServerStats(sv); + statsSvc.ServerStatsSvc.SaveChanges(); + var serverStats = statsSvc.ServerStatsSvc.Find(c => c.ServerId == serverId).FirstOrDefault(); Servers.Add(serverId, new ServerStats(server, serverStats)); } @@ -89,10 +84,11 @@ namespace StatsPlugin.Helpers { int serverId = pl.CurrentServer.GetHashCode(); var playerStats = Servers[serverId].PlayerStats; + var statsSvc = ContextThreads[serverId]; // get the client's stats from the database if it exists, otherwise create and attach a new one // if this fails we want to throw an exception - var clientStats = ClientStatSvc.Find(c => c.ClientId == pl.ClientId && c.ServerId == serverId).FirstOrDefault(); + var clientStats = statsSvc.ClientStatSvc.Find(c => c.ClientId == pl.ClientId && c.ServerId == serverId).FirstOrDefault(); if (clientStats == null) { clientStats = new EFClientStatistics() @@ -106,7 +102,7 @@ namespace StatsPlugin.Helpers SPM = 0.0, }; - clientStats = ClientStatSvc.Insert(clientStats); + clientStats = statsSvc.ClientStatSvc.Insert(clientStats); } // set these on connecting @@ -125,17 +121,32 @@ namespace StatsPlugin.Helpers return clientStats; } + /// + /// Perform stat updates for disconnecting client + /// + /// Disconnecting client + /// public async Task RemovePlayer(Player pl) { int serverId = pl.CurrentServer.GetHashCode(); var playerStats = Servers[serverId].PlayerStats; + var serverStats = Servers[serverId].ServerStatistics; + var statsSvc = ContextThreads[serverId]; + // get individual client's stats var clientStats = playerStats[pl.ClientNumber]; // remove the client from the stats dictionary as they're leaving lock (playerStats) playerStats.Remove(pl.ClientNumber); - var serverStats = ServerStatsSvc.Find(sv => sv.ServerId == serverId).FirstOrDefault(); + // sync their stats before they leave + clientStats.Client = pl; + UpdateStats(clientStats); + clientStats.Client = null; + + // todo: should this be saved every disconnect? + await statsSvc.ClientStatSvc.SaveChangesAsync(); + // increment the total play time serverStats.TotalPlayTime += (int)(DateTime.UtcNow - pl.LastConnection).TotalSeconds; } @@ -148,6 +159,8 @@ namespace StatsPlugin.Helpers { AddStandardKill(attacker, victim); + var statsSvc = ContextThreads[serverId]; + var kill = new EFClientKill() { Active = true, @@ -163,8 +176,8 @@ namespace StatsPlugin.Helpers Weapon = ParseEnum.Get(weapon, typeof(IW4Info.WeaponName)) }; - KillStatsSvc.Insert(kill); - await KillStatsSvc.SaveChangesAsync(); + statsSvc.KillStatsSvc.Insert(kill); + await statsSvc.KillStatsSvc.SaveChangesAsync(); } public void AddStandardKill(Player attacker, Player victim) @@ -183,8 +196,9 @@ namespace StatsPlugin.Helpers // immediately write changes in debug #if DEBUG - ClientStatSvc.SaveChanges(); - ServerStatsSvc.SaveChanges(); + var statsSvc = ContextThreads[serverId]; + statsSvc.ClientStatSvc.SaveChanges(); + statsSvc.ServerStatsSvc.SaveChanges(); #endif } @@ -262,7 +276,9 @@ namespace StatsPlugin.Helpers public void InitializeServerStats(Server sv) { int serverId = sv.GetHashCode(); - var serverStats = ServerStatsSvc.Find(s => s.ServerId == serverId).FirstOrDefault(); + var statsSvc = ContextThreads[serverId]; + + var serverStats = statsSvc.ServerStatsSvc.Find(s => s.ServerId == serverId).FirstOrDefault(); if (serverStats == null) { Log.WriteDebug($"Initializing server stats for {sv}"); @@ -275,29 +291,46 @@ namespace StatsPlugin.Helpers TotalPlayTime = 0, }; - var ieClientStats = ClientStatSvc.Find(cs => cs.ServerId == serverId); + var ieClientStats = statsSvc.ClientStatSvc.Find(cs => cs.ServerId == serverId); // set these incase they've we've imported settings serverStats.TotalKills = ieClientStats.Sum(cs => cs.Kills); serverStats.TotalPlayTime = Manager.GetClientService().GetTotalPlayTime().Result; - ServerStatsSvc.Insert(serverStats); + statsSvc.ServerStatsSvc.Insert(serverStats); } } - public async Task Sync() + public async Task AddMessageAsync(int clientId, int serverId, string message) { + var messageSvc = ContextThreads[serverId].MessageSvc; + messageSvc.Insert(new EFClientMessage() + { + Active = true, + ClientId = clientId, + Message = message, + ServerId = serverId, + TimeSent = DateTime.UtcNow + }); + await messageSvc.SaveChangesAsync(); + } + + public async Task Sync(Server sv) + { + int serverId = sv.GetHashCode(); + var statsSvc = ContextThreads[serverId]; + Log.WriteDebug("Syncing server stats"); - await ServerStatsSvc.SaveChangesAsync(); + await statsSvc.ServerStatsSvc.SaveChangesAsync(); Log.WriteDebug("Syncing client stats"); - await ClientStatSvc.SaveChangesAsync(); + await statsSvc.ClientStatSvc.SaveChangesAsync(); Log.WriteDebug("Syncing kill stats"); - await KillStatsSvc.SaveChangesAsync(); + await statsSvc.KillStatsSvc.SaveChangesAsync(); Log.WriteDebug("Syncing servers"); - await ServerSvc.SaveChangesAsync(); + await statsSvc.ServerSvc.SaveChangesAsync(); } } } diff --git a/Plugins/SimpleStats/Helpers/ThreadSafeStatsService.cs b/Plugins/SimpleStats/Helpers/ThreadSafeStatsService.cs new file mode 100644 index 00000000..d8063b2e --- /dev/null +++ b/Plugins/SimpleStats/Helpers/ThreadSafeStatsService.cs @@ -0,0 +1,29 @@ +using SharedLibrary.Services; +using StatsPlugin.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StatsPlugin.Helpers +{ + public class ThreadSafeStatsService + { + + public GenericRepository ClientStatSvc { get; private set; } + public GenericRepository ServerSvc { get; private set; } + public GenericRepository KillStatsSvc { get; private set; } + public GenericRepository ServerStatsSvc { get; private set; } + public GenericRepository MessageSvc { get; private set; } + + public ThreadSafeStatsService() + { + ClientStatSvc = new GenericRepository(); + ServerSvc = new GenericRepository(); + KillStatsSvc = new GenericRepository(); + ServerStatsSvc = new GenericRepository(); + MessageSvc = new GenericRepository(); + } + } +} diff --git a/Plugins/SimpleStats/Models/EFClientMessage.cs b/Plugins/SimpleStats/Models/EFClientMessage.cs new file mode 100644 index 00000000..b6e466f4 --- /dev/null +++ b/Plugins/SimpleStats/Models/EFClientMessage.cs @@ -0,0 +1,25 @@ +using SharedLibrary.Database.Models; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StatsPlugin.Models +{ + public class EFClientMessage : SharedEntity + { + [Key] + public long MessageId { get; set; } + public int ServerId { get; set; } + [ForeignKey("ServerId")] + public virtual EFServer Server { get; set; } + public int ClientId { get; set; } + [ForeignKey("ClientId")] + public virtual EFClient Client { get; set; } + public string Message { get; set; } + public DateTime TimeSent { get; set; } + } +} diff --git a/Plugins/SimpleStats/Pages/ClientMessageJson.cs b/Plugins/SimpleStats/Pages/ClientMessageJson.cs new file mode 100644 index 00000000..ff6dcc0f --- /dev/null +++ b/Plugins/SimpleStats/Pages/ClientMessageJson.cs @@ -0,0 +1,44 @@ +using SharedLibrary; +using SharedLibrary.Database.Models; +using SharedLibrary.Services; +using StatsPlugin.Models; +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StatsPlugin.Pages +{ + public class ClientMessageJson : IPage + { + public string GetName() => "Client Chat JSON"; + public string GetPath() => "/_clientchat"; + public string GetContentType() => "application/json"; + public bool Visible() => false; + + public async Task GetPage(NameValueCollection querySet, IDictionary headers) + { + int clientId = Convert.ToInt32(querySet["clientid"]); + var messageSvc = new GenericRepository(); + var clientMessages = (await messageSvc.FindAsync(m => m.ClientId == clientId)); + + HttpResponse resp = new HttpResponse() + { + contentType = GetContentType(), + content = clientMessages.Select(c => new + { + ClientID = c.ClientId, + ServerID = c.ServerId, + c.Message, + c.TimeSent, + ClientName = c.Client.Name, + }), + additionalHeaders = new Dictionary() + }; + + return resp; + } + } +} diff --git a/Plugins/SimpleStats/Pages/ClientMessages.cs b/Plugins/SimpleStats/Pages/ClientMessages.cs new file mode 100644 index 00000000..cd23b379 --- /dev/null +++ b/Plugins/SimpleStats/Pages/ClientMessages.cs @@ -0,0 +1,30 @@ +using SharedLibrary; +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StatsPlugin.Pages +{ + public class ClientMessages : HTMLPage + { + public override string GetContent(NameValueCollection querySet, IDictionary headers) + { + StringBuilder S = new StringBuilder(); + S.Append(LoadHeader()); + + IFile chat = new IFile("webfront\\chat.html"); + S.Append(chat.GetText()); + chat.Close(); + + S.Append(LoadFooter()); + + return S.ToString(); + } + + public override string GetName() => "Word Cloud"; + public override string GetPath() => "/chat"; + } +} diff --git a/Plugins/SimpleStats/Pages/LiveStats.cs b/Plugins/SimpleStats/Pages/LiveStats.cs new file mode 100644 index 00000000..aee30f0d --- /dev/null +++ b/Plugins/SimpleStats/Pages/LiveStats.cs @@ -0,0 +1,30 @@ +using SharedLibrary; +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StatsPlugin.Pages +{ + public class LiveStats : HTMLPage + { + public override string GetContent(NameValueCollection querySet, IDictionary headers) + { + StringBuilder S = new StringBuilder(); + S.Append(LoadHeader()); + + IFile stats = new IFile("webfront\\stats.html"); + S.Append(stats.GetText()); + stats.Close(); + + S.Append(LoadFooter()); + + return S.ToString(); + } + + public override string GetName() => "Server Stats"; + public override string GetPath() => "/stats"; + } +} diff --git a/Plugins/SimpleStats/StatsPage.cs b/Plugins/SimpleStats/Pages/LiveStatsJson.cs similarity index 66% rename from Plugins/SimpleStats/StatsPage.cs rename to Plugins/SimpleStats/Pages/LiveStatsJson.cs index 6786a3c8..e791df0e 100644 --- a/Plugins/SimpleStats/StatsPage.cs +++ b/Plugins/SimpleStats/Pages/LiveStatsJson.cs @@ -1,45 +1,25 @@ -using System; +using SharedLibrary; +using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using System.Text; using System.Threading.Tasks; -using SharedLibrary; - -namespace StatsPlugin +namespace StatsPlugin.Pages { - public class StatsPage : HTMLPage - { - public override string GetContent(NameValueCollection querySet, IDictionary headers) - { - StringBuilder S = new StringBuilder(); - S.Append(LoadHeader()); - - IFile stats = new IFile("webfront\\stats.html"); - S.Append(stats.GetText()); - stats.Close(); - - S.Append(LoadFooter()); - - return S.ToString(); - } - - public override string GetName() => "Stats"; - public override string GetPath() => "/stats"; - } - - class KillStatsJSON : IPage + class LiveStatsJson : IPage { public string GetName() => "Kill Stats JSON"; public string GetPath() => "/_killstats"; public string GetContentType() => "application/json"; public bool Visible() => false; - public HttpResponse GetPage(NameValueCollection querySet, IDictionary headers) + public async Task GetPage(NameValueCollection querySet, IDictionary headers) { - - int selectCount = Stats.MAX_KILLEVENTS; + // todo: redo this + return await Task.FromResult(new HttpResponse()); + /*int selectCount = Stats.MAX_KILLEVENTS; if (querySet.Get("count") != null) selectCount = Int32.Parse(querySet.Get("count")); @@ -62,7 +42,7 @@ namespace StatsPlugin }, additionalHeaders = new Dictionary() }; - return resp; + return resp;*/ } } } diff --git a/Plugins/SimpleStats/Pages/WordCloudJson.cs b/Plugins/SimpleStats/Pages/WordCloudJson.cs new file mode 100644 index 00000000..614d4f7c --- /dev/null +++ b/Plugins/SimpleStats/Pages/WordCloudJson.cs @@ -0,0 +1,32 @@ +using SharedLibrary; +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StatsPlugin.Pages +{ + + public class WordCloudJson : IPage + { + public string GetName() => "Word Cloud JSON"; + public string GetPath() => "/_words"; + public string GetContentType() => "application/json"; + public bool Visible() => false; + + public async Task GetPage(NameValueCollection querySet, IDictionary headers) + { + // todo: this + HttpResponse resp = new HttpResponse() + { + contentType = GetContentType(), + content = null, + additionalHeaders = new Dictionary() + }; + + return resp; + } + } +} diff --git a/Plugins/SimpleStats/Plugin.cs b/Plugins/SimpleStats/Plugin.cs index 412571cc..bc6ff099 100644 --- a/Plugins/SimpleStats/Plugin.cs +++ b/Plugins/SimpleStats/Plugin.cs @@ -10,6 +10,7 @@ using SharedLibrary.Interfaces; using SharedLibrary.Services; using StatsPlugin.Helpers; using StatsPlugin.Models; +using StatsPlugin.Pages; namespace StatsPlugin { @@ -22,6 +23,7 @@ namespace StatsPlugin public string Author => "RaidMax"; private StatManager Manager; + private IManager ServerManager; public async Task OnEventAsync(Event E, Server S) { @@ -39,11 +41,13 @@ namespace StatsPlugin await Manager.RemovePlayer(E.Origin); break; case Event.GType.Say: + if (E.Data != string.Empty) + await Manager.AddMessageAsync(E.Origin.ClientId, E.Owner.GetHashCode(), E.Data); break; case Event.GType.MapChange: break; case Event.GType.MapEnd: - await Manager.Sync(); + await Manager.Sync(S); break; case Event.GType.Broadcast: break; @@ -66,7 +70,7 @@ namespace StatsPlugin case Event.GType.Kill: string[] killInfo = (E.Data != null) ? E.Data.Split(';') : new string[0]; if (killInfo.Length >= 9 && killInfo[0].Contains("ScriptKill")) - await Manager.AddScriptKill(E.Origin, E.Target, S.GetHashCode(), S.CurrentMap.Name, killInfo[7], killInfo[8], killInfo[5], killInfo[6], killInfo[3], killInfo[4]); + await Manager.AddScriptKill(E.Origin, E.Target, S.GetHashCode(), S.CurrentMap.Name, killInfo[7], killInfo[8], killInfo[5], killInfo[6], killInfo[3], killInfo[4]); break; case Event.GType.Death: break; @@ -75,15 +79,11 @@ namespace StatsPlugin public Task OnLoadAsync(IManager manager) { - /* - * - ManagerInstance.GetMessageTokens().Add(new MessageToken("TOTALKILLS", GetTotalKills)); - ManagerInstance.GetMessageTokens().Add(new MessageToken("TOTALPLAYTIME", GetTotalPlaytime)); -*/ + // todo: is this fast? string totalKills() { var serverStats = new GenericRepository(); - return serverStats.GetQuery(s => s.Active) + return serverStats.Find(s => s.Active) .Sum(c => c.TotalKills).ToString(); } @@ -96,6 +96,13 @@ namespace StatsPlugin manager.GetMessageTokens().Add(new MessageToken("TOTALKILLS", totalKills)); manager.GetMessageTokens().Add(new MessageToken("TOTALPLAYTIME", totalPlayTime)); + + WebService.PageList.Add(new ClientMessageJson()); + WebService.PageList.Add(new ClientMessages()); + WebService.PageList.Add(new LiveStats()); + + ServerManager = manager; + return Task.FromResult( Manager = new StatManager(manager) ); @@ -103,14 +110,13 @@ namespace StatsPlugin public async Task OnTickAsync(Server S) { - + } - public Task OnUnloadAsync() + public async Task OnUnloadAsync() { - return Task.FromResult( - Manager = null - ); + foreach (var sv in ServerManager.GetServers()) + await Manager.Sync(sv); } } } diff --git a/Plugins/SimpleStats/StatsPlugin.csproj b/Plugins/SimpleStats/StatsPlugin.csproj index f86f7e4c..81cfb2d5 100644 --- a/Plugins/SimpleStats/StatsPlugin.csproj +++ b/Plugins/SimpleStats/StatsPlugin.csproj @@ -67,23 +67,28 @@ - - - + + + + + + + + + + - - diff --git a/Plugins/SimpleStats/TrustedGroupCommands.cs b/Plugins/SimpleStats/TrustedGroupCommands.cs deleted file mode 100644 index 98e7b540..00000000 --- a/Plugins/SimpleStats/TrustedGroupCommands.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -using SharedLibrary; -using SharedLibrary.Interfaces; -using SharedLibrary.Helpers; - -namespace StatsPlugin -{ - public class CEnableTrusted : Command - { - public CEnableTrusted() : base("enabletrusted", "enable trusted player group for the server", "et", Player.Permission.Owner, false) { } - - public override async Task ExecuteAsync(Event E) - { - var config = new ConfigurationManager(E.Owner); - if (config.GetProperty("EnableTrusted") == null) - config.AddProperty(new KeyValuePair("EnableTrusted", true)); - else - config.UpdateProperty(new KeyValuePair("EnableTrusted", true)); - - await E.Origin.Tell("Trusted group has been disabled for this server"); - } - } - - public class CDisableTrusted : Command - { - public CDisableTrusted() : base("disabletrusted", "disable trusted player group for the server", "dt", Player.Permission.Owner, false) { } - - public override async Task ExecuteAsync(Event E) - { - var config = new ConfigurationManager(E.Owner); - if (config.GetProperty("EnableTrusted") == null) - config.AddProperty(new KeyValuePair("EnableTrusted", false)); - else - config.UpdateProperty(new KeyValuePair("EnableTrusted", false)); - - await E.Origin.Tell("Trusted group has been disabled for this server"); - } - } -} diff --git a/Plugins/SimpleStats/_Plugin.cs b/Plugins/SimpleStats/_Plugin.cs index d426bfc1..59cce204 100644 --- a/Plugins/SimpleStats/_Plugin.cs +++ b/Plugins/SimpleStats/_Plugin.cs @@ -13,145 +13,6 @@ using StatsPlugin.Models; namespace StatsPlugin { - public class CViewStats : Command - { - public CViewStats() : base("stats", "view your stats", "xlrstats", Player.Permission.User, false, new CommandArgument[] - { - new CommandArgument() - { - Name = "player", - Required = false - } - }) - { } - - public override async Task ExecuteAsync(Event E) - { - - if (E.Origin.ClientNumber < 0) - { - await E.Origin.Tell("You must be ingame to view your stats"); - return; - } - - String statLine; - EFClientStatistics pStats; - - if (E.Data.Length > 0 && E.Target == null) - { - await E.Origin.Tell("Cannot find the player you specified"); - return; - } - - if (E.Target != null) - { - pStats = Stats.statLists.Find(x => x.Port == E.Owner.GetPort()).clientStats[E.Origin.ClientNumber]; - statLine = String.Format("^5{0} ^7KILLS | ^5{1} ^7DEATHS | ^5{2} ^7KDR | ^5{3} ^7SKILL", pStats.Kills, pStats.Deaths, pStats.KDR, pStats.Skill); - } - - else - { - pStats = Stats.statLists.Find(x => x.Port == E.Owner.GetPort()).clientStats[E.Origin.ClientNumber]; - statLine = String.Format("^5{0} ^7KILLS | ^5{1} ^7DEATHS | ^5{2} ^7KDR | ^5{3} ^7SKILL", pStats.Kills, pStats.Deaths, pStats.KDR, pStats.Skill); - } - - if (E.Message.IsBroadcastCommand()) - { - string name = E.Target == null ? E.Origin.Name : E.Target.Name; - await E.Owner.Broadcast($"Stats for ^5{name}^7"); - await E.Owner.Broadcast(statLine); - } - - else - { - if (E.Target != null) - await E.Origin.Tell($"Stats for ^5{E.Target.Name}^7"); - await E.Origin.Tell(statLine); - } - } - } - - public class CViewTopStats : Command - { - public CViewTopStats() : - base("topstats", "view the top 5 players on this server", "ts", Player.Permission.User, false) - { } - - public override async Task ExecuteAsync(Event E) - { - List> pStats = Stats.statLists.Find(x => x.Port == E.Owner.GetPort()).playerStats.GetTopStats(); - StringBuilder msgBlder = new StringBuilder(); - - await E.Origin.Tell("^5--Top Players--"); - foreach (KeyValuePair pStat in pStats) - { - /* Player P = E.Owner.Manager.GetDatabase().GetClient(pStat.Key) as Player; - if (P == null) - continue; - await E.Origin.Tell(String.Format("^3{0}^7 - ^5{1} ^7KDR | ^5{2} ^7SKILL", P.Name, pStat.Value.KDR, pStat.Value.Skill));*/ - } - } - } - - - public class CResetStats : Command - { - public CResetStats() : base("resetstats", "reset your stats to factory-new", "rs", Player.Permission.User, false) { } - - public override async Task ExecuteAsync(Event E) - { - if (E.Origin.ClientNumber >= 0) - { - var svc = new SharedLibrary.Services.GenericService(); - var stats = Stats.statLists[E.Owner.GetPort()].clientStats[E.Origin.ClientNumber]; - await svc.Delete(stats); - await E.Origin.Tell("Your stats have been reset"); - } - - else - { - await E.Origin.Tell("You must be connected to a server to reset your stats"); - } - } - } - - public class CPruneAdmins : Command - { - public CPruneAdmins() : base("prune", "demote any admins that have not connected recently (defaults to 30 days)", "p", Player.Permission.Owner, false, new CommandArgument[] - { - new CommandArgument() - { - Name = "inactive days", - Required = false - } - }) - { } - - public override async Task ExecuteAsync(Event E) - { - int inactiveDays = 30; - - try - { - if (E.Data.Length > 0) - { - inactiveDays = Int32.Parse(E.Data); - if (inactiveDays < 1) - throw new FormatException(); - } - } - - catch (FormatException) - { - await E.Origin.Tell("Invalid number of inactive days"); - return; - } - - var inactiveAdmins = await E.Owner.Manager.GetDatabase().PruneInactivePrivilegedClients(inactiveDays); - await E.Origin.Tell($"Pruned inactive {inactiveAdmins.Count} privileged users"); - - } - } /// /// Each server runs from the same plugin ( for easier reloading and reduced memory usage ). @@ -181,63 +42,7 @@ namespace StatsPlugin public Queue GetKillQueue() { return KillQueue; } } - public class KillInfo - { - public IW4Info.HitLocation HitLoc { get; set; } - public string HitLocString => HitLoc.ToString(); - public IW4Info.MeansOfDeath DeathType { get; set; } - public string DeathTypeString => DeathType.ToString(); - public int Damage { get; set; } - public IW4Info.WeaponName Weapon { get; set; } - public string WeaponString => Weapon.ToString(); - public Vector3 KillOrigin { get; set; } - public Vector3 DeathOrigin { get; set; } - // http://wiki.modsrepository.com/index.php?title=Call_of_Duty_5:_Gameplay_standards for conversion to meters - public double Distance => Vector3.Distance(KillOrigin, DeathOrigin) * 0.0254; - public string KillerPlayer { get; set; } - public int KillerPlayerID { get; set; } - public string VictimPlayer { get; set; } - public int VictimPlayerID { get; set; } - public IW4Info.MapName Map { get; set; } - public int ID => GetHashCode(); - - public KillInfo() { } - - public KillInfo(int killer, int victim, string map, string hit, string type, string damage, string weapon, string kOrigin, string dOrigin) - { - KillerPlayerID = killer; - VictimPlayerID = victim; - Map = ParseEnum.Get(map, typeof(IW4Info.MapName)); - HitLoc = ParseEnum.Get(hit, typeof(IW4Info.HitLocation)); - DeathType = ParseEnum.Get(type, typeof(IW4Info.MeansOfDeath)); - Damage = Int32.Parse(damage); - Weapon = ParseEnum.Get(weapon, typeof(IW4Info.WeaponName)); - KillOrigin = Vector3.Parse(kOrigin); - DeathOrigin = Vector3.Parse(dOrigin); - } - } - - public static List statLists; - - public class StatTracking - { - public DateTime[] lastKill, connectionTime; - public int[] inactiveMinutes, Kills, deathStreaks, killStreaks; - public int Port; - public Models.EFClientStatistics[] clientStats; - - public StatTracking(int port) - { - clientStats = new Models.EFClientStatistics[18]; - inactiveMinutes = new int[18]; - Kills = new int[18]; - deathStreaks = new int[18]; - killStreaks = new int[18]; - lastKill = new DateTime[18]; - connectionTime = new DateTime[18]; - Port = port; - } - } + public string Name => "Basic Stats"; @@ -365,214 +170,9 @@ namespace StatsPlugin ServerStats[S.GetPort()].GetKillQueue().Clear(); ServerStats[S.GetPort()].RoundStartTime = DateTime.Now; } - - if (E.Type == Event.GType.Disconnect) - { - CalculateAndSaveSkill(E.Origin, statLists.Find(x => x.Port == S.GetPort())); - ResetCounters(E.Origin.ClientNumber, S.GetPort()); - E.Owner.Logger.WriteInfo($"Updated skill for disconnecting client {E.Origin}"); - } - - if (E.Type == Event.GType.Kill) - { - if (E.Origin == E.Target || E.Origin == null) - return; - - string[] killInfo = (E.Data != null) ? E.Data.Split(';') : new string[0]; - - if (killInfo.Length >= 9 && killInfo[0].Contains("ScriptKill")) - { - var killEvent = new KillInfo(E.Origin.ClientNumber, E.Target.ClientNumber, S.CurrentMap.Name, killInfo[7], killInfo[8], killInfo[5], killInfo[6], killInfo[3], killInfo[4]) - { - KillerPlayer = E.Origin.Name, - VictimPlayer = E.Target.Name, - }; - - if (ServerStats[S.GetPort()].GetKillQueue().Count > MAX_KILLEVENTS - 1) - ServerStats[S.GetPort()].GetKillQueue().Dequeue(); - ServerStats[S.GetPort()].GetKillQueue().Enqueue(killEvent); - //S.Logger.WriteInfo($"{E.Origin.Name} killed {E.Target.Name} with a {killEvent.Weapon} from a distance of {Vector3.Distance(killEvent.KillOrigin, killEvent.DeathOrigin)} with {killEvent.Damage} damage, at {killEvent.HitLoc}"); - var cs = statLists.Find(x => x.Port == S.GetPort()); - cs.playerStats.AddKill(killEvent); - } - - Player Killer = E.Origin; - StatTracking curServer = statLists.Find(x => x.Port == S.GetPort()); - var killerStats = curServer.clientStats[] - - if (killerStats == null) - killerStats = new PlayerStats(0, 0, 0, 0, 0, 0); - - curServer.lastKill[E.Origin.ClientNumber] = DateTime.Now; - curServer.Kills[E.Origin.ClientNumber]++; - - if ((DateTime.Now - curServer.lastKill[E.Origin.ClientNumber]).TotalSeconds > 120) - curServer.inactiveMinutes[E.Origin.ClientNumber] += 2; - - killerStats.Kills++; - - killerStats.KDR = (killerStats.Deaths == 0) ? killerStats.Kills : killerStats.KDR = Math.Round((double)killerStats.Kills / (double)killerStats.Deaths, 2); - - - - curServer.playerStats.UpdateStats(Killer, killerStats); - - curServer.killStreaks[Killer.ClientNumber] += 1; - curServer.deathStreaks[Killer.ClientNumber] = 0; - - await Killer.Tell(MessageOnStreak(curServer.killStreaks[Killer.ClientNumber], curServer.deathStreaks[Killer.ClientNumber])); - } - - if (E.Type == Event.GType.Death) - { - if (E.Origin == E.Target || E.Origin == null) - return; - - Player Victim = E.Origin; - StatTracking curServer = statLists.Find(x => x.Port == S.GetPort()); - PlayerStats victimStats = curServer.playerStats.GetStats(Victim); - - if (victimStats == null) - victimStats = new PlayerStats(0, 0, 0, 0, 0, 0); - - victimStats.Deaths++; - victimStats.KDR = Math.Round(victimStats.Kills / (double)victimStats.Deaths, 2); - - curServer.playerStats.UpdateStats(Victim, victimStats); - - curServer.deathStreaks[Victim.ClientNumber] += 1; - curServer.killStreaks[Victim.ClientNumber] = 0; - - await Victim.Tell(MessageOnStreak(curServer.killStreaks[Victim.ClientNumber], curServer.deathStreaks[Victim.ClientNumber])); - } - - if (E.Type == Event.GType.Say) - { - ChatDB.AddChatHistory(E.Origin.ClientNumber, E.Owner.GetPort(), E.Data); - } } - - catch (Exception e) - { - S.Logger.WriteWarning("StatsPlugin::OnEventAsync failed to complete"); - S.Logger.WriteDebug($"Server:{S}\r\nOrigin:{E.Origin}\r\nTarget:{E.Target}"); - S.Logger.WriteDebug($"Exception: {e.Message}"); - } - } - - public static string GetTotalKills() - { - long Kills = 0; - foreach (var S in statLists) - Kills += S.playerStats.GetTotalServerKills(); - return Kills.ToString("#,##0"); - } - - public static string GetTotalPlaytime() - { - long Playtime = 0; - foreach (var S in statLists) - Playtime += S.playerStats.GetTotalServerPlaytime(); - return Playtime.ToString("#,##0"); - } - - private void CalculateAndSaveSkill(Player P, StatTracking curServer) - { - if (P == null) - return; - - var DisconnectingPlayerStats = curServer.clientStats[P.ClientNumber]; - - if (curServer.Kills[P.ClientNumber] == 0) - return; - - //else if (curServer.lastKill[P.ClientNumber] > curServer.connectionTime[P.ClientNumber]) - // curServer.inactiveMinutes[P.ClientNumber] += (int)(DateTime.Now - curServer.lastKill[P.ClientNumber]).TotalMinutes; - - int newPlayTime = (int)(DateTime.Now - P.LastConnection).TotalMinutes; - // (int)(DateTime.Now - curServer.connectionTime[P.ClientNumber]).TotalMinutes - curServer.inactiveMinutes[P.ClientNumber]; - // calculate the players Score Per Minute for the current session - double SessionSPM = curServer.Kills[P.ClientNumber] * 100 / Math.Max(1, newPlayTime); - // calculate how much the KDR should way - // 1.637 is a Eddie-Generated number that weights the KDR nicely - double KDRWeight = Math.Round(Math.Pow(DisconnectingPlayerStats.KDR, 1.637 / Math.E), 3); - double SPMWeightAgainstAverage; - - // if no SPM, weight is 1 else the weight is the current sessions spm / lifetime average score per minute - SPMWeightAgainstAverage = (DisconnectingPlayerStats.SPM == 1) ? 1 : SessionSPM / DisconnectingPlayerStats.SPM; - - // calculate the weight of the new play time againmst lifetime playtime - double SPMAgainstPlayWeight = newPlayTime / Math.Min(600, P.TotalConnectionTime + newPlayTime); - // calculate the new weight against average times the weight against play time - double newSkillFactor = SPMWeightAgainstAverage * SPMAgainstPlayWeight * SessionSPM; - - // if the weight is greater than 1, add, else subtract - DisconnectingPlayerStats.SPM += (SPMWeightAgainstAverage >= 1) ? newSkillFactor : -newSkillFactor; - - DisconnectingPlayerStats.Skill = DisconnectingPlayerStats.SPM * KDRWeight * 10; - - ClientStatsSvc.Update(DisconnectingPlayerStats); - } - - private void ResetCounters(int cID, int serverPort) - { - StatTracking selectedPlayers = statLists.Find(x => x.Port == serverPort); - - if (selectedPlayers == null) - return; - - selectedPlayers.Kills[cID] = 0; - selectedPlayers.connectionTime[cID] = DateTime.Now; - selectedPlayers.inactiveMinutes[cID] = 0; - selectedPlayers.deathStreaks[cID] = 0; - selectedPlayers.killStreaks[cID] = 0; - } - - private String MessageOnStreak(int killStreak, int deathStreak) - { - String Message = ""; - switch (killStreak) - { - case 5: - Message = "Great job! You're on a ^55 killstreak!"; - break; - case 10: - Message = "Amazing! ^510 kills ^7without dying!"; - break; - } - - switch (deathStreak) - { - case 5: - Message = "Pick it up soldier, you've died ^55 times ^7in a row..."; - break; - case 10: - Message = "Seriously? ^510 deaths ^7without getting a kill?"; - break; - } - - return Message; - } } + - public class PlayerStats - { - public PlayerStats(int K, int D, double DR, double S, double sc, int P) - { - Kills = K; - Deaths = D; - KDR = DR; - Skill = S; - scorePerMinute = sc; - TotalPlayTime = P; - } - - public int Kills; - public int Deaths; - public double KDR; - public double Skill; - public double scorePerMinute; - public int TotalPlayTime; - } -} \ No newline at end of file + \ No newline at end of file diff --git a/SharedLibrary/Commands/NativeCommands.cs b/SharedLibrary/Commands/NativeCommands.cs index c306e065..4eae89f6 100644 --- a/SharedLibrary/Commands/NativeCommands.cs +++ b/SharedLibrary/Commands/NativeCommands.cs @@ -7,7 +7,9 @@ using System.Threading.Tasks; using SharedLibrary.Network; using SharedLibrary.Helpers; using SharedLibrary.Objects; - +using SharedLibrary.Database; +using System.Data.Entity; +using SharedLibrary.Database.Models; namespace SharedLibrary.Commands { @@ -917,4 +919,56 @@ namespace SharedLibrary.Commands await E.Origin.Tell($"Your external IP is ^5{E.Origin.IPAddress}"); } } + + public class CPruneAdmins : Command + { + public CPruneAdmins() : base("prune", "demote any admins that have not connected recently (defaults to 30 days)", "p", Player.Permission.Owner, false, new CommandArgument[] + { + new CommandArgument() + { + Name = "inactive days", + Required = false + } + }) + { } + + public override async Task ExecuteAsync(Event E) + { + int inactiveDays = 30; + + try + { + if (E.Data.Length > 0) + { + inactiveDays = Int32.Parse(E.Data); + if (inactiveDays < 1) + throw new FormatException(); + } + } + + catch (FormatException) + { + await E.Origin.Tell("Invalid number of inactive days"); + return; + } + + List inactiveUsers = null; + + // update user roles + using (var context = new DatabaseContext()) + { + var lastActive = DateTime.UtcNow.AddDays(-inactiveDays); + inactiveUsers = await context.Clients + .Where(c => c.Level > Player.Permission.Flagged && c.Level <= Player.Permission.Moderator) + .Where(c => c.LastConnection < lastActive) + .ToListAsync(); + inactiveUsers.ForEach(c => c.Level = Player.Permission.User); + await context.SaveChangesAsync(); + } + await E.Origin.Tell($"Pruned inactive {inactiveUsers.Count} privileged users"); + + } + } } + + diff --git a/SharedLibrary/Database/DatabaseContext.cs b/SharedLibrary/Database/DatabaseContext.cs index 5743b258..f6f8763b 100644 --- a/SharedLibrary/Database/DatabaseContext.cs +++ b/SharedLibrary/Database/DatabaseContext.cs @@ -21,7 +21,7 @@ namespace SharedLibrary.Database public DatabaseContext() : base("DefaultConnection") { System.Data.Entity.Database.SetInitializer(new Initializer()); - Configuration.LazyLoadingEnabled = false; + Configuration.LazyLoadingEnabled = true; } protected override void OnModelCreating(DbModelBuilder modelBuilder) diff --git a/SharedLibrary/Helpers/AsyncStatus.cs b/SharedLibrary/Helpers/AsyncStatus.cs index 73da78a6..6162f85d 100644 --- a/SharedLibrary/Helpers/AsyncStatus.cs +++ b/SharedLibrary/Helpers/AsyncStatus.cs @@ -9,17 +9,17 @@ namespace SharedLibrary.Helpers { public sealed class AsyncStatus { - CancellationToken Token; DateTime StartTime; int TimesRun; int UpdateFrequency; public double RunAverage { get; private set; } public object Dependant { get; private set; } public Task RequestedTask { get; private set; } + public CancellationTokenSource TokenSrc { get; private set; } public AsyncStatus(object dependant, int frequency) { - Token = new CancellationToken(); + TokenSrc = new CancellationTokenSource(); StartTime = DateTime.Now; Dependant = dependant; UpdateFrequency = frequency; @@ -29,7 +29,7 @@ namespace SharedLibrary.Helpers public CancellationToken GetToken() { - return Token; + return TokenSrc.Token; } public double ElapsedMillisecondsTime() @@ -39,6 +39,10 @@ namespace SharedLibrary.Helpers public void Update(Task T) { + // reset the token source + TokenSrc.Dispose(); + TokenSrc = new CancellationTokenSource(); + RequestedTask = T; // Console.WriteLine($"Starting Task {T.Id} "); RequestedTask.Start(); diff --git a/SharedLibrary/RCON.cs b/SharedLibrary/RCON.cs index c1b7f8b5..e4562cde 100644 --- a/SharedLibrary/RCON.cs +++ b/SharedLibrary/RCON.cs @@ -26,77 +26,78 @@ namespace SharedLibrary.Network static string[] SendQuery(QueryType Type, Server QueryServer, string Parameters = "") { - if ((DateTime.Now - LastQuery).TotalMilliseconds < 300) - Task.Delay(300).Wait(); - LastQuery = DateTime.Now; - var ServerOOBConnection = new UdpClient(); - ServerOOBConnection.Client.SendTimeout = 1000; - ServerOOBConnection.Client.ReceiveTimeout = 1000; - var Endpoint = new IPEndPoint(IPAddress.Parse(QueryServer.GetIP()), QueryServer.GetPort()); - - string QueryString = String.Empty; - - switch (Type) + using (var ServerOOBConnection = new UdpClient()) { - case QueryType.DVAR: - case QueryType.COMMAND: - QueryString = $"ÿÿÿÿrcon {QueryServer.Password} {Parameters}"; - break; - case QueryType.GET_STATUS: - QueryString = "ÿÿÿÿ getstatus"; - break; - } + // prevent flooding + if ((DateTime.Now - LastQuery).TotalMilliseconds < 300) + Task.Delay(300).Wait(); + LastQuery = DateTime.Now; - byte[] Payload = GetRequestBytes(QueryString); + ServerOOBConnection.Client.SendTimeout = 1000; + ServerOOBConnection.Client.ReceiveTimeout = 1000; + var Endpoint = new IPEndPoint(IPAddress.Parse(QueryServer.GetIP()), QueryServer.GetPort()); - int attempts = 0; - retry: + string QueryString = String.Empty; - try - { - ServerOOBConnection.Connect(Endpoint); - ServerOOBConnection.Send(Payload, Payload.Length); - - byte[] ReceiveBuffer = new byte[8192]; - StringBuilder QueryResponseString = new StringBuilder(); - - do + switch (Type) { - ReceiveBuffer = ServerOOBConnection.Receive(ref Endpoint); - QueryResponseString.Append(Encoding.ASCII.GetString(ReceiveBuffer).TrimEnd('\0')); - } while (ServerOOBConnection.Available > 0 && ServerOOBConnection.Client.Connected); - - ServerOOBConnection.Close(); - - if (QueryResponseString.ToString().Contains("Invalid password")) - throw new Exceptions.NetworkException("RCON password is invalid"); - if (QueryResponseString.ToString().Contains("rcon_password")) - throw new Exceptions.NetworkException("RCON password has not been set"); - - int num = int.Parse("0a", System.Globalization.NumberStyles.AllowHexSpecifier); - string[] SplitResponse = QueryResponseString.ToString().Split(new char[] { (char)num }, StringSplitOptions.RemoveEmptyEntries); - return SplitResponse; - } - - catch (Exceptions.NetworkException e) - { - throw e; - } - - catch (Exception e) - { - attempts++; - if (attempts > 2) - { - var ne = new Exceptions.NetworkException("Could not communicate with the server"); - ne.Data["internal_exception"] = e.Message; - ne.Data["server_address"] = ServerOOBConnection.Client.RemoteEndPoint.ToString(); - ServerOOBConnection.Close(); - throw ne; + case QueryType.DVAR: + case QueryType.COMMAND: + QueryString = $"ÿÿÿÿrcon {QueryServer.Password} {Parameters}"; + break; + case QueryType.GET_STATUS: + QueryString = "ÿÿÿÿ getstatus"; + break; } - Thread.Sleep(1000); - goto retry; + byte[] Payload = GetRequestBytes(QueryString); + + int attempts = 0; + retry: + + try + { + ServerOOBConnection.Connect(Endpoint); + ServerOOBConnection.Send(Payload, Payload.Length); + + byte[] ReceiveBuffer = new byte[8192]; + StringBuilder QueryResponseString = new StringBuilder(); + + do + { + ReceiveBuffer = ServerOOBConnection.Receive(ref Endpoint); + QueryResponseString.Append(Encoding.ASCII.GetString(ReceiveBuffer).TrimEnd('\0')); + } while (ServerOOBConnection.Available > 0 && ServerOOBConnection.Client.Connected); + + if (QueryResponseString.ToString().Contains("Invalid password")) + throw new Exceptions.NetworkException("RCON password is invalid"); + if (QueryResponseString.ToString().Contains("rcon_password")) + throw new Exceptions.NetworkException("RCON password has not been set"); + + int num = int.Parse("0a", System.Globalization.NumberStyles.AllowHexSpecifier); + string[] SplitResponse = QueryResponseString.ToString().Split(new char[] { (char)num }, StringSplitOptions.RemoveEmptyEntries); + return SplitResponse; + } + + catch (Exceptions.NetworkException e) + { + throw e; + } + + catch (Exception e) + { + attempts++; + if (attempts > 2) + { + var ne = new Exceptions.NetworkException("Could not communicate with the server"); + ne.Data["internal_exception"] = e.Message; + ne.Data["server_address"] = ServerOOBConnection.Client.RemoteEndPoint.ToString(); + throw ne; + } + + Thread.Sleep(1000); + goto retry; + } } } diff --git a/SharedLibrary/ServerConfiguration.cs b/SharedLibrary/ServerConfiguration.cs index 5e8b6f7d..2b6aa0c3 100644 --- a/SharedLibrary/ServerConfiguration.cs +++ b/SharedLibrary/ServerConfiguration.cs @@ -9,6 +9,7 @@ namespace SharedLibrary public string Password; public string FtpPrefix; public bool AllowMultipleOwners; + public bool AllowTrustedRank; public override string Filename() { diff --git a/SharedLibrary/Services/GenericRepository.cs b/SharedLibrary/Services/GenericRepository.cs index e58f9e26..6ef44935 100644 --- a/SharedLibrary/Services/GenericRepository.cs +++ b/SharedLibrary/Services/GenericRepository.cs @@ -41,11 +41,17 @@ namespace SharedLibrary.Services } } + public virtual async Task> FindAsync(Expression> predicate, Func, IOrderedQueryable> orderExpression = null) + { + return await this.GetQuery(predicate, orderExpression).ToListAsync(); + } + public virtual IEnumerable Find(Expression> predicate, Func, IOrderedQueryable> orderExpression = null) { return this.GetQuery(predicate, orderExpression).AsEnumerable(); } + public virtual IQueryable GetQuery(Expression> predicate = null, Func, IOrderedQueryable> orderExpression = null) { IQueryable qry = this.DBSet; @@ -131,16 +137,10 @@ namespace SharedLibrary.Services this.Context.SaveChanges(); } - public virtual async Task SaveChangesAsync() + public virtual Task SaveChangesAsync() { - try - { - await this.Context.SaveChangesAsync(); - } - catch (Exception e) - { - throw e; - } + return this.Context.SaveChangesAsync(); } + } } diff --git a/SharedLibrary/WebService.cs b/SharedLibrary/WebService.cs index 7b1bcc12..921e2c60 100644 --- a/SharedLibrary/WebService.cs +++ b/SharedLibrary/WebService.cs @@ -87,8 +87,8 @@ namespace SharedLibrary { return new Dictionary(); } + abstract public string GetContent(System.Collections.Specialized.NameValueCollection querySet, IDictionary headers); - public async Task GetPage(System.Collections.Specialized.NameValueCollection querySet, IDictionary headers) {