diff --git a/Admin/Main.cs b/Admin/Main.cs index cac5b6c9f..92d05d5aa 100644 --- a/Admin/Main.cs +++ b/Admin/Main.cs @@ -1,5 +1,4 @@  -#define USINGMEMORY using System; using System.Runtime.InteropServices; using SharedLibrary; diff --git a/Admin/Manager.cs b/Admin/Manager.cs index 8988867ec..7a9cc8b94 100644 --- a/Admin/Manager.cs +++ b/Admin/Manager.cs @@ -199,6 +199,12 @@ namespace IW4MAdmin var Status = TaskStatuses[i]; 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]"); diff --git a/Admin/Server.cs b/Admin/Server.cs index 7335ab826..601050d04 100644 --- a/Admin/Server.cs +++ b/Admin/Server.cs @@ -34,6 +34,7 @@ namespace IW4MAdmin { // update their ping Players[polledPlayer.ClientNumber].Ping = polledPlayer.Ping; + Players[polledPlayer.ClientNumber].Score = polledPlayer.Score; return true; } @@ -112,6 +113,7 @@ namespace IW4MAdmin // NewPlayer.Level = Player.Permission.Flagged; // Do the player specific stuff player.ClientNumber = polledPlayer.ClientNumber; + player.Score = polledPlayer.Score; Players[player.ClientNumber] = player; Logger.WriteInfo($"Client {player} connecting..."); @@ -210,7 +212,15 @@ namespace IW4MAdmin if (C.RequiresTarget || Args.Length > 0) { int cNum = -1; - int.TryParse(Args[0], out cNum); + try + { + cNum = Convert.ToInt32(Args[0]); + } + + catch(FormatException) + { + + } if (Args[0][0] == '@') // user specifying target by database ID { @@ -227,7 +237,7 @@ namespace IW4MAdmin } } - else if (Args[0].Length < 3 && cNum > -1 && cNum < 18) // user specifying target by client num + else if (Args[0].Length < 3 && cNum > -1 && cNum < MaxClients) // user specifying target by client num { if (Players[cNum] != null) { @@ -250,6 +260,13 @@ 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()) + { + await E.Origin.Tell($"Not enough arguments supplied!"); + await E.Origin.Tell(C.Syntax); + throw new SharedLibrary.Exceptions.CommandException($"{E.Origin} did not supply enough arguments for \"{C.Name}\""); + } } } @@ -267,6 +284,13 @@ namespace IW4MAdmin { E.Target = matchingPlayers.First(); E.Data = Regex.Replace(E.Data, $"{E.Target.Name}", "", RegexOptions.IgnoreCase).Trim(); + + if (E.Data.Trim() == E.Target.Name.ToLower().Trim()) + { + await E.Origin.Tell($"Not enough arguments supplied!"); + await E.Origin.Tell(C.Syntax); + throw new SharedLibrary.Exceptions.CommandException($"{E.Origin} did not supply enough arguments for \"{C.Name}\""); + } } } diff --git a/Admin/lib/SharedLibrary.dll b/Admin/lib/SharedLibrary.dll index 46fc44684..0a35b3e5b 100644 Binary files a/Admin/lib/SharedLibrary.dll and b/Admin/lib/SharedLibrary.dll differ diff --git a/Plugins/SimpleStats/Helpers/ServerStats.cs b/Plugins/SimpleStats/Helpers/ServerStats.cs index 73798d9e9..6f375ba41 100644 --- a/Plugins/SimpleStats/Helpers/ServerStats.cs +++ b/Plugins/SimpleStats/Helpers/ServerStats.cs @@ -11,11 +11,13 @@ namespace StatsPlugin.Helpers public class ServerStats { public Dictionary PlayerStats { get; set; } + public EFServerStatistics ServerStatistics { get; private set; } public EFServer Server { get; private set; } - public ServerStats(EFServer sv) + public ServerStats(EFServer sv, EFServerStatistics st) { PlayerStats = new Dictionary(); + ServerStatistics = st; Server = sv; } } diff --git a/Plugins/SimpleStats/Helpers/StatManager.cs b/Plugins/SimpleStats/Helpers/StatManager.cs index d0475d338..1e6e63b03 100644 --- a/Plugins/SimpleStats/Helpers/StatManager.cs +++ b/Plugins/SimpleStats/Helpers/StatManager.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading.Tasks; using SharedLibrary; using SharedLibrary.Helpers; @@ -19,7 +18,8 @@ namespace StatsPlugin.Helpers private IManager Manager; private GenericRepository ClientStatSvc; private GenericRepository ServerSvc; - private GenericRepository KillSvc; + private GenericRepository KillStatsSvc; + private GenericRepository ServerStatsSvc; public StatManager(IManager mgr) { @@ -28,13 +28,13 @@ namespace StatsPlugin.Helpers Manager = mgr; ClientStatSvc = new GenericRepository(); ServerSvc = new GenericRepository(); - KillSvc = new GenericRepository(); + KillStatsSvc = new GenericRepository(); + ServerStatsSvc = new GenericRepository(); } ~StatManager() { Servers.Clear(); - Log.WriteInfo("Cleared StatManager servers"); Log = null; Servers = null; } @@ -48,6 +48,7 @@ namespace StatsPlugin.Helpers try { int serverId = sv.GetHashCode(); + // 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(); if (server == null) @@ -64,7 +65,13 @@ namespace StatsPlugin.Helpers // this doesn't need to be async as it's during initialization ServerSvc.SaveChanges(); - Servers.Add(sv.GetHashCode(), new ServerStats(server)); + InitializeServerStats(sv); + ServerStatsSvc.SaveChanges(); + + var serverStats = ServerStatsSvc.Find(c => c.ServerId == serverId).FirstOrDefault(); + // check to see if the stats have ever been initialized + + Servers.Add(serverId, new ServerStats(server, serverStats)); } catch (Exception e) @@ -102,6 +109,10 @@ namespace StatsPlugin.Helpers clientStats = ClientStatSvc.Insert(clientStats); } + // set these on connecting + clientStats.LastActive = DateTime.UtcNow; + clientStats.LastStatCalculation = DateTime.UtcNow; + lock (playerStats) { if (playerStats.ContainsKey(pl.ClientNumber)) @@ -123,17 +134,9 @@ namespace StatsPlugin.Helpers // remove the client from the stats dictionary as they're leaving lock (playerStats) playerStats.Remove(pl.ClientNumber); - // allow accessing certain properties - //clientStats.Client = pl; - // update skill - // clientStats = UpdateStats(clientStats); - // reset for EF cache - //clientStats.SessionDeaths = 0; - // clientStats.SessionKills = 0; - // prevent mismatched primary key - //clientStats.Client = null; - // update in database - //await ClientStatSvc.SaveChangesAsync(); + + var serverStats = ServerStatsSvc.Find(sv => sv.ServerId == serverId).FirstOrDefault(); + serverStats.TotalPlayTime += (int)(DateTime.UtcNow - pl.LastConnection).TotalSeconds; } /// @@ -160,18 +163,29 @@ namespace StatsPlugin.Helpers Weapon = ParseEnum.Get(weapon, typeof(IW4Info.WeaponName)) }; - KillSvc.Insert(kill); - await KillSvc.SaveChangesAsync(); + KillStatsSvc.Insert(kill); + await KillStatsSvc.SaveChangesAsync(); } public void AddStandardKill(Player attacker, Player victim) { - var attackerStats = Servers[attacker.CurrentServer.GetHashCode()].PlayerStats[attacker.ClientNumber]; - // set to access total time + int serverId = attacker.CurrentServer.GetHashCode(); + var attackerStats = Servers[serverId].PlayerStats[attacker.ClientNumber]; + // set to access total time played attackerStats.Client = attacker; - var victimStats = Servers[victim.CurrentServer.GetHashCode()].PlayerStats[victim.ClientNumber]; + var victimStats = Servers[serverId].PlayerStats[victim.ClientNumber]; + // update the total stats + Servers[serverId].ServerStatistics.TotalKills += 1; + + // calculate for the clients CalculateKill(attackerStats, victimStats); + + // immediately write changes in debug +#if DEBUG + ClientStatSvc.SaveChanges(); + ServerStatsSvc.SaveChanges(); +#endif } /// @@ -195,10 +209,9 @@ namespace StatsPlugin.Helpers UpdateStats(attackerStats); attackerStats.Client = null; - // immediately write changes in debug -#if DEBUG - ClientStatSvc.SaveChanges(); -#endif + // update after calculation + attackerStats.LastActive = DateTime.UtcNow; + victimStats.LastActive = DateTime.UtcNow; } /// @@ -208,44 +221,83 @@ namespace StatsPlugin.Helpers /// private EFClientStatistics UpdateStats(EFClientStatistics clientStats) { - // if it's their first kill we need to set the last kill as the time they joined - clientStats.LastStatCalculation = (clientStats.LastStatCalculation == DateTime.MinValue) ? DateTime.UtcNow : clientStats.LastStatCalculation; double timeSinceLastCalc = (DateTime.UtcNow - clientStats.LastStatCalculation).TotalSeconds / 60.0; + double timeSinceLastActive = (DateTime.UtcNow - clientStats.LastActive).TotalSeconds / 60.0; - // each 'session' is one minute - if (timeSinceLastCalc >= 1) - { - Log.WriteDebug($"Updated stats for {clientStats.ClientId} ({clientStats.SessionKills})"); - // calculate the players Score Per Minute for the current session - // todo: score should be based on gamemode - double killSPM = clientStats.SessionKills * 100.0; + // prevent NaN or inactive time lowering SPM + if (timeSinceLastCalc == 0 || timeSinceLastActive > 3) + return clientStats; - // calculate how much the KDR should weigh - // 1.637 is a Eddie-Generated number that weights the KDR nicely - double KDRWeight = Math.Round(Math.Pow(clientStats.KDR, 1.637 / Math.E), 3); + // calculate the players Score Per Minute for the current session + int currentScore = Manager.GetActiveClients() + .First(c => c.ClientId == clientStats.ClientId) + .Score; + double killSPM = currentScore / (timeSinceLastCalc * 60.0); - // if no SPM, weight is 1 else the weight ishe current session's spm / lifetime average score per minute - double SPMWeightAgainstAverage = (clientStats.SPM < 1) ? 1 : killSPM / clientStats.SPM; + // calculate how much the KDR should weigh + // 1.637 is a Eddie-Generated number that weights the KDR nicely + double KDRWeight = Math.Round(Math.Pow(clientStats.KDR, 1.637 / Math.E), 3); - // calculate the weight of the new play time against last 10 hours of gameplay - int totalConnectionTime = (clientStats.Client.TotalConnectionTime == 0) ? - (int)(DateTime.UtcNow - clientStats.Client.FirstConnection).TotalSeconds : - clientStats.Client.TotalConnectionTime + (int)(DateTime.UtcNow - clientStats.Client.LastConnection).TotalSeconds; + // if no SPM, weight is 1 else the weight ishe current session's spm / lifetime average score per minute + double SPMWeightAgainstAverage = (clientStats.SPM < 1) ? 1 : killSPM / clientStats.SPM; - double SPMAgainstPlayWeight = timeSinceLastCalc / Math.Min(600, (totalConnectionTime / 60.0)); + // calculate the weight of the new play time against last 10 hours of gameplay + int totalConnectionTime = (clientStats.Client.TotalConnectionTime == 0) ? + (int)(DateTime.UtcNow - clientStats.Client.FirstConnection).TotalSeconds : + clientStats.Client.TotalConnectionTime + (int)(DateTime.UtcNow - clientStats.Client.LastConnection).TotalSeconds; - // calculate the new weight against average times the weight against play time - clientStats.SPM = (killSPM * SPMAgainstPlayWeight) + (clientStats.SPM * (1 - SPMAgainstPlayWeight)); - clientStats.SPM = Math.Round(clientStats.SPM, 3); - clientStats.Skill = Math.Round((clientStats.SPM * KDRWeight) / 10.0, 3); + double SPMAgainstPlayWeight = timeSinceLastCalc / Math.Min(600, (totalConnectionTime / 60.0)); - clientStats.SessionKills = 0; - clientStats.SessionDeaths = 0; + // calculate the new weight against average times the weight against play time + clientStats.SPM = (killSPM * SPMAgainstPlayWeight) + (clientStats.SPM * (1 - SPMAgainstPlayWeight)); + clientStats.SPM = Math.Round(clientStats.SPM, 3); + clientStats.Skill = Math.Round((clientStats.SPM * KDRWeight), 3); - clientStats.LastStatCalculation = DateTime.UtcNow; - } + clientStats.LastStatCalculation = DateTime.UtcNow; + clientStats.LastScore = currentScore; return clientStats; } + + public void InitializeServerStats(Server sv) + { + int serverId = sv.GetHashCode(); + var serverStats = ServerStatsSvc.Find(s => s.ServerId == serverId).FirstOrDefault(); + if (serverStats == null) + { + Log.WriteDebug($"Initializing server stats for {sv}"); + // server stats have never been generated before + serverStats = new EFServerStatistics() + { + Active = true, + ServerId = serverId, + TotalKills = 0, + TotalPlayTime = 0, + }; + + var ieClientStats = 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); + } + } + + public async Task Sync() + { + Log.WriteDebug("Syncing server stats"); + await ServerStatsSvc.SaveChangesAsync(); + + Log.WriteDebug("Syncing client stats"); + await ClientStatSvc.SaveChangesAsync(); + + Log.WriteDebug("Syncing kill stats"); + await KillStatsSvc.SaveChangesAsync(); + + Log.WriteDebug("Syncing servers"); + await ServerSvc.SaveChangesAsync(); + } } } diff --git a/Plugins/SimpleStats/Helpers/StreakMessage.cs b/Plugins/SimpleStats/Helpers/StreakMessage.cs new file mode 100644 index 000000000..0bc04b9e6 --- /dev/null +++ b/Plugins/SimpleStats/Helpers/StreakMessage.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StatsPlugin.Helpers +{ + public class StreakMessage + { + public static 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; + } + } +} diff --git a/Plugins/SimpleStats/IW4Info.cs b/Plugins/SimpleStats/IW4Info.cs index 7d40cfefb..76fa08823 100644 --- a/Plugins/SimpleStats/IW4Info.cs +++ b/Plugins/SimpleStats/IW4Info.cs @@ -1333,7 +1333,8 @@ namespace StatsPlugin ak74u_silencer_thermal_mp, ak74u_silencer_xmags_mp, ak74u_thermal_xmags_mp, - m40a3_mp = 1194, + m16_reflex_silencer_mp, + m40a3_mp, peacekeeper_mp, dragunov_mp, cobra_player_minigun_mp diff --git a/Plugins/SimpleStats/Models/EFClientStatistics.cs b/Plugins/SimpleStats/Models/EFClientStatistics.cs index f514af24b..bf264e562 100644 --- a/Plugins/SimpleStats/Models/EFClientStatistics.cs +++ b/Plugins/SimpleStats/Models/EFClientStatistics.cs @@ -45,5 +45,9 @@ namespace StatsPlugin.Models public int DeathStreak { get; set; } [NotMapped] public DateTime LastStatCalculation { get; set; } + [NotMapped] + public int LastScore { get; set; } + [NotMapped] + public DateTime LastActive { get; set; } } } diff --git a/Plugins/SimpleStats/Models/EFServerStatistics.cs b/Plugins/SimpleStats/Models/EFServerStatistics.cs new file mode 100644 index 000000000..1dc9fc62b --- /dev/null +++ b/Plugins/SimpleStats/Models/EFServerStatistics.cs @@ -0,0 +1,17 @@ +using SharedLibrary.Database.Models; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace StatsPlugin.Models +{ + public class EFServerStatistics : SharedEntity + { + [Key] + public int StatisticId { get; set; } + public int ServerId { get; set; } + [ForeignKey("ServerId")] + public virtual EFServer Server { get; set; } + public long TotalKills { get; set; } + public long TotalPlayTime { get; set; } + } +} diff --git a/Plugins/SimpleStats/Plugin.cs b/Plugins/SimpleStats/Plugin.cs index b043c670c..412571cce 100644 --- a/Plugins/SimpleStats/Plugin.cs +++ b/Plugins/SimpleStats/Plugin.cs @@ -5,8 +5,11 @@ using System.Text; using System.Threading.Tasks; using SharedLibrary; +using SharedLibrary.Helpers; using SharedLibrary.Interfaces; +using SharedLibrary.Services; using StatsPlugin.Helpers; +using StatsPlugin.Models; namespace StatsPlugin { @@ -40,6 +43,7 @@ namespace StatsPlugin case Event.GType.MapChange: break; case Event.GType.MapEnd: + await Manager.Sync(); break; case Event.GType.Broadcast: break; @@ -71,6 +75,27 @@ namespace StatsPlugin public Task OnLoadAsync(IManager manager) { + /* + * + ManagerInstance.GetMessageTokens().Add(new MessageToken("TOTALKILLS", GetTotalKills)); + ManagerInstance.GetMessageTokens().Add(new MessageToken("TOTALPLAYTIME", GetTotalPlaytime)); +*/ + string totalKills() + { + var serverStats = new GenericRepository(); + return serverStats.GetQuery(s => s.Active) + .Sum(c => c.TotalKills).ToString(); + } + + string totalPlayTime() + { + var serverStats = new GenericRepository(); + return serverStats.GetQuery(s => s.Active) + .Sum(c => c.TotalPlayTime).ToString(); + } + + manager.GetMessageTokens().Add(new MessageToken("TOTALKILLS", totalKills)); + manager.GetMessageTokens().Add(new MessageToken("TOTALPLAYTIME", totalPlayTime)); return Task.FromResult( Manager = new StatManager(manager) ); diff --git a/Plugins/SimpleStats/StatsPlugin.csproj b/Plugins/SimpleStats/StatsPlugin.csproj index 2e964951d..f86f7e4cb 100644 --- a/Plugins/SimpleStats/StatsPlugin.csproj +++ b/Plugins/SimpleStats/StatsPlugin.csproj @@ -72,11 +72,13 @@ + + diff --git a/SharedLibrary/Commands/NativeCommands.cs b/SharedLibrary/Commands/NativeCommands.cs index 1cc29c833..c306e0653 100644 --- a/SharedLibrary/Commands/NativeCommands.cs +++ b/SharedLibrary/Commands/NativeCommands.cs @@ -744,7 +744,7 @@ namespace SharedLibrary.Commands E.Data = E.Data.RemoveWords(1); E.Owner.Reports.Add(new Report(E.Target, E.Origin, E.Data)); - await E.Origin.Tell($"Thank you for your report, and administrator has been notified"); + await E.Origin.Tell($"Thank you for your report, an administrator has been notified"); await E.Owner.ExecuteEvent(new Event(Event.GType.Report, E.Data, E.Origin, E.Target, E.Owner)); await E.Owner.ToAdmins(String.Format("^5{0}^7->^1{1}^7: {2}", E.Origin.Name, E.Target.Name, E.Data)); } diff --git a/SharedLibrary/Objects/Player.cs b/SharedLibrary/Objects/Player.cs index d6edb3bd7..74355b9c4 100644 --- a/SharedLibrary/Objects/Player.cs +++ b/SharedLibrary/Objects/Player.cs @@ -73,6 +73,8 @@ namespace SharedLibrary.Objects public DateTime ConnectionTime { get; set; } [NotMapped] public Server CurrentServer { get; set; } + [NotMapped] + public int Score { get; set; } private string _ipaddress; public override string IPAddress diff --git a/SharedLibrary/RCON.cs b/SharedLibrary/RCON.cs index 2780fc50e..c1b7f8b52 100644 --- a/SharedLibrary/RCON.cs +++ b/SharedLibrary/RCON.cs @@ -26,8 +26,8 @@ namespace SharedLibrary.Network static string[] SendQuery(QueryType Type, Server QueryServer, string Parameters = "") { - if ((DateTime.Now - LastQuery).TotalMilliseconds < 100) - Task.Delay(100).Wait(); + if ((DateTime.Now - LastQuery).TotalMilliseconds < 300) + Task.Delay(300).Wait(); LastQuery = DateTime.Now; var ServerOOBConnection = new UdpClient(); ServerOOBConnection.Client.SendTimeout = 1000; diff --git a/SharedLibrary/Services/ClientService.cs b/SharedLibrary/Services/ClientService.cs index b024525db..cdf08f2ad 100644 --- a/SharedLibrary/Services/ClientService.cs +++ b/SharedLibrary/Services/ClientService.cs @@ -233,6 +233,12 @@ namespace SharedLibrary.Services { throw new NotImplementedException(); } + + public async Task GetTotalPlayTime() + { + using (var context = new DatabaseContext()) + return await context.Clients.SumAsync(c => c.TotalConnectionTime); + } #endregion } } diff --git a/SharedLibrary/Utilities.cs b/SharedLibrary/Utilities.cs index 7d77e58c7..96427cf14 100644 --- a/SharedLibrary/Utilities.cs +++ b/SharedLibrary/Utilities.cs @@ -62,7 +62,9 @@ namespace SharedLibrary int.TryParse(playerInfo[0], out cID); var regex = Regex.Match(responseLine, @"\d+\.\d+\.\d+.\d+\:\d{1,5}"); string cIP = regex.Value.Split(':')[0]; - Player P = new Player() { Name = cName, NetworkId = npID, ClientNumber = cID, IPAddress = cIP, Ping = Ping }; + regex = Regex.Match(responseLine, @"[0-9]{1,2}\s+[0-9]+\s+"); + int score = Int32.Parse(regex.Value.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)[1]); + Player P = new Player() { Name = cName, NetworkId = npID, ClientNumber = cID, IPAddress = cIP, Ping = Ping, Score = score}; StatusPlayers.Add(P); } }