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