[application] added chat context to profile page

[iw4script] reworked balance to balance based on performance rating
[stats] log penalty context to database
This commit is contained in:
RaidMax 2018-06-05 16:31:36 -05:00
parent ba023ceeb5
commit e60f612f95
37 changed files with 1390 additions and 170 deletions

View File

@ -30,6 +30,17 @@ namespace IW4MAdmin.Application.EventParsers
}
}
if(cleanedEventLine.Contains("JoinTeam"))
{
return new GameEvent()
{
Type = GameEvent.EventType.JoinTeam,
Data = cleanedEventLine,
//Origin = server.GetPlayersAsList().First(c => c.NetworkId == lineSplit[1].ConvertLong()),
Owner = server
};
}
if (cleanedEventLine == "say" || cleanedEventLine == "sayteam")
{
string message = lineSplit[4].Replace("\x15", "");

View File

@ -33,7 +33,8 @@ namespace IW4MAdmin.Application
gameEvent.Type == GameEvent.EventType.Damage ||
gameEvent.Type == GameEvent.EventType.ScriptDamage ||
gameEvent.Type == GameEvent.EventType.ScriptKill ||
gameEvent.Type == GameEvent.EventType.MapChange)
gameEvent.Type == GameEvent.EventType.MapChange ||
gameEvent.Type == GameEvent.EventType.JoinTeam)
{
#if DEBUG
Manager.GetLogger().WriteDebug($"Added sensitive event to queue");

View File

@ -228,16 +228,16 @@ namespace IW4MAdmin
Player Leaving = Players[cNum];
Logger.WriteInfo($"Client {Leaving} disconnecting...");
Leaving.TotalConnectionTime += (int)(DateTime.UtcNow - Leaving.ConnectionTime).TotalSeconds;
Leaving.LastConnection = DateTime.UtcNow;
await Manager.GetClientService().Update(Leaving);
Players[cNum] = null;
var e = new GameEvent(GameEvent.EventType.Disconnect, "", Leaving, null, this);
Manager.GetEventHandler().AddEvent(e);
// wait until the disconnect event is complete
e.OnProcessed.Wait();
Leaving.TotalConnectionTime += (int)(DateTime.UtcNow - Leaving.ConnectionTime).TotalSeconds;
Leaving.LastConnection = DateTime.UtcNow;
await Manager.GetClientService().Update(Leaving);
Players[cNum] = null;
}
}
@ -739,7 +739,10 @@ namespace IW4MAdmin
public async Task Initialize()
{
RconParser = ServerConfig.UseT6MParser ? (IRConParser)new T6MRConParser() : new IW3RConParser();
RconParser = ServerConfig.UseT6MParser ?
(IRConParser)new T6MRConParser() :
new IW3RConParser();
if (ServerConfig.UseIW5MParser)
RconParser = new IW5MRConParser();

View File

@ -1,5 +1,6 @@
using SharedLibraryCore;
using SharedLibraryCore.Objects;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
@ -9,55 +10,187 @@ namespace IW4ScriptCommands.Commands
{
class Balance : Command
{
private class TeamAssignment
{
public IW4MAdmin.Plugins.Stats.IW4Info.Team CurrentTeam { get; set; }
public int Num { get; set; }
public IW4MAdmin.Plugins.Stats.Models.EFClientStatistics Stats { get; set; }
}
public Balance() : base("balance", "balance teams", "bal", Player.Permission.Trusted, false, null)
{
}
public override async Task ExecuteAsync(GameEvent E)
{
List<string> teamAssignments = new List<string>();
string teamsString = (await E.Owner.GetDvarAsync<string>("sv_iw4madmin_teams")).Value;
var clients = E.Owner.GetPlayersAsList().Select(c => new
var scriptClientTeams = teamsString.Split(';', StringSplitOptions.RemoveEmptyEntries)
.Select(c => c.Split(','))
.Select(c => new TeamAssignment()
{
Num = c.ClientNumber,
Elo = IW4MAdmin.Plugins.Stats.Plugin.Manager.GetClientStats(c.ClientId, E.Owner.GetHashCode()).EloRating,
CurrentTeam = IW4MAdmin.Plugins.Stats.Plugin.Manager.GetClientStats(c.ClientId, E.Owner.GetHashCode()).Team
CurrentTeam = (IW4MAdmin.Plugins.Stats.IW4Info.Team)Enum.Parse(typeof(IW4MAdmin.Plugins.Stats.IW4Info.Team), c[1]),
Num = E.Owner.Players.FirstOrDefault(p => p?.NetworkId == c[0].ConvertLong())?.ClientNumber ?? -1,
Stats = IW4MAdmin.Plugins.Stats.Plugin.Manager.GetClientStats(E.Owner.Players.FirstOrDefault(p => p?.NetworkId == c[0].ConvertLong()).ClientId, E.Owner.GetHashCode())
})
.OrderByDescending(c => c.Elo)
.ToList();
int team = 0;
for (int i = 0; i < clients.Count(); i++)
// at least one team is full so we can't balance
if (scriptClientTeams.Count(ct => ct.CurrentTeam == IW4MAdmin.Plugins.Stats.IW4Info.Team.Axis) >= Math.Floor(E.Owner.MaxClients / 2.0)
|| scriptClientTeams.Count(ct => ct.CurrentTeam == IW4MAdmin.Plugins.Stats.IW4Info.Team.Allies) >= Math.Floor(E.Owner.MaxClients / 2.0))
{
if (i == 0)
{
team = 1;
continue;
}
if (i == 1)
{
team = 2;
continue;
}
if (i == 2)
{
team = 2;
continue;
}
if (i % 2 == 0)
{
if (team == 1)
team = 2;
else
team = 1;
await E.Origin?.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_BALANCE_FAIL"]);
return;
}
teamAssignments.Add($"{clients[i].Num},{team}");
List<string> teamAssignments = new List<string>();
var activeClients = E.Owner.GetPlayersAsList().Select(c => new TeamAssignment()
{
Num = c.ClientNumber,
Stats = IW4MAdmin.Plugins.Stats.Plugin.Manager.GetClientStats(c.ClientId, E.Owner.GetHashCode()),
CurrentTeam = IW4MAdmin.Plugins.Stats.Plugin.Manager.GetClientStats(c.ClientId, E.Owner.GetHashCode()).Team
})
.Where(c => scriptClientTeams.FirstOrDefault(sc => sc.Num == c.Num)?.CurrentTeam != IW4MAdmin.Plugins.Stats.IW4Info.Team.Spectator)
.Where(c => c.CurrentTeam != scriptClientTeams.FirstOrDefault(p => p.Num == c.Num)?.CurrentTeam)
.OrderByDescending(c => c.Stats.Performance)
.ToList();
var alliesTeam = scriptClientTeams
.Where(c => c.CurrentTeam == IW4MAdmin.Plugins.Stats.IW4Info.Team.Allies)
.Where(c => activeClients.Count(t => t.Num == c.Num) == 0)
.ToList();
var axisTeam = scriptClientTeams
.Where(c => c.CurrentTeam == IW4MAdmin.Plugins.Stats.IW4Info.Team.Axis)
.Where(c => activeClients.Count(t => t.Num == c.Num) == 0)
.ToList();
while (activeClients.Count() > 0)
{
int teamSizeDifference = alliesTeam.Count - axisTeam.Count;
double performanceDisparity = alliesTeam.Count > 0 ? alliesTeam.Average(t => t.Stats.Performance) : 0 -
axisTeam.Count > 0 ? axisTeam.Average(t => t.Stats.Performance) : 0;
if (teamSizeDifference == 0)
{
if (performanceDisparity == 0)
{
alliesTeam.Add(activeClients.First());
activeClients.RemoveAt(0);
}
else
{
if (performanceDisparity > 0)
{
axisTeam.Add(activeClients.First());
activeClients.RemoveAt(0);
}
else
{
alliesTeam.Add(activeClients.First());
activeClients.RemoveAt(0);
}
}
}
else if (teamSizeDifference > 0)
{
if (performanceDisparity > 0)
{
axisTeam.Add(activeClients.First());
activeClients.RemoveAt(0);
}
else
{
axisTeam.Add(activeClients.Last());
activeClients.RemoveAt(activeClients.Count - 1);
}
}
else
{
if (performanceDisparity > 0)
{
alliesTeam.Add(activeClients.First());
activeClients.RemoveAt(0);
}
else
{
alliesTeam.Add(activeClients.Last());
activeClients.RemoveAt(activeClients.Count - 1);
}
}
}
alliesTeam = alliesTeam.OrderByDescending(t => t.Stats.Performance)
.ToList();
axisTeam = axisTeam.OrderByDescending(t => t.Stats.Performance)
.ToList();
while (Math.Abs(alliesTeam.Count - axisTeam.Count) > 1)
{
int teamSizeDifference = alliesTeam.Count - axisTeam.Count;
double performanceDisparity = alliesTeam.Count > 0 ? alliesTeam.Average(t => t.Stats.Performance) : 0 -
axisTeam.Count > 0 ? axisTeam.Average(t => t.Stats.Performance) : 0;
if (teamSizeDifference > 0)
{
if (performanceDisparity > 0)
{
axisTeam.Add(alliesTeam.First());
alliesTeam.RemoveAt(0);
}
else
{
axisTeam.Add(alliesTeam.Last());
alliesTeam.RemoveAt(axisTeam.Count - 1);
}
}
else
{
if (performanceDisparity > 0)
{
alliesTeam.Add(axisTeam.Last());
axisTeam.RemoveAt(axisTeam.Count - 1);
}
else
{
alliesTeam.Add(axisTeam.First());
axisTeam.RemoveAt(0);
}
}
}
foreach (var assignment in alliesTeam)
{
teamAssignments.Add($"{assignment.Num},2");
assignment.Stats.Team = IW4MAdmin.Plugins.Stats.IW4Info.Team.Allies;
}
foreach (var assignment in axisTeam)
{
teamAssignments.Add($"{assignment.Num},3");
assignment.Stats.Team = IW4MAdmin.Plugins.Stats.IW4Info.Team.Axis;
}
if (alliesTeam.Count(ac => scriptClientTeams.First(sc => sc.Num == ac.Num).CurrentTeam != ac.CurrentTeam) == 0 &&
axisTeam.Count(ac => scriptClientTeams.First(sc => sc.Num == ac.Num).CurrentTeam != ac.CurrentTeam) == 0)
{
await E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_BALANCE_FAIL_BALANCED"]);
return;
}
if (E.Origin?.Level > Player.Permission.Administrator)
{
await E.Origin.Tell($"Allies Elo: {(alliesTeam.Count > 0 ? alliesTeam.Average(t => t.Stats.Performance) : 0)}");
await E.Origin.Tell($"Axis Elo: {(axisTeam.Count > 0 ? axisTeam.Average(t => t.Stats.Performance) : 0)}");
}
string args = string.Join(",", teamAssignments);
await E.Owner.SetDvarAsync("sv_iw4madmin_commandargs", args);
await E.Owner.ExecuteCommandAsync("sv_iw4madmin_command balance");
await E.Owner.ExecuteCommandAsync($"sv_iw4madmin_command \"balance:{args}\"");
await E.Origin.Tell("Balance command sent");
}
}

View File

@ -15,7 +15,20 @@ namespace IW4ScriptCommands
public string Author => "RaidMax";
public Task OnEventAsync(GameEvent E, Server S) => Task.CompletedTask;
public Task OnEventAsync(GameEvent E, Server S)
{
if (E.Type == GameEvent.EventType.JoinTeam || E.Type == GameEvent.EventType.Disconnect)
{
E.Origin = new SharedLibraryCore.Objects.Player()
{
ClientId = 1,
CurrentServer = E.Owner
};
return new Commands.Balance().ExecuteAsync(E);
}
return Task.CompletedTask;
}
public Task OnLoadAsync(IManager manager) => Task.CompletedTask;

View File

@ -19,16 +19,18 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
Strain
};
public ChangeTracking<EFACSnapshot> Tracker { get; private set; }
int Kills;
int HitCount;
Dictionary<IW4Info.HitLocation, int> HitLocationCount;
ChangeTracking Tracker;
double AngleDifferenceAverage;
EFClientStatistics ClientStats;
DateTime LastHit;
long LastOffset;
ILogger Log;
Strain Strain;
DateTime ConnectionTime = DateTime.UtcNow;
public Detection(ILogger log, EFClientStatistics clientStats)
{
@ -38,7 +40,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
HitLocationCount.Add((IW4Info.HitLocation)loc, 0);
ClientStats = clientStats;
Strain = new Strain();
Tracker = new ChangeTracking();
Tracker = new ChangeTracking<EFACSnapshot>();
}
/// <summary>
@ -351,16 +353,33 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
#endregion
#endregion
Tracker.OnChange(new DetectionTracking(ClientStats, kill, Strain));
if (result != null)
Tracker.OnChange(new EFACSnapshot()
{
foreach (string change in Tracker.GetChanges())
{
Log.WriteDebug(change);
Log.WriteDebug("--------------SNAPSHOT END-----------");
}
}
Active = true,
When = kill.When,
ClientId = ClientStats.ClientId,
SessionAngleOffset = AngleDifferenceAverage,
CurrentSessionLength = (int)(DateTime.UtcNow - ConnectionTime).TotalSeconds,
CurrentStrain = currentStrain,
CurrentViewAngle = kill.ViewAngles,
Hits = HitCount,
Kills = Kills,
Deaths = ClientStats.SessionDeaths,
HitDestination = kill.DeathOrigin,
HitOrigin = kill.KillOrigin,
EloRating = ClientStats.EloRating,
HitLocation = kill.HitLoc,
LastStrainAngle = Strain.LastAngle,
PredictedViewAngles = kill.AnglesList,
// this is in "meters"
Distance = kill.Distance,
SessionScore = ClientStats.SessionScore,
HitType = kill.DeathType,
SessionSPM = ClientStats.SessionSPM,
StrainAngleBetween = Strain.LastDistance,
TimeSinceLastEvent = (int)Strain.LastDeltaTime,
WeaponId = kill.Weapon
});
return result ?? new DetectionPenaltyResult()
{

View File

@ -1,9 +1,4 @@
using SharedLibraryCore.Objects;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IW4MAdmin.Plugins.Stats.Cheat
{

View File

@ -1,57 +0,0 @@
using IW4MAdmin.Plugins.Stats.Cheat;
using IW4MAdmin.Plugins.Stats.Models;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Text;
namespace IW4MAdmin.Plugins.Stats.Cheat
{
class DetectionTracking : ITrackable
{
EFClientStatistics Stats;
EFClientKill Hit;
Strain Strain;
public DetectionTracking(EFClientStatistics stats, EFClientKill hit, Strain strain)
{
Stats = stats;
Hit = hit;
Strain = strain;
}
public string GetTrackableValue()
{
var sb = new StringBuilder();
sb.AppendLine($"SPM = {Stats.SPM}");
sb.AppendLine($"KDR = {Stats.KDR}");
sb.AppendLine($"Kills = {Stats.Kills}");
sb.AppendLine($"Session Score = {Stats.SessionScore}");
sb.AppendLine($"Elo = {Stats.EloRating}");
sb.AppendLine($"Max Sess Strain = {Stats.MaxSessionStrain}");
sb.AppendLine($"MaxStrain = {Stats.MaxStrain}");
sb.AppendLine($"Avg Offset = {Stats.AverageHitOffset}");
sb.AppendLine($"TimePlayed, {Stats.TimePlayed}");
sb.AppendLine($"HitDamage = {Hit.Damage}");
sb.AppendLine($"HitOrigin = {Hit.KillOrigin}");
sb.AppendLine($"DeathOrigin = {Hit.DeathOrigin}");
sb.AppendLine($"ViewAngles = {Hit.ViewAngles}");
sb.AppendLine($"WeaponId = {Hit.Weapon.ToString()}");
sb.AppendLine($"Timeoffset = {Hit.TimeOffset}");
sb.AppendLine($"HitLocation = {Hit.HitLoc.ToString()}");
sb.AppendLine($"Distance = {Hit.Distance / 0.0254}");
sb.AppendLine($"HitType = {Hit.DeathType.ToString()}");
int i = 0;
foreach (var predictedAngle in Hit.AnglesList)
{
sb.AppendLine($"Predicted Angle [{i}] {predictedAngle}");
i++;
}
sb.AppendLine(Strain.GetTrackableValue());
sb.AppendLine($"VictimId = {Hit.VictimId}");
sb.AppendLine($"AttackerId = {Hit.AttackerId}");
return sb.ToString();
}
}
}

View File

@ -6,13 +6,13 @@ using System.Text;
namespace IW4MAdmin.Plugins.Stats.Cheat
{
class Strain : ITrackable
class Strain
{
private const double StrainDecayBase = 0.9;
private double CurrentStrain;
private Vector3 LastAngle;
private double LastDeltaTime;
private double LastDistance;
public double LastDistance { get; private set; }
public Vector3 LastAngle { get; private set; }
public double LastDeltaTime { get; private set; }
public int TimesReachedMaxStrain { get; private set; }
@ -53,11 +53,6 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
return CurrentStrain;
}
public string GetTrackableValue()
{
return $"Strain = {CurrentStrain}\r\n, Angle = {LastAngle}\r\n, Delta Time = {LastDeltaTime}\r\n, Angle Between = {LastDistance}";
}
private double GetDecay(double deltaTime) => Math.Pow(StrainDecayBase, Math.Pow(2.0, deltaTime / 250.0) / 1000.0);
}
}

View File

@ -26,30 +26,24 @@ namespace IW4MAdmin.Plugins.Stats.Commands
{
var loc = Utilities.CurrentLocalization.LocalizationIndex;
/*if (E.Target?.ClientNumber < 0)
{
await E.Origin.Tell(loc["PLUGINS_STATS_COMMANDS_VIEW_FAIL_INGAME"]);
return;
}
if (E.Origin.ClientNumber < 0 && E.Target == null)
{
await E.Origin.Tell(loc["PLUGINS_STATS_COMMANDS_VIEW_FAIL_INGAME_SELF"]);
return;
}*/
String statLine;
EFClientStatistics pStats;
if (E.Data.Length > 0 && E.Target == null)
{
E.Target = E.Owner.GetClientByName(E.Data).FirstOrDefault();
if (E.Target == null)
{
await E.Origin.Tell(loc["PLUGINS_STATS_COMMANDS_VIEW_FAIL"]);
return;
}
}
var clientStats = new GenericRepository<EFClientStatistics>();
int serverId = E.Owner.GetHashCode();
if (E.Target != null)
{
pStats = clientStats.Find(c => c.ServerId == serverId && c.ClientId == E.Target.ClientId).First();

View File

@ -429,12 +429,25 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
{
async Task executePenalty(Cheat.DetectionPenaltyResult penalty)
{
// prevent multiple bans from occuring
if (attacker.Level == Player.Permission.Banned)
// prevent multiple bans/flags from occuring
if (attacker.Level != Player.Permission.User)
{
return;
}
// this happens when a client is detected as cheating
if (penalty.ClientPenalty != Penalty.PenaltyType.Any)
{
using (var ctx = new DatabaseContext())
{
foreach (var change in clientDetection.Tracker.GetChanges())
{
ctx.Add(change);
}
await ctx.SaveChangesAsync();
}
}
switch (penalty.ClientPenalty)
{
case Penalty.PenaltyType.Ban:
@ -453,8 +466,6 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
});
break;
case Penalty.PenaltyType.Flag:
if (attacker.Level != Player.Permission.User)
break;
var e = new GameEvent()
{
Data = penalty.Type == Cheat.Detection.DetectionType.Bone ?
@ -824,6 +835,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
double spmMultiplier = 2.934 * Math.Pow(Servers[clientStats.ServerId].TeamCount(clientStats.Team == IW4Info.Team.Allies ? IW4Info.Team.Axis : IW4Info.Team.Allies), -0.454);
killSPM *= Math.Max(1, spmMultiplier);
// update this for ac tracking
clientStats.SessionSPM = killSPM;
// calculate how much the KDR should weigh
// 1.637 is a Eddie-Generated number that weights the KDR nicely
double currentKDR = clientStats.SessionDeaths == 0 ? clientStats.SessionKills : clientStats.SessionKills / clientStats.SessionDeaths;

View File

@ -10,9 +10,10 @@ namespace IW4MAdmin.Plugins.Stats
{
public enum Team
{
None,
Spectator,
Axis,
Allies
Allies,
Axis
}
public enum MeansOfDeath

View File

@ -0,0 +1,43 @@
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Helpers;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace IW4MAdmin.Plugins.Stats.Models
{
/// <summary>
/// This class houses the information for anticheat snapshots (used for validating a ban)
/// </summary>
public class EFACSnapshot : SharedEntity
{
[Key]
public int SnapshotId { get; set; }
public int ClientId { get; set; }
[ForeignKey("ClientId")]
public EFClient Client { get; set; }
public DateTime When { get; set; }
public int CurrentSessionLength { get; set; }
public int TimeSinceLastEvent { get; set; }
public double EloRating { get; set; }
public int SessionScore { get; set; }
public double SessionSPM { get; set; }
public int Hits { get; set; }
public int Kills { get; set; }
public int Deaths { get; set; }
public double CurrentStrain { get; set; }
public double StrainAngleBetween { get; set; }
public double SessionAngleOffset { get; set; }
public Vector3 LastStrainAngle { get; set; }
public Vector3 HitOrigin { get; set; }
public Vector3 HitDestination { get; set; }
public double Distance { get; set; }
public Vector3 CurrentViewAngle { get; set; }
public IW4Info.WeaponName WeaponId { get; set; }
public IW4Info.HitLocation HitLocation { get; set; }
public IW4Info.MeansOfDeath HitType { get; set; }
public virtual ICollection<Vector3> PredictedViewAngles { get; set; }
}
}

View File

@ -71,6 +71,7 @@ namespace IW4MAdmin.Plugins.Stats.Models
DeathStreak = 0;
LastScore = 0;
SessionScores.Add(0);
Team = IW4Info.Team.None;
}
[NotMapped]
public int SessionScore
@ -98,5 +99,7 @@ namespace IW4MAdmin.Plugins.Stats.Models
public IW4Info.Team Team { get; set; }
[NotMapped]
public DateTime LastStatHistoryUpdate { get; set; } = DateTime.UtcNow;
[NotMapped]
public double SessionSPM { get; set; }
}
}

View File

@ -55,6 +55,8 @@ namespace IW4MAdmin.Plugins.Stats
break;
case GameEvent.EventType.MapEnd:
break;
case GameEvent.EventType.JoinTeam:
break;
case GameEvent.EventType.Broadcast:
break;
case GameEvent.EventType.Tell:
@ -240,7 +242,8 @@ namespace IW4MAdmin.Plugins.Stats
{
Key = "EventMessage",
Value = m.Message,
When = m.TimeSent
When = m.TimeSent,
Extra = m.ServerId.ToString()
}).ToList();
messageMeta.Add(new ProfileMeta()
{

View File

@ -14,6 +14,16 @@
<Configurations>Debug;Release;Prerelease</Configurations>
</PropertyGroup>
<ItemGroup>
<None Remove="Web\Views\_MessageContext.cshtml" />
</ItemGroup>
<ItemGroup>
<Content Include="Web\Views\Stats\_MessageContext.cshtml">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
<ProjectReference Include="..\..\WebfrontCore\WebfrontCore.csproj" />

View File

@ -1,7 +1,10 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using SharedLibraryCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WebfrontCore.Controllers;
@ -24,5 +27,55 @@ namespace IW4MAdmin.Plugins.Stats.Web.Controllers
{
return View("_List", await Plugin.Manager.GetTopStats(offset, count));
}
[HttpGet]
public async Task<IActionResult> GetMessageAsync(int serverId, DateTime when)
{
var whenUpper = when.AddMinutes(5);
var whenLower = when.AddMinutes(-5);
using (var ctx = new SharedLibraryCore.Database.DatabaseContext())
{
var iqMessages = from message in ctx.Set<Models.EFClientMessage>()
where message.ServerId == serverId
where message.TimeSent >= whenLower
where message.TimeSent <= whenUpper
select new SharedLibraryCore.Dtos.ChatInfo()
{
Message = message.Message,
Name = message.Client.CurrentAlias.Name,
Time = message.TimeSent
};
var messages = await iqMessages.ToListAsync();
return View("_MessageContext", messages);
}
}
[HttpGet]
[Authorize]
public async Task<IActionResult> GetAutomatedPenaltyInfoAsync(int clientId)
{
using (var ctx = new SharedLibraryCore.Database.DatabaseContext())
{
var penaltyInfo = await ctx.Set<Models.EFACSnapshot>()
.Where(s => s.ClientId == clientId)
.Include(s => s.LastStrainAngle)
.Include(s => s.HitOrigin)
.Include(s => s.HitDestination)
.Include(s => s.CurrentViewAngle)
.Include(s => s.PredictedViewAngles)
.OrderBy(s => s.When)
.ToListAsync();
if (penaltyInfo != null)
{
return View("_PenaltyInfo", penaltyInfo);
}
return NotFound();
}
}
}
}

View File

@ -0,0 +1,12 @@
@model IEnumerable<SharedLibraryCore.Dtos.ChatInfo>
@{
Layout = null;
}
<div class="client-message-context bg-dark p-2 mt-2 mb-2 border-top border-bottom">
<h5>@Model.First().Time.ToString()</h5>
@foreach (var message in Model)
{
<span class="text-white">@message.Name</span><span> &mdash; @message.Message</span><br />
}
</div>

View File

@ -0,0 +1,29 @@
@model IEnumerable<IW4MAdmin.Plugins.Stats.Models.EFACSnapshot>
@{
Layout = null;
}
<div class="client-message-context bg-dark p-2 mt-2 mb-2 border-top border-bottom">
@foreach (var snapshot in Model)
{
<!-- this is not ideal, but I didn't want to manually write out all the properties-->
var snapProperties = typeof(IW4MAdmin.Plugins.Stats.Models.EFACSnapshot).GetProperties();
foreach (var prop in snapProperties)
{
<!-- this is another ugly hack-->
@if (prop.GetValue(snapshot) is System.Collections.Generic.HashSet<SharedLibraryCore.Helpers.Vector3>)
{
<span class="text-white">@prop.Name </span>
foreach (var v in (System.Collections.Generic.HashSet<SharedLibraryCore.Helpers.Vector3>)prop.GetValue(snapshot))
{
<span>@v.ToString(),</span><br />
}
}
else
{
<span class="text-white">@prop.Name </span> <span>&mdash; @prop.GetValue(snapshot)</span><br />
}
}
<div class="w-100 mt-1 mb-1 border-bottom"></div>
}
</div>

View File

@ -1255,11 +1255,18 @@ namespace SharedLibraryCore.Commands
{
string mapRotation = (await s.GetDvarAsync<string>("sv_mapRotation")).Value.ToLower();
var regexMatches = Regex.Matches(mapRotation, @"(gametype +([a-z]{1,4}))? *map ([a-z|_]+)", RegexOptions.IgnoreCase).ToList();
// find the current map in the rotation
var currentMap = regexMatches.Where(m => m.Groups[3].ToString() == s.CurrentMap.Name);
var lastMap = regexMatches.LastOrDefault();
Map nextMap = null;
// no maprotation at all
if (regexMatches.Count() == 0)
{
return $"{Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_NEXTMAP_SUCCESS"]} ^5{s.CurrentMap.Alias}/{Utilities.GetLocalizedGametype(s.Gametype)}";
}
// the current map is not in rotation
if (currentMap.Count() == 0)
{

View File

@ -12,6 +12,7 @@ namespace SharedLibraryCore.Dtos
public string WhenString => Utilities.GetTimePassed(When, false);
public string Key { get; set; }
public dynamic Value { get; set; }
public string Extra { get; set; }
public virtual string Class => Value.GetType().ToString();
}
}

View File

@ -5,5 +5,6 @@ namespace SharedLibraryCore.Dtos
{
public bool Sensitive { get; set; }
public bool Show { get; set; } = true;
public int Id {get;set;}
}
}

View File

@ -38,6 +38,7 @@ namespace SharedLibraryCore
Kill,
Damage,
Death,
JoinTeam,
}
public GameEvent(EventType t, string d, Player O, Player T, Server S)

View File

@ -5,27 +5,27 @@ using System.Text;
namespace SharedLibraryCore.Helpers
{
public class ChangeTracking
/// <summary>
/// This class provides a way to keep track of changes to an entity
/// </summary>
/// <typeparam name="T">Type of entity to keep track of changes to</typeparam>
public class ChangeTracking<T>
{
List<string> Values;
List<T> Values;
public ChangeTracking()
{
Values = new List<string>();
Values = new List<T>();
}
public void OnChange(ITrackable value)
public void OnChange(T value)
{
// clear the first value when count max count reached
if (Values.Count > 30)
Values.RemoveAt(0);
Values.Add($"{DateTime.Now.ToString("HH:mm:ss.fff")} {value.GetTrackableValue()}");
Values.Add(value);
}
public void ClearChanges()
{
Values.Clear();
}
public string[] GetChanges() => Values.ToArray();
public List<T> GetChanges() => Values;
}
}

View File

@ -16,6 +16,11 @@ namespace SharedLibraryCore.Helpers
public float Y { get; protected set; }
public float Z { get; protected set; }
// this is for EF and really should be somewhere else
public Vector3()
{
}
public Vector3(float x, float y, float z)
{
X = x;

View File

@ -1,11 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace SharedLibraryCore.Interfaces
{
public interface ITrackable
{
string GetTrackableValue();
}
}

View File

@ -0,0 +1,639 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal;
using SharedLibraryCore.Database;
using SharedLibraryCore.Objects;
using System;
namespace SharedLibraryCore.Migrations
{
[DbContext(typeof(DatabaseContext))]
[Migration("20180605191706_AddEFACSnapshots")]
partial class AddEFACSnapshots
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.0.2-rtm-10011");
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<int>("HitLoc");
b.Property<int?>("KillOriginVector3Id");
b.Property<int>("Map");
b.Property<int>("ServerId");
b.Property<int>("VictimId");
b.Property<int?>("ViewAnglesVector3Id");
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.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("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.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,137 @@
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
namespace SharedLibraryCore.Migrations
{
public partial class AddEFACSnapshots : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "EFACSnapshotSnapshotId",
table: "Vector3",
nullable: true);
migrationBuilder.CreateTable(
name: "EFACSnapshot",
columns: table => new
{
SnapshotId = table.Column<int>(nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Active = table.Column<bool>(nullable: false),
ClientId = table.Column<int>(nullable: false),
CurrentSessionLength = table.Column<int>(nullable: false),
CurrentStrain = table.Column<double>(nullable: false),
CurrentViewAngleVector3Id = table.Column<int>(nullable: true),
Deaths = table.Column<int>(nullable: false),
Distance = table.Column<double>(nullable: false),
EloRating = table.Column<double>(nullable: false),
HitDestinationVector3Id = table.Column<int>(nullable: true),
HitLocation = table.Column<int>(nullable: false),
HitOriginVector3Id = table.Column<int>(nullable: true),
HitType = table.Column<int>(nullable: false),
Hits = table.Column<int>(nullable: false),
Kills = table.Column<int>(nullable: false),
LastStrainAngleVector3Id = table.Column<int>(nullable: true),
SessionAngleOffset = table.Column<double>(nullable: false),
SessionSPM = table.Column<double>(nullable: false),
SessionScore = table.Column<int>(nullable: false),
StrainAngleBetween = table.Column<double>(nullable: false),
TimeSinceLastEvent = table.Column<int>(nullable: false),
WeaponId = table.Column<int>(nullable: false),
When = table.Column<DateTime>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_EFACSnapshot", x => x.SnapshotId);
table.ForeignKey(
name: "FK_EFACSnapshot_EFClients_ClientId",
column: x => x.ClientId,
principalTable: "EFClients",
principalColumn: "ClientId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_EFACSnapshot_Vector3_CurrentViewAngleVector3Id",
column: x => x.CurrentViewAngleVector3Id,
principalTable: "Vector3",
principalColumn: "Vector3Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_EFACSnapshot_Vector3_HitDestinationVector3Id",
column: x => x.HitDestinationVector3Id,
principalTable: "Vector3",
principalColumn: "Vector3Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_EFACSnapshot_Vector3_HitOriginVector3Id",
column: x => x.HitOriginVector3Id,
principalTable: "Vector3",
principalColumn: "Vector3Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_EFACSnapshot_Vector3_LastStrainAngleVector3Id",
column: x => x.LastStrainAngleVector3Id,
principalTable: "Vector3",
principalColumn: "Vector3Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_Vector3_EFACSnapshotSnapshotId",
table: "Vector3",
column: "EFACSnapshotSnapshotId");
migrationBuilder.CreateIndex(
name: "IX_EFACSnapshot_ClientId",
table: "EFACSnapshot",
column: "ClientId");
migrationBuilder.CreateIndex(
name: "IX_EFACSnapshot_CurrentViewAngleVector3Id",
table: "EFACSnapshot",
column: "CurrentViewAngleVector3Id");
migrationBuilder.CreateIndex(
name: "IX_EFACSnapshot_HitDestinationVector3Id",
table: "EFACSnapshot",
column: "HitDestinationVector3Id");
migrationBuilder.CreateIndex(
name: "IX_EFACSnapshot_HitOriginVector3Id",
table: "EFACSnapshot",
column: "HitOriginVector3Id");
migrationBuilder.CreateIndex(
name: "IX_EFACSnapshot_LastStrainAngleVector3Id",
table: "EFACSnapshot",
column: "LastStrainAngleVector3Id");
/* migrationBuilder.AddForeignKey(
name: "FK_Vector3_EFACSnapshot_EFACSnapshotSnapshotId",
table: "Vector3",
column: "EFACSnapshotSnapshotId",
principalTable: "EFACSnapshot",
principalColumn: "SnapshotId",
onDelete: ReferentialAction.Restrict);*/
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Vector3_EFACSnapshot_EFACSnapshotSnapshotId",
table: "Vector3");
migrationBuilder.DropTable(
name: "EFACSnapshot");
migrationBuilder.DropIndex(
name: "IX_Vector3_EFACSnapshotSnapshotId",
table: "Vector3");
migrationBuilder.DropColumn(
name: "EFACSnapshotSnapshotId",
table: "Vector3");
}
}
}

View File

@ -20,6 +20,70 @@ namespace SharedLibraryCore.Migrations
modelBuilder
.HasAnnotation("ProductVersion", "2.0.2-rtm-10011");
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")
@ -374,6 +438,8 @@ namespace SharedLibraryCore.Migrations
b.Property<int>("Vector3Id")
.ValueGeneratedOnAdd();
b.Property<int?>("EFACSnapshotSnapshotId");
b.Property<float>("X");
b.Property<float>("Y");
@ -382,9 +448,35 @@ namespace SharedLibraryCore.Migrations
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")
@ -533,6 +625,13 @@ namespace SharedLibraryCore.Migrations
.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

@ -157,6 +157,7 @@ namespace SharedLibraryCore.Services
Key = "Event.Penalty",
Value = new PenaltyInfo
{
Id = penalty.PenaltyId,
OffenderName = victimAlias.Name,
OffenderId = victimClient.ClientId,
PunisherName = punisherAlias.Name,
@ -203,6 +204,7 @@ namespace SharedLibraryCore.Services
Key = "Event.Penalty",
Value = new PenaltyInfo
{
Id = penalty.PenaltyId,
OffenderName = victimAlias.Name,
OffenderId = victimClient.ClientId,
PunisherName = punisherAlias.Name,

View File

@ -38,6 +38,7 @@ namespace WebfrontCore.Controllers
var penaltiesDto = penalties.Select(p => new PenaltyInfo()
{
Id = p.PenaltyId,
OffenderId = p.OffenderId,
Offense = p.Offense,
PunisherId = p.PunisherId,

View File

@ -15,6 +15,7 @@ namespace WebfrontCore.ViewComponents
var penalties = await Program.Manager.GetPenaltyService().GetRecentPenalties(12, offset, showOnly);
var penaltiesDto = penalties.Select(p => new PenaltyInfo()
{
Id = p.PenaltyId,
OffenderId = p.OffenderId,
OffenderName = p.Offender.Name,
PunisherId = p.PunisherId,

View File

@ -0,0 +1,12 @@
@model IEnumerable<SharedLibraryCore.Dtos.ChatInfo>
@{
Layout = null;
}
<div class="client-message-context bg-dark p-2 mt-2 mb-2 border-top border-bottom">
<h5>@Model.First().Time.ToString()</h5>
@foreach (var message in Model)
{
<span class="text-white">@message.Name</span><span> &mdash; @message.Message</span><br />
}
</div>

View File

@ -53,6 +53,7 @@
<PackageReference Include="Bower" Version="1.3.11" />
<PackageReference Include="BuildBundlerMinifier" Version="2.6.375" />
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.7" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.3" />
</ItemGroup>
<ItemGroup>

View File

@ -201,3 +201,7 @@ select {
.client-rating-change-amount {
font-size: 1rem;
}
.client-message {
cursor:pointer;
}

View File

@ -55,6 +55,44 @@ $(document).ready(function () {
}
});
/*
* load context of chat
*/
$(document).on('click', '.client-message', function (e) {
showLoader();
const location = $(this);
$.get('/Stats/GetMessageAsync', {
'serverId': $(this).data('serverid'),
'when': $(this).data('when')
})
.done(function (response) {
$('.client-message-context').remove();
location.after(response);
hideLoader();
})
.fail(function (jqxhr, textStatus, error) {
errorLoader();
});
});
/*
* load info on ban/flag
*/
$(document).on('click', '.automated-penalty-info-detailed', function (e) {
showLoader();
const location = $(this).parent();
$.get('/Stats/GetAutomatedPenaltyInfoAsync', {
'clientId': $(this).data('clientid'),
})
.done(function (response) {
location.after(response);
hideLoader();
})
.fail(function (jqxhr, textStatus, error) {
errorLoader();
});
});
/*
get ip geolocation info into modal
*/
@ -171,10 +209,10 @@ function loadMeta(meta) {
const timeRemaining = meta.value.type === 'TempBan' && meta.value.timeRemaining.length > 0 ?
`(${meta.value.timeRemaining} remaining)` :
'';
eventString = `<div><span class="penalties-color-${meta.value.type.toLowerCase()}">${penaltyToName(meta.value.type)}</span> by <span class="text-highlight"> <a class="link-inverse" href="${meta.value.punisherId}">${meta.value.punisherName}</a></span > for <span style="color: white; ">${meta.value.offense}</span><span class="text-muted"> ${timeRemaining}</span></div>`;
eventString = `<div><span class="penalties-color-${meta.value.type.toLowerCase()}">${penaltyToName(meta.value.type)}</span> by <span class="text-highlight"> <a class="link-inverse" href="${meta.value.punisherId}">${meta.value.punisherName}</a></span > for <span style="color: white; " class="automated-penalty-info-detailed" data-clientid="${meta.value.offenderId}">${meta.value.offense}</span><span class="text-muted"> ${timeRemaining}</span></div>`;
}
else {
eventString = `<div><span class="penalties-color-${meta.value.type.toLowerCase()}">${penaltyToName(meta.value.type)} </span> <span class="text-highlight"><a class="link-inverse" href="${meta.value.offenderId}"> ${meta.value.offenderName}</a></span > for <span style="color: white; ">${meta.value.offense}</span></div>`;
eventString = `<div><span class="penalties-color-${meta.value.type.toLowerCase()}">${penaltyToName(meta.value.type)} </span> <span class="text-highlight"><a class="link-inverse" href="${meta.value.offenderId}"> ${meta.value.offenderName}</a></span > for <span style="color: white;" class="automated-penalty-info-detailed" data-clientid="${meta.value.offenderId}">${meta.value.offense}</span></div>`;
}
}
else if (meta.key.includes("Alias")) {
@ -182,7 +220,7 @@ function loadMeta(meta) {
}
// it's a message
else if (meta.key.includes("Event")) {
eventString = `<div><span style="color:white;">></span><span class="text-muted"> ${meta.value}</span></div>`;
eventString = `<div><span style="color:white;">></span><span class="client-message text-muted" data-serverid="${meta.extra}" data-when="${meta.when}"> ${meta.value}</span></div>`;
}
$('#profile_events').append(eventString);
}

View File

@ -36,12 +36,19 @@ BalanceTeams(commandArgs)
for (i = 0; i < commandArgs.size; i+= 2)
{
newTeam = i + 1 = "1" ? axis : allies;
player = level.players[i];
teamNum = commandArgs[i+1];
clientNum = commandArgs[i];
if (teamNum == "0")
newTeam = "allies";
else
newTeam = "axis";
player = level.players[clientNum];
if (!isPlayer(player))
continue;
iPrintLnBold(player.name + " " + teamNum);
switch (newTeam)
{
case "axis":