diff --git a/Application/Application.csproj b/Application/Application.csproj
index 45dca7973..9d9c84940 100644
--- a/Application/Application.csproj
+++ b/Application/Application.csproj
@@ -23,8 +23,8 @@
-
-
+
+
diff --git a/Application/GameEventHandler.cs b/Application/GameEventHandler.cs
index cebbd5926..0a4c00ec3 100644
--- a/Application/GameEventHandler.cs
+++ b/Application/GameEventHandler.cs
@@ -24,6 +24,16 @@ namespace IW4MAdmin.Application
public void AddEvent(GameEvent gameEvent)
{
+ ((Manager as ApplicationManager).OnServerEvent)(this, new GameEventArgs(null, false, gameEvent));
+ if (gameEvent.Type == GameEvent.EventType.Connect)
+ {
+ if (!gameEvent.OnProcessed.Wait(30 * 1000))
+ {
+ Manager.GetLogger().WriteError($"{Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_COMMAND_TIMEOUT"]} [{gameEvent.Id}, {gameEvent.Type}]");
+ }
+ }
+
+ return;
#if DEBUG
Manager.GetLogger().WriteDebug($"Got new event of type {gameEvent.Type} for {gameEvent.Owner} with id {gameEvent.Id}");
#endif
@@ -48,16 +58,16 @@ namespace IW4MAdmin.Application
// event occurs
if (gameEvent.Id == Interlocked.Read(ref NextEventId))
{
-//#if DEBUG == true
-// Manager.GetLogger().WriteDebug($"sent event with id {gameEvent.Id} to be processed");
-// IsProcessingEvent.Wait();
-//#else
-// if (GameEvent.IsEventTimeSensitive(gameEvent) &&
-// !IsProcessingEvent.Wait(30 * 1000))
-// {
-// Manager.GetLogger().WriteError($"{Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_COMMAND_TIMEOUT"]} [{gameEvent.Id}, {gameEvent.Type}]");
-// }
-//#endif
+ //#if DEBUG == true
+ // Manager.GetLogger().WriteDebug($"sent event with id {gameEvent.Id} to be processed");
+ // IsProcessingEvent.Wait();
+ //#else
+ // if (GameEvent.IsEventTimeSensitive(gameEvent) &&
+ // !IsProcessingEvent.Wait(30 * 1000))
+ // {
+ // Manager.GetLogger().WriteError($"{Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_COMMAND_TIMEOUT"]} [{gameEvent.Id}, {gameEvent.Type}]");
+ // }
+ //#endif
((Manager as ApplicationManager).OnServerEvent)(this, new GameEventArgs(null, false, gameEvent));
//if (GameEvent.IsEventTimeSensitive(gameEvent))
diff --git a/Application/Server.cs b/Application/Server.cs
index 620c5678b..0fb40c348 100644
--- a/Application/Server.cs
+++ b/Application/Server.cs
@@ -341,26 +341,14 @@ namespace IW4MAdmin
///
override protected async Task ProcessEvent(GameEvent E)
{
-
- if (E.Type == GameEvent.EventType.StatusUpdate)
- {
- // this event gets called before they're full connected
- if (E.Origin != null)
- {
- //var existingClient = Players[E.Origin.ClientNumber] ?? E.Origin;
- //existingClient.Ping = E.Origin.Ping;
- //existingClient.Score = E.Origin.Score;
- }
- }
-
- else if (E.Type == GameEvent.EventType.Connect)
+ if (E.Type == GameEvent.EventType.Connect)
{
E.Origin.State = Player.ClientState.Authenticated;
// add them to the server
if (!await AddPlayer(E.Origin))
{
E.Origin.State = Player.ClientState.Connecting;
- throw new ServerException("client didn't pass authorization, so we are discontinuing event");
+ throw new ServerException("client didn't pass authentication, so we are discontinuing event");
}
// hack: makes the event propgate with the correct info
E.Origin = Players[E.Origin.ClientNumber];
diff --git a/Plugins/Stats/Helpers/Extensions.cs b/Plugins/Stats/Helpers/Extensions.cs
index e771607ae..12b860d51 100644
--- a/Plugins/Stats/Helpers/Extensions.cs
+++ b/Plugins/Stats/Helpers/Extensions.cs
@@ -25,7 +25,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
public static double[] AngleStuff(Vector3 a, Vector3 b)
{
- double deltaX = 180.0 -Math.Abs(Math.Abs(a.X - b.X) - 180.0);
+ double deltaX = 180.0 - Math.Abs(Math.Abs(a.X - b.X) - 180.0);
double deltaY = 180.0 - Math.Abs(Math.Abs(a.Y - b.Y) - 180.0);
return new[] { deltaX, deltaY };
diff --git a/Plugins/Stats/Helpers/StatManager.cs b/Plugins/Stats/Helpers/StatManager.cs
index 029341686..4db734941 100644
--- a/Plugins/Stats/Helpers/StatManager.cs
+++ b/Plugins/Stats/Helpers/StatManager.cs
@@ -28,7 +28,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
private ILogger Log;
private IManager Manager;
- static readonly object LockObj = new object();
+
+ private readonly SemaphoreSlim OnProcessingPenalty;
public StatManager(IManager mgr)
{
@@ -36,6 +37,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
ContextThreads = new ConcurrentDictionary();
Log = mgr.GetLogger();
Manager = mgr;
+ OnProcessingPenalty = new SemaphoreSlim(1, 1);
}
public EFClientStatistics GetClientStats(int clientId, int serverId) => Servers[serverId].PlayerStats[clientId];
@@ -44,7 +46,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
{
var fifteenDaysAgo = DateTime.UtcNow.AddDays(-15);
return (r) => r.ServerId == serverId &&
- r.RatingHistory.Client.LastConnection > fifteenDaysAgo &&
+ r.When > fifteenDaysAgo &&
r.RatingHistory.Client.Level != Player.Permission.Banned &&
r.Newest &&
r.ActivityAmount >= Plugin.Config.Configuration().TopPlayersMinPlayTime;
@@ -90,26 +92,49 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
.Where(GetRankingFunc())
select new
{
- Ratings = rating.RatingHistory.Ratings.Where(r => r.ServerId == null),
rating.RatingHistory.ClientId,
rating.RatingHistory.Client.CurrentAlias.Name,
rating.RatingHistory.Client.LastConnection,
rating.Performance,
- }).OrderByDescending(c => c.Performance)
+ })
+ .OrderByDescending(c => c.Performance)
.Skip(start)
.Take(count);
#if DEBUG == true
- var clientRatingsSql = iqClientRatings.ToSql();
+ var clientRatingsSql = iqClientRatings.ToSql();
#endif
// materialized list
var clientRatings = await iqClientRatings.ToListAsync();
- // get all the client ids that
+ // get all the unique client ids that are in the top stats
var clientIds = clientRatings
.GroupBy(r => r.ClientId)
.Select(r => r.First().ClientId)
.ToList();
+ var iqRatingInfo = from rating in context.Set()
+ where clientIds.Contains(rating.RatingHistory.ClientId)
+ where rating.ServerId == null
+ select new
+ {
+ rating.Ranking,
+ rating.Performance,
+ rating.RatingHistory.ClientId,
+ rating.When
+ };
+
+#if DEBUG == true
+ var ratingQuery = iqRatingInfo.ToSql();
+#endif
+
+ var ratingInfo = (await iqRatingInfo.ToListAsync())
+ .GroupBy(r => r.ClientId)
+ .Select(grp => new
+ {
+ grp.Key,
+ Ratings = grp.Select(r => new { r.Performance, r.Ranking, r.When })
+ });
+
var iqStatsInfo = (from stat in context.Set()
where clientIds.Contains(stat.ClientId)
group stat by stat.ClientId into s
@@ -121,366 +146,370 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
KDR = s.Sum(c => (c.Kills / (double)(c.Deaths == 0 ? 1 : c.Deaths)) * c.TimePlayed) / s.Sum(c => c.TimePlayed),
TotalTimePlayed = s.Sum(c => c.TimePlayed)
});
+
#if DEBUG == true
- var statsInfoSql = iqStatsInfo.ToSql();
+ 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 = clientRatingsDict[s.ClientId].Ratings.First().Ranking - clientRatingsDict[s.ClientId].Ratings.Last().Ranking,
- PerformanceHistory = clientRatingsDict[s.ClientId].Ratings.Count() > 1 ?
- clientRatingsDict[s.ClientId].Ratings.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)
- {
- stat.Ranking = i;
- i++;
- }
-
- return 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)
+ ///
+ /// Add a server to the StatManager server pool
+ ///
+ ///
+ public void AddServer(Server sv)
+ {
+ try
{
- try
- {
- int serverId = sv.GetHashCode();
- var statsSvc = new ThreadSafeStatsService();
- ContextThreads.TryAdd(serverId, statsSvc);
+ 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"
- });
- }
-
- 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()
+ // 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,
- 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()
+ ServerId = serverId
};
- // insert if they've not been added
- clientStats = clientStatsSvc.Insert(clientStats);
- await clientStatsSvc.SaveChangesAsync();
+ statsSvc.ServerSvc.Insert(server);
}
- // migration for previous existing stats
- if (clientStats.HitLocations.Count == 0)
+ // 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)
{
- clientStats.HitLocations = Enum.GetValues(typeof(IW4Info.HitLocation)).OfType().Select(hl => new EFHitLocationCount()
+ 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()
{
Active = true,
HitCount = 0,
Location = hl
- })
- .ToList();
- //await statsSvc.ClientStatSvc.SaveChangesAsync();
- }
-
- // for stats before rating
- if (clientStats.EloRating == 0.0)
- {
- 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;
- }
-
- ///
- /// 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];
-
-#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);
-
- // 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();
- }
-
- 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
+ }).ToList()
};
- if (hit.DeathType == IW4Info.MeansOfDeath.MOD_SUICIDE &&
- hit.Damage == 100000)
+ // 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()
{
- // suicide by switching teams so let's not count it against them
- return;
+ Active = true,
+ HitCount = 0,
+ Location = hl
+ })
+ .ToList();
+ //await statsSvc.ClientStatSvc.SaveChangesAsync();
+ }
+
+ // for stats before rating
+ if (clientStats.EloRating == 0.0)
+ {
+ 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;
+ }
+
+ ///
+ /// 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];
+
+#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);
+
+ // 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();
+ }
+
+ 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());
}
+ }
- if (!isDamage)
+ 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)
+ {
+ async Task executePenalty(Cheat.DetectionPenaltyResult penalty)
{
- 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)
+ async Task saveLog()
{
- async Task saveLog()
+ using (var ctx = new DatabaseContext(false))
{
- using (var ctx = new DatabaseContext())
+ // todo: why does this cause duplicate primary key
+ foreach (var change in clientDetection.Tracker.GetChanges().Distinct())
{
- // todo: why does this cause duplicate primary key
- foreach (var change in clientDetection.Tracker.GetChanges().Distinct())
- {
- ctx.Add(change);
- }
+ ctx.Add(change);
await ctx.SaveChangesAsync();
}
}
+ }
+ await OnProcessingPenalty.WaitAsync();
+
+ try
+ {
switch (penalty.ClientPenalty)
{
case Penalty.PenaltyType.Ban:
if (attacker.Level == Player.Permission.Banned)
break;
- await saveLog();
await attacker.Ban(Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_STATS_CHEAT_DETECTED"], new Player()
{
ClientId = 1,
@@ -494,11 +523,11 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
}
}
});
+ await saveLog();
break;
case Penalty.PenaltyType.Flag:
if (attacker.Level != Player.Permission.User)
break;
- await saveLog();
var e = new GameEvent()
{
Data = penalty.Type == Cheat.Detection.DetectionType.Bone ?
@@ -515,129 +544,137 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
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 executePenalty(clientDetection.ProcessKill(hit, isDamage));
- await executePenalty(clientDetection.ProcessTotalRatio(clientStats));
-
- await clientStatsSvc.SaveChangesAsync();
}
- using (var ctx = new DatabaseContext())
- {
- ctx.Set().Add(hit);
- await ctx.SaveChangesAsync();
- }
- }
+ await executePenalty(clientDetection.ProcessKill(hit, isDamage));
+ await executePenalty(clientDetection.ProcessTotalRatio(clientStats));
- 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;
- }
-
-#if DEBUG
- Log.WriteDebug("Calculating standard kill");
-#endif
-
- // update the total stats
- Servers[serverId].ServerStatistics.TotalKills += 1;
-
- // this happens when the round has changed
- if (attackerStats.SessionScore == 0)
- attackerStats.LastScore = 0;
-
- if (victimStats.SessionScore == 0)
- victimStats.LastScore = 0;
-
- 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;
-
- // 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);
-
- // 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;
- }
-
- // update their performance
-#if !DEBUG
- if ((DateTime.UtcNow - attackerStats.LastStatHistoryUpdate).TotalMinutes >= 2.5)
-#endif
- {
- 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();
}
- ///
- /// 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)
+ using (var ctx = new DatabaseContext())
{
- int currentSessionTime = (int)(DateTime.UtcNow - client.LastConnection).TotalSeconds;
+ ctx.Set().Add(hit);
+ await ctx.SaveChangesAsync();
+ }
+ }
- // don't update their stat history if they haven't played long
+ 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;
+ }
+
+#if DEBUG
+ Log.WriteDebug("Calculating standard kill");
+#endif
+
+ // update the total stats
+ Servers[serverId].ServerStatistics.TotalKills += 1;
+
+ // this happens when the round has changed
+ if (attackerStats.SessionScore == 0)
+ attackerStats.LastScore = 0;
+
+ if (victimStats.SessionScore == 0)
+ victimStats.LastScore = 0;
+
+ 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;
+
+ // 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);
+
+ // 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;
+ }
+
+ // update their performance
+#if !DEBUG
+ if ((DateTime.UtcNow - attackerStats.LastStatHistoryUpdate).TotalMinutes >= 2.5)
+#endif
+ {
+ 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();
+ }
+
+ ///
+ /// 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
#if DEBUG == false
if (currentSessionTime < 60)
{
@@ -645,419 +682,419 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
}
#endif
- int currentServerTotalPlaytime = clientStats.TimePlayed + currentSessionTime;
+ int currentServerTotalPlaytime = clientStats.TimePlayed + currentSessionTime;
- using (var ctx = new DatabaseContext())
+ 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()
+ {
+ Active = true,
+ ClientId = client.ClientId,
+ Ratings = new List()
+ };
+
+ // it's the first time they've played
+ if (clientHistory.RatingHistoryId == 0)
{
- // select the rating history for client
- var iqHistoryLink = from history in ctx.Set()
- .Include(h => h.Ratings)
- where history.ClientId == client.ClientId
- select history;
+ ctx.Add(clientHistory);
+ // Log.WriteDebug($"adding first time client history {client.ClientId}");
+ await ctx.SaveChangesAsync();
+ }
- // get the client ratings
- var clientHistory = await iqHistoryLink
- .FirstOrDefaultAsync() ?? new EFClientRatingHistory()
- {
- Active = true,
- ClientId = client.ClientId,
- Ratings = new List()
- };
+ else
+ {
+ //ctx.Update(clientHistory);
+ }
- // 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();
- }
+ #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;
- 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
+ // 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)
- .OrderByDescending(r => r.When)
- .FirstOrDefault();
+ .OrderBy(r => r.When)
+ .First();
- 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,
- 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}");
+ ctx.Remove(ratingToRemove);
+ //Log.WriteDebug($"remove oldest 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
- };
+ // set the previous newest to false
+ var ratingToUnsetNewest = clientHistory.Ratings
+ .Where(r => r.ServerId == clientStats.ServerId)
+ .OrderByDescending(r => r.When)
+ .FirstOrDefault();
- var clientStatsList = await iqClientStats.ToListAsync();
-
- // add the current server's so we don't have to pull it frmo the database
- clientStatsList.Add(new
+ if (ratingToUnsetNewest != null)
+ {
+ if (ratingToUnsetNewest.Newest)
{
- 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}");
+ 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();
}
+ }
- // set the previous average newest to false
- ratingToUnsetNewest = clientHistory.Ratings
+ 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
.Where(r => r.ServerId == null)
- .OrderByDescending(r => r.When)
- .FirstOrDefault();
+ .OrderBy(r => r.When)
+ .First();
- 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}");
+ ctx.Remove(ratingToRemove);
+ //Log.WriteDebug($"remove oldest overall 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();
+
+ 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();
+ }
+ }
+
+ ///
+ /// 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;
+
+ // only update their kills if they didn't kill themselves
+ if (!suicide)
+ {
+ attackerStats.Kills += 1;
+ attackerStats.SessionKills += 1;
+ attackerStats.KillStreak += 1;
+ attackerStats.DeathStreak = 0;
}
- ///
- /// 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)
+ 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)
{
- bool suicide = attackerStats.ClientId == victimStats.ClientId;
+ /* 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);
- // 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 attackerLobbyRating = validAttackerLobbyRatings.Count() > 0 ?
+ validAttackerLobbyRatings.Average(cs => cs.Value.EloRating) :
+ attackerStats.EloRating;
- victimStats.Deaths += 1;
- victimStats.SessionDeaths += 1;
- victimStats.DeathStreak += 1;
- victimStats.KillStreak = 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);
- // process the attacker's stats after the kills
- attackerStats = UpdateStats(attackerStats);
+ double victimLobbyRating = validVictimLobbyRatings.Count() > 0 ?
+ validVictimLobbyRatings.Average(cs => cs.Value.EloRating) :
+ victimStats.EloRating;*/
- // calulate elo
- if (Servers[attackerStats.ServerId].PlayerStats.Count > 1)
- {
- /* 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);
+ 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 attackerLobbyRating = validAttackerLobbyRatings.Count() > 0 ?
- validAttackerLobbyRatings.Average(cs => cs.Value.EloRating) :
- attackerStats.EloRating;
+ // 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));
- 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);
+ attackerStats.EloRating += 6.0 * (1 - winPercentage);
+ victimStats.EloRating -= 6.0 * (1 - winPercentage);
- 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;
+ attackerStats.EloRating = Math.Max(0, Math.Round(attackerStats.EloRating, 2));
+ victimStats.EloRating = Math.Max(0, Math.Round(victimStats.EloRating, 2));
}
- ///
- /// Update the client stats (skill etc)
- ///
- /// Client statistics
- ///
- private EFClientStatistics UpdateStats(EFClientStatistics clientStats)
+ // 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;
+ }
+
+ ///
+ /// 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)
{
- // 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 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)
- {
- scoreDifference = clientStats.RoundScore + clientStats.LastScore;
- }
-
- else if (clientStats.RoundScore > 0 && clientStats.LastScore < clientStats.RoundScore)
- {
- scoreDifference = clientStats.RoundScore - clientStats.LastScore;
- }
-
- 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)
- {
- 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;
- }
-
+ // prevents idle time counting
clientStats.LastStatCalculation = DateTime.UtcNow;
- //clientStats.LastScore = clientStats.SessionScore;
-
return clientStats;
}
- public void InitializeServerStats(Server sv)
+ 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)
{
- 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);
- }
+ scoreDifference = clientStats.RoundScore + clientStats.LastScore;
}
- public void ResetKillstreaks(int serverId)
+ else if (clientStats.RoundScore > 0 && clientStats.LastScore < clientStats.RoundScore)
{
- var serverStats = Servers[serverId];
- foreach (var stat in serverStats.PlayerStats.Values)
- stat.StartNewSession();
+ scoreDifference = clientStats.RoundScore - clientStats.LastScore;
}
- public void ResetStats(int clientId, int serverId)
+ 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)
{
- var stats = Servers[serverId].PlayerStats[clientId];
- stats.Kills = 0;
- stats.Deaths = 0;
- stats.SPM = 0;
- stats.Skill = 0;
- stats.TimePlayed = 0;
- stats.EloRating = 200;
+ Log.WriteWarning("[StatManager:UpdateStats] clientStats SPM < 0");
+ Log.WriteDebug($"{scoreDifference}-{clientStats.RoundScore} - {clientStats.LastScore} - {clientStats.SessionScore}");
+ clientStats.SPM = 0;
}
- public async Task AddMessageAsync(int clientId, int serverId, string message)
- {
- // the web users can have no account
- if (clientId < 1)
- return;
+ clientStats.SPM = Math.Round(clientStats.SPM, 3);
+ clientStats.Skill = Math.Round((clientStats.SPM * KDRWeight), 3);
- var messageSvc = ContextThreads[serverId].MessageSvc;
- messageSvc.Insert(new EFClientMessage()
+ // 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;
+ }
+
+ 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()
{
Active = true,
- ClientId = clientId,
- Message = message,
ServerId = serverId,
- TimeSent = DateTime.UtcNow
- });
- await messageSvc.SaveChangesAsync();
- }
+ TotalKills = 0,
+ TotalPlayTime = 0,
+ };
- public async Task Sync(Server sv)
- {
- int serverId = sv.GetHashCode();
- var statsSvc = ContextThreads[serverId];
+ var ieClientStats = statsSvc.ClientStatSvc.Find(cs => cs.ServerId == serverId);
- // Log.WriteDebug("Syncing stats contexts");
- await statsSvc.ServerStatsSvc.SaveChangesAsync();
- //await statsSvc.ClientStatSvc.SaveChangesAsync();
- await statsSvc.KillStatsSvc.SaveChangesAsync();
- await statsSvc.ServerSvc.SaveChangesAsync();
+ // set these incase we've imported settings
+ serverStats.TotalKills = ieClientStats.Sum(cs => cs.Kills);
+ serverStats.TotalPlayTime = Manager.GetClientService().GetTotalPlayTime().Result;
- 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;
+ statsSvc.ServerStatsSvc.Insert(serverStats);
}
}
+
+ 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 45fbdae18..360e7faef 100644
--- a/Plugins/Stats/Models/ModelConfiguration.cs
+++ b/Plugins/Stats/Models/ModelConfiguration.cs
@@ -21,6 +21,15 @@ namespace Stats.Models
.Property(c => c.ServerId)
.HasColumnName("EFClientStatistics_ServerId");
+ builder.Entity()
+ .HasIndex(p => p.Performance);
+
+ builder.Entity()
+ .HasIndex(p => p.Ranking);
+
+ builder.Entity()
+ .HasIndex(p => p.When);
+
// force pluralization
builder.Entity().ToTable("EFClientKills");
builder.Entity().ToTable("EFClientMessages");
diff --git a/SharedLibraryCore/Commands/NativeCommands.cs b/SharedLibraryCore/Commands/NativeCommands.cs
index 66008365f..7951080c7 100644
--- a/SharedLibraryCore/Commands/NativeCommands.cs
+++ b/SharedLibraryCore/Commands/NativeCommands.cs
@@ -653,7 +653,7 @@ namespace SharedLibraryCore.Commands
// they're not going by another alias
string msg = P.Name.ToLower().Contains(E.Data.ToLower()) ?
$"[^3{P.Name}^7] [^3@{P.ClientId}^7] - [{ Utilities.ConvertLevelToColor(P.Level, localizedLevel)}^7] - {P.IPAddressString} | last seen {Utilities.GetTimePassed(P.LastConnection)}" :
- $"({P.AliasLink.Children.First(a => a.Name.ToLower().Contains(E.Data.ToLower())).Name})->[^3{P.Name}^7] [^3@{P.ClientId}^7] - [{ Utilities.ConvertLevelToColor(P.Level, localizedLevel)}^7] - {P.IPAddressString} | last seen {Utilities.GetTimePassed(P.LastConnection)}";
+ $"({P.AliasLink.Children.FirstOrDefault(a => a.Name.ToLower().Contains(E.Data.ToLower()))?.Name})->[^3{P.Name}^7] [^3@{P.ClientId}^7] - [{ Utilities.ConvertLevelToColor(P.Level, localizedLevel)}^7] - {P.IPAddressString} | last seen {Utilities.GetTimePassed(P.LastConnection)}";
await E.Origin.Tell(msg);
await Task.Delay(FloodProtectionInterval);
}
diff --git a/SharedLibraryCore/Database/DatabaseContext.cs b/SharedLibraryCore/Database/DatabaseContext.cs
index f0003ad24..9b1809263 100644
--- a/SharedLibraryCore/Database/DatabaseContext.cs
+++ b/SharedLibraryCore/Database/DatabaseContext.cs
@@ -99,6 +99,8 @@ namespace SharedLibraryCore.Database
modelBuilder.Entity(ent =>
{
ent.HasIndex(a => a.IPAddress);
+ ent.Property(a => a.Name).HasMaxLength(24);
+ ent.HasIndex(a => a.Name);
});
// force full name for database conversion
diff --git a/SharedLibraryCore/Database/Models/EFAlias.cs b/SharedLibraryCore/Database/Models/EFAlias.cs
index 74e75cfc8..97f75890d 100644
--- a/SharedLibraryCore/Database/Models/EFAlias.cs
+++ b/SharedLibraryCore/Database/Models/EFAlias.cs
@@ -13,6 +13,7 @@ namespace SharedLibraryCore.Database.Models
[ForeignKey("LinkId")]
public virtual EFAliasLink Link { get; set; }
[Required]
+ [MaxLength(24)]
public string Name { get; set; }
[Required]
public int IPAddress { get; set; }
diff --git a/SharedLibraryCore/Helpers/Vector3.cs b/SharedLibraryCore/Helpers/Vector3.cs
index 0c7a24c4d..b7850ecdd 100644
--- a/SharedLibraryCore/Helpers/Vector3.cs
+++ b/SharedLibraryCore/Helpers/Vector3.cs
@@ -35,13 +35,13 @@ namespace SharedLibraryCore.Helpers
public static Vector3 Parse(string s)
{
- bool valid = Regex.Match(s, @"\(-?[0-9]+.?[0-9]*,\ -?[0-9]+.?[0-9]*,\ -?[0-9]+.?[0-9]*\)").Success;
+ bool valid = Regex.Match(s, @"\((-?[0-9]+\.?[0-9]*|-?[0-9]+\.?[0-9]*e-[0-9]+),\ (-?[0-9]+\.?[0-9]*|-?[0-9]+\.?[0-9]*e-[0-9]+),\ (-?[0-9]+\.?[0-9]*|-?[0-9]+\.?[0-9]*e-[0-9]+)\)").Success;
if (!valid)
throw new FormatException("Vector3 is not in correct format");
string removeParenthesis = s.Substring(1, s.Length - 2);
string[] eachPoint = removeParenthesis.Split(',');
- return new Vector3(float.Parse(eachPoint[0]), float.Parse(eachPoint[1]), float.Parse(eachPoint[2]));
+ return new Vector3(float.Parse(eachPoint[0], System.Globalization.NumberStyles.Any), float.Parse(eachPoint[1], System.Globalization.NumberStyles.Any), float.Parse(eachPoint[2], System.Globalization.NumberStyles.Any));
}
public static double Distance(Vector3 a, Vector3 b)
diff --git a/SharedLibraryCore/Migrations/20180910221749_AddRatingIndexes.Designer.cs b/SharedLibraryCore/Migrations/20180910221749_AddRatingIndexes.Designer.cs
new file mode 100644
index 000000000..231f84763
--- /dev/null
+++ b/SharedLibraryCore/Migrations/20180910221749_AddRatingIndexes.Designer.cs
@@ -0,0 +1,677 @@
+//
+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("20180910221749_AddRatingIndexes")]
+ partial class AddRatingIndexes
+ {
+ 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.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();
+
+ b.HasKey("AliasId");
+
+ b.HasIndex("IPAddress");
+
+ b.HasIndex("LinkId");
+
+ 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("OriginEntityId");
+
+ 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/20180910221749_AddRatingIndexes.cs b/SharedLibraryCore/Migrations/20180910221749_AddRatingIndexes.cs
new file mode 100644
index 000000000..5a4d62f0d
--- /dev/null
+++ b/SharedLibraryCore/Migrations/20180910221749_AddRatingIndexes.cs
@@ -0,0 +1,40 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace SharedLibraryCore.Migrations
+{
+ public partial class AddRatingIndexes : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateIndex(
+ name: "IX_EFRating_Performance",
+ table: "EFRating",
+ column: "Performance");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_EFRating_Ranking",
+ table: "EFRating",
+ column: "Ranking");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_EFRating_When",
+ table: "EFRating",
+ column: "When");
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropIndex(
+ name: "IX_EFRating_Performance",
+ table: "EFRating");
+
+ migrationBuilder.DropIndex(
+ name: "IX_EFRating_Ranking",
+ table: "EFRating");
+
+ migrationBuilder.DropIndex(
+ name: "IX_EFRating_When",
+ table: "EFRating");
+ }
+ }
+}
diff --git a/SharedLibraryCore/Migrations/20180911184224_AddEFAliasNameIndex.Designer.cs b/SharedLibraryCore/Migrations/20180911184224_AddEFAliasNameIndex.Designer.cs
new file mode 100644
index 000000000..68ad1472e
--- /dev/null
+++ b/SharedLibraryCore/Migrations/20180911184224_AddEFAliasNameIndex.Designer.cs
@@ -0,0 +1,679 @@
+//
+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("20180911184224_AddEFAliasNameIndex")]
+ partial class AddEFAliasNameIndex
+ {
+ 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.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