diff --git a/Application/GameEventHandler.cs b/Application/GameEventHandler.cs index 74730ddae..cebbd5926 100644 --- a/Application/GameEventHandler.cs +++ b/Application/GameEventHandler.cs @@ -9,14 +9,17 @@ namespace IW4MAdmin.Application { class GameEventHandler : IEventHandler { - private readonly IManager Manager; static long NextEventId = 1; - private readonly SortedList OutOfOrderEvents; + readonly IManager Manager; + readonly SortedList OutOfOrderEvents; + readonly SemaphoreSlim IsProcessingEvent; public GameEventHandler(IManager mgr) { Manager = mgr; OutOfOrderEvents = new SortedList(); + IsProcessingEvent = new SemaphoreSlim(0); + IsProcessingEvent.Release(); } public void AddEvent(GameEvent gameEvent) @@ -45,11 +48,40 @@ 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"); -#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)) + //{ + // if( !gameEvent.OnProcessed.Wait(30 * 1000)) + // { + // Manager.GetLogger().WriteError($"{Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_EVENT_TIMEOUT"]} [{gameEvent.Id}, {gameEvent.Type}]"); + // } + //} Interlocked.Increment(ref NextEventId); + //#if DEBUG == true + // gameEvent.OnProcessed.Wait(); + //#else + // if (GameEvent.IsEventTimeSensitive(gameEvent) && + // !gameEvent.OnProcessed.Wait(30 * 1000)) + // { + // Manager.GetLogger().WriteError($"{Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_EVENT_TIMEOUT"]} [{gameEvent.Id}, {gameEvent.Type}]"); + // } + //#endif + // Interlocked.Increment(ref NextEventId); + // if (GameEvent.IsEventTimeSensitive(gameEvent)) + // { + // IsProcessingEvent.Release(); + // } } // a "newer" event has been added before and "older" one has been added (due to threads and context switching) diff --git a/Application/Manager.cs b/Application/Manager.cs index 8e7b77abc..dfb188c6e 100644 --- a/Application/Manager.cs +++ b/Application/Manager.cs @@ -88,6 +88,7 @@ namespace IW4MAdmin.Application // offload it to the player to keep newEvent.Origin.DelayedEvents.Enqueue(newEvent); + newEvent.OnProcessed.Set(); return; } @@ -97,6 +98,7 @@ namespace IW4MAdmin.Application Logger.WriteDebug($"Delaying target execution of event type {newEvent.Type} for {newEvent.Target} because they are not authed"); // offload it to the player to keep newEvent.Target.DelayedEvents.Enqueue(newEvent); + newEvent.OnProcessed.Set(); return; } @@ -120,7 +122,6 @@ namespace IW4MAdmin.Application Owner = oldEvent.Owner, Message = oldEvent.Message, Target = oldEvent.Target, - OnProcessed = oldEvent.OnProcessed, Remote = oldEvent.Remote }; diff --git a/Application/Server.cs b/Application/Server.cs index 34b1b4c64..7cd9d7d84 100644 --- a/Application/Server.cs +++ b/Application/Server.cs @@ -257,8 +257,7 @@ namespace IW4MAdmin if (cNum >= 0 && Players[cNum] != null) { Player Leaving = Players[cNum]; - Logger.WriteInfo($"Client {Leaving}, state {Leaving.State.ToString()} disconnecting..."); - + // occurs when the player disconnects via log before being authenticated by RCon if (Leaving.State != Player.ClientState.Connected) { @@ -267,8 +266,9 @@ namespace IW4MAdmin else { + Logger.WriteInfo($"Client {Leaving} [{Leaving.State.ToString().ToLower()}] disconnecting..."); Leaving.State = Player.ClientState.Disconnecting; - Leaving.TotalConnectionTime += (int)(DateTime.UtcNow - Leaving.ConnectionTime).TotalSeconds; + Leaving.TotalConnectionTime += Leaving.ConnectionLength; Leaving.LastConnection = DateTime.UtcNow; await Manager.GetClientService().Update(Leaving); Players[cNum] = null; @@ -383,27 +383,29 @@ namespace IW4MAdmin else if (E.Type == GameEvent.EventType.Quit) { - //var origin = Players.FirstOrDefault(p => p != null && p.NetworkId == E.Origin.NetworkId); + var origin = Players.FirstOrDefault(p => p != null && p.NetworkId == E.Origin.NetworkId); - //if (origin != null && - // // we only want to forward the event if they are connected. - // origin.State == Player.ClientState.Connected) - //{ - // var e = new GameEvent() - // { - // Type = GameEvent.EventType.Disconnect, - // Origin = origin, - // Owner = this - // }; + if (origin != null && + // we only want to forward the event if they are connected. + origin.State == Player.ClientState.Connected && + // make sure we don't get the disconnect event from every time the game ends + origin.ConnectionLength < Manager.GetApplicationSettings().Configuration().RConPollRate) + { + var e = new GameEvent() + { + Type = GameEvent.EventType.Disconnect, + Origin = origin, + Owner = this + }; - // Manager.GetEventHandler().AddEvent(e); - //} + Manager.GetEventHandler().AddEvent(e); + } - //else if (origin != null && - // origin.State != Player.ClientState.Connected) - //{ - // await RemovePlayer(origin.ClientNumber); - //} + else if (origin != null && + origin.State != Player.ClientState.Connected) + { + await RemovePlayer(origin.ClientNumber); + } } else if (E.Type == GameEvent.EventType.Disconnect) @@ -781,12 +783,12 @@ namespace IW4MAdmin Logger.WriteWarning("Game log file not properly initialized, restarting map..."); await this.ExecuteCommandAsync("map_restart"); logfile = await this.GetDvarAsync("g_log"); - } + } //CustomCallback = await ScriptLoaded(); string mainPath = EventParser.GetGameDir(); #if DEBUG - basepath.Value = @"D:\"; + // basepath.Value = @"D:\"; #endif string logPath = string.Empty; diff --git a/GameLogServer/GameLogServer/log_resource.py b/GameLogServer/GameLogServer/log_resource.py index 8241d0819..e7b2fc7de 100644 --- a/GameLogServer/GameLogServer/log_resource.py +++ b/GameLogServer/GameLogServer/log_resource.py @@ -7,7 +7,7 @@ class LogResource(Resource): path = urlsafe_b64decode(path).decode('utf-8') log_info = reader.read_file(path) - if not log_info: + if log_info is False: print('could not read log file ' + path) return { diff --git a/Plugins/ProfanityDeterment/Plugin.cs b/Plugins/ProfanityDeterment/Plugin.cs index 3459544c3..8a0ab4bdd 100644 --- a/Plugins/ProfanityDeterment/Plugin.cs +++ b/Plugins/ProfanityDeterment/Plugin.cs @@ -42,7 +42,7 @@ namespace IW4MAdmin.Plugins.ProfanityDeterment { foreach (string word in objectionalWords) { - containsObjectionalWord |= Regex.IsMatch(E.Origin.Name.ToLower(), word); + containsObjectionalWord |= Regex.IsMatch(E.Origin.Name.ToLower(), word, RegexOptions.IgnoreCase); } } @@ -70,7 +70,7 @@ namespace IW4MAdmin.Plugins.ProfanityDeterment foreach (string word in objectionalWords) { - containsObjectionalWord |= Regex.IsMatch(E.Origin.Name.ToLower(), word); + containsObjectionalWord |= Regex.IsMatch(E.Origin.Name.ToLower(), word, RegexOptions.IgnoreCase); // break out early because there's at least one objectional word if (containsObjectionalWord) diff --git a/Plugins/Stats/Helpers/StatManager.cs b/Plugins/Stats/Helpers/StatManager.cs index 308c0c1fb..a9b6ec318 100644 --- a/Plugins/Stats/Helpers/StatManager.cs +++ b/Plugins/Stats/Helpers/StatManager.cs @@ -16,6 +16,7 @@ using SharedLibraryCore.Database; using Microsoft.EntityFrameworkCore; using SharedLibraryCore.Database.Models; using SharedLibraryCore.Services; +using System.Linq.Expressions; namespace IW4MAdmin.Plugins.Stats.Helpers { @@ -36,6 +37,17 @@ namespace IW4MAdmin.Plugins.Stats.Helpers public EFClientStatistics GetClientStats(int clientId, int serverId) => Servers[serverId].PlayerStats[clientId]; + public Expression> GetRankingFunc(int? serverId = null) + { + var fifteenDaysAgo = DateTime.UtcNow.AddDays(-15); + return (r) => r.ServerId == serverId && + r.RatingHistory.Client.LastConnection > fifteenDaysAgo && + r.RatingHistory.Client.Level != Player.Permission.Banned && + r.Newest && + r.ActivityAmount >= Plugin.Config.Configuration().TopPlayersMinPlayTime; + } + + /// /// gets a ranking across all servers for given client id /// @@ -52,17 +64,11 @@ namespace IW4MAdmin.Plugins.Stats.Helpers .Select(r => r.Performance) .FirstOrDefaultAsync(); - var fifteenDaysAgo = DateTime.UtcNow.AddDays(-15); - var iqClientRating = (from rating in context.Set() - where rating.RatingHistory.Client.ClientId != clientId - where rating.ServerId == null - where rating.RatingHistory.Client.LastConnection > fifteenDaysAgo - where rating.RatingHistory.Client.Level != Player.Permission.Banned - where rating.Newest - where rating.ActivityAmount >= Plugin.Config.Configuration().TopPlayersMinPlayTime - where rating.Performance > clientPerformance - select rating.Ranking); - return await iqClientRating.CountAsync() + 1; + var iqClientRanking = context.Set() + .Where(r => r.RatingHistory.ClientId == clientId) + .Where(GetRankingFunc()); + + return await iqClientRanking.CountAsync() + 1; } } @@ -70,28 +76,26 @@ namespace IW4MAdmin.Plugins.Stats.Helpers { using (var context = new DatabaseContext(true)) { - var fifteenDaysAgo = DateTime.UtcNow.AddDays(-15); + // setup the query for the clients within the given rating range var iqClientRatings = (from rating in context.Set() - where rating.ServerId == null - where rating.RatingHistory.Client.LastConnection > fifteenDaysAgo - where rating.RatingHistory.Client.Level != Player.Permission.Banned - where rating.Newest - where rating.ActivityAmount >= Plugin.Config.Configuration().TopPlayersMinPlayTime - orderby rating.Performance descending + .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.RatingHistory.Client.TotalConnectionTime, rating.Performance, - }) - .Skip(start) - .Take(count); - + }).OrderByDescending(c => c.Performance) + .Skip(start) + .Take(count); +#if DEBUG == true + var clientRatingsSql = iqClientRatings.ToSql(); +#endif + // materialized list var clientRatings = await iqClientRatings.ToListAsync(); + // get all the client ids that var clientIds = clientRatings .GroupBy(r => r.ClientId) .Select(r => r.First().ClientId) @@ -99,23 +103,19 @@ namespace IW4MAdmin.Plugins.Stats.Helpers var iqStatsInfo = (from stat in context.Set() where clientIds.Contains(stat.ClientId) + group stat by stat.ClientId into s select new { - stat.ClientId, - stat.Kills, - stat.Deaths, - stat.TimePlayed, + ClientId = s.Key, + Kills = s.Sum(c => c.Kills), + Deaths = s.Sum(c => c.Deaths), + 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) }); - - var statList = await iqStatsInfo.ToListAsync(); - var topPlayers = statList.GroupBy(s => s.ClientId) - .Select(s => new - { - s.First().ClientId, - Kills = s.Sum(c => c.Kills), - Deaths = s.Sum(c => c.Deaths), - KDR = s.Sum(c => (c.Kills / (double)(c.Deaths == 0 ? 1 : c.Deaths)) * c.TimePlayed) / s.Sum(c => c.TimePlayed) - }); +#if DEBUG == true + var statsInfoSql = iqStatsInfo.ToSql(); +#endif + var topPlayers = await iqStatsInfo.ToListAsync(); var clientRatingsDict = clientRatings.ToDictionary(r => r.ClientId); var finished = topPlayers.Select(s => new TopStatsInfo() @@ -131,7 +131,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers 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(clientRatingsDict[s.ClientId].TotalConnectionTime / 3600.0, 1).ToString("#,##0"), + TimePlayed = Math.Round(s.TotalTimePlayed / 3600.0, 1).ToString("#,##0"), }) .OrderByDescending(r => r.Performance) .ToList(); @@ -316,6 +316,10 @@ namespace IW4MAdmin.Plugins.Stats.Helpers // 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); @@ -622,7 +626,15 @@ namespace IW4MAdmin.Plugins.Stats.Helpers /// private async Task UpdateStatHistory(Player client, EFClientStatistics clientStats) { - int currentServerTotalPlaytime = clientStats.TimePlayed + (int)(DateTime.UtcNow - client.LastConnection).TotalSeconds; + int currentSessionTime = (int)(DateTime.UtcNow - client.LastConnection).TotalSeconds; + + // don't update their stat history if they haven't played long + if (currentSessionTime < 60) + { + return; + } + + int currentServerTotalPlaytime = clientStats.TimePlayed + currentSessionTime; using (var ctx = new DatabaseContext()) { @@ -651,25 +663,12 @@ namespace IW4MAdmin.Plugins.Stats.Helpers Ratings = new List() }; - if (clientHistory.RatingHistoryId == 0) - { - ctx.Add(clientHistory); - } - - else - { - ctx.Update(clientHistory); - } - + #region INDIVIDUAL_SERVER_PERFORMANCE var fifteenDaysAgo = DateTime.UtcNow.AddDays(-15); // get the client ranking for the current server int individualClientRanking = await ctx.Set() - .Where(c => c.ServerId == clientStats.ServerId) - .Where(r => r.RatingHistory.Client.LastConnection > fifteenDaysAgo) - .Where(r => r.RatingHistory.Client.Level != Player.Permission.Banned) - .Where(r => r.ActivityAmount > Plugin.Config.Configuration().TopPlayersMinPlayTime) + .Where(GetRankingFunc(clientStats.ServerId)) .Where(c => c.RatingHistory.ClientId != client.ClientId) - .Where(r => r.Newest) .Where(c => c.Performance > clientStats.Performance) .CountAsync() + 1; @@ -682,10 +681,12 @@ namespace IW4MAdmin.Plugins.Stats.Helpers } // set the previous newest to false - var ratingToUnsetNewest = clientHistory.Ratings.LastOrDefault(r => r.ServerId == clientStats.ServerId); + var ratingToUnsetNewest = clientHistory.Ratings.LastOrDefault(r => r.ServerId == clientStats.ServerId && r.Newest); + if (ratingToUnsetNewest != null) { ctx.Entry(ratingToUnsetNewest).State = EntityState.Modified; + ctx.Entry(ratingToUnsetNewest).Property(p => p.Newest).IsModified = true; ratingToUnsetNewest.Newest = false; } @@ -698,9 +699,11 @@ namespace IW4MAdmin.Plugins.Stats.Helpers Newest = true, ServerId = clientStats.ServerId, RatingHistoryId = clientHistory.RatingHistoryId, - ActivityAmount = currentServerTotalPlaytime + ActivityAmount = currentServerTotalPlaytime, }); + #endregion + #region OVERALL_RATING // get other server stats var clientStatsList = await iqClientStats.ToListAsync(); @@ -719,28 +722,27 @@ namespace IW4MAdmin.Plugins.Stats.Helpers } int overallClientRanking = await ctx.Set() - .Where(r => r.ServerId == null) + .Where(GetRankingFunc()) .Where(r => r.RatingHistory.ClientId != client.ClientId) - .Where(r => r.RatingHistory.Client.LastConnection > fifteenDaysAgo) - .Where(r => r.RatingHistory.Client.Level != Player.Permission.Banned) - .Where(r => r.ActivityAmount > Plugin.Config.Configuration().TopPlayersMinPlayTime) - .Where(r => r.Newest) - .Where(r => r.Performance > performanceAverage) - .CountAsync() + 1; + .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.First(r => r.ServerId == null); + ctx.Attach(ratingToRemove); ctx.Entry(ratingToRemove).State = EntityState.Deleted; clientHistory.Ratings.Remove(ratingToRemove); } // set the previous average newest to false - ratingToUnsetNewest = clientHistory.Ratings.LastOrDefault(r => r.ServerId == null); + ratingToUnsetNewest = clientHistory.Ratings.LastOrDefault(r => r.ServerId == null && r.Newest); if (ratingToUnsetNewest != null) { + ctx.Attach(ratingToUnsetNewest); ctx.Entry(ratingToUnsetNewest).State = EntityState.Modified; + ctx.Entry(ratingToUnsetNewest).Property(p => p.Newest).IsModified = true; ratingToUnsetNewest.Newest = false; } @@ -755,16 +757,19 @@ namespace IW4MAdmin.Plugins.Stats.Helpers RatingHistoryId = clientHistory.RatingHistoryId, ActivityAmount = clientStatsList.Sum(s => s.TimePlayed) }); + #endregion - try + if (clientHistory.RatingHistoryId == 0) { - await ctx.SaveChangesAsync(); + ctx.Add(clientHistory); } - // this can happen when the client disconnects without any stat changes - catch (DbUpdateConcurrencyException) - { + else + { + ctx.Update(clientHistory); } + + await ctx.SaveChangesAsync(); } } diff --git a/Plugins/Stats/Models/EFClientStatistics.cs b/Plugins/Stats/Models/EFClientStatistics.cs index d0eab57f6..0502a7160 100644 --- a/Plugins/Stats/Models/EFClientStatistics.cs +++ b/Plugins/Stats/Models/EFClientStatistics.cs @@ -25,6 +25,7 @@ namespace IW4MAdmin.Plugins.Stats.Models public double EloRating { get; set; } public virtual ICollection HitLocations { get; set; } public double RollingWeightedKDR { get; set; } + public double VisionAverage { get; set; } [NotMapped] public double Performance { diff --git a/Plugins/Stats/Models/EFRating.cs b/Plugins/Stats/Models/EFRating.cs index cc0fd9dae..b9a77a2fc 100644 --- a/Plugins/Stats/Models/EFRating.cs +++ b/Plugins/Stats/Models/EFRating.cs @@ -1,9 +1,7 @@ using SharedLibraryCore.Database.Models; using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using System.Text; namespace IW4MAdmin.Plugins.Stats.Models { @@ -27,5 +25,7 @@ namespace IW4MAdmin.Plugins.Stats.Models public bool Newest { get; set; } [Required] public int ActivityAmount { get; set; } + [Required] + public DateTime When { get; set; } = DateTime.UtcNow; } } diff --git a/Plugins/Stats/Web/Controllers/StatsController.cs b/Plugins/Stats/Web/Controllers/StatsController.cs index 7d4b3f3f6..02896e5b0 100644 --- a/Plugins/Stats/Web/Controllers/StatsController.cs +++ b/Plugins/Stats/Web/Controllers/StatsController.cs @@ -1,15 +1,9 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Query; -using Microsoft.EntityFrameworkCore.Query.Internal; -using Microsoft.EntityFrameworkCore.Storage; using SharedLibraryCore; using System; -using System.Collections.Generic; using System.Linq; -using System.Reflection; -using System.Text; using System.Threading.Tasks; using WebfrontCore.Controllers; @@ -23,7 +17,7 @@ namespace IW4MAdmin.Plugins.Stats.Web.Controllers ViewBag.Title = Utilities.CurrentLocalization.LocalizationIndex.Set["WEBFRONT_STATS_INDEX_TITLE"]; ViewBag.Description = Utilities.CurrentLocalization.LocalizationIndex.Set["WEBFRONT_STATS_INDEX_DESC"]; - return View("Index", await Plugin.Manager.GetTopStats(0, 10)); + return View("Index", await Plugin.Manager.GetTopStats(0, 50)); } [HttpGet] @@ -78,34 +72,4 @@ namespace IW4MAdmin.Plugins.Stats.Web.Controllers } } } - -#if DEBUG == true - public static class IQueryableExtensions - { - private static readonly TypeInfo QueryCompilerTypeInfo = typeof(QueryCompiler).GetTypeInfo(); - - private static readonly FieldInfo QueryCompilerField = typeof(EntityQueryProvider).GetTypeInfo().DeclaredFields.First(x => x.Name == "_queryCompiler"); - - private static readonly FieldInfo QueryModelGeneratorField = QueryCompilerTypeInfo.DeclaredFields.First(x => x.Name == "_queryModelGenerator"); - - private static readonly FieldInfo DataBaseField = QueryCompilerTypeInfo.DeclaredFields.Single(x => x.Name == "_database"); - - private static readonly PropertyInfo DatabaseDependenciesField = typeof(Database).GetTypeInfo().DeclaredProperties.Single(x => x.Name == "Dependencies"); - - public static string ToSql(this IQueryable query) where TEntity : class - { - var queryCompiler = (QueryCompiler)QueryCompilerField.GetValue(query.Provider); - var modelGenerator = (QueryModelGenerator)QueryModelGeneratorField.GetValue(queryCompiler); - var queryModel = modelGenerator.ParseQuery(query.Expression); - var database = (IDatabase)DataBaseField.GetValue(queryCompiler); - var databaseDependencies = (DatabaseDependencies)DatabaseDependenciesField.GetValue(database); - var queryCompilationContext = databaseDependencies.QueryCompilationContextFactory.Create(false); - var modelVisitor = (RelationalQueryModelVisitor)queryCompilationContext.CreateQueryModelVisitor(); - modelVisitor.CreateQueryExecutor(queryModel); - var sql = modelVisitor.Queries.First().ToString(); - - return sql; - } - } -#endif } diff --git a/Plugins/Stats/Web/Views/Stats/Index.cshtml b/Plugins/Stats/Web/Views/Stats/Index.cshtml index 099e0fa4b..2537882ea 100644 --- a/Plugins/Stats/Web/Views/Stats/Index.cshtml +++ b/Plugins/Stats/Web/Views/Stats/Index.cshtml @@ -10,5 +10,5 @@ - + } diff --git a/Plugins/Tests/ManagerTests.cs b/Plugins/Tests/ManagerTests.cs index 4b4d97cae..4e8f447eb 100644 --- a/Plugins/Tests/ManagerTests.cs +++ b/Plugins/Tests/ManagerTests.cs @@ -43,7 +43,7 @@ namespace Tests public void AddAndRemoveClientsViaJoinShouldSucceed() { var server = Manager.GetServers().First(); - var waiters = new Queue(); + var waiters = new Queue(); int clientStartIndex = 4; int clientNum = 10; @@ -63,12 +63,12 @@ namespace Tests }; server.Manager.GetEventHandler().AddEvent(e); - waiters.Enqueue(e.OnProcessed); + waiters.Enqueue(e); } while (waiters.Count > 0) { - waiters.Dequeue().Wait(); + waiters.Dequeue().OnProcessed.Wait(); } Assert.True(server.ClientNum == clientNum, $"client num does not match added client num [{server.ClientNum}:{clientNum}]"); @@ -88,12 +88,12 @@ namespace Tests }; server.Manager.GetEventHandler().AddEvent(e); - waiters.Enqueue(e.OnProcessed); + waiters.Enqueue(e); } while (waiters.Count > 0) { - waiters.Dequeue().Wait(); + waiters.Dequeue().OnProcessed.Wait(); } Assert.True(server.ClientNum == 0, "there are still clients connected"); @@ -103,7 +103,7 @@ namespace Tests public void AddAndRemoveClientsViaRconShouldSucceed() { var server = Manager.GetServers().First(); - var waiters = new Queue(); + var waiters = new Queue(); int clientIndexStart = 1; int clientNum = 8; @@ -126,12 +126,12 @@ namespace Tests }; Manager.GetEventHandler().AddEvent(e); - waiters.Enqueue(e.OnProcessed); + waiters.Enqueue(e); } while (waiters.Count > 0) { - waiters.Dequeue().Wait(); + waiters.Dequeue().OnProcessed.Wait(); } int actualClientNum = server.GetPlayersAsList().Count(p => p.State == Player.ClientState.Connected); @@ -155,12 +155,12 @@ namespace Tests }; Manager.GetEventHandler().AddEvent(e); - waiters.Enqueue(e.OnProcessed); + waiters.Enqueue(e); } while (waiters.Count > 0) { - waiters.Dequeue().Wait(); + waiters.Dequeue().OnProcessed.Wait(); } actualClientNum = server.ClientNum; diff --git a/README.md b/README.md index 6d5d79f47..c1799e96d 100644 --- a/README.md +++ b/README.md @@ -5,14 +5,26 @@ _______ ### About **IW4MAdmin** is an administration tool for [IW4x](https://iw4xcachep26muba.onion.link/), [Pluto T6](https://forum.plutonium.pw/category/33/plutonium-t6), [Pluto IW5](https://forum.plutonium.pw/category/5/plutonium-iw5), and most Call of Duty® dedicated servers. It allows complete control of your server; from changing maps, to banning players, **IW4MAdmin** monitors and records activity on your server(s). With plugin support, extending its functionality is a breeze. ### Download -Latest binary builds are always available at https://raidmax.org/IW4MAdmin +Latest binary builds are always available at https://raidmax.org/IW4MAdmin + +--- ### Setup -**IW4MAdmin** requires minimal configuration to run. There is only one prerequisite. +**IW4MAdmin** requires minimal effort to get up and running. +#### Prerequisites * [.NET Core 2.1 Runtime](https://www.microsoft.com/net/download) *or newer* -1. Extract `IW4MAdmin-.zip` -2. Run `StartIW4MAdmin.cmd` +#### Installation +1. Install .NET Core Runtime +2. Extract `IW4MAdmin-.zip` +#### Launching +1. Run `StartIW4MAdmin.cmd` (Windows) +2. Run `StartIW4MAdmin.sh` (Linux) +2. Configure **IW4MAdmin** +### Updating +1. Extract newer version of **IW4MAdmin** into pre-existing **IW4MAdmin** folder and overwrite existing files +- _Your configuration and database will be saved_ +--- ### Help -Feel free to join the **IW4MAdmin** [Discord](https://discord.gg/ZZFK5p3) +Feel free to join the **IW4MAdmin** [Discord](https://discord.gg/ZZFK5p3) If you come across an issue, bug, or feature request please post an [issue](https://github.com/RaidMax/IW4M-Admin/issues) ___ @@ -21,45 +33,58 @@ ___ When **IW4MAdmin** is launched for the _first time_, you will be prompted to setup your configuration. `Enable webfront` -* Enables you to monitor and control your server(s) through a web interface [defaults to `http://127.0.0.1:1624`] +* Enables you to monitor and control your server(s) through a web interface +* Default — `http://0.0.0.0:1624` `Enable multiple owners` * Enables more than one client to be promoted to level of `Owner` +* Default — `false` `Enable stepped privilege hierarchy` * Allows privileged clients to promote other clients to the level below their current level +* Default — `false` `Enable custom say name` * Shows a prefix to every message send by **IW4MAdmin** -- `[Admin] message` * _This feature requires you specify a custom say name_ +* Default — `false` `Enable social link` * Shows a link to your community's social media/website on the webfront +* Default — `false` `Use Custom Encoding Parser` * Allows alternative encodings to be used for parsing game information and events * **Russian users should use this and then specify** `windows-1251` **as the encoding string** +* Default — `false` #### Server Configuration After initial configuration is finished, you will be prompted to configure your servers for **IW4MAdmin**. `Enter server IP Address` * For almost all scenarios `127.0.0.1` is sufficient +* Default — `n/a` `Enter server port` * The port that your server is listening on (can be obtained via `net_port`) +* Default — `n/a` `Enter server RCon password` * The *\(R\)emote (Con)sole* password set in your server configuration (can be obtained via `rcon_password`) +* Default — `n/a` `Use Pluto T6 parser` * Used if setting up a server for Plutonium T6 (BO2) +* Default — `false` `Use Pluto IW5 parser` * Used if setting a server for Plutonium IW5 (MW3) +* Default — `false` `Enter number of reserved slots` * The number of client slots reserver for privileged players (unavailable for regular users to occupy) +* Default — `0` + #### Advanced Configuration If you wish to further customize your experience of **IW4MAdmin**, the following configuration file(s) will allow you to changes core options using any text-editor. @@ -70,34 +95,50 @@ If you wish to further customize your experience of **IW4MAdmin**, the following * Specifies the address and port the webfront will listen on. * The value can be an [IP Address](https://en.wikipedia.org/wiki/IP_address):port or [Domain Name](https://en.wikipedia.org/wiki/Domain_name):port * Example http://gameserver.com:8080 +* Default — `http://0.0.0.0:1624` `CustomLocale` * Specifies a [locale name](https://msdn.microsoft.com/en-us/library/39cwe7zf.aspx) to use instead of system default * Locale must be from the `Equivalent Locale Name` column +* Default — `windows-1252` `ConnectionString` * Specifies the [connection string](https://www.connectionstrings.com/mysql/) to a MySQL server that is used instead of SQLite +* Default — `null` `RConPollRate` * Specifies (in milliseconds) how often to poll each server for updates +* Default — `5000` `Servers` * Specifies the list of servers **IW4MAdmin** will monitor +* Default — `[]` * `IPAddress` * Specifies the IP Address of the particular server + * Default — `n/a` * `Port` * Specifies the port of the particular server + * Default — `n/a` * `Password` * Specifies the `rcon_password` of the particular server + * Default — `n/a` +* `ManualLogPath` + * Specifies the log path to be used instead of the automatically generated one + * To use the `GameLogServer`, this should be set to the http address that the `GameLogServer` is listening on + * Example — http://gamelogserver.com/ * `AutoMessages` * Specifies the list of messages that are broadcasted to the particular server + * Default — `null` * `Rules` * Specifies the list of rules that apply to the particular server + * Default — `null` * `ReservedSlotNumber` * Specifies the number of client slots to reserve for privileged users + * Default — `0` `AutoMessagePeriod` * Specifies (in seconds) how often messages should be broadcasted to each server +* Default — `60` `AutoMessages` * Specifies the list of messages that are broadcasted to **all** servers @@ -254,11 +295,11 @@ ___ - Profane words and warning message can be specified in `ProfanityDetermentSettings.json` - If a client's name contains a word listed in the settings, they will immediately be kicked -####IW4 Script Commands +#### IW4 Script Commands - This plugin provides additional integration to IW4x - In order to take advantage of it, copy the `userraw` folder into your IW4x server directory -####VPN Detection [Script Plugin] +#### VPN Detection [Script Plugin] - This plugin detects if a client is using a VPN and kicks them if they are - To disable this plugin, delete `Plugins\VPNDetection.js` ___ @@ -282,6 +323,39 @@ ___ `Web Console` * Allows logged in privileged users to execute commands as if they are in-game --- +### Game Log Server +The game log server provides a way to remotely host your server's log over a http rest api. +This server is useful if you plan on running IW4MAdmin on a different machine than the game server +#### Requirements +- [Python 3.6](https://www.python.org/downloads/) or newer +- The following [PIP](https://pypi.org/project/pip/) packages (provided in `requirements.txt`) + ```Flask>=1.0.2 +aniso8601>=3.0.2 +click>=6.7 +Flask-RESTful>=0.3.6 +itsdangerous>=0.24 +Jinja2>=2.10 +MarkupSafe>=1.0 +pip>=9.0.3 +pytz>=2018.5 +setuptools>=39.0.1 +six>=1.11.0 +Werkzeug>=0.14.1 +``` +#### Installation +1. With Python 3 installed, open up a terminal/command prompt window in the `GameLogServer` folder and execute: + ```console + pip install -r requirements.txt + ``` +2. Allow TCP port 1625 through firewall + * [Windows Instructions](https://www.tomshardware.com/news/how-to-open-firewall-ports-in-windows-10,36451.html) + * [Linux Instructions (iptables)](https://www.digitalocean.com/community/tutorials/how-to-set-up-a-basic-iptables-firewall-on-centos-6#open-up-ports-for-selected-services) +#### Launching +With Python 3 installed, open a terminal/command prompt window open in the `GameServerLog` folder and execute: +```console +python runserver.py +``` +--- ### Extending Plugins #### Code IW4Madmin functionality can be extended by writing additional plugins in C#. @@ -347,15 +421,20 @@ Example http://127.0.0.1 Example https://discordapp.com/api/webhooks/id/token - `DiscordWebhookInformationUrl` — [optional] Discord generated URL to send information to; this includes information such as player messages - `NotifyRoleIds` — [optional] List of [discord role ids](https://discordhelp.net/role-id) to mention when notification hook is sent -#### Launching -With Python installed, open a terminal/command prompt window open in the `Webhook` folder and execute `python DiscordWebhook.py` +#### Launching +With Python installed, open a terminal/command prompt window open in the `Webhook` folder and execute: +```console +python DiscordWebhook.py +``` --- -## Misc +### Misc #### Anti-cheat This is an [IW4x](https://iw4xcachep26muba.onion.link/) only feature (wider game support planned), that uses analytics to detect aimbots and aim-assist tools. To utilize anti-cheat, enable it during setup **and** copy `_customcallbacks.gsc` from `userraw` into your `IW4x Server\userraw\scripts` folder. The anti-cheat feature is a work in progress and as such will be constantly tweaked and may not be 100% accurate, however the goal is to deter as many cheaters as possible from IW4x. #### Database Storage -By default, all **IW4MAdmin** information is stored in `Database.db`. Should you need to reset your database, this file can simply be deleted. Additionally, this file should be preserved during updates to retain client information. +By default, all **IW4MAdmin** information is stored in `Database.db`. +Should you need to reset your database, this file can simply be deleted. +Additionally, this file should be preserved during updates to retain client information. Setting the `ConnectionString` property in `IW4MAdminSettings.json` will cause **IW4MAdmin** to attempt to use a MySQL connection for database storage. \ No newline at end of file diff --git a/SharedLibraryCore/Commands/NativeCommands.cs b/SharedLibraryCore/Commands/NativeCommands.cs index e381d4125..66008365f 100644 --- a/SharedLibraryCore/Commands/NativeCommands.cs +++ b/SharedLibraryCore/Commands/NativeCommands.cs @@ -11,6 +11,7 @@ using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; +using static SharedLibraryCore.RCon.StaticHelpers; namespace SharedLibraryCore.Commands { @@ -312,6 +313,7 @@ namespace SharedLibraryCore.Commands if (P == null) continue; // todo: fix spacing + // todo: make this better :) if (P.Masked) playerList.AppendFormat("[^3{0}^7]{3}[^3{1}^7] {2}", Utilities.ConvertLevelToColor(Player.Permission.User, P.ClientPermission.Name), P.ClientNumber, P.Name, Utilities.GetSpaces(Player.Permission.SeniorAdmin.ToString().Length - Player.Permission.User.ToString().Length)); else @@ -320,6 +322,7 @@ namespace SharedLibraryCore.Commands if (count == 2 || E.Owner.GetPlayersAsList().Count == 1) { await E.Origin.Tell(playerList.ToString()); + await Task.Delay(FloodProtectionInterval); count = 0; playerList = new StringBuilder(); continue; @@ -327,6 +330,11 @@ namespace SharedLibraryCore.Commands count++; } + + if (playerList.Length > 0) + { + await E.Origin.Tell(playerList.ToString()); + } } } @@ -357,6 +365,7 @@ namespace SharedLibraryCore.Commands { await E.Origin.Tell("[^3" + C.Name + "^7] " + C.Description); await E.Origin.Tell(C.Syntax); + await Task.Delay(FloodProtectionInterval); found = true; } } @@ -382,6 +391,7 @@ namespace SharedLibraryCore.Commands await E.Owner.Broadcast(helpResponse.ToString()); else await E.Origin.Tell(helpResponse.ToString()); + await Task.Delay(FloodProtectionInterval); helpResponse = new StringBuilder(); count = 0; } @@ -564,10 +574,10 @@ namespace SharedLibraryCore.Commands { foreach (string line in OnlineAdmins(E.Owner).Split(Environment.NewLine)) { - if (E.Message[0] == '@') - await E.Owner.Broadcast(line); - else - await E.Origin.Tell(line); + var t = E.Message.IsBroadcastCommand() ? E.Owner.Broadcast(line) : E.Origin.Tell(line); + await t; + + await Task.Delay(FloodProtectionInterval); } } } @@ -645,6 +655,7 @@ namespace SharedLibraryCore.Commands $"[^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)}"; await E.Origin.Tell(msg); + await Task.Delay(FloodProtectionInterval); } } } @@ -675,10 +686,9 @@ namespace SharedLibraryCore.Commands foreach (string r in rules) { - if (E.Message.IsBroadcastCommand()) - await E.Owner.Broadcast($"- {r}"); - else - await E.Origin.Tell($"- {r}"); + var t = E.Message.IsBroadcastCommand() ? E.Owner.Broadcast($"- {r}") : E.Origin.Tell($"- {r}"); + await t; + await Task.Delay(FloodProtectionInterval); } } } @@ -927,7 +937,10 @@ namespace SharedLibraryCore.Commands } foreach (Report R in E.Owner.Reports) + { await E.Origin.Tell(String.Format("^5{0}^7->^1{1}^7: {2}", R.Origin.Name, R.Target.Name, R.Reason)); + await Task.Delay(FloodProtectionInterval); + } } } @@ -1054,6 +1067,7 @@ namespace SharedLibraryCore.Commands foreach (var P in Plugins.PluginImporter.ActivePlugins) { await E.Origin.Tell(String.Format("^3{0} ^7[v^3{1}^7] by ^5{2}^7", P.Name, P.Version, P.Author)); + await Task.Delay(FloodProtectionInterval); } } } diff --git a/SharedLibraryCore/Database/DatabaseContext.cs b/SharedLibraryCore/Database/DatabaseContext.cs index e9c673624..f0003ad24 100644 --- a/SharedLibraryCore/Database/DatabaseContext.cs +++ b/SharedLibraryCore/Database/DatabaseContext.cs @@ -27,7 +27,9 @@ namespace SharedLibraryCore.Database public DatabaseContext(DbContextOptions opt) : base(opt) { } - public DatabaseContext(bool disableTracking = false) + public DatabaseContext() { } + + public DatabaseContext(bool disableTracking) { if (disableTracking) { diff --git a/SharedLibraryCore/Events/GameEvent.cs b/SharedLibraryCore/Events/GameEvent.cs index fc5c537f0..59be67b38 100644 --- a/SharedLibraryCore/Events/GameEvent.cs +++ b/SharedLibraryCore/Events/GameEvent.cs @@ -51,7 +51,7 @@ namespace SharedLibraryCore public GameEvent() { - OnProcessed = new ManualResetEventSlim(); + OnProcessed = new ManualResetEventSlim(false); Time = DateTime.UtcNow; Id = GetNextEventId(); } @@ -105,5 +105,7 @@ namespace SharedLibraryCore queuedEvent.Target.State != Player.ClientState.Connected && queuedEvent.Target.NetworkId != 0; } + + public static bool IsEventTimeSensitive(GameEvent gameEvent) => gameEvent.Type == EventType.Connect; } } diff --git a/SharedLibraryCore/Migrations/20180907020706_AddVision.Designer.cs b/SharedLibraryCore/Migrations/20180907020706_AddVision.Designer.cs new file mode 100644 index 000000000..f152174d9 --- /dev/null +++ b/SharedLibraryCore/Migrations/20180907020706_AddVision.Designer.cs @@ -0,0 +1,669 @@ +// +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("20180907020706_AddVision")] + partial class AddVision + { + 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.HasKey("RatingId"); + + b.HasIndex("RatingHistoryId"); + + b.HasIndex("ServerId"); + + 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/20180907020706_AddVision.cs b/SharedLibraryCore/Migrations/20180907020706_AddVision.cs new file mode 100644 index 000000000..9d2a216ce --- /dev/null +++ b/SharedLibraryCore/Migrations/20180907020706_AddVision.cs @@ -0,0 +1,23 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace SharedLibraryCore.Migrations +{ + public partial class AddVision : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "VisionAverage", + table: "EFClientStatistics", + nullable: false, + defaultValue: 0.0); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "VisionAverage", + table: "EFClientStatistics"); + } + } +} diff --git a/SharedLibraryCore/Migrations/20180908004053_AddWhenToRating.Designer.cs b/SharedLibraryCore/Migrations/20180908004053_AddWhenToRating.Designer.cs new file mode 100644 index 000000000..be77e226a --- /dev/null +++ b/SharedLibraryCore/Migrations/20180908004053_AddWhenToRating.Designer.cs @@ -0,0 +1,669 @@ +// +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("20180908004053_AddWhenToRating")] + partial class AddWhenToRating + { + 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("RatingHistoryId"); + + b.HasIndex("ServerId"); + + 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.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/20180908004053_AddWhenToRating.cs b/SharedLibraryCore/Migrations/20180908004053_AddWhenToRating.cs new file mode 100644 index 000000000..8f5e6fd44 --- /dev/null +++ b/SharedLibraryCore/Migrations/20180908004053_AddWhenToRating.cs @@ -0,0 +1,25 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace SharedLibraryCore.Migrations +{ + public partial class AddWhenToRating : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "When", + table: "EFRating", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "When", + table: "EFRating"); + } + } +} diff --git a/SharedLibraryCore/Migrations/DatabaseContextModelSnapshot.cs b/SharedLibraryCore/Migrations/DatabaseContextModelSnapshot.cs index 65ae83a50..9102988f1 100644 --- a/SharedLibraryCore/Migrations/DatabaseContextModelSnapshot.cs +++ b/SharedLibraryCore/Migrations/DatabaseContextModelSnapshot.cs @@ -198,6 +198,8 @@ namespace SharedLibraryCore.Migrations b.Property("TimePlayed"); + b.Property("VisionAverage"); + b.HasKey("ClientId", "ServerId"); b.HasIndex("ServerId"); @@ -254,6 +256,8 @@ namespace SharedLibraryCore.Migrations b.Property("ServerId"); + b.Property("When"); + b.HasKey("RatingId"); b.HasIndex("RatingHistoryId"); @@ -475,6 +479,8 @@ namespace SharedLibraryCore.Migrations b.HasKey("Vector3Id"); + b.HasIndex("EFACSnapshotSnapshotId"); + b.ToTable("Vector3"); }); diff --git a/SharedLibraryCore/Objects/Player.cs b/SharedLibraryCore/Objects/Player.cs index 1aa64087d..ab2bf2df8 100644 --- a/SharedLibraryCore/Objects/Player.cs +++ b/SharedLibraryCore/Objects/Player.cs @@ -146,6 +146,8 @@ namespace SharedLibraryCore.Objects [NotMapped] public DateTime ConnectionTime { get; set; } [NotMapped] + public int ConnectionLength => (int)(DateTime.UtcNow - ConnectionTime).TotalSeconds; + [NotMapped] public Server CurrentServer { get; set; } [NotMapped] public int Score { get; set; } diff --git a/SharedLibraryCore/RCon/Connection.cs b/SharedLibraryCore/RCon/Connection.cs index 99b8d0429..710a11f35 100644 --- a/SharedLibraryCore/RCon/Connection.cs +++ b/SharedLibraryCore/RCon/Connection.cs @@ -35,7 +35,6 @@ namespace SharedLibraryCore.RCon ILogger Log; int FailedSends; int FailedReceives; - static DateTime LastQuery; string response; ManualResetEvent OnConnected; @@ -142,14 +141,14 @@ namespace SharedLibraryCore.RCon public async Task SendQueryAsync(StaticHelpers.QueryType type, string parameters = "", bool waitForResponse = true) { - // will this really prevent flooding? - if ((DateTime.Now - LastQuery).TotalMilliseconds < 350) - { - Thread.Sleep(350); - //await Task.Delay(350); - } + //// will this really prevent flooding? + //if ((DateTime.Now - LastQuery).TotalMilliseconds < 350) + //{ + // Thread.Sleep(350); + // //await Task.Delay(350); + //} - LastQuery = DateTime.Now; + // LastQuery = DateTime.Now; OnSent.Reset(); OnReceived.Reset(); diff --git a/SharedLibraryCore/RCon/StaticHelpers.cs b/SharedLibraryCore/RCon/StaticHelpers.cs index 0782d0391..76c2e1625 100644 --- a/SharedLibraryCore/RCon/StaticHelpers.cs +++ b/SharedLibraryCore/RCon/StaticHelpers.cs @@ -4,15 +4,45 @@ namespace SharedLibraryCore.RCon { public static class StaticHelpers { + /// + /// defines the type of RCon query sent to a server + /// public enum QueryType { + /// + /// retrieve the status of a server + /// does not require RCon password + /// GET_STATUS, + /// + /// retrieve the information of a server + /// server responds with key/value pairs + /// RCon password is required + /// GET_INFO, + /// + /// retrieve the value of a DVAR + /// RCon password is required + /// DVAR, + /// + /// execute a command + /// RCon password is required + /// COMMAND, } + /// + /// line seperator char included in response from the server + /// public static char SeperatorChar = (char)int.Parse("0a", System.Globalization.NumberStyles.AllowHexSpecifier); + /// + /// timeout in seconds to wait for a socket send or receive before giving up + /// public static readonly TimeSpan SocketTimeout = new TimeSpan(0, 0, 10); + /// + /// interval in milliseconds to wait before sending the next RCon request + /// + public static readonly int FloodProtectionInterval = 350; } } diff --git a/SharedLibraryCore/SharedLibraryCore.csproj b/SharedLibraryCore/SharedLibraryCore.csproj index 66678d892..04068ecf5 100644 --- a/SharedLibraryCore/SharedLibraryCore.csproj +++ b/SharedLibraryCore/SharedLibraryCore.csproj @@ -18,16 +18,16 @@ - - - - - - - - + + + + + + + + - + diff --git a/SharedLibraryCore/Utilities.cs b/SharedLibraryCore/Utilities.cs index a5ab6802f..4554d86fd 100644 --- a/SharedLibraryCore/Utilities.cs +++ b/SharedLibraryCore/Utilities.cs @@ -12,6 +12,10 @@ using System.Threading.Tasks; using System.Globalization; using System.Diagnostics; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Query.Internal; +using Microsoft.EntityFrameworkCore.Storage; + namespace SharedLibraryCore { public static class Utilities @@ -494,5 +498,34 @@ namespace SharedLibraryCore var response = await server.RemoteConnection.SendQueryAsync(RCon.StaticHelpers.QueryType.GET_INFO); return response.FirstOrDefault(r => r[0] == '\\')?.DictionaryFromKeyValue(); } + + +#if DEBUG == true + + private static readonly TypeInfo QueryCompilerTypeInfo = typeof(QueryCompiler).GetTypeInfo(); + + private static readonly FieldInfo QueryCompilerField = typeof(EntityQueryProvider).GetTypeInfo().DeclaredFields.First(x => x.Name == "_queryCompiler"); + + private static readonly FieldInfo QueryModelGeneratorField = QueryCompilerTypeInfo.DeclaredFields.First(x => x.Name == "_queryModelGenerator"); + + private static readonly FieldInfo DataBaseField = QueryCompilerTypeInfo.DeclaredFields.Single(x => x.Name == "_database"); + + private static readonly PropertyInfo DatabaseDependenciesField = typeof(Microsoft.EntityFrameworkCore.Storage.Database).GetTypeInfo().DeclaredProperties.Single(x => x.Name == "Dependencies"); + + public static string ToSql(this IQueryable query) where TEntity : class + { + var queryCompiler = (QueryCompiler)QueryCompilerField.GetValue(query.Provider); + var modelGenerator = (QueryModelGenerator)QueryModelGeneratorField.GetValue(queryCompiler); + var queryModel = modelGenerator.ParseQuery(query.Expression); + var database = (IDatabase)DataBaseField.GetValue(queryCompiler); + var databaseDependencies = (DatabaseDependencies)DatabaseDependenciesField.GetValue(database); + var queryCompilationContext = databaseDependencies.QueryCompilationContextFactory.Create(false); + var modelVisitor = (RelationalQueryModelVisitor)queryCompilationContext.CreateQueryModelVisitor(); + modelVisitor.CreateQueryExecutor(queryModel); + var sql = modelVisitor.Queries.First().ToString(); + + return sql; + } +#endif } } diff --git a/WebfrontCore/Controllers/API/APIController.cs b/WebfrontCore/Controllers/API/APIController.cs index 707329a2c..6d14ea4ea 100644 --- a/WebfrontCore/Controllers/API/APIController.cs +++ b/WebfrontCore/Controllers/API/APIController.cs @@ -41,7 +41,7 @@ namespace WebfrontCore.Controllers.API player.Ping, State = player.State.ToString(), player.ClientNumber, - ConnectionTime = Math.Round((DateTime.UtcNow - player.ConnectionTime).TotalSeconds, 0), + ConnectionTime = player.ConnectionLength, Level = player.Level.ToLocalizedLevelName(), }) }); diff --git a/WebfrontCore/wwwroot/js/loader.js b/WebfrontCore/wwwroot/js/loader.js index d54c15a76..15682520e 100644 --- a/WebfrontCore/wwwroot/js/loader.js +++ b/WebfrontCore/wwwroot/js/loader.js @@ -43,7 +43,7 @@ if ($(loaderResponseId).length === 1) { */ $('html').bind('mousewheel DOMMouseScroll', function (e) { - var delta = (e.originalEvent.wheelDelta || -e.originalEvent.detail); + var delta = e.originalEvent.wheelDelta || -e.originalEvent.detail; if (delta < 0 && !hasScrollBar) { loadMoreItems();