2018-02-07 00:19:06 -05:00
|
|
|
|
using System;
|
2018-02-15 23:01:28 -05:00
|
|
|
|
using System.Collections.Concurrent;
|
2018-02-07 00:19:06 -05:00
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Threading.Tasks;
|
2018-04-09 23:33:42 -04:00
|
|
|
|
|
2018-04-08 17:50:58 -04:00
|
|
|
|
using SharedLibraryCore;
|
|
|
|
|
using SharedLibraryCore.Helpers;
|
|
|
|
|
using SharedLibraryCore.Interfaces;
|
|
|
|
|
using SharedLibraryCore.Objects;
|
|
|
|
|
using SharedLibraryCore.Commands;
|
2018-04-09 23:33:42 -04:00
|
|
|
|
using IW4MAdmin.Plugins.Stats.Models;
|
2018-05-24 15:48:57 -04:00
|
|
|
|
using System.Text.RegularExpressions;
|
2018-05-28 21:30:31 -04:00
|
|
|
|
using IW4MAdmin.Plugins.Stats.Web.Dtos;
|
|
|
|
|
using SharedLibraryCore.Database;
|
|
|
|
|
using Microsoft.EntityFrameworkCore;
|
2018-05-30 21:50:20 -04:00
|
|
|
|
using SharedLibraryCore.Database.Models;
|
|
|
|
|
using SharedLibraryCore.Services;
|
2018-02-07 00:19:06 -05:00
|
|
|
|
|
2018-04-08 17:50:58 -04:00
|
|
|
|
namespace IW4MAdmin.Plugins.Stats.Helpers
|
2018-02-07 00:19:06 -05:00
|
|
|
|
{
|
|
|
|
|
public class StatManager
|
|
|
|
|
{
|
2018-02-15 23:01:28 -05:00
|
|
|
|
private ConcurrentDictionary<int, ServerStats> Servers;
|
|
|
|
|
private ConcurrentDictionary<int, ThreadSafeStatsService> ContextThreads;
|
2018-02-07 00:19:06 -05:00
|
|
|
|
private ILogger Log;
|
|
|
|
|
private IManager Manager;
|
|
|
|
|
|
|
|
|
|
public StatManager(IManager mgr)
|
|
|
|
|
{
|
2018-02-15 23:01:28 -05:00
|
|
|
|
Servers = new ConcurrentDictionary<int, ServerStats>();
|
|
|
|
|
ContextThreads = new ConcurrentDictionary<int, ThreadSafeStatsService>();
|
2018-02-07 00:19:06 -05:00
|
|
|
|
Log = mgr.GetLogger();
|
|
|
|
|
Manager = mgr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
~StatManager()
|
|
|
|
|
{
|
|
|
|
|
Servers.Clear();
|
|
|
|
|
Log = null;
|
|
|
|
|
Servers = null;
|
|
|
|
|
}
|
|
|
|
|
|
2018-05-28 21:30:31 -04:00
|
|
|
|
public EFClientStatistics GetClientStats(int clientId, int serverId) => Servers[serverId].PlayerStats[clientId];
|
|
|
|
|
|
|
|
|
|
public async Task<List<TopStatsInfo>> GetTopStats(int start, int count)
|
|
|
|
|
{
|
|
|
|
|
using (var context = new DatabaseContext())
|
|
|
|
|
{
|
|
|
|
|
context.ChangeTracker.AutoDetectChangesEnabled = false;
|
|
|
|
|
context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
|
|
|
|
|
|
|
|
|
var thirtyDaysAgo = DateTime.UtcNow.AddMonths(-1);
|
|
|
|
|
var iqClientIds = (from stat in context.Set<EFClientStatistics>()
|
|
|
|
|
#if !DEBUG
|
2018-05-30 21:50:20 -04:00
|
|
|
|
.Where(s => s.TimePlayed > 3600)
|
|
|
|
|
.Where(s => s.EloRating > 60.0)
|
2018-05-28 21:30:31 -04:00
|
|
|
|
#endif
|
2018-05-30 21:50:20 -04:00
|
|
|
|
where stat.Client.Level != Player.Permission.Banned
|
|
|
|
|
where stat.Client.LastConnection >= thirtyDaysAgo
|
|
|
|
|
|
|
|
|
|
group stat by stat.ClientId into sj
|
|
|
|
|
let performance = sj.Sum(s => (s.EloRating + s.Skill) * s.TimePlayed) / sj.Sum(st => st.TimePlayed)
|
|
|
|
|
orderby performance
|
|
|
|
|
select new
|
|
|
|
|
{
|
|
|
|
|
// sj.First().Client.CurrentAlias.Name,
|
|
|
|
|
sj.First().Client.ClientId,
|
|
|
|
|
Skill = sj.Select(s => s.Skill)
|
|
|
|
|
})
|
|
|
|
|
/*
|
|
|
|
|
join averageStats in context.Set<EFClientAverageStatHistory>().Include(c => c.Ratings)
|
|
|
|
|
on stat.ClientId equals averageStats.ClientId
|
|
|
|
|
where averageStats.Ratings.Count > 0
|
|
|
|
|
group new { stat, averageStats } by averageStats.ClientId into avg
|
|
|
|
|
orderby avg.Select(c => c.averageStats.Ratings.OrderByDescending(r => r.RatingId).First().Performance).First()
|
|
|
|
|
select new
|
|
|
|
|
{
|
|
|
|
|
avg.First().stat.Client.CurrentAlias.Name,//sj.Select(c => c.Client.CurrentAlias.Name),
|
|
|
|
|
avg.First().stat.ClientId,//sj.First().ClientId,
|
|
|
|
|
avg.First().stat.Kills,//Kills = sj.Select(s => s.Kills),
|
|
|
|
|
avg.First().stat.Deaths,//Deaths = sj.Select(s => s.Deaths),
|
|
|
|
|
avg.First().stat.Performance,//Performance = sj.Select(c => new { c.Performance, c.TimePlayed }),
|
|
|
|
|
// KDR = stat.Kills / stat.Deaths,//KDR = sj.Select(c => new { KDR = c.Kills / (double)c.Deaths, c.TimePlayed }),
|
|
|
|
|
//TotalPlayTime = sj.Select(c => c.TimePlayed),
|
|
|
|
|
avg.First().stat.Client.LastConnection,//sj.First().Client.LastConnection,
|
|
|
|
|
avg.First().stat.Client.TotalConnectionTime,//sj.First().Client.TotalConnectionTime,
|
|
|
|
|
avg.First().stat.TimePlayed,
|
|
|
|
|
RatingId = 0
|
|
|
|
|
// todo: eventually replace this in favor of joining
|
|
|
|
|
//AverageHistory = context.Set<EFClientAverageStatHistory>().SingleOrDefault(r => r.ClientId == stat.ClientId)
|
|
|
|
|
})*/
|
2018-05-28 21:30:31 -04:00
|
|
|
|
.Skip(start)
|
|
|
|
|
.Take(count);
|
|
|
|
|
|
2018-05-30 21:50:20 -04:00
|
|
|
|
var stats = await iqClientIds.ToListAsync();
|
2018-05-28 21:30:31 -04:00
|
|
|
|
|
2018-05-30 21:50:20 -04:00
|
|
|
|
var groupedSelection = stats.GroupBy(c => c.ClientId).Select(s =>
|
|
|
|
|
new TopStatsInfo()
|
|
|
|
|
{
|
|
|
|
|
/* Name = s.First().Name,
|
|
|
|
|
// weighted based on time played
|
|
|
|
|
Performance = s.OrderByDescending(r => r.RatingId).First().Performance,
|
|
|
|
|
// ditto
|
|
|
|
|
KDR = s.First().Deaths == 0 ? s.First().Kills : (double)s.First().Kills / s.First().Deaths,
|
|
|
|
|
ClientId = s.First().ClientId,
|
|
|
|
|
Deaths = s.First().Deaths,
|
|
|
|
|
Kills = s.First().Kills,
|
|
|
|
|
LastSeen = Utilities.GetTimePassed(s.First().LastConnection, false),
|
|
|
|
|
TimePlayed = Math.Round(s.First().TotalConnectionTime / 3600.0, 1).ToString("#,##0"),
|
|
|
|
|
/ PerformanceHistory = s.AverageHistory == null || s.AverageHistory?.Ratings.Count < 2 ?
|
|
|
|
|
new List<double>()
|
2018-05-28 21:30:31 -04:00
|
|
|
|
{
|
2018-05-30 21:50:20 -04:00
|
|
|
|
s.Performance,
|
|
|
|
|
s.Performance
|
|
|
|
|
} :
|
|
|
|
|
s.AverageHistory.Ratings.Select(r => Math.Round(r.Performance, 1)).ToList()*/
|
|
|
|
|
});
|
2018-05-28 21:30:31 -04:00
|
|
|
|
|
|
|
|
|
var statList = groupedSelection.OrderByDescending(s => s.Performance).ToList();
|
|
|
|
|
|
|
|
|
|
// set the ranking numerically
|
|
|
|
|
int i = start + 1;
|
|
|
|
|
foreach (var stat in statList)
|
|
|
|
|
{
|
|
|
|
|
stat.Ranking = i;
|
|
|
|
|
i++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return statList;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-07 00:19:06 -05:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Add a server to the StatManager server pool
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="sv"></param>
|
|
|
|
|
public void AddServer(Server sv)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
int serverId = sv.GetHashCode();
|
2018-02-10 01:26:38 -05:00
|
|
|
|
var statsSvc = new ThreadSafeStatsService();
|
2018-02-15 23:01:28 -05:00
|
|
|
|
ContextThreads.TryAdd(serverId, statsSvc);
|
2018-02-09 02:21:25 -05:00
|
|
|
|
|
2018-02-07 00:19:06 -05:00
|
|
|
|
// get the server from the database if it exists, otherwise create and insert a new one
|
2018-02-10 01:26:38 -05:00
|
|
|
|
var server = statsSvc.ServerSvc.Find(c => c.ServerId == serverId).FirstOrDefault();
|
2018-02-07 00:19:06 -05:00
|
|
|
|
if (server == null)
|
|
|
|
|
{
|
|
|
|
|
server = new EFServer()
|
|
|
|
|
{
|
|
|
|
|
Port = sv.GetPort(),
|
|
|
|
|
Active = true,
|
|
|
|
|
ServerId = serverId
|
|
|
|
|
};
|
|
|
|
|
|
2018-02-10 01:26:38 -05:00
|
|
|
|
statsSvc.ServerSvc.Insert(server);
|
2018-02-07 00:19:06 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// this doesn't need to be async as it's during initialization
|
2018-02-10 01:26:38 -05:00
|
|
|
|
statsSvc.ServerSvc.SaveChanges();
|
2018-02-09 02:21:25 -05:00
|
|
|
|
// check to see if the stats have ever been initialized
|
2018-02-10 01:26:38 -05:00
|
|
|
|
InitializeServerStats(sv);
|
|
|
|
|
statsSvc.ServerStatsSvc.SaveChanges();
|
2018-02-09 02:21:25 -05:00
|
|
|
|
|
2018-02-10 01:26:38 -05:00
|
|
|
|
var serverStats = statsSvc.ServerStatsSvc.Find(c => c.ServerId == serverId).FirstOrDefault();
|
2018-05-24 15:48:57 -04:00
|
|
|
|
Servers.TryAdd(serverId, new ServerStats(server, serverStats)
|
|
|
|
|
{
|
|
|
|
|
IsTeamBased = sv.Gametype != "dm"
|
|
|
|
|
});
|
2018-02-07 00:19:06 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
{
|
2018-05-08 00:58:46 -04:00
|
|
|
|
Log.WriteError($"{Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_STATS_ERROR_ADD"]} - {e.Message}");
|
2018-02-07 00:19:06 -05:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Add Player to the player stats
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="pl">Player to add/retrieve stats for</param>
|
|
|
|
|
/// <returns>EFClientStatistic of specified player</returns>
|
2018-03-06 02:22:19 -05:00
|
|
|
|
public async Task<EFClientStatistics> AddPlayer(Player pl)
|
2018-02-07 00:19:06 -05:00
|
|
|
|
{
|
|
|
|
|
int serverId = pl.CurrentServer.GetHashCode();
|
2018-03-06 02:22:19 -05:00
|
|
|
|
|
|
|
|
|
if (!Servers.ContainsKey(serverId))
|
|
|
|
|
{
|
|
|
|
|
Log.WriteError($"[Stats::AddPlayer] Server with id {serverId} could not be found");
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-07 00:19:06 -05:00
|
|
|
|
var playerStats = Servers[serverId].PlayerStats;
|
2018-02-10 01:26:38 -05:00
|
|
|
|
var statsSvc = ContextThreads[serverId];
|
2018-04-26 20:19:42 -04:00
|
|
|
|
var detectionStats = Servers[serverId].PlayerDetections;
|
|
|
|
|
|
|
|
|
|
if (playerStats.ContainsKey(pl.ClientId))
|
|
|
|
|
{
|
|
|
|
|
Log.WriteWarning($"Duplicate ClientId in stats {pl.ClientId}");
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2018-02-07 00:19:06 -05:00
|
|
|
|
|
|
|
|
|
// 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
|
2018-05-14 13:55:10 -04:00
|
|
|
|
var clientStatsSvc = statsSvc.ClientStatSvc;
|
|
|
|
|
var clientStats = clientStatsSvc.Find(c => c.ClientId == pl.ClientId && c.ServerId == serverId).FirstOrDefault();
|
2018-02-15 23:01:28 -05:00
|
|
|
|
|
2018-02-07 00:19:06 -05:00
|
|
|
|
if (clientStats == null)
|
|
|
|
|
{
|
|
|
|
|
clientStats = new EFClientStatistics()
|
|
|
|
|
{
|
|
|
|
|
Active = true,
|
|
|
|
|
ClientId = pl.ClientId,
|
|
|
|
|
Deaths = 0,
|
|
|
|
|
Kills = 0,
|
|
|
|
|
ServerId = serverId,
|
|
|
|
|
Skill = 0.0,
|
|
|
|
|
SPM = 0.0,
|
2018-05-16 00:57:37 -04:00
|
|
|
|
EloRating = 200.0,
|
2018-03-06 02:22:19 -05:00
|
|
|
|
HitLocations = Enum.GetValues(typeof(IW4Info.HitLocation)).OfType<IW4Info.HitLocation>().Select(hl => new EFHitLocationCount()
|
|
|
|
|
{
|
|
|
|
|
Active = true,
|
|
|
|
|
HitCount = 0,
|
|
|
|
|
Location = hl
|
2018-05-30 21:50:20 -04:00
|
|
|
|
}).ToList()
|
2018-02-07 00:19:06 -05:00
|
|
|
|
};
|
|
|
|
|
|
2018-05-11 00:52:20 -04:00
|
|
|
|
// insert if they've not been added
|
|
|
|
|
clientStats = clientStatsSvc.Insert(clientStats);
|
|
|
|
|
await clientStatsSvc.SaveChangesAsync();
|
2018-05-03 01:25:49 -04:00
|
|
|
|
}
|
|
|
|
|
|
2018-03-06 02:22:19 -05:00
|
|
|
|
// migration for previous existing stats
|
2018-05-03 01:25:49 -04:00
|
|
|
|
if (clientStats.HitLocations.Count == 0)
|
2018-03-06 02:22:19 -05:00
|
|
|
|
{
|
|
|
|
|
clientStats.HitLocations = Enum.GetValues(typeof(IW4Info.HitLocation)).OfType<IW4Info.HitLocation>().Select(hl => new EFHitLocationCount()
|
|
|
|
|
{
|
|
|
|
|
Active = true,
|
|
|
|
|
HitCount = 0,
|
|
|
|
|
Location = hl
|
|
|
|
|
})
|
|
|
|
|
.ToList();
|
2018-05-03 01:25:49 -04:00
|
|
|
|
//await statsSvc.ClientStatSvc.SaveChangesAsync();
|
2018-02-07 00:19:06 -05:00
|
|
|
|
}
|
|
|
|
|
|
2018-05-17 19:31:58 -04:00
|
|
|
|
// for stats before rating
|
2018-05-16 00:57:37 -04:00
|
|
|
|
if (clientStats.EloRating == 0.0)
|
|
|
|
|
{
|
|
|
|
|
clientStats.EloRating = clientStats.Skill;
|
|
|
|
|
}
|
|
|
|
|
|
2018-05-17 19:31:58 -04:00
|
|
|
|
if (clientStats.RollingWeightedKDR == 0)
|
|
|
|
|
{
|
|
|
|
|
clientStats.RollingWeightedKDR = clientStats.KDR;
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-09 02:21:25 -05:00
|
|
|
|
// set these on connecting
|
|
|
|
|
clientStats.LastActive = DateTime.UtcNow;
|
|
|
|
|
clientStats.LastStatCalculation = DateTime.UtcNow;
|
2018-03-06 02:22:19 -05:00
|
|
|
|
clientStats.SessionScore = pl.Score;
|
2018-02-09 02:21:25 -05:00
|
|
|
|
|
2018-04-26 20:19:42 -04:00
|
|
|
|
Log.WriteInfo($"Adding {pl} to stats");
|
2018-02-26 23:24:19 -05:00
|
|
|
|
|
2018-04-26 20:19:42 -04:00
|
|
|
|
if (!playerStats.TryAdd(pl.ClientId, clientStats))
|
|
|
|
|
Log.WriteDebug($"Could not add client to stats {pl}");
|
2018-02-26 23:24:19 -05:00
|
|
|
|
|
2018-04-26 20:19:42 -04:00
|
|
|
|
if (!detectionStats.TryAdd(pl.ClientId, new Cheat.Detection(Log, clientStats)))
|
|
|
|
|
Log.WriteDebug("Could not add client to detection");
|
2018-03-06 02:22:19 -05:00
|
|
|
|
|
2018-02-07 00:19:06 -05:00
|
|
|
|
return clientStats;
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-10 01:26:38 -05:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Perform stat updates for disconnecting client
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="pl">Disconnecting client</param>
|
|
|
|
|
/// <returns></returns>
|
2018-02-07 00:19:06 -05:00
|
|
|
|
public async Task RemovePlayer(Player pl)
|
|
|
|
|
{
|
2018-03-06 02:22:19 -05:00
|
|
|
|
Log.WriteInfo($"Removing {pl} from stats");
|
|
|
|
|
|
2018-02-07 00:19:06 -05:00
|
|
|
|
int serverId = pl.CurrentServer.GetHashCode();
|
|
|
|
|
var playerStats = Servers[serverId].PlayerStats;
|
2018-02-26 23:24:19 -05:00
|
|
|
|
var detectionStats = Servers[serverId].PlayerDetections;
|
2018-02-10 01:26:38 -05:00
|
|
|
|
var serverStats = Servers[serverId].ServerStatistics;
|
|
|
|
|
var statsSvc = ContextThreads[serverId];
|
|
|
|
|
|
2018-03-06 02:22:19 -05:00
|
|
|
|
if (!playerStats.ContainsKey(pl.ClientId))
|
|
|
|
|
{
|
|
|
|
|
Log.WriteWarning($"Client disconnecting not in stats {pl}");
|
2018-04-26 20:19:42 -04:00
|
|
|
|
// 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);
|
2018-03-06 02:22:19 -05:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-07 00:19:06 -05:00
|
|
|
|
// get individual client's stats
|
2018-03-06 02:22:19 -05:00
|
|
|
|
var clientStats = playerStats[pl.ClientId];
|
2018-04-26 20:19:42 -04:00
|
|
|
|
|
2018-02-07 00:19:06 -05:00
|
|
|
|
// remove the client from the stats dictionary as they're leaving
|
2018-04-26 20:19:42 -04:00
|
|
|
|
playerStats.TryRemove(pl.ClientId, out EFClientStatistics removedValue3);
|
|
|
|
|
detectionStats.TryRemove(pl.ClientId, out Cheat.Detection removedValue4);
|
2018-02-09 02:21:25 -05:00
|
|
|
|
|
2018-05-10 01:34:29 -04:00
|
|
|
|
// sync their stats before they leave
|
2018-05-14 13:55:10 -04:00
|
|
|
|
var clientStatsSvc = statsSvc.ClientStatSvc;
|
|
|
|
|
clientStats = UpdateStats(clientStats);
|
|
|
|
|
clientStatsSvc.Update(clientStats);
|
|
|
|
|
await clientStatsSvc.SaveChangesAsync();
|
2018-02-10 01:26:38 -05:00
|
|
|
|
|
|
|
|
|
// increment the total play time
|
2018-02-09 02:21:25 -05:00
|
|
|
|
serverStats.TotalPlayTime += (int)(DateTime.UtcNow - pl.LastConnection).TotalSeconds;
|
2018-05-03 01:25:49 -04:00
|
|
|
|
}
|
|
|
|
|
|
2018-05-24 15:48:57 -04:00
|
|
|
|
public void AddDamageEvent(string eventLine, int attackerClientId, int victimClientId, int serverId)
|
2018-05-03 01:25:49 -04:00
|
|
|
|
{
|
2018-05-24 15:48:57 -04:00
|
|
|
|
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;
|
|
|
|
|
}
|
2018-02-07 00:19:06 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Process stats for kill event
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <returns></returns>
|
2018-05-10 01:34:29 -04:00
|
|
|
|
public async Task AddScriptHit(bool isDamage, DateTime time, Player attacker, Player victim, int serverId, string map, string hitLoc, string type,
|
2018-05-03 01:25:49 -04:00
|
|
|
|
string damage, string weapon, string killOrigin, string deathOrigin, string viewAngles, string offset, string isKillstreakKill, string Ads, string snapAngles)
|
2018-02-07 00:19:06 -05:00
|
|
|
|
{
|
2018-02-10 01:26:38 -05:00
|
|
|
|
var statsSvc = ContextThreads[serverId];
|
2018-04-09 23:33:42 -04:00
|
|
|
|
Vector3 vDeathOrigin = null;
|
|
|
|
|
Vector3 vKillOrigin = null;
|
2018-04-26 02:13:04 -04:00
|
|
|
|
Vector3 vViewAngles = null;
|
2018-04-09 23:33:42 -04:00
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
vDeathOrigin = Vector3.Parse(deathOrigin);
|
|
|
|
|
vKillOrigin = Vector3.Parse(killOrigin);
|
2018-04-26 02:13:04 -04:00
|
|
|
|
vViewAngles = Vector3.Parse(viewAngles).FixIW4Angles();
|
2018-04-09 23:33:42 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
catch (FormatException)
|
|
|
|
|
{
|
2018-04-28 01:22:18 -04:00
|
|
|
|
Log.WriteWarning("Could not parse kill or death origin or viewangle vectors");
|
2018-04-28 21:11:13 -04:00
|
|
|
|
Log.WriteDebug($"Kill - {killOrigin} Death - {deathOrigin} ViewAngle - {viewAngles}");
|
2018-04-09 23:33:42 -04:00
|
|
|
|
await AddStandardKill(attacker, victim);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2018-05-03 01:25:49 -04:00
|
|
|
|
var snapshotAngles = new List<Vector3>();
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
2018-05-08 00:58:46 -04:00
|
|
|
|
foreach (string angle in snapAngles.Split(':', StringSplitOptions.RemoveEmptyEntries))
|
2018-05-03 01:25:49 -04:00
|
|
|
|
{
|
|
|
|
|
snapshotAngles.Add(Vector3.Parse(angle).FixIW4Angles());
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-05-08 00:58:46 -04:00
|
|
|
|
|
2018-05-03 01:25:49 -04:00
|
|
|
|
catch (FormatException)
|
|
|
|
|
{
|
|
|
|
|
Log.WriteWarning("Could not parse snapshot angles");
|
|
|
|
|
return;
|
|
|
|
|
}
|
2018-05-08 00:58:46 -04:00
|
|
|
|
|
2018-02-07 00:19:06 -05:00
|
|
|
|
var kill = new EFClientKill()
|
|
|
|
|
{
|
|
|
|
|
Active = true,
|
|
|
|
|
AttackerId = attacker.ClientId,
|
|
|
|
|
VictimId = victim.ClientId,
|
|
|
|
|
ServerId = serverId,
|
|
|
|
|
Map = ParseEnum<IW4Info.MapName>.Get(map, typeof(IW4Info.MapName)),
|
2018-04-09 23:33:42 -04:00
|
|
|
|
DeathOrigin = vDeathOrigin,
|
|
|
|
|
KillOrigin = vKillOrigin,
|
2018-02-07 00:19:06 -05:00
|
|
|
|
DeathType = ParseEnum<IW4Info.MeansOfDeath>.Get(type, typeof(IW4Info.MeansOfDeath)),
|
|
|
|
|
Damage = Int32.Parse(damage),
|
|
|
|
|
HitLoc = ParseEnum<IW4Info.HitLocation>.Get(hitLoc, typeof(IW4Info.HitLocation)),
|
2018-03-22 14:50:09 -04:00
|
|
|
|
Weapon = ParseEnum<IW4Info.WeaponName>.Get(weapon, typeof(IW4Info.WeaponName)),
|
2018-04-26 02:13:04 -04:00
|
|
|
|
ViewAngles = vViewAngles,
|
2018-03-22 14:50:09 -04:00
|
|
|
|
TimeOffset = Int64.Parse(offset),
|
2018-05-03 01:25:49 -04:00
|
|
|
|
When = time,
|
2018-03-26 00:51:25 -04:00
|
|
|
|
IsKillstreakKill = isKillstreakKill[0] != '0',
|
2018-05-03 01:25:49 -04:00
|
|
|
|
AdsPercent = float.Parse(Ads),
|
|
|
|
|
AnglesList = snapshotAngles
|
2018-02-07 00:19:06 -05:00
|
|
|
|
};
|
|
|
|
|
|
2018-03-06 02:22:19 -05:00
|
|
|
|
if (kill.DeathType == IW4Info.MeansOfDeath.MOD_SUICIDE &&
|
2018-03-13 17:30:22 -04:00
|
|
|
|
kill.Damage == 100000)
|
2018-03-06 02:22:19 -05:00
|
|
|
|
{
|
|
|
|
|
// suicide by switching teams so let's not count it against them
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2018-05-08 00:58:46 -04:00
|
|
|
|
if (!isDamage)
|
|
|
|
|
{
|
|
|
|
|
await AddStandardKill(attacker, victim);
|
|
|
|
|
}
|
2018-03-06 02:22:19 -05:00
|
|
|
|
|
2018-03-26 00:51:25 -04:00
|
|
|
|
if (kill.IsKillstreakKill)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2018-04-10 20:25:44 -04:00
|
|
|
|
var clientDetection = Servers[serverId].PlayerDetections[attacker.ClientId];
|
|
|
|
|
var clientStats = Servers[serverId].PlayerStats[attacker.ClientId];
|
2018-05-14 13:55:10 -04:00
|
|
|
|
var clientStatsSvc = statsSvc.ClientStatSvc;
|
|
|
|
|
clientStatsSvc.Update(clientStats);
|
2018-03-06 02:22:19 -05:00
|
|
|
|
|
|
|
|
|
// increment their hit count
|
|
|
|
|
if (kill.DeathType == IW4Info.MeansOfDeath.MOD_PISTOL_BULLET ||
|
2018-03-22 14:50:09 -04:00
|
|
|
|
kill.DeathType == IW4Info.MeansOfDeath.MOD_RIFLE_BULLET ||
|
|
|
|
|
kill.DeathType == IW4Info.MeansOfDeath.MOD_HEAD_SHOT)
|
2018-03-06 02:22:19 -05:00
|
|
|
|
{
|
2018-04-10 20:25:44 -04:00
|
|
|
|
clientStats.HitLocations.Single(hl => hl.Location == kill.HitLoc).HitCount += 1;
|
|
|
|
|
|
2018-05-14 13:55:10 -04:00
|
|
|
|
//statsSvc.ClientStatSvc.Update(clientStats);
|
2018-05-08 00:58:46 -04:00
|
|
|
|
// await statsSvc.ClientStatSvc.SaveChangesAsync();
|
2018-03-06 02:22:19 -05:00
|
|
|
|
}
|
2018-02-26 23:24:19 -05:00
|
|
|
|
|
2018-04-02 01:25:06 -04:00
|
|
|
|
//statsSvc.KillStatsSvc.Insert(kill);
|
|
|
|
|
//await statsSvc.KillStatsSvc.SaveChangesAsync();
|
2018-03-22 14:50:09 -04:00
|
|
|
|
|
|
|
|
|
if (Plugin.Config.Configuration().EnableAntiCheat)
|
2018-03-06 02:22:19 -05:00
|
|
|
|
{
|
2018-03-13 17:30:22 -04:00
|
|
|
|
async Task executePenalty(Cheat.DetectionPenaltyResult penalty)
|
2018-03-06 02:22:19 -05:00
|
|
|
|
{
|
2018-05-03 01:25:49 -04:00
|
|
|
|
// prevent multiple bans from occuring
|
|
|
|
|
if (attacker.Level == Player.Permission.Banned)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-13 17:30:22 -04:00
|
|
|
|
switch (penalty.ClientPenalty)
|
|
|
|
|
{
|
|
|
|
|
case Penalty.PenaltyType.Ban:
|
2018-05-11 00:52:20 -04:00
|
|
|
|
await attacker.Ban(Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_STATS_CHEAT_DETECTED"], new Player()
|
|
|
|
|
{
|
2018-05-30 21:50:20 -04:00
|
|
|
|
ClientId = 1,
|
|
|
|
|
AdministeredPenalties = new List<EFPenalty>()
|
|
|
|
|
{
|
|
|
|
|
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}",
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-05-11 00:52:20 -04:00
|
|
|
|
});
|
2018-03-13 17:30:22 -04:00
|
|
|
|
break;
|
|
|
|
|
case Penalty.PenaltyType.Flag:
|
|
|
|
|
if (attacker.Level != Player.Permission.User)
|
|
|
|
|
break;
|
2018-05-11 00:52:20 -04:00
|
|
|
|
var e = new GameEvent()
|
2018-03-13 17:30:22 -04:00
|
|
|
|
{
|
2018-05-11 00:52:20 -04:00
|
|
|
|
Data = penalty.Type == Cheat.Detection.DetectionType.Bone ?
|
|
|
|
|
$"{penalty.Type}-{(int)penalty.Location}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}" :
|
2018-05-14 13:55:10 -04:00
|
|
|
|
$"{penalty.Type}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}",
|
2018-05-11 00:52:20 -04:00
|
|
|
|
Origin = new Player()
|
|
|
|
|
{
|
|
|
|
|
ClientId = 1,
|
|
|
|
|
Level = Player.Permission.Console,
|
|
|
|
|
ClientNumber = -1,
|
|
|
|
|
CurrentServer = attacker.CurrentServer
|
|
|
|
|
},
|
|
|
|
|
Target = attacker,
|
|
|
|
|
Owner = attacker.CurrentServer,
|
|
|
|
|
Type = GameEvent.EventType.Flag
|
|
|
|
|
};
|
|
|
|
|
await new CFlag().ExecuteAsync(e);
|
2018-03-06 02:22:19 -05:00
|
|
|
|
break;
|
2018-03-13 17:30:22 -04:00
|
|
|
|
}
|
2018-03-06 02:22:19 -05:00
|
|
|
|
}
|
|
|
|
|
|
2018-05-08 00:58:46 -04:00
|
|
|
|
await executePenalty(clientDetection.ProcessKill(kill, isDamage));
|
2018-04-10 20:25:44 -04:00
|
|
|
|
await executePenalty(clientDetection.ProcessTotalRatio(clientStats));
|
2018-05-03 01:25:49 -04:00
|
|
|
|
|
2018-05-14 13:55:10 -04:00
|
|
|
|
await clientStatsSvc.SaveChangesAsync();
|
2018-03-13 17:30:22 -04:00
|
|
|
|
}
|
2018-02-07 00:19:06 -05:00
|
|
|
|
}
|
|
|
|
|
|
2018-02-10 23:33:42 -05:00
|
|
|
|
public async Task AddStandardKill(Player attacker, Player victim)
|
2018-02-07 00:19:06 -05:00
|
|
|
|
{
|
2018-02-09 02:21:25 -05:00
|
|
|
|
int serverId = attacker.CurrentServer.GetHashCode();
|
2018-03-06 02:22:19 -05:00
|
|
|
|
EFClientStatistics attackerStats = null;
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
attackerStats = Servers[serverId].PlayerStats[attacker.ClientId];
|
|
|
|
|
}
|
2018-02-10 23:33:42 -05:00
|
|
|
|
|
2018-03-06 02:22:19 -05:00
|
|
|
|
catch (KeyNotFoundException)
|
2018-02-10 23:33:42 -05:00
|
|
|
|
{
|
2018-05-03 01:25:49 -04:00
|
|
|
|
// happens when the client has disconnected before the last status update
|
|
|
|
|
Log.WriteWarning($"[Stats::AddStandardKill] kill attacker ClientId is invalid {attacker.ClientId}-{attacker}");
|
2018-02-10 23:33:42 -05:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-06 02:22:19 -05:00
|
|
|
|
EFClientStatistics victimStats = null;
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
victimStats = Servers[serverId].PlayerStats[victim.ClientId];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
catch (KeyNotFoundException)
|
|
|
|
|
{
|
2018-05-03 01:25:49 -04:00
|
|
|
|
Log.WriteWarning($"[Stats::AddStandardKill] kill victim ClientId is invalid {victim.ClientId}-{victim}");
|
2018-03-06 02:22:19 -05:00
|
|
|
|
return;
|
|
|
|
|
}
|
2018-02-08 02:23:45 -05:00
|
|
|
|
|
2018-04-28 01:22:18 -04:00
|
|
|
|
#if DEBUG
|
|
|
|
|
Log.WriteDebug("Calculating standard kill");
|
|
|
|
|
#endif
|
|
|
|
|
|
2018-02-09 02:21:25 -05:00
|
|
|
|
// update the total stats
|
|
|
|
|
Servers[serverId].ServerStatistics.TotalKills += 1;
|
|
|
|
|
|
2018-04-28 01:22:18 -04:00
|
|
|
|
// 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;
|
2018-03-06 02:22:19 -05:00
|
|
|
|
|
2018-02-09 02:21:25 -05:00
|
|
|
|
// calculate for the clients
|
2018-02-08 02:23:45 -05:00
|
|
|
|
CalculateKill(attackerStats, victimStats);
|
2018-04-26 02:13:04 -04:00
|
|
|
|
// this should fix the negative SPM
|
2018-04-28 01:22:18 -04:00
|
|
|
|
// updates their last score after being calculated
|
2018-04-26 02:13:04 -04:00
|
|
|
|
attackerStats.LastScore = attacker.Score;
|
|
|
|
|
victimStats.LastScore = victim.Score;
|
2018-02-09 02:21:25 -05:00
|
|
|
|
|
2018-02-10 23:33:42 -05:00
|
|
|
|
// show encouragement/discouragement
|
2018-02-15 23:01:28 -05:00
|
|
|
|
string streakMessage = (attackerStats.ClientId != victimStats.ClientId) ?
|
2018-03-18 22:25:11 -04:00
|
|
|
|
StreakMessage.MessageOnStreak(attackerStats.KillStreak, attackerStats.DeathStreak) :
|
|
|
|
|
StreakMessage.MessageOnStreak(-1, -1);
|
2018-02-10 23:33:42 -05:00
|
|
|
|
|
|
|
|
|
if (streakMessage != string.Empty)
|
|
|
|
|
await attacker.Tell(streakMessage);
|
2018-04-10 20:25:44 -04:00
|
|
|
|
|
2018-04-11 18:24:21 -04:00
|
|
|
|
// fixme: why?
|
2018-04-13 02:32:30 -04:00
|
|
|
|
if (double.IsNaN(victimStats.SPM) || double.IsNaN(victimStats.Skill))
|
2018-04-11 18:24:21 -04:00
|
|
|
|
{
|
2018-04-13 02:32:30 -04:00
|
|
|
|
Log.WriteDebug($"[StatManager::AddStandardKill] victim SPM/SKILL {victimStats.SPM} {victimStats.Skill}");
|
2018-04-11 18:24:21 -04:00
|
|
|
|
victimStats.SPM = 0.0;
|
|
|
|
|
victimStats.Skill = 0.0;
|
|
|
|
|
}
|
|
|
|
|
|
2018-04-13 02:32:30 -04:00
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2018-05-30 21:50:20 -04:00
|
|
|
|
// update their performance
|
|
|
|
|
#if !DEBUG
|
|
|
|
|
if ((DateTime.UtcNow - attackerStats.LastStatHistoryUpdate).TotalMinutes >= 10)
|
|
|
|
|
#endif
|
|
|
|
|
{
|
|
|
|
|
await UpdateStatHistory(attacker, attackerStats);
|
|
|
|
|
attackerStats.LastStatHistoryUpdate = DateTime.UtcNow;
|
|
|
|
|
}
|
|
|
|
|
|
2018-04-02 01:25:06 -04:00
|
|
|
|
// todo: do we want to save this immediately?
|
2018-05-14 13:55:10 -04:00
|
|
|
|
var clientStatsSvc = ContextThreads[serverId].ClientStatSvc;
|
|
|
|
|
clientStatsSvc.Update(attackerStats);
|
|
|
|
|
clientStatsSvc.Update(victimStats);
|
|
|
|
|
await clientStatsSvc.SaveChangesAsync();
|
2018-02-08 02:23:45 -05:00
|
|
|
|
}
|
|
|
|
|
|
2018-05-30 21:50:20 -04:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Update the invidual and average stat history for a client
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="client">client to update</param>
|
|
|
|
|
/// <param name="clientStats">stats of client that is being updated</param>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
private async Task UpdateStatHistory(Player client, EFClientStatistics clientStats)
|
|
|
|
|
{
|
|
|
|
|
int currentServerTotalPlaytime = clientStats.TimePlayed + (int)(DateTime.UtcNow - client.LastConnection).TotalSeconds;
|
|
|
|
|
|
|
|
|
|
using (var ctx = new DatabaseContext())
|
|
|
|
|
{
|
|
|
|
|
// select the individual history for current server
|
|
|
|
|
var iqIndividualStatHistory = from statHistory in ctx.Set<EFClientStatHistory>()
|
|
|
|
|
where statHistory.ClientId == client.ClientId
|
|
|
|
|
where statHistory.ServerId == clientStats.ServerId
|
|
|
|
|
select statHistory;
|
|
|
|
|
|
|
|
|
|
// select the average history for current client
|
|
|
|
|
var iqAverageHistory = from stat in ctx.Set<EFClientAverageStatHistory>()
|
|
|
|
|
where stat.ClientId == client.ClientId
|
|
|
|
|
select stat;
|
|
|
|
|
|
|
|
|
|
// select all stats for current client
|
|
|
|
|
var iqClientStats = from stats in ctx.Set<EFClientStatistics>()
|
|
|
|
|
where stats.ClientId == client.ClientId
|
|
|
|
|
where stats.ServerId != clientStats.ServerId
|
|
|
|
|
select new
|
|
|
|
|
{
|
|
|
|
|
stats.Performance,
|
|
|
|
|
stats.TimePlayed
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// get the client ranking for the current server
|
|
|
|
|
int individualClientRanking = await ctx.Set<EFClientStatHistory>()
|
|
|
|
|
.Where(c => c.ClientId != client.ClientId)
|
|
|
|
|
.Where(c => c.ServerId == clientStats.ServerId)
|
|
|
|
|
.Where(c => c.Ratings.OrderByDescending(r => r.RatingId).FirstOrDefault().Performance > clientStats.Performance)
|
|
|
|
|
.CountAsync() + 1;
|
|
|
|
|
|
|
|
|
|
var currentServerHistory = await iqIndividualStatHistory
|
|
|
|
|
.Include(r => r.Ratings)
|
|
|
|
|
.FirstOrDefaultAsync() ?? new EFClientStatHistory()
|
|
|
|
|
{
|
|
|
|
|
Active = true,
|
|
|
|
|
ClientId = client.ClientId,
|
|
|
|
|
Ratings = new List<EFRating>(),
|
|
|
|
|
ServerId = clientStats.ServerId
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var averageHistory = await iqAverageHistory
|
|
|
|
|
.Include(r => r.Ratings)
|
|
|
|
|
.FirstOrDefaultAsync() ?? new EFClientAverageStatHistory()
|
|
|
|
|
{
|
|
|
|
|
ClientId = client.ClientId,
|
|
|
|
|
Ratings = new List<EFRating>(),
|
|
|
|
|
Active = true,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (currentServerHistory.StatHistoryId == 0)
|
|
|
|
|
{
|
|
|
|
|
ctx.Add(currentServerHistory);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
ctx.Update(currentServerHistory);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (averageHistory.Ratings.Count == 0)
|
|
|
|
|
{
|
|
|
|
|
ctx.Add(averageHistory);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
ctx.Update(averageHistory);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (currentServerHistory.Ratings.Count > 30)
|
|
|
|
|
{
|
|
|
|
|
ctx.Entry(currentServerHistory.Ratings.First()).State = EntityState.Deleted;
|
|
|
|
|
currentServerHistory.Ratings.Remove(currentServerHistory.Ratings.First());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
currentServerHistory.Ratings.Add(new EFRating()
|
|
|
|
|
{
|
|
|
|
|
Performance = clientStats.Performance,
|
|
|
|
|
Ranking = individualClientRanking,
|
|
|
|
|
Active = true,
|
|
|
|
|
ClientId = client.ClientId,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
var clientStatsList = await iqClientStats.ToListAsync();
|
|
|
|
|
clientStatsList.Add(new
|
|
|
|
|
{
|
|
|
|
|
clientStats.Performance,
|
|
|
|
|
TimePlayed = currentServerTotalPlaytime
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// weight the performance based on play time
|
|
|
|
|
var performanceAverage = clientStatsList.Sum(p => (p.Performance * p.TimePlayed)) / clientStatsList.Sum(p => p.TimePlayed);
|
|
|
|
|
|
|
|
|
|
int overallClientRanking = await ctx.Set<EFClientAverageStatHistory>()
|
|
|
|
|
.Where(c => c.ClientId != client.ClientId)
|
|
|
|
|
.Where(c => c.Ratings.OrderByDescending(r => r.RatingId).FirstOrDefault().Performance > performanceAverage)
|
|
|
|
|
.CountAsync() + 1;
|
|
|
|
|
|
|
|
|
|
if (averageHistory.Ratings.Count > 30)
|
|
|
|
|
{
|
|
|
|
|
ctx.Entry(averageHistory.Ratings.First()).State = EntityState.Deleted;
|
|
|
|
|
averageHistory.Ratings.Remove(averageHistory.Ratings.First());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
averageHistory.Ratings.Add(new EFRating()
|
|
|
|
|
{
|
|
|
|
|
Performance = performanceAverage,
|
|
|
|
|
Ranking = overallClientRanking,
|
|
|
|
|
Active = true,
|
|
|
|
|
ClientId = client.ClientId,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await ctx.SaveChangesAsync();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-08 02:23:45 -05:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Performs the incrementation of kills and deaths for client statistics
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="attackerStats">Stats of the attacker</param>
|
|
|
|
|
/// <param name="victimStats">Stats of the victim</param>
|
|
|
|
|
public void CalculateKill(EFClientStatistics attackerStats, EFClientStatistics victimStats)
|
|
|
|
|
{
|
2018-02-10 23:33:42 -05:00
|
|
|
|
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;
|
|
|
|
|
}
|
2018-02-08 02:23:45 -05:00
|
|
|
|
|
|
|
|
|
victimStats.Deaths += 1;
|
|
|
|
|
victimStats.SessionDeaths += 1;
|
|
|
|
|
victimStats.DeathStreak += 1;
|
|
|
|
|
victimStats.KillStreak = 0;
|
|
|
|
|
|
|
|
|
|
// process the attacker's stats after the kills
|
2018-05-14 13:55:10 -04:00
|
|
|
|
attackerStats = UpdateStats(attackerStats);
|
2018-02-08 02:23:45 -05:00
|
|
|
|
|
2018-05-16 00:57:37 -04:00
|
|
|
|
// calulate elo
|
|
|
|
|
if (Servers[attackerStats.ServerId].PlayerStats.Count > 1)
|
|
|
|
|
{
|
2018-05-28 21:30:31 -04:00
|
|
|
|
/* 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)
|
2018-05-24 15:48:57 -04:00
|
|
|
|
.Where(cs => cs.Value.Team != IW4Info.Team.Spectator);
|
2018-05-16 00:57:37 -04:00
|
|
|
|
|
2018-05-28 21:30:31 -04:00
|
|
|
|
double attackerLobbyRating = validAttackerLobbyRatings.Count() > 0 ?
|
|
|
|
|
validAttackerLobbyRatings.Average(cs => cs.Value.EloRating) :
|
|
|
|
|
attackerStats.EloRating;
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
double victimLobbyRating = validVictimLobbyRatings.Count() > 0 ?
|
|
|
|
|
validVictimLobbyRatings.Average(cs => cs.Value.EloRating) :
|
|
|
|
|
victimStats.EloRating;*/
|
2018-05-24 15:48:57 -04:00
|
|
|
|
|
|
|
|
|
double attackerEloDifference = Math.Log(Math.Max(1, victimStats.EloRating)) - Math.Log(Math.Max(1, attackerStats.EloRating));
|
2018-05-20 22:35:56 -04:00
|
|
|
|
double winPercentage = 1.0 / (1 + Math.Pow(10, attackerEloDifference / Math.E));
|
2018-05-16 00:57:37 -04:00
|
|
|
|
|
2018-05-28 21:30:31 -04:00
|
|
|
|
// 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));
|
2018-05-16 00:57:37 -04:00
|
|
|
|
|
2018-05-24 15:48:57 -04:00
|
|
|
|
attackerStats.EloRating += 6.0 * (1 - winPercentage);
|
|
|
|
|
victimStats.EloRating -= 6.0 * (1 - winPercentage);
|
2018-05-16 00:57:37 -04:00
|
|
|
|
|
|
|
|
|
attackerStats.EloRating = Math.Max(0, Math.Round(attackerStats.EloRating, 2));
|
|
|
|
|
victimStats.EloRating = Math.Max(0, Math.Round(victimStats.EloRating, 2));
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-09 02:21:25 -05:00
|
|
|
|
// update after calculation
|
2018-02-10 23:33:42 -05:00
|
|
|
|
attackerStats.TimePlayed += (int)(DateTime.UtcNow - attackerStats.LastActive).TotalSeconds;
|
|
|
|
|
victimStats.TimePlayed += (int)(DateTime.UtcNow - victimStats.LastActive).TotalSeconds;
|
2018-02-09 02:21:25 -05:00
|
|
|
|
attackerStats.LastActive = DateTime.UtcNow;
|
|
|
|
|
victimStats.LastActive = DateTime.UtcNow;
|
2018-02-08 02:23:45 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Update the client stats (skill etc)
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="clientStats">Client statistics</param>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
private EFClientStatistics UpdateStats(EFClientStatistics clientStats)
|
|
|
|
|
{
|
2018-02-09 02:21:25 -05:00
|
|
|
|
// prevent NaN or inactive time lowering SPM
|
2018-04-13 02:32:30 -04:00
|
|
|
|
if ((DateTime.UtcNow - clientStats.LastStatCalculation).TotalSeconds / 60.0 < 0.01 ||
|
2018-04-26 20:19:42 -04:00
|
|
|
|
(DateTime.UtcNow - clientStats.LastActive).TotalSeconds / 60.0 > 3 ||
|
2018-04-28 01:22:18 -04:00
|
|
|
|
clientStats.SessionScore == 0)
|
|
|
|
|
{
|
|
|
|
|
// prevents idle time counting
|
|
|
|
|
clientStats.LastStatCalculation = DateTime.UtcNow;
|
2018-02-09 02:21:25 -05:00
|
|
|
|
return clientStats;
|
2018-04-28 01:22:18 -04:00
|
|
|
|
}
|
2018-02-08 02:23:45 -05:00
|
|
|
|
|
2018-04-10 20:25:44 -04:00
|
|
|
|
double timeSinceLastCalc = (DateTime.UtcNow - clientStats.LastStatCalculation).TotalSeconds / 60.0;
|
|
|
|
|
double timeSinceLastActive = (DateTime.UtcNow - clientStats.LastActive).TotalSeconds / 60.0;
|
|
|
|
|
|
2018-05-08 00:58:46 -04:00
|
|
|
|
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;
|
|
|
|
|
}
|
2018-05-24 15:48:57 -04:00
|
|
|
|
|
2018-05-10 01:34:29 -04:00
|
|
|
|
else if (clientStats.RoundScore > 0 && clientStats.LastScore < clientStats.RoundScore)
|
2018-05-08 00:58:46 -04:00
|
|
|
|
{
|
|
|
|
|
scoreDifference = clientStats.RoundScore - clientStats.LastScore;
|
|
|
|
|
}
|
2018-05-03 01:25:49 -04:00
|
|
|
|
|
2018-04-10 20:25:44 -04:00
|
|
|
|
double killSPM = scoreDifference / timeSinceLastCalc;
|
2018-05-24 15:48:57 -04:00
|
|
|
|
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);
|
2018-02-08 02:23:45 -05:00
|
|
|
|
|
2018-02-09 02:21:25 -05:00
|
|
|
|
// calculate how much the KDR should weigh
|
|
|
|
|
// 1.637 is a Eddie-Generated number that weights the KDR nicely
|
2018-05-17 19:31:58 -04:00
|
|
|
|
double currentKDR = clientStats.SessionDeaths == 0 ? clientStats.SessionKills : clientStats.SessionKills / clientStats.SessionDeaths;
|
|
|
|
|
double alpha = Math.Sqrt(2) / Math.Min(600, clientStats.Kills + clientStats.Deaths);
|
2018-05-20 22:35:56 -04:00
|
|
|
|
clientStats.RollingWeightedKDR = (alpha * currentKDR) + (1.0 - alpha) * clientStats.KDR;
|
2018-05-17 19:31:58 -04:00
|
|
|
|
double KDRWeight = Math.Round(Math.Pow(clientStats.RollingWeightedKDR, 1.637 / Math.E), 3);
|
2018-02-08 02:23:45 -05:00
|
|
|
|
|
2018-02-09 02:21:25 -05:00
|
|
|
|
// calculate the weight of the new play time against last 10 hours of gameplay
|
2018-02-15 23:01:28 -05:00
|
|
|
|
int totalPlayTime = (clientStats.TimePlayed == 0) ?
|
|
|
|
|
(int)(DateTime.UtcNow - clientStats.LastActive).TotalSeconds :
|
|
|
|
|
clientStats.TimePlayed + (int)(DateTime.UtcNow - clientStats.LastActive).TotalSeconds;
|
2018-02-08 02:23:45 -05:00
|
|
|
|
|
2018-02-15 23:01:28 -05:00
|
|
|
|
double SPMAgainstPlayWeight = timeSinceLastCalc / Math.Min(600, (totalPlayTime / 60.0));
|
2018-02-08 02:23:45 -05:00
|
|
|
|
|
2018-02-09 02:21:25 -05:00
|
|
|
|
// calculate the new weight against average times the weight against play time
|
|
|
|
|
clientStats.SPM = (killSPM * SPMAgainstPlayWeight) + (clientStats.SPM * (1 - SPMAgainstPlayWeight));
|
2018-04-29 16:44:04 -04:00
|
|
|
|
|
|
|
|
|
if (clientStats.SPM < 0)
|
|
|
|
|
{
|
|
|
|
|
Log.WriteWarning("[StatManager:UpdateStats] clientStats SPM < 0");
|
|
|
|
|
Log.WriteDebug($"{scoreDifference}-{clientStats.RoundScore} - {clientStats.LastScore} - {clientStats.SessionScore}");
|
|
|
|
|
clientStats.SPM = 0;
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-09 02:21:25 -05:00
|
|
|
|
clientStats.SPM = Math.Round(clientStats.SPM, 3);
|
|
|
|
|
clientStats.Skill = Math.Round((clientStats.SPM * KDRWeight), 3);
|
2018-02-08 02:23:45 -05:00
|
|
|
|
|
2018-04-11 18:24:21 -04:00
|
|
|
|
// fixme: how does this happen?
|
2018-04-13 02:32:30 -04:00
|
|
|
|
if (double.IsNaN(clientStats.SPM) || double.IsNaN(clientStats.Skill))
|
2018-04-11 18:24:21 -04:00
|
|
|
|
{
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-09 02:21:25 -05:00
|
|
|
|
clientStats.LastStatCalculation = DateTime.UtcNow;
|
2018-04-26 02:13:04 -04:00
|
|
|
|
//clientStats.LastScore = clientStats.SessionScore;
|
2018-02-08 02:23:45 -05:00
|
|
|
|
|
|
|
|
|
return clientStats;
|
2018-02-07 00:19:06 -05:00
|
|
|
|
}
|
2018-02-09 02:21:25 -05:00
|
|
|
|
|
|
|
|
|
public void InitializeServerStats(Server sv)
|
|
|
|
|
{
|
|
|
|
|
int serverId = sv.GetHashCode();
|
2018-02-10 01:26:38 -05:00
|
|
|
|
var statsSvc = ContextThreads[serverId];
|
|
|
|
|
|
|
|
|
|
var serverStats = statsSvc.ServerStatsSvc.Find(s => s.ServerId == serverId).FirstOrDefault();
|
2018-02-09 02:21:25 -05:00
|
|
|
|
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,
|
|
|
|
|
};
|
|
|
|
|
|
2018-02-10 01:26:38 -05:00
|
|
|
|
var ieClientStats = statsSvc.ClientStatSvc.Find(cs => cs.ServerId == serverId);
|
2018-02-09 02:21:25 -05:00
|
|
|
|
|
2018-05-08 00:58:46 -04:00
|
|
|
|
// set these incase we've imported settings
|
2018-02-09 02:21:25 -05:00
|
|
|
|
serverStats.TotalKills = ieClientStats.Sum(cs => cs.Kills);
|
|
|
|
|
serverStats.TotalPlayTime = Manager.GetClientService().GetTotalPlayTime().Result;
|
|
|
|
|
|
2018-02-10 01:26:38 -05:00
|
|
|
|
statsSvc.ServerStatsSvc.Insert(serverStats);
|
2018-02-09 02:21:25 -05:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-06 02:22:19 -05:00
|
|
|
|
public void ResetKillstreaks(int serverId)
|
|
|
|
|
{
|
|
|
|
|
var serverStats = Servers[serverId];
|
|
|
|
|
foreach (var stat in serverStats.PlayerStats.Values)
|
2018-04-28 01:22:18 -04:00
|
|
|
|
stat.StartNewSession();
|
2018-03-06 02:22:19 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
2018-04-28 01:22:18 -04:00
|
|
|
|
stats.TimePlayed = 0;
|
2018-05-17 19:31:58 -04:00
|
|
|
|
stats.EloRating = 200;
|
2018-03-06 02:22:19 -05:00
|
|
|
|
}
|
|
|
|
|
|
2018-02-10 01:26:38 -05:00
|
|
|
|
public async Task AddMessageAsync(int clientId, int serverId, string message)
|
2018-02-09 02:21:25 -05:00
|
|
|
|
{
|
2018-02-11 20:17:20 -05:00
|
|
|
|
// the web users can have no account
|
|
|
|
|
if (clientId < 1)
|
|
|
|
|
return;
|
|
|
|
|
|
2018-02-10 01:26:38 -05:00
|
|
|
|
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];
|
|
|
|
|
|
2018-05-08 00:58:46 -04:00
|
|
|
|
Log.WriteDebug("Syncing stats contexts");
|
2018-02-10 01:26:38 -05:00
|
|
|
|
await statsSvc.ServerStatsSvc.SaveChangesAsync();
|
2018-05-14 13:55:10 -04:00
|
|
|
|
//await statsSvc.ClientStatSvc.SaveChangesAsync();
|
2018-02-10 01:26:38 -05:00
|
|
|
|
await statsSvc.KillStatsSvc.SaveChangesAsync();
|
|
|
|
|
await statsSvc.ServerSvc.SaveChangesAsync();
|
2018-05-04 00:22:10 -04:00
|
|
|
|
|
|
|
|
|
statsSvc = null;
|
|
|
|
|
// this should prevent the gunk for having a long lasting context.
|
|
|
|
|
ContextThreads[serverId] = new ThreadSafeStatsService();
|
2018-02-09 02:21:25 -05:00
|
|
|
|
}
|
2018-05-24 15:48:57 -04:00
|
|
|
|
|
|
|
|
|
public void SetTeamBased(int serverId, bool isTeamBased)
|
|
|
|
|
{
|
|
|
|
|
Servers[serverId].IsTeamBased = isTeamBased;
|
|
|
|
|
}
|
2018-02-07 00:19:06 -05:00
|
|
|
|
}
|
|
|
|
|
}
|