From 4a46abc46d5f4bfba80a333a027f8f5f670e28cc Mon Sep 17 00:00:00 2001 From: RaidMax Date: Sun, 16 Sep 2018 15:34:16 -0500 Subject: [PATCH] add index to time sent in EFCLientMessage, so we can retrieve faster in context view set the maximum height of the add link to profile on client chat move change history into a seperate service move around AC penalty processing --- Application/Application.csproj | 2 +- Application/GameEventHandler.cs | 4 +- Application/Manager.cs | 13 +- Plugins/IW4ScriptCommands/Commands/Balance.cs | 348 ++-- Plugins/Stats/Config/StatsConfiguration.cs | 2 + Plugins/Stats/Helpers/StatManager.cs | 1634 +++++++++-------- Plugins/Stats/Models/ModelConfiguration.cs | 3 + .../Stats/Web/Controllers/StatsController.cs | 5 + .../Web/Views/Stats/_MessageContext.cshtml | 2 +- SharedLibraryCore/Database/DatabaseContext.cs | 9 + .../Database/Models/EFChangeHistory.cs | 3 +- SharedLibraryCore/Dtos/ChatInfo.cs | 1 + SharedLibraryCore/Events/EventAPI.cs | 89 +- SharedLibraryCore/Helpers/ChangeTracking.cs | 21 +- .../Interfaces/IEntityService.cs | 1 - SharedLibraryCore/Interfaces/IManager.cs | 1 + ...3111_AddIndexToMessageTimeSent.Designer.cs | 686 +++++++ ...0180915163111_AddIndexToMessageTimeSent.cs | 22 + ...orceAutoIncrementChangeHistory.Designer.cs | 688 +++++++ ...5164118_ForceAutoIncrementChangeHistory.cs | 25 + .../DatabaseContextModelSnapshot.cs | 6 +- SharedLibraryCore/Services/AliasService.cs | 25 +- .../Services/ChangeHistoryService.cs | 95 + SharedLibraryCore/Services/ClientService.cs | 58 +- SharedLibraryCore/Utilities.cs | 22 + WebfrontCore/Controllers/BaseController.cs | 2 + WebfrontCore/Views/Shared/_Layout.cshtml | 43 +- WebfrontCore/wwwroot/css/bootstrap-custom.css | 8 + .../wwwroot/css/bootstrap-custom.scss | 10 + WebfrontCore/wwwroot/js/server.js | 8 +- 30 files changed, 2662 insertions(+), 1174 deletions(-) create mode 100644 SharedLibraryCore/Migrations/20180915163111_AddIndexToMessageTimeSent.Designer.cs create mode 100644 SharedLibraryCore/Migrations/20180915163111_AddIndexToMessageTimeSent.cs create mode 100644 SharedLibraryCore/Migrations/20180915164118_ForceAutoIncrementChangeHistory.Designer.cs create mode 100644 SharedLibraryCore/Migrations/20180915164118_ForceAutoIncrementChangeHistory.cs create mode 100644 SharedLibraryCore/Services/ChangeHistoryService.cs diff --git a/Application/Application.csproj b/Application/Application.csproj index 997ef1fd9..9c322e34b 100644 --- a/Application/Application.csproj +++ b/Application/Application.csproj @@ -5,7 +5,7 @@ netcoreapp2.1 false RaidMax.IW4MAdmin.Application - 2.1.9.1 + 2.1.9.2 RaidMax Forever None IW4MAdmin diff --git a/Application/GameEventHandler.cs b/Application/GameEventHandler.cs index ef7cd695a..a3481412b 100644 --- a/Application/GameEventHandler.cs +++ b/Application/GameEventHandler.cs @@ -23,9 +23,9 @@ namespace IW4MAdmin.Application public void AddEvent(GameEvent gameEvent) { - // IsProcessingEvent.Wait(); + //IsProcessingEvent.Wait(); ((Manager as ApplicationManager).OnServerEvent)(this, new GameEventArgs(null, false, gameEvent)); - // IsProcessingEvent.Release(1); + //IsProcessingEvent.Release(1); //if (gameEvent.Type == GameEvent.EventType.Connect) //{ // IsProcessingEvent.Wait(); diff --git a/Application/Manager.cs b/Application/Manager.cs index d60f81818..4ba5edd01 100644 --- a/Application/Manager.cs +++ b/Application/Manager.cs @@ -39,6 +39,7 @@ namespace IW4MAdmin.Application // expose the event handler so we can execute the events public OnServerEventEventHandler OnServerEvent { get; set; } public DateTime StartTime { get; private set; } + public string Version => Assembly.GetEntryAssembly().GetName().Version.ToString(); static ApplicationManager Instance; readonly List TaskStatuses; @@ -102,9 +103,12 @@ namespace IW4MAdmin.Application return; } + await newEvent.Owner.ExecuteEvent(newEvent); + // todo: this is a hacky mess if (newEvent.Origin?.DelayedEvents.Count > 0 && - newEvent.Origin?.State == Player.ClientState.Connected) + (newEvent.Origin?.State == Player.ClientState.Connected || + newEvent.Type == GameEvent.EventType.Connect)) { var events = newEvent.Origin.DelayedEvents; @@ -144,8 +148,6 @@ namespace IW4MAdmin.Application } } - await newEvent.Owner.ExecuteEvent(newEvent); - #if DEBUG Logger.WriteDebug($"Processed event with id {newEvent.Id}"); #endif @@ -175,6 +177,9 @@ namespace IW4MAdmin.Application } // tell anyone waiting for the output that we're done newEvent.OnProcessed.Set(); + + var changeHistorySvc = new ChangeHistoryService(); + await changeHistorySvc.Add(args.Event); } public IList GetServers() @@ -263,7 +268,7 @@ namespace IW4MAdmin.Application await new ContextSeed(db).Seed(); } - // todo: optimize this + // todo: optimize this (or replace it) var ipList = (await ClientSvc.Find(c => c.Level > Player.Permission.Trusted)) .Select(c => new { diff --git a/Plugins/IW4ScriptCommands/Commands/Balance.cs b/Plugins/IW4ScriptCommands/Commands/Balance.cs index 2c811542c..e24492ded 100644 --- a/Plugins/IW4ScriptCommands/Commands/Balance.cs +++ b/Plugins/IW4ScriptCommands/Commands/Balance.cs @@ -1,197 +1,197 @@ -using SharedLibraryCore; -using SharedLibraryCore.Objects; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +//using SharedLibraryCore; +//using SharedLibraryCore.Objects; +//using System; +//using System.Collections.Generic; +//using System.Linq; +//using System.Text; +//using System.Threading.Tasks; -namespace IW4ScriptCommands.Commands -{ - class Balance : Command - { - private class TeamAssignment - { - public IW4MAdmin.Plugins.Stats.IW4Info.Team CurrentTeam { get; set; } - public int Num { get; set; } - public IW4MAdmin.Plugins.Stats.Models.EFClientStatistics Stats { get; set; } - } - public Balance() : base("balance", "balance teams", "bal", Player.Permission.Trusted, false, null) - { - } +//namespace IW4ScriptCommands.Commands +//{ +// class Balance : Command +// { +// private class TeamAssignment +// { +// public IW4MAdmin.Plugins.Stats.IW4Info.Team CurrentTeam { get; set; } +// public int Num { get; set; } +// public IW4MAdmin.Plugins.Stats.Models.EFClientStatistics Stats { get; set; } +// } +// public Balance() : base("balance", "balance teams", "bal", Player.Permission.Trusted, false, null) +// { +// } - public override async Task ExecuteAsync(GameEvent E) - { - string teamsString = (await E.Owner.GetDvarAsync("sv_iw4madmin_teams")).Value; +// public override async Task ExecuteAsync(GameEvent E) +// { +// string teamsString = (await E.Owner.GetDvarAsync("sv_iw4madmin_teams")).Value; - var scriptClientTeams = teamsString.Split(';', StringSplitOptions.RemoveEmptyEntries) - .Select(c => c.Split(',')) - .Select(c => new TeamAssignment() - { - CurrentTeam = (IW4MAdmin.Plugins.Stats.IW4Info.Team)Enum.Parse(typeof(IW4MAdmin.Plugins.Stats.IW4Info.Team), c[1]), - Num = E.Owner.Players.FirstOrDefault(p => p?.NetworkId == c[0].ConvertLong())?.ClientNumber ?? -1, - Stats = IW4MAdmin.Plugins.Stats.Plugin.Manager.GetClientStats(E.Owner.Players.FirstOrDefault(p => p?.NetworkId == c[0].ConvertLong()).ClientId, E.Owner.GetHashCode()) - }) - .ToList(); +// var scriptClientTeams = teamsString.Split(';', StringSplitOptions.RemoveEmptyEntries) +// .Select(c => c.Split(',')) +// .Select(c => new TeamAssignment() +// { +// CurrentTeam = (IW4MAdmin.Plugins.Stats.IW4Info.Team)Enum.Parse(typeof(IW4MAdmin.Plugins.Stats.IW4Info.Team), c[1]), +// Num = E.Owner.Players.FirstOrDefault(p => p?.NetworkId == c[0].ConvertLong())?.ClientNumber ?? -1, +// Stats = IW4MAdmin.Plugins.Stats.Plugin.Manager.GetClientStats(E.Owner.Players.FirstOrDefault(p => p?.NetworkId == c[0].ConvertLong()).ClientId, E.Owner.GetHashCode()) +// }) +// .ToList(); - // at least one team is full so we can't balance - if (scriptClientTeams.Count(ct => ct.CurrentTeam == IW4MAdmin.Plugins.Stats.IW4Info.Team.Axis) >= Math.Floor(E.Owner.MaxClients / 2.0) - || scriptClientTeams.Count(ct => ct.CurrentTeam == IW4MAdmin.Plugins.Stats.IW4Info.Team.Allies) >= Math.Floor(E.Owner.MaxClients / 2.0)) - { - await E.Origin?.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_BALANCE_FAIL"]); - return; - } +// // at least one team is full so we can't balance +// if (scriptClientTeams.Count(ct => ct.CurrentTeam == IW4MAdmin.Plugins.Stats.IW4Info.Team.Axis) >= Math.Floor(E.Owner.MaxClients / 2.0) +// || scriptClientTeams.Count(ct => ct.CurrentTeam == IW4MAdmin.Plugins.Stats.IW4Info.Team.Allies) >= Math.Floor(E.Owner.MaxClients / 2.0)) +// { +// await E.Origin?.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_BALANCE_FAIL"]); +// return; +// } - List teamAssignments = new List(); +// List teamAssignments = new List(); - var activeClients = E.Owner.GetPlayersAsList().Select(c => new TeamAssignment() - { - Num = c.ClientNumber, - Stats = IW4MAdmin.Plugins.Stats.Plugin.Manager.GetClientStats(c.ClientId, E.Owner.GetHashCode()), - CurrentTeam = IW4MAdmin.Plugins.Stats.Plugin.Manager.GetClientStats(c.ClientId, E.Owner.GetHashCode()).Team - }) - .Where(c => scriptClientTeams.FirstOrDefault(sc => sc.Num == c.Num)?.CurrentTeam != IW4MAdmin.Plugins.Stats.IW4Info.Team.Spectator) - .Where(c => c.CurrentTeam != scriptClientTeams.FirstOrDefault(p => p.Num == c.Num)?.CurrentTeam) - .OrderByDescending(c => c.Stats.Performance) - .ToList(); +// var activeClients = E.Owner.GetPlayersAsList().Select(c => new TeamAssignment() +// { +// Num = c.ClientNumber, +// Stats = IW4MAdmin.Plugins.Stats.Plugin.Manager.GetClientStats(c.ClientId, E.Owner.GetHashCode()), +// CurrentTeam = IW4MAdmin.Plugins.Stats.Plugin.Manager.GetClientStats(c.ClientId, E.Owner.GetHashCode()).Team +// }) +// .Where(c => scriptClientTeams.FirstOrDefault(sc => sc.Num == c.Num)?.CurrentTeam != IW4MAdmin.Plugins.Stats.IW4Info.Team.Spectator) +// .Where(c => c.CurrentTeam != scriptClientTeams.FirstOrDefault(p => p.Num == c.Num)?.CurrentTeam) +// .OrderByDescending(c => c.Stats.Performance) +// .ToList(); - var alliesTeam = scriptClientTeams - .Where(c => c.CurrentTeam == IW4MAdmin.Plugins.Stats.IW4Info.Team.Allies) - .Where(c => activeClients.Count(t => t.Num == c.Num) == 0) - .ToList(); +// var alliesTeam = scriptClientTeams +// .Where(c => c.CurrentTeam == IW4MAdmin.Plugins.Stats.IW4Info.Team.Allies) +// .Where(c => activeClients.Count(t => t.Num == c.Num) == 0) +// .ToList(); - var axisTeam = scriptClientTeams - .Where(c => c.CurrentTeam == IW4MAdmin.Plugins.Stats.IW4Info.Team.Axis) - .Where(c => activeClients.Count(t => t.Num == c.Num) == 0) - .ToList(); +// var axisTeam = scriptClientTeams +// .Where(c => c.CurrentTeam == IW4MAdmin.Plugins.Stats.IW4Info.Team.Axis) +// .Where(c => activeClients.Count(t => t.Num == c.Num) == 0) +// .ToList(); - while (activeClients.Count() > 0) - { - int teamSizeDifference = alliesTeam.Count - axisTeam.Count; - double performanceDisparity = alliesTeam.Count > 0 ? alliesTeam.Average(t => t.Stats.Performance) : 0 - - axisTeam.Count > 0 ? axisTeam.Average(t => t.Stats.Performance) : 0; +// while (activeClients.Count() > 0) +// { +// int teamSizeDifference = alliesTeam.Count - axisTeam.Count; +// double performanceDisparity = alliesTeam.Count > 0 ? alliesTeam.Average(t => t.Stats.Performance) : 0 - +// axisTeam.Count > 0 ? axisTeam.Average(t => t.Stats.Performance) : 0; - if (teamSizeDifference == 0) - { - if (performanceDisparity == 0) - { - alliesTeam.Add(activeClients.First()); - activeClients.RemoveAt(0); - } - else - { - if (performanceDisparity > 0) - { - axisTeam.Add(activeClients.First()); - activeClients.RemoveAt(0); - } - else - { - alliesTeam.Add(activeClients.First()); - activeClients.RemoveAt(0); - } - } - } - else if (teamSizeDifference > 0) - { - if (performanceDisparity > 0) - { - axisTeam.Add(activeClients.First()); - activeClients.RemoveAt(0); - } +// if (teamSizeDifference == 0) +// { +// if (performanceDisparity == 0) +// { +// alliesTeam.Add(activeClients.First()); +// activeClients.RemoveAt(0); +// } +// else +// { +// if (performanceDisparity > 0) +// { +// axisTeam.Add(activeClients.First()); +// activeClients.RemoveAt(0); +// } +// else +// { +// alliesTeam.Add(activeClients.First()); +// activeClients.RemoveAt(0); +// } +// } +// } +// else if (teamSizeDifference > 0) +// { +// if (performanceDisparity > 0) +// { +// axisTeam.Add(activeClients.First()); +// activeClients.RemoveAt(0); +// } - else - { - axisTeam.Add(activeClients.Last()); - activeClients.RemoveAt(activeClients.Count - 1); - } - } - else - { - if (performanceDisparity > 0) - { - alliesTeam.Add(activeClients.First()); - activeClients.RemoveAt(0); - } +// else +// { +// axisTeam.Add(activeClients.Last()); +// activeClients.RemoveAt(activeClients.Count - 1); +// } +// } +// else +// { +// if (performanceDisparity > 0) +// { +// alliesTeam.Add(activeClients.First()); +// activeClients.RemoveAt(0); +// } - else - { - alliesTeam.Add(activeClients.Last()); - activeClients.RemoveAt(activeClients.Count - 1); - } - } - } +// else +// { +// alliesTeam.Add(activeClients.Last()); +// activeClients.RemoveAt(activeClients.Count - 1); +// } +// } +// } - alliesTeam = alliesTeam.OrderByDescending(t => t.Stats.Performance) - .ToList(); +// alliesTeam = alliesTeam.OrderByDescending(t => t.Stats.Performance) +// .ToList(); - axisTeam = axisTeam.OrderByDescending(t => t.Stats.Performance) - .ToList(); +// axisTeam = axisTeam.OrderByDescending(t => t.Stats.Performance) +// .ToList(); - while (Math.Abs(alliesTeam.Count - axisTeam.Count) > 1) - { - int teamSizeDifference = alliesTeam.Count - axisTeam.Count; - double performanceDisparity = alliesTeam.Count > 0 ? alliesTeam.Average(t => t.Stats.Performance) : 0 - - axisTeam.Count > 0 ? axisTeam.Average(t => t.Stats.Performance) : 0; +// while (Math.Abs(alliesTeam.Count - axisTeam.Count) > 1) +// { +// int teamSizeDifference = alliesTeam.Count - axisTeam.Count; +// double performanceDisparity = alliesTeam.Count > 0 ? alliesTeam.Average(t => t.Stats.Performance) : 0 - +// axisTeam.Count > 0 ? axisTeam.Average(t => t.Stats.Performance) : 0; - if (teamSizeDifference > 0) - { - if (performanceDisparity > 0) - { - axisTeam.Add(alliesTeam.First()); - alliesTeam.RemoveAt(0); - } +// if (teamSizeDifference > 0) +// { +// if (performanceDisparity > 0) +// { +// axisTeam.Add(alliesTeam.First()); +// alliesTeam.RemoveAt(0); +// } - else - { - axisTeam.Add(alliesTeam.Last()); - alliesTeam.RemoveAt(axisTeam.Count - 1); - } - } +// else +// { +// axisTeam.Add(alliesTeam.Last()); +// alliesTeam.RemoveAt(axisTeam.Count - 1); +// } +// } - else - { - if (performanceDisparity > 0) - { - alliesTeam.Add(axisTeam.Last()); - axisTeam.RemoveAt(axisTeam.Count - 1); - } +// else +// { +// if (performanceDisparity > 0) +// { +// alliesTeam.Add(axisTeam.Last()); +// axisTeam.RemoveAt(axisTeam.Count - 1); +// } - else - { - alliesTeam.Add(axisTeam.First()); - axisTeam.RemoveAt(0); - } - } - } +// else +// { +// alliesTeam.Add(axisTeam.First()); +// axisTeam.RemoveAt(0); +// } +// } +// } - foreach (var assignment in alliesTeam) - { - teamAssignments.Add($"{assignment.Num},2"); - assignment.Stats.Team = IW4MAdmin.Plugins.Stats.IW4Info.Team.Allies; - } - foreach (var assignment in axisTeam) - { - teamAssignments.Add($"{assignment.Num},3"); - assignment.Stats.Team = IW4MAdmin.Plugins.Stats.IW4Info.Team.Axis; - } +// foreach (var assignment in alliesTeam) +// { +// teamAssignments.Add($"{assignment.Num},2"); +// assignment.Stats.Team = IW4MAdmin.Plugins.Stats.IW4Info.Team.Allies; +// } +// foreach (var assignment in axisTeam) +// { +// teamAssignments.Add($"{assignment.Num},3"); +// assignment.Stats.Team = IW4MAdmin.Plugins.Stats.IW4Info.Team.Axis; +// } - if (alliesTeam.Count(ac => scriptClientTeams.First(sc => sc.Num == ac.Num).CurrentTeam != ac.CurrentTeam) == 0 && - axisTeam.Count(ac => scriptClientTeams.First(sc => sc.Num == ac.Num).CurrentTeam != ac.CurrentTeam) == 0) - { - await E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_BALANCE_FAIL_BALANCED"]); - return; - } +// if (alliesTeam.Count(ac => scriptClientTeams.First(sc => sc.Num == ac.Num).CurrentTeam != ac.CurrentTeam) == 0 && +// axisTeam.Count(ac => scriptClientTeams.First(sc => sc.Num == ac.Num).CurrentTeam != ac.CurrentTeam) == 0) +// { +// await E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_BALANCE_FAIL_BALANCED"]); +// return; +// } - if (E.Origin?.Level > Player.Permission.Administrator) - { - await E.Origin.Tell($"Allies Elo: {(alliesTeam.Count > 0 ? alliesTeam.Average(t => t.Stats.Performance) : 0)}"); - await E.Origin.Tell($"Axis Elo: {(axisTeam.Count > 0 ? axisTeam.Average(t => t.Stats.Performance) : 0)}"); - } +// if (E.Origin?.Level > Player.Permission.Administrator) +// { +// await E.Origin.Tell($"Allies Elo: {(alliesTeam.Count > 0 ? alliesTeam.Average(t => t.Stats.Performance) : 0)}"); +// await E.Origin.Tell($"Axis Elo: {(axisTeam.Count > 0 ? axisTeam.Average(t => t.Stats.Performance) : 0)}"); +// } - string args = string.Join(",", teamAssignments); - await E.Owner.ExecuteCommandAsync($"sv_iw4madmin_command \"balance:{args}\""); - await E.Origin.Tell("Balance command sent"); - } - } -} +// string args = string.Join(",", teamAssignments); +// await E.Owner.ExecuteCommandAsync($"sv_iw4madmin_command \"balance:{args}\""); +// await E.Origin.Tell("Balance command sent"); +// } +// } +//} diff --git a/Plugins/Stats/Config/StatsConfiguration.cs b/Plugins/Stats/Config/StatsConfiguration.cs index 07dccbace..6908a3767 100644 --- a/Plugins/Stats/Config/StatsConfiguration.cs +++ b/Plugins/Stats/Config/StatsConfiguration.cs @@ -10,6 +10,7 @@ namespace IW4MAdmin.Plugins.Stats.Config public List KillstreakMessages { get; set; } public List DeathstreakMessages { get; set; } public int TopPlayersMinPlayTime { get; set; } + public bool StoreClientKills { get; set; } public string Name() => "Stats"; public IBaseConfiguration Generate() { @@ -49,6 +50,7 @@ namespace IW4MAdmin.Plugins.Stats.Config }; TopPlayersMinPlayTime = 3600 * 3; + StoreClientKills = false; return this; } diff --git a/Plugins/Stats/Helpers/StatManager.cs b/Plugins/Stats/Helpers/StatManager.cs index 4db734941..96981eaa9 100644 --- a/Plugins/Stats/Helpers/StatManager.cs +++ b/Plugins/Stats/Helpers/StatManager.cs @@ -150,531 +150,555 @@ namespace IW4MAdmin.Plugins.Stats.Helpers #if DEBUG == true var statsInfoSql = iqStatsInfo.ToSql(); #endif - var topPlayers = await iqStatsInfo.ToListAsync(); + var topPlayers = await iqStatsInfo.ToListAsync(); - var clientRatingsDict = clientRatings.ToDictionary(r => r.ClientId); - var finished = topPlayers.Select(s => new TopStatsInfo() - { - ClientId = s.ClientId, - Deaths = s.Deaths, - Kills = s.Kills, - KDR = Math.Round(s.KDR, 2), - LastSeen = Utilities.GetTimePassed(clientRatingsDict[s.ClientId].LastConnection, false), - Name = clientRatingsDict[s.ClientId].Name, - Performance = Math.Round(clientRatingsDict[s.ClientId].Performance, 2), - RatingChange = ratingInfo.First(r => r.Key == s.ClientId).Ratings.First().Ranking - ratingInfo.First(r => r.Key == s.ClientId).Ratings.Last().Ranking, - PerformanceHistory = ratingInfo.First(r => r.Key == s.ClientId).Ratings.Count() > 1 ? - ratingInfo.First(r => r.Key == s.ClientId).Ratings.OrderBy(r => r.When).Select(r => r.Performance).ToList() : - new List() { clientRatingsDict[s.ClientId].Performance, clientRatingsDict[s.ClientId].Performance }, - TimePlayed = Math.Round(s.TotalTimePlayed / 3600.0, 1).ToString("#,##0"), - }) - .OrderByDescending(r => r.Performance) - .ToList(); + var clientRatingsDict = clientRatings.ToDictionary(r => r.ClientId); + var finished = topPlayers.Select(s => new TopStatsInfo() + { + ClientId = s.ClientId, + Deaths = s.Deaths, + Kills = s.Kills, + KDR = Math.Round(s.KDR, 2), + LastSeen = Utilities.GetTimePassed(clientRatingsDict[s.ClientId].LastConnection, false), + Name = clientRatingsDict[s.ClientId].Name, + Performance = Math.Round(clientRatingsDict[s.ClientId].Performance, 2), + RatingChange = ratingInfo.First(r => r.Key == s.ClientId).Ratings.First().Ranking - ratingInfo.First(r => r.Key == s.ClientId).Ratings.Last().Ranking, + PerformanceHistory = ratingInfo.First(r => r.Key == s.ClientId).Ratings.Count() > 1 ? + ratingInfo.First(r => r.Key == s.ClientId).Ratings.OrderBy(r => r.When).Select(r => r.Performance).ToList() : + new List() { clientRatingsDict[s.ClientId].Performance, clientRatingsDict[s.ClientId].Performance }, + TimePlayed = Math.Round(s.TotalTimePlayed / 3600.0, 1).ToString("#,##0"), + }) + .OrderByDescending(r => r.Performance) + .ToList(); - // set the ranking numerically - int i = start + 1; - foreach (var stat in finished) + // set the ranking numerically + int i = start + 1; + foreach (var stat in finished) + { + stat.Ranking = i; + i++; + } + + return finished; + } + } + + /// + /// Add a server to the StatManager server pool + /// + /// + public void AddServer(Server sv) + { + try { - stat.Ranking = i; - i++; + int serverId = sv.GetHashCode(); + var statsSvc = new ThreadSafeStatsService(); + ContextThreads.TryAdd(serverId, statsSvc); + + // get the server from the database if it exists, otherwise create and insert a new one + var server = statsSvc.ServerSvc.Find(c => c.ServerId == serverId).FirstOrDefault(); + if (server == null) + { + server = new EFServer() + { + Port = sv.GetPort(), + Active = true, + ServerId = serverId + }; + + statsSvc.ServerSvc.Insert(server); + } + + // this doesn't need to be async as it's during initialization + 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.TryAdd(serverId, new ServerStats(server, serverStats) + { + IsTeamBased = sv.Gametype != "dm" + }); } - return finished; - } - } - - /// - /// Add a server to the StatManager server pool - /// - /// - public void AddServer(Server sv) - { - try - { - int serverId = sv.GetHashCode(); - var statsSvc = new ThreadSafeStatsService(); - ContextThreads.TryAdd(serverId, statsSvc); - - // get the server from the database if it exists, otherwise create and insert a new one - var server = statsSvc.ServerSvc.Find(c => c.ServerId == serverId).FirstOrDefault(); - if (server == null) + catch (Exception e) { - server = new EFServer() + Log.WriteError($"{Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_STATS_ERROR_ADD"]} - {e.Message}"); + } + } + + /// + /// Add Player to the player stats + /// + /// Player to add/retrieve stats for + /// EFClientStatistic of specified player + public async Task AddPlayer(Player pl) + { + int serverId = pl.CurrentServer.GetHashCode(); + + if (!Servers.ContainsKey(serverId)) + { + Log.WriteError($"[Stats::AddPlayer] Server with id {serverId} could not be found"); + return null; + } + + var playerStats = Servers[serverId].PlayerStats; + var statsSvc = ContextThreads[serverId]; + var detectionStats = Servers[serverId].PlayerDetections; + + if (playerStats.ContainsKey(pl.ClientId)) + { + Log.WriteWarning($"Duplicate ClientId in stats {pl.ClientId}"); + return null; + } + + // 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 clientStatsSvc = statsSvc.ClientStatSvc; + var clientStats = clientStatsSvc.Find(c => c.ClientId == pl.ClientId && c.ServerId == serverId).FirstOrDefault(); + + if (clientStats == null) + { + clientStats = new EFClientStatistics() { - Port = sv.GetPort(), Active = true, - ServerId = serverId + ClientId = pl.ClientId, + Deaths = 0, + Kills = 0, + ServerId = serverId, + Skill = 0.0, + SPM = 0.0, + EloRating = 200.0, + HitLocations = Enum.GetValues(typeof(IW4Info.HitLocation)).OfType().Select(hl => new EFHitLocationCount() + { + Active = true, + HitCount = 0, + Location = hl + }).ToList() }; - statsSvc.ServerSvc.Insert(server); + // insert if they've not been added + clientStats = clientStatsSvc.Insert(clientStats); + await clientStatsSvc.SaveChangesAsync(); } - // this doesn't need to be async as it's during initialization - 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.TryAdd(serverId, new ServerStats(server, serverStats) + // migration for previous existing stats + if (clientStats.HitLocations.Count == 0) { - IsTeamBased = sv.Gametype != "dm" - }); - } - - catch (Exception e) - { - Log.WriteError($"{Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_STATS_ERROR_ADD"]} - {e.Message}"); - } - } - - /// - /// Add Player to the player stats - /// - /// Player to add/retrieve stats for - /// EFClientStatistic of specified player - public async Task AddPlayer(Player pl) - { - int serverId = pl.CurrentServer.GetHashCode(); - - if (!Servers.ContainsKey(serverId)) - { - Log.WriteError($"[Stats::AddPlayer] Server with id {serverId} could not be found"); - return null; - } - - var playerStats = Servers[serverId].PlayerStats; - var statsSvc = ContextThreads[serverId]; - var detectionStats = Servers[serverId].PlayerDetections; - - if (playerStats.ContainsKey(pl.ClientId)) - { - Log.WriteWarning($"Duplicate ClientId in stats {pl.ClientId}"); - return null; - } - - // 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 clientStatsSvc = statsSvc.ClientStatSvc; - var clientStats = clientStatsSvc.Find(c => c.ClientId == pl.ClientId && c.ServerId == serverId).FirstOrDefault(); - - if (clientStats == null) - { - clientStats = new EFClientStatistics() - { - Active = true, - ClientId = pl.ClientId, - Deaths = 0, - Kills = 0, - ServerId = serverId, - Skill = 0.0, - SPM = 0.0, - EloRating = 200.0, - HitLocations = Enum.GetValues(typeof(IW4Info.HitLocation)).OfType().Select(hl => new EFHitLocationCount() + clientStats.HitLocations = Enum.GetValues(typeof(IW4Info.HitLocation)).OfType().Select(hl => new EFHitLocationCount() { Active = true, HitCount = 0, Location = hl - }).ToList() - }; + }) + .ToList(); + //await statsSvc.ClientStatSvc.SaveChangesAsync(); + } - // insert if they've not been added - clientStats = clientStatsSvc.Insert(clientStats); - await clientStatsSvc.SaveChangesAsync(); - } - - // migration for previous existing stats - if (clientStats.HitLocations.Count == 0) - { - clientStats.HitLocations = Enum.GetValues(typeof(IW4Info.HitLocation)).OfType().Select(hl => new EFHitLocationCount() + // for stats before rating + if (clientStats.EloRating == 0.0) { - Active = true, - HitCount = 0, - Location = hl - }) - .ToList(); - //await statsSvc.ClientStatSvc.SaveChangesAsync(); + clientStats.EloRating = clientStats.Skill; + } + + if (clientStats.RollingWeightedKDR == 0) + { + clientStats.RollingWeightedKDR = clientStats.KDR; + } + + // set these on connecting + clientStats.LastActive = DateTime.UtcNow; + clientStats.LastStatCalculation = DateTime.UtcNow; + clientStats.SessionScore = pl.Score; + clientStats.LastScore = pl.Score; + + Log.WriteInfo($"Adding {pl} to stats"); + + if (!playerStats.TryAdd(pl.ClientId, clientStats)) + Log.WriteDebug($"Could not add client to stats {pl}"); + + if (!detectionStats.TryAdd(pl.ClientId, new Cheat.Detection(Log, clientStats))) + Log.WriteDebug("Could not add client to detection"); + + return clientStats; } - // for stats before rating - if (clientStats.EloRating == 0.0) + /// + /// Perform stat updates for disconnecting client + /// + /// Disconnecting client + /// + public async Task RemovePlayer(Player pl) { - clientStats.EloRating = clientStats.Skill; - } + Log.WriteInfo($"Removing {pl} from stats"); - if (clientStats.RollingWeightedKDR == 0) - { - clientStats.RollingWeightedKDR = clientStats.KDR; - } + int serverId = pl.CurrentServer.GetHashCode(); + var playerStats = Servers[serverId].PlayerStats; + var detectionStats = Servers[serverId].PlayerDetections; + var serverStats = Servers[serverId].ServerStatistics; + var statsSvc = ContextThreads[serverId]; - // set these on connecting - clientStats.LastActive = DateTime.UtcNow; - clientStats.LastStatCalculation = DateTime.UtcNow; - clientStats.SessionScore = pl.Score; - clientStats.LastScore = pl.Score; + if (!playerStats.ContainsKey(pl.ClientId)) + { + Log.WriteWarning($"Client disconnecting not in stats {pl}"); + // remove the client from the stats dictionary as they're leaving + playerStats.TryRemove(pl.ClientId, out EFClientStatistics removedValue1); + detectionStats.TryRemove(pl.ClientId, out Cheat.Detection removedValue2); + return; + } - Log.WriteInfo($"Adding {pl} to stats"); - - if (!playerStats.TryAdd(pl.ClientId, clientStats)) - Log.WriteDebug($"Could not add client to stats {pl}"); - - if (!detectionStats.TryAdd(pl.ClientId, new Cheat.Detection(Log, clientStats))) - Log.WriteDebug("Could not add client to detection"); - - return clientStats; - } - - /// - /// Perform stat updates for disconnecting client - /// - /// Disconnecting client - /// - public async Task RemovePlayer(Player pl) - { - Log.WriteInfo($"Removing {pl} from stats"); - - int serverId = pl.CurrentServer.GetHashCode(); - var playerStats = Servers[serverId].PlayerStats; - var detectionStats = Servers[serverId].PlayerDetections; - var serverStats = Servers[serverId].ServerStatistics; - var statsSvc = ContextThreads[serverId]; - - if (!playerStats.ContainsKey(pl.ClientId)) - { - Log.WriteWarning($"Client disconnecting not in stats {pl}"); - // remove the client from the stats dictionary as they're leaving - playerStats.TryRemove(pl.ClientId, out EFClientStatistics removedValue1); - detectionStats.TryRemove(pl.ClientId, out Cheat.Detection removedValue2); - return; - } - - // get individual client's stats - var clientStats = playerStats[pl.ClientId]; + // get individual client's stats + var clientStats = playerStats[pl.ClientId]; #if DEBUG == true await UpdateStatHistory(pl, clientStats); #endif - // remove the client from the stats dictionary as they're leaving - playerStats.TryRemove(pl.ClientId, out EFClientStatistics removedValue3); - detectionStats.TryRemove(pl.ClientId, out Cheat.Detection removedValue4); + // remove the client from the stats dictionary as they're leaving + playerStats.TryRemove(pl.ClientId, out EFClientStatistics removedValue3); + detectionStats.TryRemove(pl.ClientId, out Cheat.Detection removedValue4); - // sync their stats before they leave - var clientStatsSvc = statsSvc.ClientStatSvc; - clientStats = UpdateStats(clientStats); - clientStatsSvc.Update(clientStats); - await clientStatsSvc.SaveChangesAsync(); + // sync their stats before they leave + var clientStatsSvc = statsSvc.ClientStatSvc; + clientStats = UpdateStats(clientStats); + clientStatsSvc.Update(clientStats); + await clientStatsSvc.SaveChangesAsync(); - // increment the total play time - serverStats.TotalPlayTime += (int)(DateTime.UtcNow - pl.LastConnection).TotalSeconds; - } - - public void AddDamageEvent(string eventLine, int attackerClientId, int victimClientId, int serverId) - { - string regex = @"^(D);(.+);([0-9]+);(allies|axis);(.+);([0-9]+);(allies|axis);(.+);(.+);([0-9]+);(.+);(.+)$"; - var match = Regex.Match(eventLine, regex, RegexOptions.IgnoreCase); - - if (match.Success) - { - // this gives us what time the player is on - var attackerStats = Servers[serverId].PlayerStats[attackerClientId]; - var victimStats = Servers[serverId].PlayerStats[victimClientId]; - IW4Info.Team victimTeam = (IW4Info.Team)Enum.Parse(typeof(IW4Info.Team), match.Groups[4].ToString()); - IW4Info.Team attackerTeam = (IW4Info.Team)Enum.Parse(typeof(IW4Info.Team), match.Groups[7].ToString()); - attackerStats.Team = attackerTeam; - victimStats.Team = victimTeam; - } - } - - /// - /// Process stats for kill event - /// - /// - public async Task AddScriptHit(bool isDamage, DateTime time, Player attacker, Player victim, int serverId, string map, string hitLoc, string type, - string damage, string weapon, string killOrigin, string deathOrigin, string viewAngles, string offset, string isKillstreakKill, string Ads, - string fraction, string visibilityPercentage, string snapAngles) - { - var statsSvc = ContextThreads[serverId]; - Vector3 vDeathOrigin = null; - Vector3 vKillOrigin = null; - Vector3 vViewAngles = null; - - try - { - vDeathOrigin = Vector3.Parse(deathOrigin); - vKillOrigin = Vector3.Parse(killOrigin); - vViewAngles = Vector3.Parse(viewAngles).FixIW4Angles(); + // increment the total play time + serverStats.TotalPlayTime += (int)(DateTime.UtcNow - pl.LastConnection).TotalSeconds; } - catch (FormatException) + public void AddDamageEvent(string eventLine, int attackerClientId, int victimClientId, int serverId) { - Log.WriteWarning("Could not parse kill or death origin or viewangle vectors"); - Log.WriteDebug($"Kill - {killOrigin} Death - {deathOrigin} ViewAngle - {viewAngles}"); - await AddStandardKill(attacker, victim); - return; - } + string regex = @"^(D);(.+);([0-9]+);(allies|axis);(.+);([0-9]+);(allies|axis);(.+);(.+);([0-9]+);(.+);(.+)$"; + var match = Regex.Match(eventLine, regex, RegexOptions.IgnoreCase); - var snapshotAngles = new List(); - - try - { - foreach (string angle in snapAngles.Split(':', StringSplitOptions.RemoveEmptyEntries)) + if (match.Success) { - snapshotAngles.Add(Vector3.Parse(angle).FixIW4Angles()); + // this gives us what time the player is on + var attackerStats = Servers[serverId].PlayerStats[attackerClientId]; + var victimStats = Servers[serverId].PlayerStats[victimClientId]; + IW4Info.Team victimTeam = (IW4Info.Team)Enum.Parse(typeof(IW4Info.Team), match.Groups[4].ToString()); + IW4Info.Team attackerTeam = (IW4Info.Team)Enum.Parse(typeof(IW4Info.Team), match.Groups[7].ToString()); + attackerStats.Team = attackerTeam; + victimStats.Team = victimTeam; } } - catch (FormatException) + /// + /// Process stats for kill event + /// + /// + public async Task AddScriptHit(bool isDamage, DateTime time, Player attacker, Player victim, int serverId, string map, string hitLoc, string type, + string damage, string weapon, string killOrigin, string deathOrigin, string viewAngles, string offset, string isKillstreakKill, string Ads, + string fraction, string visibilityPercentage, string snapAngles) { - Log.WriteWarning("Could not parse snapshot angles"); - return; - } + var statsSvc = ContextThreads[serverId]; + Vector3 vDeathOrigin = null; + Vector3 vKillOrigin = null; + Vector3 vViewAngles = null; - var hit = new EFClientKill() - { - Active = true, - AttackerId = attacker.ClientId, - VictimId = victim.ClientId, - ServerId = serverId, - Map = ParseEnum.Get(map, typeof(IW4Info.MapName)), - DeathOrigin = vDeathOrigin, - KillOrigin = vKillOrigin, - DeathType = ParseEnum.Get(type, typeof(IW4Info.MeansOfDeath)), - Damage = Int32.Parse(damage), - HitLoc = ParseEnum.Get(hitLoc, typeof(IW4Info.HitLocation)), - Weapon = ParseEnum.Get(weapon, typeof(IW4Info.WeaponName)), - ViewAngles = vViewAngles, - TimeOffset = Int64.Parse(offset), - When = time, - IsKillstreakKill = isKillstreakKill[0] != '0', - AdsPercent = float.Parse(Ads), - Fraction = double.Parse(fraction), - VisibilityPercentage = double.Parse(visibilityPercentage), - IsKill = !isDamage, - AnglesList = snapshotAngles - }; - - if (hit.DeathType == IW4Info.MeansOfDeath.MOD_SUICIDE && - hit.Damage == 100000) - { - // suicide by switching teams so let's not count it against them - return; - } - - if (!isDamage) - { - await AddStandardKill(attacker, victim); - } - - if (hit.IsKillstreakKill) - { - return; - } - - var clientDetection = Servers[serverId].PlayerDetections[attacker.ClientId]; - var clientStats = Servers[serverId].PlayerStats[attacker.ClientId]; - var clientStatsSvc = statsSvc.ClientStatSvc; - clientStatsSvc.Update(clientStats); - - // increment their hit count - if (hit.DeathType == IW4Info.MeansOfDeath.MOD_PISTOL_BULLET || - hit.DeathType == IW4Info.MeansOfDeath.MOD_RIFLE_BULLET || - hit.DeathType == IW4Info.MeansOfDeath.MOD_HEAD_SHOT) - { - clientStats.HitLocations.Single(hl => hl.Location == hit.HitLoc).HitCount += 1; - } - - if (Plugin.Config.Configuration().EnableAntiCheat) - { - async Task executePenalty(Cheat.DetectionPenaltyResult penalty) + try { - async Task saveLog() - { - using (var ctx = new DatabaseContext(false)) - { - // todo: why does this cause duplicate primary key - foreach (var change in clientDetection.Tracker.GetChanges().Distinct()) - { - ctx.Add(change); - await ctx.SaveChangesAsync(); - } - } - } + vDeathOrigin = Vector3.Parse(deathOrigin); + vKillOrigin = Vector3.Parse(killOrigin); + vViewAngles = Vector3.Parse(viewAngles).FixIW4Angles(); + } - await OnProcessingPenalty.WaitAsync(); + catch (FormatException) + { + Log.WriteWarning("Could not parse kill or death origin or viewangle vectors"); + Log.WriteDebug($"Kill - {killOrigin} Death - {deathOrigin} ViewAngle - {viewAngles}"); + await AddStandardKill(attacker, victim); + return; + } + + var snapshotAngles = new List(); + + try + { + foreach (string angle in snapAngles.Split(':', StringSplitOptions.RemoveEmptyEntries)) + { + snapshotAngles.Add(Vector3.Parse(angle).FixIW4Angles()); + } + } + + catch (FormatException) + { + Log.WriteWarning("Could not parse snapshot angles"); + return; + } + + var hit = new EFClientKill() + { + Active = true, + AttackerId = attacker.ClientId, + VictimId = victim.ClientId, + ServerId = serverId, + Map = ParseEnum.Get(map, typeof(IW4Info.MapName)), + DeathOrigin = vDeathOrigin, + KillOrigin = vKillOrigin, + DeathType = ParseEnum.Get(type, typeof(IW4Info.MeansOfDeath)), + Damage = Int32.Parse(damage), + HitLoc = ParseEnum.Get(hitLoc, typeof(IW4Info.HitLocation)), + Weapon = ParseEnum.Get(weapon, typeof(IW4Info.WeaponName)), + ViewAngles = vViewAngles, + TimeOffset = Int64.Parse(offset), + When = time, + IsKillstreakKill = isKillstreakKill[0] != '0', + AdsPercent = float.Parse(Ads), + Fraction = double.Parse(fraction), + VisibilityPercentage = double.Parse(visibilityPercentage), + IsKill = !isDamage, + AnglesList = snapshotAngles + }; + + if (hit.DeathType == IW4Info.MeansOfDeath.MOD_SUICIDE && + hit.Damage == 100000) + { + // suicide by switching teams so let's not count it against them + return; + } + + if (!isDamage) + { + await AddStandardKill(attacker, victim); + } + + if (hit.IsKillstreakKill) + { + return; + } + + var clientDetection = Servers[serverId].PlayerDetections[attacker.ClientId]; + var clientStats = Servers[serverId].PlayerStats[attacker.ClientId]; + var clientStatsSvc = statsSvc.ClientStatSvc; + clientStatsSvc.Update(clientStats); + + // increment their hit count + if (hit.DeathType == IW4Info.MeansOfDeath.MOD_PISTOL_BULLET || + hit.DeathType == IW4Info.MeansOfDeath.MOD_RIFLE_BULLET || + hit.DeathType == IW4Info.MeansOfDeath.MOD_HEAD_SHOT) + { + clientStats.HitLocations.Single(hl => hl.Location == hit.HitLoc).HitCount += 1; + } + + if (Plugin.Config.Configuration().EnableAntiCheat) + { + await ApplyPenalty(clientDetection.ProcessKill(hit, isDamage), clientDetection, attacker); + await ApplyPenalty(clientDetection.ProcessTotalRatio(clientStats), clientDetection, attacker); + + await clientStatsSvc.SaveChangesAsync(); + } + + if (Plugin.Config.Configuration().StoreClientKills) + { + using (var ctx = new DatabaseContext()) + { + ctx.Set().Add(hit); + await ctx.SaveChangesAsync(); + } + } + } + + async Task ApplyPenalty(Cheat.DetectionPenaltyResult penalty, Cheat.Detection clientDetection, Player attacker) + { + await OnProcessingPenalty.WaitAsync(); + + try + { + switch (penalty.ClientPenalty) + { + case Penalty.PenaltyType.Ban: + if (attacker.Level == Player.Permission.Banned) + { + break; + } + if (clientDetection.Tracker.HasChanges) + { + await SaveTrackedSnapshots(clientDetection); + } + await attacker.Ban(Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_STATS_CHEAT_DETECTED"], new Player() + { + ClientId = 1, + AdministeredPenalties = new List() + { + new EFPenalty() + { + AutomatedOffense = penalty.Type == Cheat.Detection.DetectionType.Bone ? + $"{penalty.Type}-{(int)penalty.Location}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}" : + $"{penalty.Type}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}", + } + } + }); + break; + case Penalty.PenaltyType.Flag: + if (attacker.Level != Player.Permission.User) + { + break; + } + if (clientDetection.Tracker.HasChanges) + { + await SaveTrackedSnapshots(clientDetection); + } + var e = new GameEvent() + { + Data = penalty.Type == Cheat.Detection.DetectionType.Bone ? + $"{penalty.Type}-{(int)penalty.Location}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}" : + $"{penalty.Type}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}", + Origin = new Player() + { + ClientId = 1, + Level = Player.Permission.Console, + ClientNumber = -1, + CurrentServer = attacker.CurrentServer + }, + Target = attacker, + Owner = attacker.CurrentServer, + Type = GameEvent.EventType.Flag + }; + // because we created an event it must be processed by the manager + // even if it didn't really do anything + Manager.GetEventHandler().AddEvent(e); + await new CFlag().ExecuteAsync(e); + break; + } + OnProcessingPenalty.Release(1); + } + catch + { + OnProcessingPenalty.Release(1); + } + } + + async Task SaveTrackedSnapshots(Cheat.Detection clientDetection) + { + using (var ctx = new DatabaseContext(true)) + { + // todo: why does this cause duplicate primary key + foreach (var change in clientDetection.Tracker + .GetChanges() + .Where(c => c.SnapshotId == 0)) + { + ctx.Add(change); + } try { - switch (penalty.ClientPenalty) - { - case Penalty.PenaltyType.Ban: - if (attacker.Level == Player.Permission.Banned) - break; - await attacker.Ban(Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_STATS_CHEAT_DETECTED"], new Player() - { - ClientId = 1, - AdministeredPenalties = new List() - { - new EFPenalty() - { - AutomatedOffense = penalty.Type == Cheat.Detection.DetectionType.Bone ? - $"{penalty.Type}-{(int)penalty.Location}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}" : - $"{penalty.Type}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}", - } - } - }); - await saveLog(); - break; - case Penalty.PenaltyType.Flag: - if (attacker.Level != Player.Permission.User) - break; - var e = new GameEvent() - { - Data = penalty.Type == Cheat.Detection.DetectionType.Bone ? - $"{penalty.Type}-{(int)penalty.Location}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}" : - $"{penalty.Type}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}", - Origin = new Player() - { - ClientId = 1, - Level = Player.Permission.Console, - ClientNumber = -1, - CurrentServer = attacker.CurrentServer - }, - Target = attacker, - Owner = attacker.CurrentServer, - Type = GameEvent.EventType.Flag - }; - await saveLog(); - // because we created an event it must be processed by the manager - // even if it didn't really do anything - Manager.GetEventHandler().AddEvent(e); - await new CFlag().ExecuteAsync(e); - break; - } - OnProcessingPenalty.Release(); - } - catch - { - OnProcessingPenalty.Release(); + await ctx.SaveChangesAsync(); + clientDetection.Tracker.ClearChanges(); } + catch (Exception ex) + { + Log.WriteWarning(ex.GetExceptionInfo()); + } + } + } + + public async Task AddStandardKill(Player attacker, Player victim) + { + int serverId = attacker.CurrentServer.GetHashCode(); + EFClientStatistics attackerStats = null; + try + { + attackerStats = Servers[serverId].PlayerStats[attacker.ClientId]; } - await executePenalty(clientDetection.ProcessKill(hit, isDamage)); - await executePenalty(clientDetection.ProcessTotalRatio(clientStats)); + catch (KeyNotFoundException) + { + // happens when the client has disconnected before the last status update + Log.WriteWarning($"[Stats::AddStandardKill] kill attacker ClientId is invalid {attacker.ClientId}-{attacker}"); + return; + } - await clientStatsSvc.SaveChangesAsync(); - } + EFClientStatistics victimStats = null; + try + { + victimStats = Servers[serverId].PlayerStats[victim.ClientId]; + } - using (var ctx = new DatabaseContext()) - { - ctx.Set().Add(hit); - await ctx.SaveChangesAsync(); - } - } - - public async Task AddStandardKill(Player attacker, Player victim) - { - int serverId = attacker.CurrentServer.GetHashCode(); - EFClientStatistics attackerStats = null; - try - { - attackerStats = Servers[serverId].PlayerStats[attacker.ClientId]; - } - - catch (KeyNotFoundException) - { - // happens when the client has disconnected before the last status update - Log.WriteWarning($"[Stats::AddStandardKill] kill attacker ClientId is invalid {attacker.ClientId}-{attacker}"); - return; - } - - EFClientStatistics victimStats = null; - try - { - victimStats = Servers[serverId].PlayerStats[victim.ClientId]; - } - - catch (KeyNotFoundException) - { - Log.WriteWarning($"[Stats::AddStandardKill] kill victim ClientId is invalid {victim.ClientId}-{victim}"); - return; - } + catch (KeyNotFoundException) + { + Log.WriteWarning($"[Stats::AddStandardKill] kill victim ClientId is invalid {victim.ClientId}-{victim}"); + return; + } #if DEBUG Log.WriteDebug("Calculating standard kill"); #endif - // update the total stats - Servers[serverId].ServerStatistics.TotalKills += 1; + // update the total stats + Servers[serverId].ServerStatistics.TotalKills += 1; - // this happens when the round has changed - if (attackerStats.SessionScore == 0) - attackerStats.LastScore = 0; + // this happens when the round has changed + if (attackerStats.SessionScore == 0) + attackerStats.LastScore = 0; - if (victimStats.SessionScore == 0) - victimStats.LastScore = 0; + if (victimStats.SessionScore == 0) + victimStats.LastScore = 0; - attackerStats.SessionScore = attacker.Score; - victimStats.SessionScore = victim.Score; + attackerStats.SessionScore = attacker.Score; + victimStats.SessionScore = victim.Score; - // calculate for the clients - CalculateKill(attackerStats, victimStats); - // this should fix the negative SPM - // updates their last score after being calculated - attackerStats.LastScore = attacker.Score; - victimStats.LastScore = victim.Score; + // calculate for the clients + CalculateKill(attackerStats, victimStats); + // this should fix the negative SPM + // updates their last score after being calculated + attackerStats.LastScore = attacker.Score; + victimStats.LastScore = victim.Score; - // show encouragement/discouragement - string streakMessage = (attackerStats.ClientId != victimStats.ClientId) ? - StreakMessage.MessageOnStreak(attackerStats.KillStreak, attackerStats.DeathStreak) : - StreakMessage.MessageOnStreak(-1, -1); + // show encouragement/discouragement + string streakMessage = (attackerStats.ClientId != victimStats.ClientId) ? + StreakMessage.MessageOnStreak(attackerStats.KillStreak, attackerStats.DeathStreak) : + StreakMessage.MessageOnStreak(-1, -1); - if (streakMessage != string.Empty) - await attacker.Tell(streakMessage); + if (streakMessage != string.Empty) + await attacker.Tell(streakMessage); - // fixme: why? - if (double.IsNaN(victimStats.SPM) || double.IsNaN(victimStats.Skill)) - { - Log.WriteDebug($"[StatManager::AddStandardKill] victim SPM/SKILL {victimStats.SPM} {victimStats.Skill}"); - victimStats.SPM = 0.0; - victimStats.Skill = 0.0; - } + // fixme: why? + if (double.IsNaN(victimStats.SPM) || double.IsNaN(victimStats.Skill)) + { + Log.WriteDebug($"[StatManager::AddStandardKill] victim SPM/SKILL {victimStats.SPM} {victimStats.Skill}"); + victimStats.SPM = 0.0; + victimStats.Skill = 0.0; + } - if (double.IsNaN(attackerStats.SPM) || double.IsNaN(attackerStats.Skill)) - { - Log.WriteDebug($"[StatManager::AddStandardKill] attacker SPM/SKILL {victimStats.SPM} {victimStats.Skill}"); - attackerStats.SPM = 0.0; - attackerStats.Skill = 0.0; - } + if (double.IsNaN(attackerStats.SPM) || double.IsNaN(attackerStats.Skill)) + { + Log.WriteDebug($"[StatManager::AddStandardKill] attacker SPM/SKILL {victimStats.SPM} {victimStats.Skill}"); + attackerStats.SPM = 0.0; + attackerStats.Skill = 0.0; + } - // update their performance + // update their performance #if !DEBUG if ((DateTime.UtcNow - attackerStats.LastStatHistoryUpdate).TotalMinutes >= 2.5) #endif - { - attackerStats.LastStatHistoryUpdate = DateTime.UtcNow; - await UpdateStatHistory(attacker, attackerStats); + { + attackerStats.LastStatHistoryUpdate = DateTime.UtcNow; + await UpdateStatHistory(attacker, attackerStats); + } + + // todo: do we want to save this immediately? + var clientStatsSvc = ContextThreads[serverId].ClientStatSvc; + clientStatsSvc.Update(attackerStats); + clientStatsSvc.Update(victimStats); + await clientStatsSvc.SaveChangesAsync(); } - // todo: do we want to save this immediately? - var clientStatsSvc = ContextThreads[serverId].ClientStatSvc; - clientStatsSvc.Update(attackerStats); - clientStatsSvc.Update(victimStats); - await clientStatsSvc.SaveChangesAsync(); - } + /// + /// Update the invidual and average stat history for a client + /// + /// client to update + /// stats of client that is being updated + /// + private async Task UpdateStatHistory(Player client, EFClientStatistics clientStats) + { + int currentSessionTime = (int)(DateTime.UtcNow - client.LastConnection).TotalSeconds; - /// - /// Update the invidual and average stat history for a client - /// - /// client to update - /// stats of client that is being updated - /// - private async Task UpdateStatHistory(Player client, EFClientStatistics clientStats) - { - int currentSessionTime = (int)(DateTime.UtcNow - client.LastConnection).TotalSeconds; - - // don't update their stat history if they haven't played long + // don't update their stat history if they haven't played long #if DEBUG == false if (currentSessionTime < 60) { @@ -682,419 +706,419 @@ namespace IW4MAdmin.Plugins.Stats.Helpers } #endif - int currentServerTotalPlaytime = clientStats.TimePlayed + currentSessionTime; + int currentServerTotalPlaytime = clientStats.TimePlayed + currentSessionTime; - using (var ctx = new DatabaseContext()) - { - // select the rating history for client - var iqHistoryLink = from history in ctx.Set() - .Include(h => h.Ratings) - where history.ClientId == client.ClientId - select history; + using (var ctx = new DatabaseContext()) + { + // select the rating history for client + var iqHistoryLink = from history in ctx.Set() + .Include(h => h.Ratings) + where history.ClientId == client.ClientId + select history; - // get the client ratings - var clientHistory = await iqHistoryLink - .FirstOrDefaultAsync() ?? new EFClientRatingHistory() + // get the client ratings + var clientHistory = await iqHistoryLink + .FirstOrDefaultAsync() ?? new EFClientRatingHistory() + { + Active = true, + ClientId = client.ClientId, + Ratings = new List() + }; + + // it's the first time they've played + if (clientHistory.RatingHistoryId == 0) { + ctx.Add(clientHistory); + // Log.WriteDebug($"adding first time client history {client.ClientId}"); + await ctx.SaveChangesAsync(); + } + + else + { + //ctx.Update(clientHistory); + } + + #region INDIVIDUAL_SERVER_PERFORMANCE + // get the client ranking for the current server + int individualClientRanking = await ctx.Set() + .Where(GetRankingFunc(clientStats.ServerId)) + // ignore themselves in the query + .Where(c => c.RatingHistory.ClientId != client.ClientId) + .Where(c => c.Performance > clientStats.Performance) + .CountAsync() + 1; + + // limit max history per server to 40 + if (clientHistory.Ratings.Count(r => r.ServerId == clientStats.ServerId) >= 40) + { + // select the oldest one + var ratingToRemove = clientHistory.Ratings + .Where(r => r.ServerId == clientStats.ServerId) + .OrderBy(r => r.When) + .First(); + + ctx.Remove(ratingToRemove); + //Log.WriteDebug($"remove oldest rating {client.ClientId}"); + await ctx.SaveChangesAsync(); + } + + // set the previous newest to false + var ratingToUnsetNewest = clientHistory.Ratings + .Where(r => r.ServerId == clientStats.ServerId) + .OrderByDescending(r => r.When) + .FirstOrDefault(); + + if (ratingToUnsetNewest != null) + { + if (ratingToUnsetNewest.Newest) + { + ctx.Update(ratingToUnsetNewest); + ctx.Entry(ratingToUnsetNewest).Property(r => r.Newest).IsModified = true; + ratingToUnsetNewest.Newest = false; + //Log.WriteDebug($"unsetting previous newest flag {client.ClientId}"); + await ctx.SaveChangesAsync(); + } + } + + var newServerRating = new EFRating() + { + Performance = clientStats.Performance, + Ranking = individualClientRanking, Active = true, - ClientId = client.ClientId, - Ratings = new List() + Newest = true, + ServerId = clientStats.ServerId, + RatingHistoryId = clientHistory.RatingHistoryId, + ActivityAmount = currentServerTotalPlaytime, }; - // it's the first time they've played - if (clientHistory.RatingHistoryId == 0) - { - ctx.Add(clientHistory); - // Log.WriteDebug($"adding first time client history {client.ClientId}"); + // add new rating for current server + ctx.Add(newServerRating); + + //Log.WriteDebug($"adding new server rating {client.ClientId}"); await ctx.SaveChangesAsync(); - } - else - { - //ctx.Update(clientHistory); - } + #endregion + #region OVERALL_RATING + // select all performance & time played for current client + var iqClientStats = from stats in ctx.Set() + where stats.ClientId == client.ClientId + where stats.ServerId != clientStats.ServerId + select new + { + stats.Performance, + stats.TimePlayed + }; - #region INDIVIDUAL_SERVER_PERFORMANCE - // get the client ranking for the current server - int individualClientRanking = await ctx.Set() - .Where(GetRankingFunc(clientStats.ServerId)) - // ignore themselves in the query - .Where(c => c.RatingHistory.ClientId != client.ClientId) - .Where(c => c.Performance > clientStats.Performance) - .CountAsync() + 1; + var clientStatsList = await iqClientStats.ToListAsync(); - // limit max history per server to 40 - if (clientHistory.Ratings.Count(r => r.ServerId == clientStats.ServerId) >= 40) - { - // select the oldest one - var ratingToRemove = clientHistory.Ratings - .Where(r => r.ServerId == clientStats.ServerId) - .OrderBy(r => r.When) - .First(); - - ctx.Remove(ratingToRemove); - //Log.WriteDebug($"remove oldest rating {client.ClientId}"); - await ctx.SaveChangesAsync(); - } - - // set the previous newest to false - var ratingToUnsetNewest = clientHistory.Ratings - .Where(r => r.ServerId == clientStats.ServerId) - .OrderByDescending(r => r.When) - .FirstOrDefault(); - - if (ratingToUnsetNewest != null) - { - if (ratingToUnsetNewest.Newest) + // add the current server's so we don't have to pull it frmo the database + clientStatsList.Add(new { - ctx.Update(ratingToUnsetNewest); - ctx.Entry(ratingToUnsetNewest).Property(r => r.Newest).IsModified = true; - ratingToUnsetNewest.Newest = false; - //Log.WriteDebug($"unsetting previous newest flag {client.ClientId}"); + clientStats.Performance, + TimePlayed = currentServerTotalPlaytime + }); + + // weight the overall performance based on play time + double performanceAverage = clientStatsList.Sum(p => (p.Performance * p.TimePlayed)) / clientStatsList.Sum(p => p.TimePlayed); + + // shouldn't happen but just in case the sum of time played is 0 + if (double.IsNaN(performanceAverage)) + { + performanceAverage = clientStatsList.Average(p => p.Performance); + } + + int overallClientRanking = await ctx.Set() + .Where(GetRankingFunc()) + .Where(r => r.RatingHistory.ClientId != client.ClientId) + .Where(r => r.Performance > performanceAverage) + .CountAsync() + 1; + + // limit max average history to 40 + if (clientHistory.Ratings.Count(r => r.ServerId == null) >= 40) + { + var ratingToRemove = clientHistory.Ratings + .Where(r => r.ServerId == null) + .OrderBy(r => r.When) + .First(); + + ctx.Remove(ratingToRemove); + //Log.WriteDebug($"remove oldest overall rating {client.ClientId}"); await ctx.SaveChangesAsync(); } - } - var newServerRating = new EFRating() - { - Performance = clientStats.Performance, - Ranking = individualClientRanking, - Active = true, - Newest = true, - ServerId = clientStats.ServerId, - RatingHistoryId = clientHistory.RatingHistoryId, - ActivityAmount = currentServerTotalPlaytime, - }; - - // add new rating for current server - ctx.Add(newServerRating); - - //Log.WriteDebug($"adding new server rating {client.ClientId}"); - await ctx.SaveChangesAsync(); - - #endregion - #region OVERALL_RATING - // select all performance & time played for current client - var iqClientStats = from stats in ctx.Set() - where stats.ClientId == client.ClientId - where stats.ServerId != clientStats.ServerId - select new - { - stats.Performance, - stats.TimePlayed - }; - - var clientStatsList = await iqClientStats.ToListAsync(); - - // add the current server's so we don't have to pull it frmo the database - clientStatsList.Add(new - { - clientStats.Performance, - TimePlayed = currentServerTotalPlaytime - }); - - // weight the overall performance based on play time - double performanceAverage = clientStatsList.Sum(p => (p.Performance * p.TimePlayed)) / clientStatsList.Sum(p => p.TimePlayed); - - // shouldn't happen but just in case the sum of time played is 0 - if (double.IsNaN(performanceAverage)) - { - performanceAverage = clientStatsList.Average(p => p.Performance); - } - - int overallClientRanking = await ctx.Set() - .Where(GetRankingFunc()) - .Where(r => r.RatingHistory.ClientId != client.ClientId) - .Where(r => r.Performance > performanceAverage) - .CountAsync() + 1; - - // limit max average history to 40 - if (clientHistory.Ratings.Count(r => r.ServerId == null) >= 40) - { - var ratingToRemove = clientHistory.Ratings + // set the previous average newest to false + ratingToUnsetNewest = clientHistory.Ratings .Where(r => r.ServerId == null) - .OrderBy(r => r.When) - .First(); + .OrderByDescending(r => r.When) + .FirstOrDefault(); - ctx.Remove(ratingToRemove); - //Log.WriteDebug($"remove oldest overall rating {client.ClientId}"); + if (ratingToUnsetNewest != null) + { + if (ratingToUnsetNewest.Newest) + { + ctx.Update(ratingToUnsetNewest); + ctx.Entry(ratingToUnsetNewest).Property(r => r.Newest).IsModified = true; + ratingToUnsetNewest.Newest = false; + //Log.WriteDebug($"unsetting overall newest rating {client.ClientId}"); + await ctx.SaveChangesAsync(); + } + } + + // add new average rating + var averageRating = new EFRating() + { + Active = true, + Newest = true, + Performance = performanceAverage, + Ranking = overallClientRanking, + ServerId = null, + RatingHistoryId = clientHistory.RatingHistoryId, + ActivityAmount = clientStatsList.Sum(s => s.TimePlayed) + }; + + ctx.Add(averageRating); + #endregion + //Log.WriteDebug($"adding new average rating {client.ClientId}"); await ctx.SaveChangesAsync(); } + } - // set the previous average newest to false - ratingToUnsetNewest = clientHistory.Ratings - .Where(r => r.ServerId == null) - .OrderByDescending(r => r.When) - .FirstOrDefault(); + /// + /// Performs the incrementation of kills and deaths for client statistics + /// + /// Stats of the attacker + /// Stats of the victim + public void CalculateKill(EFClientStatistics attackerStats, EFClientStatistics victimStats) + { + bool suicide = attackerStats.ClientId == victimStats.ClientId; - if (ratingToUnsetNewest != null) + // only update their kills if they didn't kill themselves + if (!suicide) { - if (ratingToUnsetNewest.Newest) - { - ctx.Update(ratingToUnsetNewest); - ctx.Entry(ratingToUnsetNewest).Property(r => r.Newest).IsModified = true; - ratingToUnsetNewest.Newest = false; - //Log.WriteDebug($"unsetting overall newest rating {client.ClientId}"); - await ctx.SaveChangesAsync(); - } + attackerStats.Kills += 1; + attackerStats.SessionKills += 1; + attackerStats.KillStreak += 1; + attackerStats.DeathStreak = 0; } - // add new average rating - var averageRating = new EFRating() + victimStats.Deaths += 1; + victimStats.SessionDeaths += 1; + victimStats.DeathStreak += 1; + victimStats.KillStreak = 0; + + // process the attacker's stats after the kills + attackerStats = UpdateStats(attackerStats); + + // calulate elo + if (Servers[attackerStats.ServerId].PlayerStats.Count > 1) { - Active = true, - Newest = true, - Performance = performanceAverage, - Ranking = overallClientRanking, - ServerId = null, - RatingHistoryId = clientHistory.RatingHistoryId, - ActivityAmount = clientStatsList.Sum(s => s.TimePlayed) - }; + /* var validAttackerLobbyRatings = Servers[attackerStats.ServerId].PlayerStats + .Where(cs => cs.Value.ClientId != attackerStats.ClientId) + .Where(cs => + Servers[attackerStats.ServerId].IsTeamBased ? + cs.Value.Team != attackerStats.Team : + cs.Value.Team != IW4Info.Team.Spectator) + .Where(cs => cs.Value.Team != IW4Info.Team.Spectator); - ctx.Add(averageRating); - #endregion - //Log.WriteDebug($"adding new average rating {client.ClientId}"); - await ctx.SaveChangesAsync(); - } - } + double attackerLobbyRating = validAttackerLobbyRatings.Count() > 0 ? + validAttackerLobbyRatings.Average(cs => cs.Value.EloRating) : + attackerStats.EloRating; - /// - /// Performs the incrementation of kills and deaths for client statistics - /// - /// Stats of the attacker - /// Stats of the victim - public void CalculateKill(EFClientStatistics attackerStats, EFClientStatistics victimStats) - { - bool suicide = attackerStats.ClientId == victimStats.ClientId; + var validVictimLobbyRatings = Servers[victimStats.ServerId].PlayerStats + .Where(cs => cs.Value.ClientId != victimStats.ClientId) + .Where(cs => + Servers[attackerStats.ServerId].IsTeamBased ? + cs.Value.Team != victimStats.Team : + cs.Value.Team != IW4Info.Team.Spectator) + .Where(cs => cs.Value.Team != IW4Info.Team.Spectator); - // only update their kills if they didn't kill themselves - if (!suicide) - { - attackerStats.Kills += 1; - attackerStats.SessionKills += 1; - attackerStats.KillStreak += 1; - attackerStats.DeathStreak = 0; + double victimLobbyRating = validVictimLobbyRatings.Count() > 0 ? + validVictimLobbyRatings.Average(cs => cs.Value.EloRating) : + victimStats.EloRating;*/ + + double attackerEloDifference = Math.Log(Math.Max(1, victimStats.EloRating)) - Math.Log(Math.Max(1, attackerStats.EloRating)); + double winPercentage = 1.0 / (1 + Math.Pow(10, attackerEloDifference / Math.E)); + + // double victimEloDifference = Math.Log(Math.Max(1, attackerStats.EloRating)) - Math.Log(Math.Max(1, victimStats.EloRating)); + // double lossPercentage = 1.0 / (1 + Math.Pow(10, victimEloDifference/ Math.E)); + + attackerStats.EloRating += 6.0 * (1 - winPercentage); + victimStats.EloRating -= 6.0 * (1 - winPercentage); + + attackerStats.EloRating = Math.Max(0, Math.Round(attackerStats.EloRating, 2)); + victimStats.EloRating = Math.Max(0, Math.Round(victimStats.EloRating, 2)); + } + + // update after calculation + attackerStats.TimePlayed += (int)(DateTime.UtcNow - attackerStats.LastActive).TotalSeconds; + victimStats.TimePlayed += (int)(DateTime.UtcNow - victimStats.LastActive).TotalSeconds; + attackerStats.LastActive = DateTime.UtcNow; + victimStats.LastActive = DateTime.UtcNow; } - victimStats.Deaths += 1; - victimStats.SessionDeaths += 1; - victimStats.DeathStreak += 1; - victimStats.KillStreak = 0; - - // process the attacker's stats after the kills - attackerStats = UpdateStats(attackerStats); - - // calulate elo - if (Servers[attackerStats.ServerId].PlayerStats.Count > 1) + /// + /// Update the client stats (skill etc) + /// + /// Client statistics + /// + private EFClientStatistics UpdateStats(EFClientStatistics clientStats) { - /* var validAttackerLobbyRatings = Servers[attackerStats.ServerId].PlayerStats - .Where(cs => cs.Value.ClientId != attackerStats.ClientId) - .Where(cs => - Servers[attackerStats.ServerId].IsTeamBased ? - cs.Value.Team != attackerStats.Team : - cs.Value.Team != IW4Info.Team.Spectator) - .Where(cs => cs.Value.Team != IW4Info.Team.Spectator); + // prevent NaN or inactive time lowering SPM + if ((DateTime.UtcNow - clientStats.LastStatCalculation).TotalSeconds / 60.0 < 0.01 || + (DateTime.UtcNow - clientStats.LastActive).TotalSeconds / 60.0 > 3 || + clientStats.SessionScore == 0) + { + // prevents idle time counting + clientStats.LastStatCalculation = DateTime.UtcNow; + return clientStats; + } - double attackerLobbyRating = validAttackerLobbyRatings.Count() > 0 ? - validAttackerLobbyRatings.Average(cs => cs.Value.EloRating) : - attackerStats.EloRating; + double timeSinceLastCalc = (DateTime.UtcNow - clientStats.LastStatCalculation).TotalSeconds / 60.0; + double timeSinceLastActive = (DateTime.UtcNow - clientStats.LastActive).TotalSeconds / 60.0; - var validVictimLobbyRatings = Servers[victimStats.ServerId].PlayerStats - .Where(cs => cs.Value.ClientId != victimStats.ClientId) - .Where(cs => - Servers[attackerStats.ServerId].IsTeamBased ? - cs.Value.Team != victimStats.Team : - cs.Value.Team != IW4Info.Team.Spectator) - .Where(cs => cs.Value.Team != IW4Info.Team.Spectator); + int scoreDifference = 0; + // this means they've been tking or suicide and is the only time they can have a negative SPM + if (clientStats.RoundScore < 0) + { + scoreDifference = clientStats.RoundScore + clientStats.LastScore; + } - double victimLobbyRating = validVictimLobbyRatings.Count() > 0 ? - validVictimLobbyRatings.Average(cs => cs.Value.EloRating) : - victimStats.EloRating;*/ + else if (clientStats.RoundScore > 0 && clientStats.LastScore < clientStats.RoundScore) + { + scoreDifference = clientStats.RoundScore - clientStats.LastScore; + } - double attackerEloDifference = Math.Log(Math.Max(1, victimStats.EloRating)) - Math.Log(Math.Max(1, attackerStats.EloRating)); - double winPercentage = 1.0 / (1 + Math.Pow(10, attackerEloDifference / Math.E)); + double killSPM = scoreDifference / timeSinceLastCalc; + double spmMultiplier = 2.934 * Math.Pow(Servers[clientStats.ServerId].TeamCount(clientStats.Team == IW4Info.Team.Allies ? IW4Info.Team.Axis : IW4Info.Team.Allies), -0.454); + killSPM *= Math.Max(1, spmMultiplier); - // double victimEloDifference = Math.Log(Math.Max(1, attackerStats.EloRating)) - Math.Log(Math.Max(1, victimStats.EloRating)); - // double lossPercentage = 1.0 / (1 + Math.Pow(10, victimEloDifference/ Math.E)); + // update this for ac tracking + clientStats.SessionSPM = killSPM; - attackerStats.EloRating += 6.0 * (1 - winPercentage); - victimStats.EloRating -= 6.0 * (1 - winPercentage); + // calculate how much the KDR should weigh + // 1.637 is a Eddie-Generated number that weights the KDR nicely + double currentKDR = clientStats.SessionDeaths == 0 ? clientStats.SessionKills : clientStats.SessionKills / clientStats.SessionDeaths; + double alpha = Math.Sqrt(2) / Math.Min(600, Math.Max(clientStats.Kills + clientStats.Deaths, 1)); + clientStats.RollingWeightedKDR = (alpha * currentKDR) + (1.0 - alpha) * clientStats.KDR; + double KDRWeight = Math.Round(Math.Pow(clientStats.RollingWeightedKDR, 1.637 / Math.E), 3); - attackerStats.EloRating = Math.Max(0, Math.Round(attackerStats.EloRating, 2)); - victimStats.EloRating = Math.Max(0, Math.Round(victimStats.EloRating, 2)); - } + // calculate the weight of the new play time against last 10 hours of gameplay + int totalPlayTime = (clientStats.TimePlayed == 0) ? + (int)(DateTime.UtcNow - clientStats.LastActive).TotalSeconds : + clientStats.TimePlayed + (int)(DateTime.UtcNow - clientStats.LastActive).TotalSeconds; - // update after calculation - attackerStats.TimePlayed += (int)(DateTime.UtcNow - attackerStats.LastActive).TotalSeconds; - victimStats.TimePlayed += (int)(DateTime.UtcNow - victimStats.LastActive).TotalSeconds; - attackerStats.LastActive = DateTime.UtcNow; - victimStats.LastActive = DateTime.UtcNow; - } + double SPMAgainstPlayWeight = timeSinceLastCalc / Math.Min(600, (totalPlayTime / 60.0)); + + // calculate the new weight against average times the weight against play time + clientStats.SPM = (killSPM * SPMAgainstPlayWeight) + (clientStats.SPM * (1 - SPMAgainstPlayWeight)); + + if (clientStats.SPM < 0) + { + Log.WriteWarning("[StatManager:UpdateStats] clientStats SPM < 0"); + Log.WriteDebug($"{scoreDifference}-{clientStats.RoundScore} - {clientStats.LastScore} - {clientStats.SessionScore}"); + clientStats.SPM = 0; + } + + clientStats.SPM = Math.Round(clientStats.SPM, 3); + clientStats.Skill = Math.Round((clientStats.SPM * KDRWeight), 3); + + // fixme: how does this happen? + if (double.IsNaN(clientStats.SPM) || double.IsNaN(clientStats.Skill)) + { + Log.WriteWarning("[StatManager::UpdateStats] clientStats SPM/Skill NaN"); + Log.WriteDebug($"{killSPM}-{KDRWeight}-{totalPlayTime}-{SPMAgainstPlayWeight}-{clientStats.SPM}-{clientStats.Skill}-{scoreDifference}"); + clientStats.SPM = 0; + clientStats.Skill = 0; + } - /// - /// Update the client stats (skill etc) - /// - /// Client statistics - /// - private EFClientStatistics UpdateStats(EFClientStatistics clientStats) - { - // prevent NaN or inactive time lowering SPM - if ((DateTime.UtcNow - clientStats.LastStatCalculation).TotalSeconds / 60.0 < 0.01 || - (DateTime.UtcNow - clientStats.LastActive).TotalSeconds / 60.0 > 3 || - clientStats.SessionScore == 0) - { - // prevents idle time counting clientStats.LastStatCalculation = DateTime.UtcNow; + //clientStats.LastScore = clientStats.SessionScore; + return clientStats; } - double timeSinceLastCalc = (DateTime.UtcNow - clientStats.LastStatCalculation).TotalSeconds / 60.0; - double timeSinceLastActive = (DateTime.UtcNow - clientStats.LastActive).TotalSeconds / 60.0; - - int scoreDifference = 0; - // this means they've been tking or suicide and is the only time they can have a negative SPM - if (clientStats.RoundScore < 0) + public void InitializeServerStats(Server sv) { - scoreDifference = clientStats.RoundScore + clientStats.LastScore; + int serverId = sv.GetHashCode(); + var statsSvc = ContextThreads[serverId]; + + var serverStats = statsSvc.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 = statsSvc.ClientStatSvc.Find(cs => cs.ServerId == serverId); + + // set these incase we've imported settings + serverStats.TotalKills = ieClientStats.Sum(cs => cs.Kills); + serverStats.TotalPlayTime = Manager.GetClientService().GetTotalPlayTime().Result; + + statsSvc.ServerStatsSvc.Insert(serverStats); + } } - else if (clientStats.RoundScore > 0 && clientStats.LastScore < clientStats.RoundScore) + public void ResetKillstreaks(int serverId) { - scoreDifference = clientStats.RoundScore - clientStats.LastScore; + var serverStats = Servers[serverId]; + foreach (var stat in serverStats.PlayerStats.Values) + stat.StartNewSession(); } - double killSPM = scoreDifference / timeSinceLastCalc; - double spmMultiplier = 2.934 * Math.Pow(Servers[clientStats.ServerId].TeamCount(clientStats.Team == IW4Info.Team.Allies ? IW4Info.Team.Axis : IW4Info.Team.Allies), -0.454); - killSPM *= Math.Max(1, spmMultiplier); - - // update this for ac tracking - clientStats.SessionSPM = killSPM; - - // calculate how much the KDR should weigh - // 1.637 is a Eddie-Generated number that weights the KDR nicely - double currentKDR = clientStats.SessionDeaths == 0 ? clientStats.SessionKills : clientStats.SessionKills / clientStats.SessionDeaths; - double alpha = Math.Sqrt(2) / Math.Min(600, Math.Max(clientStats.Kills + clientStats.Deaths, 1)); - clientStats.RollingWeightedKDR = (alpha * currentKDR) + (1.0 - alpha) * clientStats.KDR; - double KDRWeight = Math.Round(Math.Pow(clientStats.RollingWeightedKDR, 1.637 / Math.E), 3); - - // calculate the weight of the new play time against last 10 hours of gameplay - int totalPlayTime = (clientStats.TimePlayed == 0) ? - (int)(DateTime.UtcNow - clientStats.LastActive).TotalSeconds : - clientStats.TimePlayed + (int)(DateTime.UtcNow - clientStats.LastActive).TotalSeconds; - - double SPMAgainstPlayWeight = timeSinceLastCalc / Math.Min(600, (totalPlayTime / 60.0)); - - // calculate the new weight against average times the weight against play time - clientStats.SPM = (killSPM * SPMAgainstPlayWeight) + (clientStats.SPM * (1 - SPMAgainstPlayWeight)); - - if (clientStats.SPM < 0) + public void ResetStats(int clientId, int serverId) { - Log.WriteWarning("[StatManager:UpdateStats] clientStats SPM < 0"); - Log.WriteDebug($"{scoreDifference}-{clientStats.RoundScore} - {clientStats.LastScore} - {clientStats.SessionScore}"); - clientStats.SPM = 0; + var stats = Servers[serverId].PlayerStats[clientId]; + stats.Kills = 0; + stats.Deaths = 0; + stats.SPM = 0; + stats.Skill = 0; + stats.TimePlayed = 0; + stats.EloRating = 200; } - clientStats.SPM = Math.Round(clientStats.SPM, 3); - clientStats.Skill = Math.Round((clientStats.SPM * KDRWeight), 3); - - // fixme: how does this happen? - if (double.IsNaN(clientStats.SPM) || double.IsNaN(clientStats.Skill)) + public async Task AddMessageAsync(int clientId, int serverId, string message) { - Log.WriteWarning("[StatManager::UpdateStats] clientStats SPM/Skill NaN"); - Log.WriteDebug($"{killSPM}-{KDRWeight}-{totalPlayTime}-{SPMAgainstPlayWeight}-{clientStats.SPM}-{clientStats.Skill}-{scoreDifference}"); - clientStats.SPM = 0; - clientStats.Skill = 0; - } + // the web users can have no account + if (clientId < 1) + return; - clientStats.LastStatCalculation = DateTime.UtcNow; - //clientStats.LastScore = clientStats.SessionScore; - - return clientStats; - } - - public void InitializeServerStats(Server sv) - { - int serverId = sv.GetHashCode(); - var statsSvc = ContextThreads[serverId]; - - var serverStats = statsSvc.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() + var messageSvc = ContextThreads[serverId].MessageSvc; + messageSvc.Insert(new EFClientMessage() { Active = true, + ClientId = clientId, + Message = message, ServerId = serverId, - TotalKills = 0, - TotalPlayTime = 0, - }; + TimeSent = DateTime.UtcNow + }); + await messageSvc.SaveChangesAsync(); + } - var ieClientStats = statsSvc.ClientStatSvc.Find(cs => cs.ServerId == serverId); + public async Task Sync(Server sv) + { + int serverId = sv.GetHashCode(); + var statsSvc = ContextThreads[serverId]; - // set these incase we've imported settings - serverStats.TotalKills = ieClientStats.Sum(cs => cs.Kills); - serverStats.TotalPlayTime = Manager.GetClientService().GetTotalPlayTime().Result; + // Log.WriteDebug("Syncing stats contexts"); + await statsSvc.ServerStatsSvc.SaveChangesAsync(); + //await statsSvc.ClientStatSvc.SaveChangesAsync(); + await statsSvc.KillStatsSvc.SaveChangesAsync(); + await statsSvc.ServerSvc.SaveChangesAsync(); - statsSvc.ServerStatsSvc.Insert(serverStats); + statsSvc = null; + // this should prevent the gunk for having a long lasting context. + ContextThreads[serverId] = new ThreadSafeStatsService(); + } + + public void SetTeamBased(int serverId, bool isTeamBased) + { + Servers[serverId].IsTeamBased = isTeamBased; } } - - public void ResetKillstreaks(int serverId) - { - var serverStats = Servers[serverId]; - foreach (var stat in serverStats.PlayerStats.Values) - stat.StartNewSession(); - } - - public void ResetStats(int clientId, int serverId) - { - var stats = Servers[serverId].PlayerStats[clientId]; - stats.Kills = 0; - stats.Deaths = 0; - stats.SPM = 0; - stats.Skill = 0; - stats.TimePlayed = 0; - stats.EloRating = 200; - } - - public async Task AddMessageAsync(int clientId, int serverId, string message) - { - // the web users can have no account - if (clientId < 1) - return; - - 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 stats contexts"); - await statsSvc.ServerStatsSvc.SaveChangesAsync(); - //await statsSvc.ClientStatSvc.SaveChangesAsync(); - await statsSvc.KillStatsSvc.SaveChangesAsync(); - await statsSvc.ServerSvc.SaveChangesAsync(); - - statsSvc = null; - // this should prevent the gunk for having a long lasting context. - ContextThreads[serverId] = new ThreadSafeStatsService(); - } - - public void SetTeamBased(int serverId, bool isTeamBased) - { - Servers[serverId].IsTeamBased = isTeamBased; - } -} } diff --git a/Plugins/Stats/Models/ModelConfiguration.cs b/Plugins/Stats/Models/ModelConfiguration.cs index 360e7faef..c21803c4f 100644 --- a/Plugins/Stats/Models/ModelConfiguration.cs +++ b/Plugins/Stats/Models/ModelConfiguration.cs @@ -30,6 +30,9 @@ namespace Stats.Models builder.Entity() .HasIndex(p => p.When); + builder.Entity() + .HasIndex(p => p.TimeSent); + // force pluralization builder.Entity().ToTable("EFClientKills"); builder.Entity().ToTable("EFClientMessages"); diff --git a/Plugins/Stats/Web/Controllers/StatsController.cs b/Plugins/Stats/Web/Controllers/StatsController.cs index 02896e5b0..334ae80b9 100644 --- a/Plugins/Stats/Web/Controllers/StatsController.cs +++ b/Plugins/Stats/Web/Controllers/StatsController.cs @@ -40,11 +40,16 @@ namespace IW4MAdmin.Plugins.Stats.Web.Controllers where message.TimeSent <= whenUpper select new SharedLibraryCore.Dtos.ChatInfo() { + ClientId = message.ClientId, Message = message.Message, Name = message.Client.CurrentAlias.Name, Time = message.TimeSent }; +#if DEBUG == true + var messagesSql = iqMessages.ToSql(); +#endif + var messages = await iqMessages.ToListAsync(); return View("_MessageContext", messages); diff --git a/Plugins/Stats/Web/Views/Stats/_MessageContext.cshtml b/Plugins/Stats/Web/Views/Stats/_MessageContext.cshtml index c40a9fba2..74076f45d 100644 --- a/Plugins/Stats/Web/Views/Stats/_MessageContext.cshtml +++ b/Plugins/Stats/Web/Views/Stats/_MessageContext.cshtml @@ -7,6 +7,6 @@
@Model.First().Time.ToString()
@foreach (var message in Model) { - @message.Name — @message.Message
+ @Html.ActionLink(@message.Name, "ProfileAsync", "Client", new { id = message.ClientId}) — @message.Message
} \ No newline at end of file diff --git a/SharedLibraryCore/Database/DatabaseContext.cs b/SharedLibraryCore/Database/DatabaseContext.cs index 9b1809263..449e09288 100644 --- a/SharedLibraryCore/Database/DatabaseContext.cs +++ b/SharedLibraryCore/Database/DatabaseContext.cs @@ -8,6 +8,8 @@ using System.Linq; using Microsoft.Data.Sqlite; using SharedLibraryCore.Interfaces; using System.Runtime.InteropServices; +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore.Metadata; namespace SharedLibraryCore.Database { @@ -103,6 +105,13 @@ namespace SharedLibraryCore.Database ent.HasIndex(a => a.Name); }); + modelBuilder.Entity(ent => + { + ent.Property(c => c.ChangeHistoryId) + .ValueGeneratedOnAdd() + .HasAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + }); + // force full name for database conversion modelBuilder.Entity().ToTable("EFClients"); modelBuilder.Entity().ToTable("EFAlias"); diff --git a/SharedLibraryCore/Database/Models/EFChangeHistory.cs b/SharedLibraryCore/Database/Models/EFChangeHistory.cs index 33be8cf50..26e66d72c 100644 --- a/SharedLibraryCore/Database/Models/EFChangeHistory.cs +++ b/SharedLibraryCore/Database/Models/EFChangeHistory.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; using System.Text; namespace SharedLibraryCore.Database.Models @@ -15,7 +16,7 @@ namespace SharedLibraryCore.Database.Models Permission, Ban } - + [Key] public int ChangeHistoryId { get; set; } public int OriginEntityId { get; set; } diff --git a/SharedLibraryCore/Dtos/ChatInfo.cs b/SharedLibraryCore/Dtos/ChatInfo.cs index 4bdd24cd2..6c2d089b5 100644 --- a/SharedLibraryCore/Dtos/ChatInfo.cs +++ b/SharedLibraryCore/Dtos/ChatInfo.cs @@ -4,6 +4,7 @@ namespace SharedLibraryCore.Dtos { public class ChatInfo { + public int ClientId { get; set; } public string Message { get; set; } public DateTime Time { get; set; } public string Name { get; set; } diff --git a/SharedLibraryCore/Events/EventAPI.cs b/SharedLibraryCore/Events/EventAPI.cs index a8fbac192..acc124062 100644 --- a/SharedLibraryCore/Events/EventAPI.cs +++ b/SharedLibraryCore/Events/EventAPI.cs @@ -25,92 +25,7 @@ namespace SharedLibraryCore.Events return eventList; } - private static async Task SaveChangeHistory(GameEvent e) - { - EFChangeHistory change = null; - - switch (e.Type) - { - case GameEvent.EventType.Unknown: - break; - case GameEvent.EventType.Start: - break; - case GameEvent.EventType.Stop: - break; - case GameEvent.EventType.Connect: - break; - case GameEvent.EventType.Join: - break; - case GameEvent.EventType.Quit: - break; - case GameEvent.EventType.Disconnect: - break; - case GameEvent.EventType.MapEnd: - break; - case GameEvent.EventType.MapChange: - break; - case GameEvent.EventType.Say: - break; - case GameEvent.EventType.Warn: - break; - case GameEvent.EventType.Report: - break; - case GameEvent.EventType.Flag: - break; - case GameEvent.EventType.Unflag: - break; - case GameEvent.EventType.Kick: - break; - case GameEvent.EventType.TempBan: - break; - case GameEvent.EventType.Ban: - change = new EFChangeHistory() - { - OriginEntityId = e.Origin.ClientId, - TargetEntityId = e.Target.ClientId, - TypeOfChange = EFChangeHistory.ChangeType.Ban, - Comment = e.Data - }; - break; - case GameEvent.EventType.Command: - break; - case GameEvent.EventType.ChangePermission: - change = new EFChangeHistory() - { - OriginEntityId = e.Origin.ClientId, - TargetEntityId = e.Target.ClientId, - TypeOfChange = EFChangeHistory.ChangeType.Permission, - PreviousValue = ((Change)e.Extra).PreviousValue, - CurrentValue = ((Change)e.Extra).NewValue - }; - break; - case GameEvent.EventType.Broadcast: - break; - case GameEvent.EventType.Tell: - break; - case GameEvent.EventType.ScriptDamage: - break; - case GameEvent.EventType.ScriptKill: - break; - case GameEvent.EventType.Damage: - break; - case GameEvent.EventType.Kill: - break; - case GameEvent.EventType.JoinTeam: - break; - } - - if (change != null) - { - using (var ctx = new DatabaseContext(true)) - { - ctx.EFChangeHistory.Add(change); - await ctx.SaveChangesAsync(); - } - } - } - - public static async void OnGameEvent(object sender, GameEventArgs eventState) + public static void OnGameEvent(object sender, GameEventArgs eventState) { var E = eventState.Event; // don't want to clog up the api with unknown events @@ -150,8 +65,6 @@ namespace SharedLibraryCore.Events // add the new event to the list AddNewEvent(apiEvent); - - await SaveChangeHistory(E); } /// diff --git a/SharedLibraryCore/Helpers/ChangeTracking.cs b/SharedLibraryCore/Helpers/ChangeTracking.cs index 5757f4c6d..cba58b23f 100644 --- a/SharedLibraryCore/Helpers/ChangeTracking.cs +++ b/SharedLibraryCore/Helpers/ChangeTracking.cs @@ -20,12 +20,23 @@ namespace SharedLibraryCore.Helpers public void OnChange(T value) { - // clear the first value when count max count reached - if (Values.Count > 30) - Values.RemoveAt(0); - Values.Add(value); + lock (value) + { + // clear the first value when count max count reached + if (Values.Count > 30) + Values.RemoveAt(0); + Values.Add(value); + } } - public List GetChanges() => Values; + public T[] GetChanges() => Values.ToArray(); + + public bool HasChanges => Values.Count > 0; + + public void ClearChanges() + { + lock (Values) + Values.Clear(); + } } } diff --git a/SharedLibraryCore/Interfaces/IEntityService.cs b/SharedLibraryCore/Interfaces/IEntityService.cs index 642b20960..30e9d49eb 100644 --- a/SharedLibraryCore/Interfaces/IEntityService.cs +++ b/SharedLibraryCore/Interfaces/IEntityService.cs @@ -9,7 +9,6 @@ namespace SharedLibraryCore.Interfaces { public interface IEntityService { - Task CreateProxy(); Task Create(T entity); Task Delete(T entity); Task Update(T entity); diff --git a/SharedLibraryCore/Interfaces/IManager.cs b/SharedLibraryCore/Interfaces/IManager.cs index 88198d143..0345c44b9 100644 --- a/SharedLibraryCore/Interfaces/IManager.cs +++ b/SharedLibraryCore/Interfaces/IManager.cs @@ -39,5 +39,6 @@ namespace SharedLibraryCore.Interfaces /// /// IPageList GetPageList(); + string Version { get;} } } diff --git a/SharedLibraryCore/Migrations/20180915163111_AddIndexToMessageTimeSent.Designer.cs b/SharedLibraryCore/Migrations/20180915163111_AddIndexToMessageTimeSent.Designer.cs new file mode 100644 index 000000000..ce6881460 --- /dev/null +++ b/SharedLibraryCore/Migrations/20180915163111_AddIndexToMessageTimeSent.Designer.cs @@ -0,0 +1,686 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SharedLibraryCore.Database; + +namespace SharedLibraryCore.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20180915163111_AddIndexToMessageTimeSent")] + partial class AddIndexToMessageTimeSent + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.1.2-rtm-30932"); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFACSnapshot", b => + { + b.Property("SnapshotId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("ClientId"); + + b.Property("CurrentSessionLength"); + + b.Property("CurrentStrain"); + + b.Property("CurrentViewAngleVector3Id"); + + b.Property("Deaths"); + + b.Property("Distance"); + + b.Property("EloRating"); + + b.Property("HitDestinationVector3Id"); + + b.Property("HitLocation"); + + b.Property("HitOriginVector3Id"); + + b.Property("HitType"); + + b.Property("Hits"); + + b.Property("Kills"); + + b.Property("LastStrainAngleVector3Id"); + + b.Property("SessionAngleOffset"); + + b.Property("SessionSPM"); + + b.Property("SessionScore"); + + b.Property("StrainAngleBetween"); + + b.Property("TimeSinceLastEvent"); + + b.Property("WeaponId"); + + b.Property("When"); + + b.HasKey("SnapshotId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CurrentViewAngleVector3Id"); + + b.HasIndex("HitDestinationVector3Id"); + + b.HasIndex("HitOriginVector3Id"); + + b.HasIndex("LastStrainAngleVector3Id"); + + b.ToTable("EFACSnapshot"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientKill", b => + { + b.Property("KillId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("AttackerId"); + + b.Property("Damage"); + + b.Property("DeathOriginVector3Id"); + + b.Property("DeathType"); + + b.Property("Fraction"); + + b.Property("HitLoc"); + + b.Property("IsKill"); + + b.Property("KillOriginVector3Id"); + + b.Property("Map"); + + b.Property("ServerId"); + + b.Property("VictimId"); + + b.Property("ViewAnglesVector3Id"); + + b.Property("VisibilityPercentage"); + + b.Property("Weapon"); + + b.Property("When"); + + b.HasKey("KillId"); + + b.HasIndex("AttackerId"); + + b.HasIndex("DeathOriginVector3Id"); + + b.HasIndex("KillOriginVector3Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("VictimId"); + + b.HasIndex("ViewAnglesVector3Id"); + + b.ToTable("EFClientKills"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientMessage", b => + { + b.Property("MessageId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("ClientId"); + + b.Property("Message"); + + b.Property("ServerId"); + + b.Property("TimeSent"); + + b.HasKey("MessageId"); + + b.HasIndex("ClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("TimeSent"); + + b.ToTable("EFClientMessages"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientRatingHistory", b => + { + b.Property("RatingHistoryId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("ClientId"); + + b.HasKey("RatingHistoryId"); + + b.HasIndex("ClientId"); + + b.ToTable("EFClientRatingHistory"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics", b => + { + b.Property("ClientId"); + + b.Property("ServerId"); + + b.Property("Active"); + + b.Property("Deaths"); + + b.Property("EloRating"); + + b.Property("Kills"); + + b.Property("MaxStrain"); + + b.Property("RollingWeightedKDR"); + + b.Property("SPM"); + + b.Property("Skill"); + + b.Property("TimePlayed"); + + b.Property("VisionAverage"); + + b.HasKey("ClientId", "ServerId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientStatistics"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFHitLocationCount", b => + { + b.Property("HitLocationCountId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("ClientId") + .HasColumnName("EFClientStatistics_ClientId"); + + b.Property("HitCount"); + + b.Property("HitOffsetAverage"); + + b.Property("Location"); + + b.Property("MaxAngleDistance"); + + b.Property("ServerId") + .HasColumnName("EFClientStatistics_ServerId"); + + b.HasKey("HitLocationCountId"); + + b.HasIndex("ServerId"); + + b.HasIndex("ClientId", "ServerId"); + + b.ToTable("EFHitLocationCounts"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFRating", b => + { + b.Property("RatingId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("ActivityAmount"); + + b.Property("Newest"); + + b.Property("Performance"); + + b.Property("Ranking"); + + b.Property("RatingHistoryId"); + + b.Property("ServerId"); + + b.Property("When"); + + b.HasKey("RatingId"); + + b.HasIndex("Performance"); + + b.HasIndex("Ranking"); + + b.HasIndex("RatingHistoryId"); + + b.HasIndex("ServerId"); + + b.HasIndex("When"); + + b.ToTable("EFRating"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServer", b => + { + b.Property("ServerId"); + + b.Property("Active"); + + b.Property("Port"); + + b.HasKey("ServerId"); + + b.ToTable("EFServers"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServerStatistics", b => + { + b.Property("StatisticId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("ServerId"); + + b.Property("TotalKills"); + + b.Property("TotalPlayTime"); + + b.HasKey("StatisticId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerStatistics"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAlias", b => + { + b.Property("AliasId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("DateAdded"); + + b.Property("IPAddress"); + + b.Property("LinkId"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(24); + + b.HasKey("AliasId"); + + b.HasIndex("IPAddress"); + + b.HasIndex("LinkId"); + + b.HasIndex("Name"); + + b.ToTable("EFAlias"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAliasLink", b => + { + b.Property("AliasLinkId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.HasKey("AliasLinkId"); + + b.ToTable("EFAliasLinks"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFChangeHistory", b => + { + b.Property("ChangeHistoryId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("Comment") + .HasMaxLength(128); + + b.Property("CurrentValue"); + + b.Property("OriginEntityId"); + + b.Property("PreviousValue"); + + b.Property("TargetEntityId"); + + b.Property("TimeChanged"); + + b.Property("TypeOfChange"); + + b.HasKey("ChangeHistoryId"); + + b.ToTable("EFChangeHistory"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFClient", b => + { + b.Property("ClientId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("AliasLinkId"); + + b.Property("Connections"); + + b.Property("CurrentAliasId"); + + b.Property("FirstConnection"); + + b.Property("LastConnection"); + + b.Property("Level"); + + b.Property("Masked"); + + b.Property("NetworkId"); + + b.Property("Password"); + + b.Property("PasswordSalt"); + + b.Property("TotalConnectionTime"); + + b.HasKey("ClientId"); + + b.HasIndex("AliasLinkId"); + + b.HasIndex("CurrentAliasId"); + + b.HasIndex("NetworkId") + .IsUnique(); + + b.ToTable("EFClients"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFMeta", b => + { + b.Property("MetaId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("ClientId"); + + b.Property("Created"); + + b.Property("Extra"); + + b.Property("Key") + .IsRequired(); + + b.Property("Updated"); + + b.Property("Value") + .IsRequired(); + + b.HasKey("MetaId"); + + b.HasIndex("ClientId"); + + b.ToTable("EFMeta"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFPenalty", b => + { + b.Property("PenaltyId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("AutomatedOffense"); + + b.Property("Expires"); + + b.Property("LinkId"); + + b.Property("OffenderId"); + + b.Property("Offense") + .IsRequired(); + + b.Property("PunisherId"); + + b.Property("Type"); + + b.Property("When"); + + b.HasKey("PenaltyId"); + + b.HasIndex("LinkId"); + + b.HasIndex("OffenderId"); + + b.HasIndex("PunisherId"); + + b.ToTable("EFPenalties"); + }); + + modelBuilder.Entity("SharedLibraryCore.Helpers.Vector3", b => + { + b.Property("Vector3Id") + .ValueGeneratedOnAdd(); + + b.Property("EFACSnapshotSnapshotId"); + + b.Property("X"); + + b.Property("Y"); + + b.Property("Z"); + + b.HasKey("Vector3Id"); + + b.HasIndex("EFACSnapshotSnapshotId"); + + b.ToTable("Vector3"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFACSnapshot", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "CurrentViewAngle") + .WithMany() + .HasForeignKey("CurrentViewAngleVector3Id"); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "HitDestination") + .WithMany() + .HasForeignKey("HitDestinationVector3Id"); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "HitOrigin") + .WithMany() + .HasForeignKey("HitOriginVector3Id"); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "LastStrainAngle") + .WithMany() + .HasForeignKey("LastStrainAngleVector3Id"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientKill", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Attacker") + .WithMany() + .HasForeignKey("AttackerId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "DeathOrigin") + .WithMany() + .HasForeignKey("DeathOriginVector3Id"); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "KillOrigin") + .WithMany() + .HasForeignKey("KillOriginVector3Id"); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Victim") + .WithMany() + .HasForeignKey("VictimId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "ViewAngles") + .WithMany() + .HasForeignKey("ViewAnglesVector3Id"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientMessage", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientRatingHistory", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFHitLocationCount", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics") + .WithMany("HitLocations") + .HasForeignKey("ClientId", "ServerId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFRating", b => + { + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFClientRatingHistory", "RatingHistory") + .WithMany("Ratings") + .HasForeignKey("RatingHistoryId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServerStatistics", b => + { + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAlias", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFAliasLink", "Link") + .WithMany("Children") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Restrict); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFClient", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFAliasLink", "AliasLink") + .WithMany() + .HasForeignKey("AliasLinkId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("SharedLibraryCore.Database.Models.EFAlias", "CurrentAlias") + .WithMany() + .HasForeignKey("CurrentAliasId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFMeta", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client") + .WithMany("Meta") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFPenalty", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFAliasLink", "Link") + .WithMany("ReceivedPenalties") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Offender") + .WithMany("ReceivedPenalties") + .HasForeignKey("OffenderId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Punisher") + .WithMany("AdministeredPenalties") + .HasForeignKey("PunisherId") + .OnDelete(DeleteBehavior.Restrict); + }); + + modelBuilder.Entity("SharedLibraryCore.Helpers.Vector3", b => + { + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFACSnapshot") + .WithMany("PredictedViewAngles") + .HasForeignKey("EFACSnapshotSnapshotId"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SharedLibraryCore/Migrations/20180915163111_AddIndexToMessageTimeSent.cs b/SharedLibraryCore/Migrations/20180915163111_AddIndexToMessageTimeSent.cs new file mode 100644 index 000000000..0e74e6312 --- /dev/null +++ b/SharedLibraryCore/Migrations/20180915163111_AddIndexToMessageTimeSent.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace SharedLibraryCore.Migrations +{ + public partial class AddIndexToMessageTimeSent : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateIndex( + name: "IX_EFClientMessages_TimeSent", + table: "EFClientMessages", + column: "TimeSent"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_EFClientMessages_TimeSent", + table: "EFClientMessages"); + } + } +} diff --git a/SharedLibraryCore/Migrations/20180915164118_ForceAutoIncrementChangeHistory.Designer.cs b/SharedLibraryCore/Migrations/20180915164118_ForceAutoIncrementChangeHistory.Designer.cs new file mode 100644 index 000000000..19b4b537e --- /dev/null +++ b/SharedLibraryCore/Migrations/20180915164118_ForceAutoIncrementChangeHistory.Designer.cs @@ -0,0 +1,688 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SharedLibraryCore.Database; + +namespace SharedLibraryCore.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20180915164118_ForceAutoIncrementChangeHistory")] + partial class ForceAutoIncrementChangeHistory + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.1.2-rtm-30932"); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFACSnapshot", b => + { + b.Property("SnapshotId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("ClientId"); + + b.Property("CurrentSessionLength"); + + b.Property("CurrentStrain"); + + b.Property("CurrentViewAngleVector3Id"); + + b.Property("Deaths"); + + b.Property("Distance"); + + b.Property("EloRating"); + + b.Property("HitDestinationVector3Id"); + + b.Property("HitLocation"); + + b.Property("HitOriginVector3Id"); + + b.Property("HitType"); + + b.Property("Hits"); + + b.Property("Kills"); + + b.Property("LastStrainAngleVector3Id"); + + b.Property("SessionAngleOffset"); + + b.Property("SessionSPM"); + + b.Property("SessionScore"); + + b.Property("StrainAngleBetween"); + + b.Property("TimeSinceLastEvent"); + + b.Property("WeaponId"); + + b.Property("When"); + + b.HasKey("SnapshotId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CurrentViewAngleVector3Id"); + + b.HasIndex("HitDestinationVector3Id"); + + b.HasIndex("HitOriginVector3Id"); + + b.HasIndex("LastStrainAngleVector3Id"); + + b.ToTable("EFACSnapshot"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientKill", b => + { + b.Property("KillId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("AttackerId"); + + b.Property("Damage"); + + b.Property("DeathOriginVector3Id"); + + b.Property("DeathType"); + + b.Property("Fraction"); + + b.Property("HitLoc"); + + b.Property("IsKill"); + + b.Property("KillOriginVector3Id"); + + b.Property("Map"); + + b.Property("ServerId"); + + b.Property("VictimId"); + + b.Property("ViewAnglesVector3Id"); + + b.Property("VisibilityPercentage"); + + b.Property("Weapon"); + + b.Property("When"); + + b.HasKey("KillId"); + + b.HasIndex("AttackerId"); + + b.HasIndex("DeathOriginVector3Id"); + + b.HasIndex("KillOriginVector3Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("VictimId"); + + b.HasIndex("ViewAnglesVector3Id"); + + b.ToTable("EFClientKills"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientMessage", b => + { + b.Property("MessageId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("ClientId"); + + b.Property("Message"); + + b.Property("ServerId"); + + b.Property("TimeSent"); + + b.HasKey("MessageId"); + + b.HasIndex("ClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("TimeSent"); + + b.ToTable("EFClientMessages"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientRatingHistory", b => + { + b.Property("RatingHistoryId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("ClientId"); + + b.HasKey("RatingHistoryId"); + + b.HasIndex("ClientId"); + + b.ToTable("EFClientRatingHistory"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics", b => + { + b.Property("ClientId"); + + b.Property("ServerId"); + + b.Property("Active"); + + b.Property("Deaths"); + + b.Property("EloRating"); + + b.Property("Kills"); + + b.Property("MaxStrain"); + + b.Property("RollingWeightedKDR"); + + b.Property("SPM"); + + b.Property("Skill"); + + b.Property("TimePlayed"); + + b.Property("VisionAverage"); + + b.HasKey("ClientId", "ServerId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientStatistics"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFHitLocationCount", b => + { + b.Property("HitLocationCountId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("ClientId") + .HasColumnName("EFClientStatistics_ClientId"); + + b.Property("HitCount"); + + b.Property("HitOffsetAverage"); + + b.Property("Location"); + + b.Property("MaxAngleDistance"); + + b.Property("ServerId") + .HasColumnName("EFClientStatistics_ServerId"); + + b.HasKey("HitLocationCountId"); + + b.HasIndex("ServerId"); + + b.HasIndex("ClientId", "ServerId"); + + b.ToTable("EFHitLocationCounts"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFRating", b => + { + b.Property("RatingId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("ActivityAmount"); + + b.Property("Newest"); + + b.Property("Performance"); + + b.Property("Ranking"); + + b.Property("RatingHistoryId"); + + b.Property("ServerId"); + + b.Property("When"); + + b.HasKey("RatingId"); + + b.HasIndex("Performance"); + + b.HasIndex("Ranking"); + + b.HasIndex("RatingHistoryId"); + + b.HasIndex("ServerId"); + + b.HasIndex("When"); + + b.ToTable("EFRating"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServer", b => + { + b.Property("ServerId"); + + b.Property("Active"); + + b.Property("Port"); + + b.HasKey("ServerId"); + + b.ToTable("EFServers"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServerStatistics", b => + { + b.Property("StatisticId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("ServerId"); + + b.Property("TotalKills"); + + b.Property("TotalPlayTime"); + + b.HasKey("StatisticId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerStatistics"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAlias", b => + { + b.Property("AliasId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("DateAdded"); + + b.Property("IPAddress"); + + b.Property("LinkId"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(24); + + b.HasKey("AliasId"); + + b.HasIndex("IPAddress"); + + b.HasIndex("LinkId"); + + b.HasIndex("Name"); + + b.ToTable("EFAlias"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAliasLink", b => + { + b.Property("AliasLinkId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.HasKey("AliasLinkId"); + + b.ToTable("EFAliasLinks"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFChangeHistory", b => + { + b.Property("ChangeHistoryId") + .ValueGeneratedOnAdd() + .HasAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + b.Property("Active"); + + b.Property("Comment") + .HasMaxLength(128); + + b.Property("CurrentValue"); + + b.Property("OriginEntityId"); + + b.Property("PreviousValue"); + + b.Property("TargetEntityId"); + + b.Property("TimeChanged"); + + b.Property("TypeOfChange"); + + b.HasKey("ChangeHistoryId"); + + b.ToTable("EFChangeHistory"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFClient", b => + { + b.Property("ClientId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("AliasLinkId"); + + b.Property("Connections"); + + b.Property("CurrentAliasId"); + + b.Property("FirstConnection"); + + b.Property("LastConnection"); + + b.Property("Level"); + + b.Property("Masked"); + + b.Property("NetworkId"); + + b.Property("Password"); + + b.Property("PasswordSalt"); + + b.Property("TotalConnectionTime"); + + b.HasKey("ClientId"); + + b.HasIndex("AliasLinkId"); + + b.HasIndex("CurrentAliasId"); + + b.HasIndex("NetworkId") + .IsUnique(); + + b.ToTable("EFClients"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFMeta", b => + { + b.Property("MetaId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("ClientId"); + + b.Property("Created"); + + b.Property("Extra"); + + b.Property("Key") + .IsRequired(); + + b.Property("Updated"); + + b.Property("Value") + .IsRequired(); + + b.HasKey("MetaId"); + + b.HasIndex("ClientId"); + + b.ToTable("EFMeta"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFPenalty", b => + { + b.Property("PenaltyId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("AutomatedOffense"); + + b.Property("Expires"); + + b.Property("LinkId"); + + b.Property("OffenderId"); + + b.Property("Offense") + .IsRequired(); + + b.Property("PunisherId"); + + b.Property("Type"); + + b.Property("When"); + + b.HasKey("PenaltyId"); + + b.HasIndex("LinkId"); + + b.HasIndex("OffenderId"); + + b.HasIndex("PunisherId"); + + b.ToTable("EFPenalties"); + }); + + modelBuilder.Entity("SharedLibraryCore.Helpers.Vector3", b => + { + b.Property("Vector3Id") + .ValueGeneratedOnAdd(); + + b.Property("EFACSnapshotSnapshotId"); + + b.Property("X"); + + b.Property("Y"); + + b.Property("Z"); + + b.HasKey("Vector3Id"); + + b.HasIndex("EFACSnapshotSnapshotId"); + + b.ToTable("Vector3"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFACSnapshot", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "CurrentViewAngle") + .WithMany() + .HasForeignKey("CurrentViewAngleVector3Id"); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "HitDestination") + .WithMany() + .HasForeignKey("HitDestinationVector3Id"); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "HitOrigin") + .WithMany() + .HasForeignKey("HitOriginVector3Id"); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "LastStrainAngle") + .WithMany() + .HasForeignKey("LastStrainAngleVector3Id"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientKill", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Attacker") + .WithMany() + .HasForeignKey("AttackerId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "DeathOrigin") + .WithMany() + .HasForeignKey("DeathOriginVector3Id"); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "KillOrigin") + .WithMany() + .HasForeignKey("KillOriginVector3Id"); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Victim") + .WithMany() + .HasForeignKey("VictimId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "ViewAngles") + .WithMany() + .HasForeignKey("ViewAnglesVector3Id"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientMessage", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientRatingHistory", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFHitLocationCount", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics") + .WithMany("HitLocations") + .HasForeignKey("ClientId", "ServerId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFRating", b => + { + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFClientRatingHistory", "RatingHistory") + .WithMany("Ratings") + .HasForeignKey("RatingHistoryId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServerStatistics", b => + { + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAlias", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFAliasLink", "Link") + .WithMany("Children") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Restrict); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFClient", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFAliasLink", "AliasLink") + .WithMany() + .HasForeignKey("AliasLinkId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("SharedLibraryCore.Database.Models.EFAlias", "CurrentAlias") + .WithMany() + .HasForeignKey("CurrentAliasId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFMeta", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client") + .WithMany("Meta") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFPenalty", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFAliasLink", "Link") + .WithMany("ReceivedPenalties") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Offender") + .WithMany("ReceivedPenalties") + .HasForeignKey("OffenderId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Punisher") + .WithMany("AdministeredPenalties") + .HasForeignKey("PunisherId") + .OnDelete(DeleteBehavior.Restrict); + }); + + modelBuilder.Entity("SharedLibraryCore.Helpers.Vector3", b => + { + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFACSnapshot") + .WithMany("PredictedViewAngles") + .HasForeignKey("EFACSnapshotSnapshotId"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SharedLibraryCore/Migrations/20180915164118_ForceAutoIncrementChangeHistory.cs b/SharedLibraryCore/Migrations/20180915164118_ForceAutoIncrementChangeHistory.cs new file mode 100644 index 000000000..846e6f2df --- /dev/null +++ b/SharedLibraryCore/Migrations/20180915164118_ForceAutoIncrementChangeHistory.cs @@ -0,0 +1,25 @@ +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace SharedLibraryCore.Migrations +{ + public partial class ForceAutoIncrementChangeHistory : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + // hack: we can't alter the column on SQLite, but we need max length limit for the Index in MySQL etc + if (migrationBuilder.ActiveProvider != "Microsoft.EntityFrameworkCore.Sqlite") + { + migrationBuilder.AlterColumn( + name: "ChangeHistoryId", + table: "EFChangeHistory" + ).Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + } + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/SharedLibraryCore/Migrations/DatabaseContextModelSnapshot.cs b/SharedLibraryCore/Migrations/DatabaseContextModelSnapshot.cs index 9e579405d..f30c8183a 100644 --- a/SharedLibraryCore/Migrations/DatabaseContextModelSnapshot.cs +++ b/SharedLibraryCore/Migrations/DatabaseContextModelSnapshot.cs @@ -2,6 +2,7 @@ using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using SharedLibraryCore.Database; @@ -155,6 +156,8 @@ namespace SharedLibraryCore.Migrations b.HasIndex("ServerId"); + b.HasIndex("TimeSent"); + b.ToTable("EFClientMessages"); }); @@ -349,7 +352,8 @@ namespace SharedLibraryCore.Migrations modelBuilder.Entity("SharedLibraryCore.Database.Models.EFChangeHistory", b => { b.Property("ChangeHistoryId") - .ValueGeneratedOnAdd(); + .ValueGeneratedOnAdd() + .HasAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); b.Property("Active"); diff --git a/SharedLibraryCore/Services/AliasService.cs b/SharedLibraryCore/Services/AliasService.cs index 34679d5b1..fe4b4d2dc 100644 --- a/SharedLibraryCore/Services/AliasService.cs +++ b/SharedLibraryCore/Services/AliasService.cs @@ -54,9 +54,10 @@ namespace SharedLibraryCore.Services public async Task> Find(Func expression) { + // todo: max better? return await Task.Run(() => { - using (var context = new DatabaseContext()) + using (var context = new DatabaseContext(true)) return context.Aliases .AsNoTracking() .Include(a => a.Link.Children) @@ -67,10 +68,9 @@ namespace SharedLibraryCore.Services public async Task Get(int entityID) { - using (var context = new DatabaseContext()) + using (var context = new DatabaseContext(true)) return await context.Aliases - .AsNoTracking() - .SingleOrDefaultAsync(e => e.AliasId == entityID); + .FirstOrDefaultAsync(e => e.AliasId == entityID); } public Task GetUnique(long entityProperty) @@ -81,23 +81,6 @@ namespace SharedLibraryCore.Services public async Task Update(EFAlias entity) { throw await Task.FromResult(new Exception()); - /*using (var context = new DatabaseContext()) - { - entity = context.Aliases.Attach(entity); - context.Entry(entity).State = EntityState.Modified; - await context.SaveChangesAsync(); - return entity; - }*/ - } - - public async Task CreateLink(EFAliasLink link) - { - using (var context = new DatabaseContext()) - { - context.AliasLinks.Add(link); - await context.SaveChangesAsync(); - return link; - } } } } diff --git a/SharedLibraryCore/Services/ChangeHistoryService.cs b/SharedLibraryCore/Services/ChangeHistoryService.cs new file mode 100644 index 000000000..097d105f6 --- /dev/null +++ b/SharedLibraryCore/Services/ChangeHistoryService.cs @@ -0,0 +1,95 @@ +using SharedLibraryCore.Database; +using SharedLibraryCore.Database.Models; +using SharedLibraryCore.Events; +using SharedLibraryCore.Interfaces; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace SharedLibraryCore.Services +{ + public class ChangeHistoryService : IEntityService + { + public Task Create(EFChangeHistory entity) + { + throw new NotImplementedException(); + } + + public async Task Add(GameEvent e) + { + EFChangeHistory change = null; + + switch (e.Type) + { + case GameEvent.EventType.Ban: + change = new EFChangeHistory() + { + OriginEntityId = e.Origin.ClientId, + TargetEntityId = e.Target.ClientId, + TypeOfChange = EFChangeHistory.ChangeType.Ban, + Comment = e.Data + }; + break; + case GameEvent.EventType.Command: + break; + case GameEvent.EventType.ChangePermission: + change = new EFChangeHistory() + { + OriginEntityId = e.Origin.ClientId, + TargetEntityId = e.Target.ClientId, + TypeOfChange = EFChangeHistory.ChangeType.Permission, + PreviousValue = ((Change)e.Extra).PreviousValue, + CurrentValue = ((Change)e.Extra).NewValue + }; + break; + default: + break; + } + + if (change != null) + { + using (var ctx = new DatabaseContext(true)) + { + ctx.EFChangeHistory.Add(change); + try + { + await ctx.SaveChangesAsync(); + } + + catch (Exception ex) + { + e.Owner.Logger.WriteDebug(ex.GetExceptionInfo()); + } + } + } + + return change; + } + + public Task Delete(EFChangeHistory entity) + { + throw new NotImplementedException(); + } + + public Task> Find(Func expression) + { + throw new NotImplementedException(); + } + + public Task Get(int entityID) + { + throw new NotImplementedException(); + } + + public Task GetUnique(long entityProperty) + { + throw new NotImplementedException(); + } + + public Task Update(EFChangeHistory entity) + { + throw new NotImplementedException(); + } + } +} diff --git a/SharedLibraryCore/Services/ClientService.cs b/SharedLibraryCore/Services/ClientService.cs index 8c0bbf5c8..c3c27587a 100644 --- a/SharedLibraryCore/Services/ClientService.cs +++ b/SharedLibraryCore/Services/ClientService.cs @@ -223,7 +223,7 @@ namespace SharedLibraryCore.Services } } -#region ServiceSpecific + #region ServiceSpecific public async Task> GetOwners() { using (var context = new DatabaseContext()) @@ -262,7 +262,7 @@ namespace SharedLibraryCore.Services name = name.ToLower(); - using (var context = new DatabaseContext(true)) + using (var context = new DatabaseContext(disableTracking: true)) { int asIP = name.ConvertToIP(); // hack: so IW4MAdmin and bots don't show up in search results @@ -287,57 +287,9 @@ namespace SharedLibraryCore.Services } } - public async Task> GetClientByIP(int ipAddress) - { - using (var context = new DatabaseContext(true)) - { - var iqClients = (from alias in context.Aliases - .AsNoTracking() - where alias.IPAddress == ipAddress - join link in context.AliasLinks - on alias.LinkId equals link.AliasLinkId - join client in context.Clients - .AsNoTracking() - on alias.LinkId equals client.AliasLinkId - select client) - .Distinct() - .Include(c => c.CurrentAlias) - .Include(c => c.AliasLink.Children); - - return await iqClients.ToListAsync(); - } - } - - public async Task> GetRecentClients(int offset, int count) - { - using (var context = new DatabaseContext()) - return await context.Clients - .AsNoTracking() - .Include(c => c.CurrentAlias) - .Include(p => p.AliasLink) - .OrderByDescending(p => p.ClientId) - .Skip(offset) - .Take(count) - .ToListAsync(); - } - - public async Task> PruneInactivePrivilegedClients(int inactiveDays) - { - using (var context = new DatabaseContext()) - { - var inactive = await context.Clients.Where(c => c.Level > Objects.Player.Permission.Flagged) - .AsNoTracking() - .Where(c => (DateTime.UtcNow - c.LastConnection).TotalDays >= inactiveDays) - .ToListAsync(); - inactive.ForEach(c => c.Level = Player.Permission.User); - await context.SaveChangesAsync(); - return inactive; - } - } - public async Task GetTotalClientsAsync() { - using (var context = new DatabaseContext()) + using (var context = new DatabaseContext(true)) return await context.Clients .CountAsync(); } @@ -349,9 +301,9 @@ namespace SharedLibraryCore.Services public async Task GetTotalPlayTime() { - using (var context = new DatabaseContext()) + using (var context = new DatabaseContext(true)) return await context.Clients.SumAsync(c => c.TotalConnectionTime); } -#endregion + #endregion } } diff --git a/SharedLibraryCore/Utilities.cs b/SharedLibraryCore/Utilities.cs index 3ea5ab93a..38890b905 100644 --- a/SharedLibraryCore/Utilities.cs +++ b/SharedLibraryCore/Utilities.cs @@ -66,6 +66,28 @@ namespace SharedLibraryCore return newStr; } + /// + /// helper method to get the information about an exception and inner exceptions + /// + /// + /// + public static string GetExceptionInfo(this Exception ex) + { + var sb = new StringBuilder(); + int depth = 0; + while (ex != null) + { + sb.AppendLine($"Exception[{depth}] Name: {ex.GetType().FullName}"); + sb.AppendLine($"Exception[{depth}] Message: {ex.Message}"); + sb.AppendLine($"Exception[{depth}] Call Stack: {ex.StackTrace}"); + sb.AppendLine($"Exception[{depth}] Source: {ex.Source}"); + depth++; + ex = ex.InnerException; + } + + return sb.ToString(); + } + public static Player.Permission MatchPermission(String str) { String lookingFor = str.ToLower(); diff --git a/WebfrontCore/Controllers/BaseController.cs b/WebfrontCore/Controllers/BaseController.cs index 536fc2905..4e9377a0d 100644 --- a/WebfrontCore/Controllers/BaseController.cs +++ b/WebfrontCore/Controllers/BaseController.cs @@ -37,6 +37,8 @@ namespace WebfrontCore.Controllers SocialLink = Manager.GetApplicationSettings().Configuration().SocialLinkAddress; SocialTitle = Manager.GetApplicationSettings().Configuration().SocialLinkTitle; } + + ViewBag.Version = Manager.Version; } public override void OnActionExecuting(ActionExecutingContext context) diff --git a/WebfrontCore/Views/Shared/_Layout.cshtml b/WebfrontCore/Views/Shared/_Layout.cshtml index 15cdb3679..86bcbf159 100644 --- a/WebfrontCore/Views/Shared/_Layout.cshtml +++ b/WebfrontCore/Views/Shared/_Layout.cshtml @@ -38,7 +38,7 @@ - @foreach (var _page in ViewBag.Pages) + @foreach (var _page in ViewBag.Pages) { @@ -135,7 +135,22 @@
@RenderBody() -
+
diff --git a/WebfrontCore/wwwroot/css/bootstrap-custom.css b/WebfrontCore/wwwroot/css/bootstrap-custom.css index 3c36c97cd..515966361 100644 --- a/WebfrontCore/wwwroot/css/bootstrap-custom.css +++ b/WebfrontCore/wwwroot/css/bootstrap-custom.css @@ -6409,3 +6409,11 @@ form *, select { .client-message, .automated-penalty-info-detailed { cursor: pointer; } +#footer_text { + font-size: 0.85rem; } + +.footer-mobile { + position: fixed; + bottom: 1em; + right: 1em; } + diff --git a/WebfrontCore/wwwroot/css/bootstrap-custom.scss b/WebfrontCore/wwwroot/css/bootstrap-custom.scss index 583b04b95..7bd509ff3 100644 --- a/WebfrontCore/wwwroot/css/bootstrap-custom.scss +++ b/WebfrontCore/wwwroot/css/bootstrap-custom.scss @@ -209,3 +209,13 @@ form *, select { .client-message, .automated-penalty-info-detailed { cursor: pointer; } + +#footer_text { + font-size: 0.85rem; +} + +.footer-mobile { + position: fixed; + bottom: 1em; + right: 1em; +} diff --git a/WebfrontCore/wwwroot/js/server.js b/WebfrontCore/wwwroot/js/server.js index 2aeb04f46..29b690e1a 100644 --- a/WebfrontCore/wwwroot/js/server.js +++ b/WebfrontCore/wwwroot/js/server.js @@ -1,4 +1,4 @@ -function getPlayerHistoryChart(playerHistory, i, width, color) { +function getPlayerHistoryChart(playerHistory, i, width, color, maxClients) { /////////////////////////////////////// // thanks to canvasjs :( playerHistory.forEach(function (item, i) { @@ -29,6 +29,7 @@ lineThickness: 0, tickThickness: 0, minimum: 0, + maximum: maxClients + 1, margin: 0, valueFormatString: " ", labelMaxWidth: 0 @@ -53,9 +54,10 @@ var charts = {}; $('.server-history-row').each(function (index, element) { let clientHistory = $(this).data('clienthistory'); let serverId = $(this).data('serverid'); + let maxClients = parseInt($('#server_header_' + serverId + ' .server-maxclients').text()); let color = $(this).data('online') === 'True' ? 'rgba(0, 122, 204, 0.432)' : '#ff6060' let width = $('.server-header').first().width(); - let historyChart = getPlayerHistoryChart(clientHistory, serverId, width, color); + let historyChart = getPlayerHistoryChart(clientHistory, serverId, width, color, maxClients); historyChart.render(); charts[serverId] = historyChart; }); @@ -82,7 +84,7 @@ function refreshClientActivity() { $('#server_clientactivity_' + serverId).html(response); }) .fail(function (jqxhr, textStatus, error) { - $('#server_clientactivity_' + serverId).html("Could not load client activity - " + error); + $('#server_clientactivity_' + serverId).html(" Could not load client activity - " + error); }); }); }