diff --git a/Application/Application.csproj b/Application/Application.csproj index 997ef1fd..9c322e34 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 ef7cd695..a3481412 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 d60f8181..4ba5edd0 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 2c811542..e24492de 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 07dccbac..6908a376 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 4db73494..96981eaa 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 360e7fae..c21803c4 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 02896e5b..334ae80b 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 c40a9fba..74076f45 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 9b180926..449e0928 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 33be8cf5..26e66d72 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 4bdd24cd..6c2d089b 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 a8fbac19..acc12406 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 5757f4c6..cba58b23 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 642b2096..30e9d49e 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 88198d14..0345c44b 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 00000000..ce688146 --- /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 00000000..0e74e631 --- /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 00000000..19b4b537 --- /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 00000000..846e6f2d --- /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 9e579405..f30c8183 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 34679d5b..fe4b4d2d 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 00000000..097d105f --- /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 8c0bbf5c..c3c27587 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 3ea5ab93..38890b90 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 536fc290..4e9377a0 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 15cdb367..86bcbf15 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 3c36c97c..51596636 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 583b04b9..7bd509ff 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 2aeb04f4..29b690e1 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); }); }); }