update readme

add vision average to client stats
other stuff
This commit is contained in:
RaidMax 2018-09-07 22:29:42 -05:00
parent 385879618d
commit ba5b1e19a6
27 changed files with 1750 additions and 192 deletions

View File

@ -9,14 +9,17 @@ namespace IW4MAdmin.Application
{
class GameEventHandler : IEventHandler
{
private readonly IManager Manager;
static long NextEventId = 1;
private readonly SortedList<long, GameEvent> OutOfOrderEvents;
readonly IManager Manager;
readonly SortedList<long, GameEvent> OutOfOrderEvents;
readonly SemaphoreSlim IsProcessingEvent;
public GameEventHandler(IManager mgr)
{
Manager = mgr;
OutOfOrderEvents = new SortedList<long, GameEvent>();
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)

View File

@ -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
};

View File

@ -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<string>("g_log");
}
}
//CustomCallback = await ScriptLoaded();
string mainPath = EventParser.GetGameDir();
#if DEBUG
basepath.Value = @"D:\";
// basepath.Value = @"D:\";
#endif
string logPath = string.Empty;

View File

@ -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 {

View File

@ -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)

View File

@ -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<Func<EFRating, bool>> 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;
}
/// <summary>
/// gets a ranking across all servers for given client id
/// </summary>
@ -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<EFRating>()
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<EFRating>()
.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<EFRating>()
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<EFClientStatistics>()
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<double>() { 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
/// <returns></returns>
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<EFRating>()
};
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<EFRating>()
.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<EFRating>()
.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();
}
}

View File

@ -25,6 +25,7 @@ namespace IW4MAdmin.Plugins.Stats.Models
public double EloRating { get; set; }
public virtual ICollection<EFHitLocationCount> HitLocations { get; set; }
public double RollingWeightedKDR { get; set; }
public double VisionAverage { get; set; }
[NotMapped]
public double Performance
{

View File

@ -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;
}
}

View File

@ -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<TEntity>(this IQueryable<TEntity> 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<TEntity>(queryModel);
var sql = modelVisitor.Queries.First().ToString();
return sql;
}
}
#endif
}

View File

@ -10,5 +10,5 @@
<script type="text/javascript" src="~/js/loader.js"></script>
<script type="text/javascript" src="~/js/stats.js"></script>
</environment>
<script>initLoader('/Stats/GetTopPlayersAsync', '#stats_top_players');</script>
<script>initLoader('/Stats/GetTopPlayersAsync', '#stats_top_players', 50);</script>
}

View File

@ -43,7 +43,7 @@ namespace Tests
public void AddAndRemoveClientsViaJoinShouldSucceed()
{
var server = Manager.GetServers().First();
var waiters = new Queue<SemaphoreSlim>();
var waiters = new Queue<GameEvent>();
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<SemaphoreSlim>();
var waiters = new Queue<GameEvent>();
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;

103
README.md
View File

@ -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-<version>.zip`
2. Run `StartIW4MAdmin.cmd`
#### Installation
1. Install .NET Core Runtime
2. Extract `IW4MAdmin-<version>.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 &mdash; `http://0.0.0.0:1624`
`Enable multiple owners`
* Enables more than one client to be promoted to level of `Owner`
* Default &mdash; `false`
`Enable stepped privilege hierarchy`
* Allows privileged clients to promote other clients to the level below their current level
* Default &mdash; `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 &mdash; `false`
`Enable social link`
* Shows a link to your community's social media/website on the webfront
* Default &mdash; `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 &mdash; `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 &mdash; `n/a`
`Enter server port`
* The port that your server is listening on (can be obtained via `net_port`)
* Default &mdash; `n/a`
`Enter server RCon password`
* The *\(R\)emote (Con)sole* password set in your server configuration (can be obtained via `rcon_password`)
* Default &mdash; `n/a`
`Use Pluto T6 parser`
* Used if setting up a server for Plutonium T6 (BO2)
* Default &mdash; `false`
`Use Pluto IW5 parser`
* Used if setting a server for Plutonium IW5 (MW3)
* Default &mdash; `false`
`Enter number of reserved slots`
* The number of client slots reserver for privileged players (unavailable for regular users to occupy)
* Default &mdash; `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 &mdash; `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 &mdash; `windows-1252`
`ConnectionString`
* Specifies the [connection string](https://www.connectionstrings.com/mysql/) to a MySQL server that is used instead of SQLite
* Default &mdash; `null`
`RConPollRate`
* Specifies (in milliseconds) how often to poll each server for updates
* Default &mdash; `5000`
`Servers`
* Specifies the list of servers **IW4MAdmin** will monitor
* Default &mdash; `[]`
* `IPAddress`
* Specifies the IP Address of the particular server
* Default &mdash; `n/a`
* `Port`
* Specifies the port of the particular server
* Default &mdash; `n/a`
* `Password`
* Specifies the `rcon_password` of the particular server
* Default &mdash; `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 &mdash; http://gamelogserver.com/
* `AutoMessages`
* Specifies the list of messages that are broadcasted to the particular server
* Default &mdash; `null`
* `Rules`
* Specifies the list of rules that apply to the particular server
* Default &mdash; `null`
* `ReservedSlotNumber`
* Specifies the number of client slots to reserve for privileged users
* Default &mdash; `0`
`AutoMessagePeriod`
* Specifies (in seconds) how often messages should be broadcasted to each server
* Default &mdash; `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` &mdash; [optional] Discord generated URL to send information to; this includes information such as player messages
- `NotifyRoleIds` &mdash; [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.

View File

@ -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);
}
}
}

View File

@ -27,7 +27,9 @@ namespace SharedLibraryCore.Database
public DatabaseContext(DbContextOptions<DatabaseContext> opt) : base(opt) { }
public DatabaseContext(bool disableTracking = false)
public DatabaseContext() { }
public DatabaseContext(bool disableTracking)
{
if (disableTracking)
{

View File

@ -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;
}
}

View File

@ -0,0 +1,669 @@
// <auto-generated />
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<int>("SnapshotId")
.ValueGeneratedOnAdd();
b.Property<bool>("Active");
b.Property<int>("ClientId");
b.Property<int>("CurrentSessionLength");
b.Property<double>("CurrentStrain");
b.Property<int?>("CurrentViewAngleVector3Id");
b.Property<int>("Deaths");
b.Property<double>("Distance");
b.Property<double>("EloRating");
b.Property<int?>("HitDestinationVector3Id");
b.Property<int>("HitLocation");
b.Property<int?>("HitOriginVector3Id");
b.Property<int>("HitType");
b.Property<int>("Hits");
b.Property<int>("Kills");
b.Property<int?>("LastStrainAngleVector3Id");
b.Property<double>("SessionAngleOffset");
b.Property<double>("SessionSPM");
b.Property<int>("SessionScore");
b.Property<double>("StrainAngleBetween");
b.Property<int>("TimeSinceLastEvent");
b.Property<int>("WeaponId");
b.Property<DateTime>("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<long>("KillId")
.ValueGeneratedOnAdd();
b.Property<bool>("Active");
b.Property<int>("AttackerId");
b.Property<int>("Damage");
b.Property<int?>("DeathOriginVector3Id");
b.Property<int>("DeathType");
b.Property<double>("Fraction");
b.Property<int>("HitLoc");
b.Property<bool>("IsKill");
b.Property<int?>("KillOriginVector3Id");
b.Property<int>("Map");
b.Property<int>("ServerId");
b.Property<int>("VictimId");
b.Property<int?>("ViewAnglesVector3Id");
b.Property<double>("VisibilityPercentage");
b.Property<int>("Weapon");
b.Property<DateTime>("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<long>("MessageId")
.ValueGeneratedOnAdd();
b.Property<bool>("Active");
b.Property<int>("ClientId");
b.Property<string>("Message");
b.Property<int>("ServerId");
b.Property<DateTime>("TimeSent");
b.HasKey("MessageId");
b.HasIndex("ClientId");
b.HasIndex("ServerId");
b.ToTable("EFClientMessages");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientRatingHistory", b =>
{
b.Property<int>("RatingHistoryId")
.ValueGeneratedOnAdd();
b.Property<bool>("Active");
b.Property<int>("ClientId");
b.HasKey("RatingHistoryId");
b.HasIndex("ClientId");
b.ToTable("EFClientRatingHistory");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics", b =>
{
b.Property<int>("ClientId");
b.Property<int>("ServerId");
b.Property<bool>("Active");
b.Property<int>("Deaths");
b.Property<double>("EloRating");
b.Property<int>("Kills");
b.Property<double>("MaxStrain");
b.Property<double>("RollingWeightedKDR");
b.Property<double>("SPM");
b.Property<double>("Skill");
b.Property<int>("TimePlayed");
b.Property<double>("VisionAverage");
b.HasKey("ClientId", "ServerId");
b.HasIndex("ServerId");
b.ToTable("EFClientStatistics");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFHitLocationCount", b =>
{
b.Property<int>("HitLocationCountId")
.ValueGeneratedOnAdd();
b.Property<bool>("Active");
b.Property<int>("ClientId")
.HasColumnName("EFClientStatistics_ClientId");
b.Property<int>("HitCount");
b.Property<float>("HitOffsetAverage");
b.Property<int>("Location");
b.Property<float>("MaxAngleDistance");
b.Property<int>("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<int>("RatingId")
.ValueGeneratedOnAdd();
b.Property<bool>("Active");
b.Property<int>("ActivityAmount");
b.Property<bool>("Newest");
b.Property<double>("Performance");
b.Property<int>("Ranking");
b.Property<int>("RatingHistoryId");
b.Property<int?>("ServerId");
b.HasKey("RatingId");
b.HasIndex("RatingHistoryId");
b.HasIndex("ServerId");
b.ToTable("EFRating");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServer", b =>
{
b.Property<int>("ServerId");
b.Property<bool>("Active");
b.Property<int>("Port");
b.HasKey("ServerId");
b.ToTable("EFServers");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServerStatistics", b =>
{
b.Property<int>("StatisticId")
.ValueGeneratedOnAdd();
b.Property<bool>("Active");
b.Property<int>("ServerId");
b.Property<long>("TotalKills");
b.Property<long>("TotalPlayTime");
b.HasKey("StatisticId");
b.HasIndex("ServerId");
b.ToTable("EFServerStatistics");
});
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAlias", b =>
{
b.Property<int>("AliasId")
.ValueGeneratedOnAdd();
b.Property<bool>("Active");
b.Property<DateTime>("DateAdded");
b.Property<int>("IPAddress");
b.Property<int>("LinkId");
b.Property<string>("Name")
.IsRequired();
b.HasKey("AliasId");
b.HasIndex("IPAddress");
b.HasIndex("LinkId");
b.ToTable("EFAlias");
});
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAliasLink", b =>
{
b.Property<int>("AliasLinkId")
.ValueGeneratedOnAdd();
b.Property<bool>("Active");
b.HasKey("AliasLinkId");
b.ToTable("EFAliasLinks");
});
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFChangeHistory", b =>
{
b.Property<int>("ChangeHistoryId")
.ValueGeneratedOnAdd();
b.Property<bool>("Active");
b.Property<string>("Comment")
.HasMaxLength(128);
b.Property<int>("OriginEntityId");
b.Property<int>("TargetEntityId");
b.Property<DateTime>("TimeChanged");
b.Property<int>("TypeOfChange");
b.HasKey("ChangeHistoryId");
b.ToTable("EFChangeHistory");
});
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFClient", b =>
{
b.Property<int>("ClientId")
.ValueGeneratedOnAdd();
b.Property<bool>("Active");
b.Property<int>("AliasLinkId");
b.Property<int>("Connections");
b.Property<int>("CurrentAliasId");
b.Property<DateTime>("FirstConnection");
b.Property<DateTime>("LastConnection");
b.Property<int>("Level");
b.Property<bool>("Masked");
b.Property<long>("NetworkId");
b.Property<string>("Password");
b.Property<string>("PasswordSalt");
b.Property<int>("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<int>("MetaId")
.ValueGeneratedOnAdd();
b.Property<bool>("Active");
b.Property<int>("ClientId");
b.Property<DateTime>("Created");
b.Property<string>("Extra");
b.Property<string>("Key")
.IsRequired();
b.Property<DateTime>("Updated");
b.Property<string>("Value")
.IsRequired();
b.HasKey("MetaId");
b.HasIndex("ClientId");
b.ToTable("EFMeta");
});
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFPenalty", b =>
{
b.Property<int>("PenaltyId")
.ValueGeneratedOnAdd();
b.Property<bool>("Active");
b.Property<string>("AutomatedOffense");
b.Property<DateTime>("Expires");
b.Property<int>("LinkId");
b.Property<int>("OffenderId");
b.Property<string>("Offense")
.IsRequired();
b.Property<int>("PunisherId");
b.Property<int>("Type");
b.Property<DateTime>("When");
b.HasKey("PenaltyId");
b.HasIndex("LinkId");
b.HasIndex("OffenderId");
b.HasIndex("PunisherId");
b.ToTable("EFPenalties");
});
modelBuilder.Entity("SharedLibraryCore.Helpers.Vector3", b =>
{
b.Property<int>("Vector3Id")
.ValueGeneratedOnAdd();
b.Property<int?>("EFACSnapshotSnapshotId");
b.Property<float>("X");
b.Property<float>("Y");
b.Property<float>("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
}
}
}

View File

@ -0,0 +1,23 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace SharedLibraryCore.Migrations
{
public partial class AddVision : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<double>(
name: "VisionAverage",
table: "EFClientStatistics",
nullable: false,
defaultValue: 0.0);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "VisionAverage",
table: "EFClientStatistics");
}
}
}

View File

@ -0,0 +1,669 @@
// <auto-generated />
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<int>("SnapshotId")
.ValueGeneratedOnAdd();
b.Property<bool>("Active");
b.Property<int>("ClientId");
b.Property<int>("CurrentSessionLength");
b.Property<double>("CurrentStrain");
b.Property<int?>("CurrentViewAngleVector3Id");
b.Property<int>("Deaths");
b.Property<double>("Distance");
b.Property<double>("EloRating");
b.Property<int?>("HitDestinationVector3Id");
b.Property<int>("HitLocation");
b.Property<int?>("HitOriginVector3Id");
b.Property<int>("HitType");
b.Property<int>("Hits");
b.Property<int>("Kills");
b.Property<int?>("LastStrainAngleVector3Id");
b.Property<double>("SessionAngleOffset");
b.Property<double>("SessionSPM");
b.Property<int>("SessionScore");
b.Property<double>("StrainAngleBetween");
b.Property<int>("TimeSinceLastEvent");
b.Property<int>("WeaponId");
b.Property<DateTime>("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<long>("KillId")
.ValueGeneratedOnAdd();
b.Property<bool>("Active");
b.Property<int>("AttackerId");
b.Property<int>("Damage");
b.Property<int?>("DeathOriginVector3Id");
b.Property<int>("DeathType");
b.Property<double>("Fraction");
b.Property<int>("HitLoc");
b.Property<bool>("IsKill");
b.Property<int?>("KillOriginVector3Id");
b.Property<int>("Map");
b.Property<int>("ServerId");
b.Property<int>("VictimId");
b.Property<int?>("ViewAnglesVector3Id");
b.Property<double>("VisibilityPercentage");
b.Property<int>("Weapon");
b.Property<DateTime>("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<long>("MessageId")
.ValueGeneratedOnAdd();
b.Property<bool>("Active");
b.Property<int>("ClientId");
b.Property<string>("Message");
b.Property<int>("ServerId");
b.Property<DateTime>("TimeSent");
b.HasKey("MessageId");
b.HasIndex("ClientId");
b.HasIndex("ServerId");
b.ToTable("EFClientMessages");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientRatingHistory", b =>
{
b.Property<int>("RatingHistoryId")
.ValueGeneratedOnAdd();
b.Property<bool>("Active");
b.Property<int>("ClientId");
b.HasKey("RatingHistoryId");
b.HasIndex("ClientId");
b.ToTable("EFClientRatingHistory");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics", b =>
{
b.Property<int>("ClientId");
b.Property<int>("ServerId");
b.Property<bool>("Active");
b.Property<int>("Deaths");
b.Property<double>("EloRating");
b.Property<int>("Kills");
b.Property<double>("MaxStrain");
b.Property<double>("RollingWeightedKDR");
b.Property<double>("SPM");
b.Property<double>("Skill");
b.Property<int>("TimePlayed");
b.Property<double>("VisionAverage");
b.HasKey("ClientId", "ServerId");
b.HasIndex("ServerId");
b.ToTable("EFClientStatistics");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFHitLocationCount", b =>
{
b.Property<int>("HitLocationCountId")
.ValueGeneratedOnAdd();
b.Property<bool>("Active");
b.Property<int>("ClientId")
.HasColumnName("EFClientStatistics_ClientId");
b.Property<int>("HitCount");
b.Property<float>("HitOffsetAverage");
b.Property<int>("Location");
b.Property<float>("MaxAngleDistance");
b.Property<int>("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<int>("RatingId")
.ValueGeneratedOnAdd();
b.Property<bool>("Active");
b.Property<int>("ActivityAmount");
b.Property<bool>("Newest");
b.Property<double>("Performance");
b.Property<int>("Ranking");
b.Property<int>("RatingHistoryId");
b.Property<int?>("ServerId");
b.Property<DateTime>("When");
b.HasKey("RatingId");
b.HasIndex("RatingHistoryId");
b.HasIndex("ServerId");
b.ToTable("EFRating");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServer", b =>
{
b.Property<int>("ServerId");
b.Property<bool>("Active");
b.Property<int>("Port");
b.HasKey("ServerId");
b.ToTable("EFServers");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServerStatistics", b =>
{
b.Property<int>("StatisticId")
.ValueGeneratedOnAdd();
b.Property<bool>("Active");
b.Property<int>("ServerId");
b.Property<long>("TotalKills");
b.Property<long>("TotalPlayTime");
b.HasKey("StatisticId");
b.HasIndex("ServerId");
b.ToTable("EFServerStatistics");
});
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAlias", b =>
{
b.Property<int>("AliasId")
.ValueGeneratedOnAdd();
b.Property<bool>("Active");
b.Property<DateTime>("DateAdded");
b.Property<int>("IPAddress");
b.Property<int>("LinkId");
b.Property<string>("Name")
.IsRequired();
b.HasKey("AliasId");
b.HasIndex("IPAddress");
b.HasIndex("LinkId");
b.ToTable("EFAlias");
});
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAliasLink", b =>
{
b.Property<int>("AliasLinkId")
.ValueGeneratedOnAdd();
b.Property<bool>("Active");
b.HasKey("AliasLinkId");
b.ToTable("EFAliasLinks");
});
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFChangeHistory", b =>
{
b.Property<int>("ChangeHistoryId")
.ValueGeneratedOnAdd();
b.Property<bool>("Active");
b.Property<string>("Comment")
.HasMaxLength(128);
b.Property<int>("OriginEntityId");
b.Property<int>("TargetEntityId");
b.Property<DateTime>("TimeChanged");
b.Property<int>("TypeOfChange");
b.HasKey("ChangeHistoryId");
b.ToTable("EFChangeHistory");
});
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFClient", b =>
{
b.Property<int>("ClientId")
.ValueGeneratedOnAdd();
b.Property<bool>("Active");
b.Property<int>("AliasLinkId");
b.Property<int>("Connections");
b.Property<int>("CurrentAliasId");
b.Property<DateTime>("FirstConnection");
b.Property<DateTime>("LastConnection");
b.Property<int>("Level");
b.Property<bool>("Masked");
b.Property<long>("NetworkId");
b.Property<string>("Password");
b.Property<string>("PasswordSalt");
b.Property<int>("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<int>("MetaId")
.ValueGeneratedOnAdd();
b.Property<bool>("Active");
b.Property<int>("ClientId");
b.Property<DateTime>("Created");
b.Property<string>("Extra");
b.Property<string>("Key")
.IsRequired();
b.Property<DateTime>("Updated");
b.Property<string>("Value")
.IsRequired();
b.HasKey("MetaId");
b.HasIndex("ClientId");
b.ToTable("EFMeta");
});
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFPenalty", b =>
{
b.Property<int>("PenaltyId")
.ValueGeneratedOnAdd();
b.Property<bool>("Active");
b.Property<string>("AutomatedOffense");
b.Property<DateTime>("Expires");
b.Property<int>("LinkId");
b.Property<int>("OffenderId");
b.Property<string>("Offense")
.IsRequired();
b.Property<int>("PunisherId");
b.Property<int>("Type");
b.Property<DateTime>("When");
b.HasKey("PenaltyId");
b.HasIndex("LinkId");
b.HasIndex("OffenderId");
b.HasIndex("PunisherId");
b.ToTable("EFPenalties");
});
modelBuilder.Entity("SharedLibraryCore.Helpers.Vector3", b =>
{
b.Property<int>("Vector3Id")
.ValueGeneratedOnAdd();
b.Property<int?>("EFACSnapshotSnapshotId");
b.Property<float>("X");
b.Property<float>("Y");
b.Property<float>("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
}
}
}

View File

@ -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<DateTime>(
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");
}
}
}

View File

@ -198,6 +198,8 @@ namespace SharedLibraryCore.Migrations
b.Property<int>("TimePlayed");
b.Property<double>("VisionAverage");
b.HasKey("ClientId", "ServerId");
b.HasIndex("ServerId");
@ -254,6 +256,8 @@ namespace SharedLibraryCore.Migrations
b.Property<int?>("ServerId");
b.Property<DateTime>("When");
b.HasKey("RatingId");
b.HasIndex("RatingHistoryId");
@ -475,6 +479,8 @@ namespace SharedLibraryCore.Migrations
b.HasKey("Vector3Id");
b.HasIndex("EFACSnapshotSnapshotId");
b.ToTable("Vector3");
});

View File

@ -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; }

View File

@ -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<string[]> 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();

View File

@ -4,15 +4,45 @@ namespace SharedLibraryCore.RCon
{
public static class StaticHelpers
{
/// <summary>
/// defines the type of RCon query sent to a server
/// </summary>
public enum QueryType
{
/// <summary>
/// retrieve the status of a server
/// does not require RCon password
/// </summary>
GET_STATUS,
/// <summary>
/// retrieve the information of a server
/// server responds with key/value pairs
/// RCon password is required
/// </summary>
GET_INFO,
/// <summary>
/// retrieve the value of a DVAR
/// RCon password is required
/// </summary>
DVAR,
/// <summary>
/// execute a command
/// RCon password is required
/// </summary>
COMMAND,
}
/// <summary>
/// line seperator char included in response from the server
/// </summary>
public static char SeperatorChar = (char)int.Parse("0a", System.Globalization.NumberStyles.AllowHexSpecifier);
/// <summary>
/// timeout in seconds to wait for a socket send or receive before giving up
/// </summary>
public static readonly TimeSpan SocketTimeout = new TimeSpan(0, 0, 10);
/// <summary>
/// interval in milliseconds to wait before sending the next RCon request
/// </summary>
public static readonly int FloodProtectionInterval = 350;
}
}

View File

@ -18,16 +18,16 @@
<ItemGroup>
<PackageReference Include="Jint" Version="2.11.58" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.2.0-preview1-35029" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.0-preview1-35029" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.2.0-preview1-35029" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="2.2.0-preview1-35029" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.2.0-preview1-35029" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="2.2.0-preview1-35029" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="2.2.0-preview1-35029" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="2.2.0-preview1-35029" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.1.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.1.2" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="2.1.1" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="2.1.1" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="2.1.2" />
<PackageReference Include="SimpleCrypto.NetCore" Version="1.0.0" />
</ItemGroup>

View File

@ -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<TEntity>(this IQueryable<TEntity> 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<TEntity>(queryModel);
var sql = modelVisitor.Queries.First().ToString();
return sql;
}
#endif
}
}

View File

@ -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(),
})
});

View File

@ -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();