From e60f612f955b9195c74f40636b180a490cda1538 Mon Sep 17 00:00:00 2001 From: RaidMax Date: Tue, 5 Jun 2018 16:31:36 -0500 Subject: [PATCH] [application] added chat context to profile page [iw4script] reworked balance to balance based on performance rating [stats] log penalty context to database --- Application/EventParsers/IW4EventParser.cs | 11 + Application/GameEventHandler.cs | 3 +- Application/Server.cs | 15 +- Plugins/IW4ScriptCommands/Commands/Balance.cs | 187 ++++- Plugins/IW4ScriptCommands/Plugin.cs | 15 +- Plugins/Stats/Cheat/Detection.cs | 41 +- Plugins/Stats/Cheat/DetectionPenaltyResult.cs | 5 - Plugins/Stats/Cheat/DetectionTracking.cs | 57 -- Plugins/Stats/Cheat/Strain.cs | 15 +- Plugins/Stats/Commands/ViewStats.cs | 22 +- Plugins/Stats/Helpers/StatManager.cs | 24 +- Plugins/Stats/IW4Info.cs | 5 +- Plugins/Stats/Models/EFACSnapshot.cs | 43 ++ Plugins/Stats/Models/EFClientStatistics.cs | 3 + Plugins/Stats/Plugin.cs | 5 +- Plugins/Stats/Stats.csproj | 10 + .../Stats/Web/Controllers/StatsController.cs | 55 +- .../Web/Views/Stats/_MessageContext.cshtml | 12 + .../Stats/Web/Views/Stats/_PenaltyInfo.cshtml | 29 + SharedLibraryCore/Commands/NativeCommands.cs | 7 + SharedLibraryCore/Dtos/ProfileMeta.cs | 1 + SharedLibraryCore/Dtos/SharedInfo.cs | 3 +- SharedLibraryCore/Event.cs | 1 + SharedLibraryCore/Helpers/ChangeTracking.cs | 22 +- SharedLibraryCore/Helpers/Vector3.cs | 5 + SharedLibraryCore/Interfaces/ITrackable.cs | 11 - ...0180605191706_AddEFACSnapshots.Designer.cs | 639 ++++++++++++++++++ .../20180605191706_AddEFACSnapshots.cs | 137 ++++ .../DatabaseContextModelSnapshot.cs | 99 +++ SharedLibraryCore/Services/PenaltyService.cs | 2 + WebfrontCore/Controllers/PenaltyController.cs | 1 + .../PenaltyListViewComponent.cs | 1 + .../Views/Client/_MessageContext.cshtml | 12 + WebfrontCore/WebfrontCore.csproj | 1 + .../wwwroot/css/bootstrap-custom.scss | 4 + WebfrontCore/wwwroot/js/profile.js | 46 +- _commands.gsc | 11 +- 37 files changed, 1390 insertions(+), 170 deletions(-) delete mode 100644 Plugins/Stats/Cheat/DetectionTracking.cs create mode 100644 Plugins/Stats/Models/EFACSnapshot.cs create mode 100644 Plugins/Stats/Web/Views/Stats/_MessageContext.cshtml create mode 100644 Plugins/Stats/Web/Views/Stats/_PenaltyInfo.cshtml delete mode 100644 SharedLibraryCore/Interfaces/ITrackable.cs create mode 100644 SharedLibraryCore/Migrations/20180605191706_AddEFACSnapshots.Designer.cs create mode 100644 SharedLibraryCore/Migrations/20180605191706_AddEFACSnapshots.cs create mode 100644 WebfrontCore/Views/Client/_MessageContext.cshtml diff --git a/Application/EventParsers/IW4EventParser.cs b/Application/EventParsers/IW4EventParser.cs index fde869295..45eec4d38 100644 --- a/Application/EventParsers/IW4EventParser.cs +++ b/Application/EventParsers/IW4EventParser.cs @@ -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", ""); diff --git a/Application/GameEventHandler.cs b/Application/GameEventHandler.cs index ca964181a..b61209424 100644 --- a/Application/GameEventHandler.cs +++ b/Application/GameEventHandler.cs @@ -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"); diff --git a/Application/Server.cs b/Application/Server.cs index 5801cc98a..69be8fe60 100644 --- a/Application/Server.cs +++ b/Application/Server.cs @@ -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(); diff --git a/Plugins/IW4ScriptCommands/Commands/Balance.cs b/Plugins/IW4ScriptCommands/Commands/Balance.cs index d878a7cb6..2c811542c 100644 --- a/Plugins/IW4ScriptCommands/Commands/Balance.cs +++ b/Plugins/IW4ScriptCommands/Commands/Balance.cs @@ -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) { + string teamsString = (await E.Owner.GetDvarAsync("sv_iw4madmin_teams")).Value; + + var scriptClientTeams = teamsString.Split(';', StringSplitOptions.RemoveEmptyEntries) + .Select(c => c.Split(',')) + .Select(c => new TeamAssignment() + { + 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()) + }) + .ToList(); + + // 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)) + { + await E.Origin?.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_BALANCE_FAIL"]); + return; + } + List teamAssignments = new List(); - var clients = E.Owner.GetPlayersAsList().Select(c => new + var activeClients = E.Owner.GetPlayersAsList().Select(c => new TeamAssignment() { Num = c.ClientNumber, - Elo = IW4MAdmin.Plugins.Stats.Plugin.Manager.GetClientStats(c.ClientId, E.Owner.GetHashCode()).EloRating, + 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 }) - .OrderByDescending(c => c.Elo) + .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(); - int team = 0; - for (int i = 0; i < clients.Count(); i++) + 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) { - if (i == 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) { - team = 1; - continue; - } - if (i == 1) - { - team = 2; - continue; - } - if (i == 2) - { - team = 2; - continue; - } - if (i % 2 == 0) - { - if (team == 1) - team = 2; + if (performanceDisparity == 0) + { + alliesTeam.Add(activeClients.First()); + activeClients.RemoveAt(0); + } else - team = 1; + { + 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); + } } - teamAssignments.Add($"{clients[i].Num},{team}"); + 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"); } } diff --git a/Plugins/IW4ScriptCommands/Plugin.cs b/Plugins/IW4ScriptCommands/Plugin.cs index 4b84874a1..36888d932 100644 --- a/Plugins/IW4ScriptCommands/Plugin.cs +++ b/Plugins/IW4ScriptCommands/Plugin.cs @@ -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; diff --git a/Plugins/Stats/Cheat/Detection.cs b/Plugins/Stats/Cheat/Detection.cs index f02ab3747..ad70a1d5a 100644 --- a/Plugins/Stats/Cheat/Detection.cs +++ b/Plugins/Stats/Cheat/Detection.cs @@ -19,16 +19,18 @@ namespace IW4MAdmin.Plugins.Stats.Cheat Strain }; + public ChangeTracking Tracker { get; private set; } + int Kills; int HitCount; Dictionary 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(); } /// @@ -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() { diff --git a/Plugins/Stats/Cheat/DetectionPenaltyResult.cs b/Plugins/Stats/Cheat/DetectionPenaltyResult.cs index 23d4922b7..5ea3f27e0 100644 --- a/Plugins/Stats/Cheat/DetectionPenaltyResult.cs +++ b/Plugins/Stats/Cheat/DetectionPenaltyResult.cs @@ -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 { diff --git a/Plugins/Stats/Cheat/DetectionTracking.cs b/Plugins/Stats/Cheat/DetectionTracking.cs deleted file mode 100644 index c83c8fe0d..000000000 --- a/Plugins/Stats/Cheat/DetectionTracking.cs +++ /dev/null @@ -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(); - - } - } -} diff --git a/Plugins/Stats/Cheat/Strain.cs b/Plugins/Stats/Cheat/Strain.cs index f0f7b2dee..e40165aaf 100644 --- a/Plugins/Stats/Cheat/Strain.cs +++ b/Plugins/Stats/Cheat/Strain.cs @@ -6,13 +6,13 @@ using System.Text; namespace IW4MAdmin.Plugins.Stats.Cheat { - class Strain : ITrackable + class Strain { - private const double StrainDecayBase = 0.9; + 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); } } \ No newline at end of file diff --git a/Plugins/Stats/Commands/ViewStats.cs b/Plugins/Stats/Commands/ViewStats.cs index 8b37868a4..0836d13ca 100644 --- a/Plugins/Stats/Commands/ViewStats.cs +++ b/Plugins/Stats/Commands/ViewStats.cs @@ -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) { - await E.Origin.Tell(loc["PLUGINS_STATS_COMMANDS_VIEW_FAIL"]); - return; + 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(); int serverId = E.Owner.GetHashCode(); + if (E.Target != null) { pStats = clientStats.Find(c => c.ServerId == serverId && c.ClientId == E.Target.ClientId).First(); diff --git a/Plugins/Stats/Helpers/StatManager.cs b/Plugins/Stats/Helpers/StatManager.cs index 2bef137ce..fff067498 100644 --- a/Plugins/Stats/Helpers/StatManager.cs +++ b/Plugins/Stats/Helpers/StatManager.cs @@ -106,7 +106,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers LastSeen = Utilities.GetTimePassed(clientRatingsDict[s.ClientId].LastConnection, false), Name = clientRatingsDict[s.ClientId].Name, Performance = Math.Round(clientRatingsDict[s.ClientId].Performance, 2), - RatingChange = clientRatingsDict[s.ClientId].Ratings.First().Ranking - clientRatingsDict[s.ClientId].Ratings.Last().Ranking, + RatingChange = clientRatingsDict[s.ClientId].Ratings.First().Ranking - clientRatingsDict[s.ClientId].Ratings.Last().Ranking, PerformanceHistory = clientRatingsDict[s.ClientId].Ratings.Count() > 1 ? clientRatingsDict[s.ClientId].Ratings.Select(r => r.Performance).ToList() : new List() { clientRatingsDict[s.ClientId].Performance, clientRatingsDict[s.ClientId].Performance }, @@ -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; diff --git a/Plugins/Stats/IW4Info.cs b/Plugins/Stats/IW4Info.cs index ac3ce7698..b576f4a3a 100644 --- a/Plugins/Stats/IW4Info.cs +++ b/Plugins/Stats/IW4Info.cs @@ -10,9 +10,10 @@ namespace IW4MAdmin.Plugins.Stats { public enum Team { + None, Spectator, - Axis, - Allies + Allies, + Axis } public enum MeansOfDeath diff --git a/Plugins/Stats/Models/EFACSnapshot.cs b/Plugins/Stats/Models/EFACSnapshot.cs new file mode 100644 index 000000000..fedb60bc5 --- /dev/null +++ b/Plugins/Stats/Models/EFACSnapshot.cs @@ -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 +{ + /// + /// This class houses the information for anticheat snapshots (used for validating a ban) + /// + 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 PredictedViewAngles { get; set; } + } +} diff --git a/Plugins/Stats/Models/EFClientStatistics.cs b/Plugins/Stats/Models/EFClientStatistics.cs index 3ba6a7076..d0eab57f6 100644 --- a/Plugins/Stats/Models/EFClientStatistics.cs +++ b/Plugins/Stats/Models/EFClientStatistics.cs @@ -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; } } } diff --git a/Plugins/Stats/Plugin.cs b/Plugins/Stats/Plugin.cs index f159d20f2..a29d0dfa5 100644 --- a/Plugins/Stats/Plugin.cs +++ b/Plugins/Stats/Plugin.cs @@ -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() { diff --git a/Plugins/Stats/Stats.csproj b/Plugins/Stats/Stats.csproj index 7ea7c2125..0d26f2abd 100644 --- a/Plugins/Stats/Stats.csproj +++ b/Plugins/Stats/Stats.csproj @@ -14,6 +14,16 @@ Debug;Release;Prerelease + + + + + + + PreserveNewest + + + diff --git a/Plugins/Stats/Web/Controllers/StatsController.cs b/Plugins/Stats/Web/Controllers/StatsController.cs index a1d7d8022..b2b13c9f2 100644 --- a/Plugins/Stats/Web/Controllers/StatsController.cs +++ b/Plugins/Stats/Web/Controllers/StatsController.cs @@ -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 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() + 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 GetAutomatedPenaltyInfoAsync(int clientId) + { + using (var ctx = new SharedLibraryCore.Database.DatabaseContext()) + { + var penaltyInfo = await ctx.Set() + .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(); + } + } } } diff --git a/Plugins/Stats/Web/Views/Stats/_MessageContext.cshtml b/Plugins/Stats/Web/Views/Stats/_MessageContext.cshtml new file mode 100644 index 000000000..c40a9fba2 --- /dev/null +++ b/Plugins/Stats/Web/Views/Stats/_MessageContext.cshtml @@ -0,0 +1,12 @@ +@model IEnumerable +@{ + Layout = null; +} + +
+
@Model.First().Time.ToString()
+ @foreach (var message in Model) + { + @message.Name — @message.Message
+ } +
\ No newline at end of file diff --git a/Plugins/Stats/Web/Views/Stats/_PenaltyInfo.cshtml b/Plugins/Stats/Web/Views/Stats/_PenaltyInfo.cshtml new file mode 100644 index 000000000..e3771157f --- /dev/null +++ b/Plugins/Stats/Web/Views/Stats/_PenaltyInfo.cshtml @@ -0,0 +1,29 @@ +@model IEnumerable +@{ + Layout = null; +} + +
+ @foreach (var snapshot in Model) + { + + var snapProperties = typeof(IW4MAdmin.Plugins.Stats.Models.EFACSnapshot).GetProperties(); + foreach (var prop in snapProperties) + { + + @if (prop.GetValue(snapshot) is System.Collections.Generic.HashSet) + { + @prop.Name + foreach (var v in (System.Collections.Generic.HashSet)prop.GetValue(snapshot)) + { + @v.ToString(),
+ } + } + else + { + @prop.Name — @prop.GetValue(snapshot)
+ } + } +
+ } +
\ No newline at end of file diff --git a/SharedLibraryCore/Commands/NativeCommands.cs b/SharedLibraryCore/Commands/NativeCommands.cs index d4e7c6426..eb26568c8 100644 --- a/SharedLibraryCore/Commands/NativeCommands.cs +++ b/SharedLibraryCore/Commands/NativeCommands.cs @@ -1255,11 +1255,18 @@ namespace SharedLibraryCore.Commands { string mapRotation = (await s.GetDvarAsync("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) { diff --git a/SharedLibraryCore/Dtos/ProfileMeta.cs b/SharedLibraryCore/Dtos/ProfileMeta.cs index fda4492e2..7c9380520 100644 --- a/SharedLibraryCore/Dtos/ProfileMeta.cs +++ b/SharedLibraryCore/Dtos/ProfileMeta.cs @@ -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(); } } diff --git a/SharedLibraryCore/Dtos/SharedInfo.cs b/SharedLibraryCore/Dtos/SharedInfo.cs index 482a46a2a..5e142de89 100644 --- a/SharedLibraryCore/Dtos/SharedInfo.cs +++ b/SharedLibraryCore/Dtos/SharedInfo.cs @@ -5,5 +5,6 @@ namespace SharedLibraryCore.Dtos { public bool Sensitive { get; set; } public bool Show { get; set; } = true; - } + public int Id {get;set;} +} } \ No newline at end of file diff --git a/SharedLibraryCore/Event.cs b/SharedLibraryCore/Event.cs index 205b335c8..0233562a7 100644 --- a/SharedLibraryCore/Event.cs +++ b/SharedLibraryCore/Event.cs @@ -38,6 +38,7 @@ namespace SharedLibraryCore Kill, Damage, Death, + JoinTeam, } public GameEvent(EventType t, string d, Player O, Player T, Server S) diff --git a/SharedLibraryCore/Helpers/ChangeTracking.cs b/SharedLibraryCore/Helpers/ChangeTracking.cs index b535f6325..5757f4c6d 100644 --- a/SharedLibraryCore/Helpers/ChangeTracking.cs +++ b/SharedLibraryCore/Helpers/ChangeTracking.cs @@ -5,27 +5,27 @@ using System.Text; namespace SharedLibraryCore.Helpers { - public class ChangeTracking + /// + /// This class provides a way to keep track of changes to an entity + /// + /// Type of entity to keep track of changes to + public class ChangeTracking { - List Values; + List Values; public ChangeTracking() { - Values = new List(); + Values = new List(); } - 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 GetChanges() => Values; } } diff --git a/SharedLibraryCore/Helpers/Vector3.cs b/SharedLibraryCore/Helpers/Vector3.cs index 1bb147936..0c7a24c4d 100644 --- a/SharedLibraryCore/Helpers/Vector3.cs +++ b/SharedLibraryCore/Helpers/Vector3.cs @@ -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; diff --git a/SharedLibraryCore/Interfaces/ITrackable.cs b/SharedLibraryCore/Interfaces/ITrackable.cs deleted file mode 100644 index b36b1d520..000000000 --- a/SharedLibraryCore/Interfaces/ITrackable.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace SharedLibraryCore.Interfaces -{ - public interface ITrackable - { - string GetTrackableValue(); - } -} diff --git a/SharedLibraryCore/Migrations/20180605191706_AddEFACSnapshots.Designer.cs b/SharedLibraryCore/Migrations/20180605191706_AddEFACSnapshots.Designer.cs new file mode 100644 index 000000000..479412937 --- /dev/null +++ b/SharedLibraryCore/Migrations/20180605191706_AddEFACSnapshots.Designer.cs @@ -0,0 +1,639 @@ +// +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("SnapshotId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("ClientId"); + + b.Property("CurrentSessionLength"); + + b.Property("CurrentStrain"); + + b.Property("CurrentViewAngleVector3Id"); + + b.Property("Deaths"); + + b.Property("Distance"); + + b.Property("EloRating"); + + b.Property("HitDestinationVector3Id"); + + b.Property("HitLocation"); + + b.Property("HitOriginVector3Id"); + + b.Property("HitType"); + + b.Property("Hits"); + + b.Property("Kills"); + + b.Property("LastStrainAngleVector3Id"); + + b.Property("SessionAngleOffset"); + + b.Property("SessionSPM"); + + b.Property("SessionScore"); + + b.Property("StrainAngleBetween"); + + b.Property("TimeSinceLastEvent"); + + b.Property("WeaponId"); + + b.Property("When"); + + b.HasKey("SnapshotId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CurrentViewAngleVector3Id"); + + b.HasIndex("HitDestinationVector3Id"); + + b.HasIndex("HitOriginVector3Id"); + + b.HasIndex("LastStrainAngleVector3Id"); + + b.ToTable("EFACSnapshot"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientKill", b => + { + b.Property("KillId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("AttackerId"); + + b.Property("Damage"); + + b.Property("DeathOriginVector3Id"); + + b.Property("DeathType"); + + b.Property("HitLoc"); + + b.Property("KillOriginVector3Id"); + + b.Property("Map"); + + b.Property("ServerId"); + + b.Property("VictimId"); + + b.Property("ViewAnglesVector3Id"); + + b.Property("Weapon"); + + b.Property("When"); + + b.HasKey("KillId"); + + b.HasIndex("AttackerId"); + + b.HasIndex("DeathOriginVector3Id"); + + b.HasIndex("KillOriginVector3Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("VictimId"); + + b.HasIndex("ViewAnglesVector3Id"); + + b.ToTable("EFClientKills"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientMessage", b => + { + b.Property("MessageId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("ClientId"); + + b.Property("Message"); + + b.Property("ServerId"); + + b.Property("TimeSent"); + + b.HasKey("MessageId"); + + b.HasIndex("ClientId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientMessages"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientRatingHistory", b => + { + b.Property("RatingHistoryId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("ClientId"); + + b.HasKey("RatingHistoryId"); + + b.HasIndex("ClientId"); + + b.ToTable("EFClientRatingHistory"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics", b => + { + b.Property("ClientId"); + + b.Property("ServerId"); + + b.Property("Active"); + + b.Property("Deaths"); + + b.Property("EloRating"); + + b.Property("Kills"); + + b.Property("MaxStrain"); + + b.Property("RollingWeightedKDR"); + + b.Property("SPM"); + + b.Property("Skill"); + + b.Property("TimePlayed"); + + b.HasKey("ClientId", "ServerId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientStatistics"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFHitLocationCount", b => + { + b.Property("HitLocationCountId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("ClientId") + .HasColumnName("EFClientStatistics_ClientId"); + + b.Property("HitCount"); + + b.Property("HitOffsetAverage"); + + b.Property("Location"); + + b.Property("MaxAngleDistance"); + + b.Property("ServerId") + .HasColumnName("EFClientStatistics_ServerId"); + + b.HasKey("HitLocationCountId"); + + b.HasIndex("ServerId"); + + b.HasIndex("ClientId", "ServerId"); + + b.ToTable("EFHitLocationCounts"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFRating", b => + { + b.Property("RatingId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("ActivityAmount"); + + b.Property("Newest"); + + b.Property("Performance"); + + b.Property("Ranking"); + + b.Property("RatingHistoryId"); + + b.Property("ServerId"); + + b.HasKey("RatingId"); + + b.HasIndex("RatingHistoryId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFRating"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServer", b => + { + b.Property("ServerId"); + + b.Property("Active"); + + b.Property("Port"); + + b.HasKey("ServerId"); + + b.ToTable("EFServers"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServerStatistics", b => + { + b.Property("StatisticId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("ServerId"); + + b.Property("TotalKills"); + + b.Property("TotalPlayTime"); + + b.HasKey("StatisticId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerStatistics"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAlias", b => + { + b.Property("AliasId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("DateAdded"); + + b.Property("IPAddress"); + + b.Property("LinkId"); + + b.Property("Name") + .IsRequired(); + + b.HasKey("AliasId"); + + b.HasIndex("LinkId"); + + b.ToTable("EFAlias"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAliasLink", b => + { + b.Property("AliasLinkId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.HasKey("AliasLinkId"); + + b.ToTable("EFAliasLinks"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFClient", b => + { + b.Property("ClientId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("AliasLinkId"); + + b.Property("Connections"); + + b.Property("CurrentAliasId"); + + b.Property("FirstConnection"); + + b.Property("LastConnection"); + + b.Property("Level"); + + b.Property("Masked"); + + b.Property("NetworkId"); + + b.Property("Password"); + + b.Property("PasswordSalt"); + + b.Property("TotalConnectionTime"); + + b.HasKey("ClientId"); + + b.HasIndex("AliasLinkId"); + + b.HasIndex("CurrentAliasId"); + + b.HasIndex("NetworkId") + .IsUnique(); + + b.ToTable("EFClients"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFMeta", b => + { + b.Property("MetaId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("ClientId"); + + b.Property("Created"); + + b.Property("Extra"); + + b.Property("Key") + .IsRequired(); + + b.Property("Updated"); + + b.Property("Value") + .IsRequired(); + + b.HasKey("MetaId"); + + b.HasIndex("ClientId"); + + b.ToTable("EFMeta"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFPenalty", b => + { + b.Property("PenaltyId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("AutomatedOffense"); + + b.Property("Expires"); + + b.Property("LinkId"); + + b.Property("OffenderId"); + + b.Property("Offense") + .IsRequired(); + + b.Property("PunisherId"); + + b.Property("Type"); + + b.Property("When"); + + b.HasKey("PenaltyId"); + + b.HasIndex("LinkId"); + + b.HasIndex("OffenderId"); + + b.HasIndex("PunisherId"); + + b.ToTable("EFPenalties"); + }); + + modelBuilder.Entity("SharedLibraryCore.Helpers.Vector3", b => + { + b.Property("Vector3Id") + .ValueGeneratedOnAdd(); + + b.Property("EFACSnapshotSnapshotId"); + + b.Property("X"); + + b.Property("Y"); + + b.Property("Z"); + + b.HasKey("Vector3Id"); + + b.HasIndex("EFACSnapshotSnapshotId"); + + b.ToTable("Vector3"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFACSnapshot", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "CurrentViewAngle") + .WithMany() + .HasForeignKey("CurrentViewAngleVector3Id"); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "HitDestination") + .WithMany() + .HasForeignKey("HitDestinationVector3Id"); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "HitOrigin") + .WithMany() + .HasForeignKey("HitOriginVector3Id"); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "LastStrainAngle") + .WithMany() + .HasForeignKey("LastStrainAngleVector3Id"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientKill", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Attacker") + .WithMany() + .HasForeignKey("AttackerId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "DeathOrigin") + .WithMany() + .HasForeignKey("DeathOriginVector3Id"); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "KillOrigin") + .WithMany() + .HasForeignKey("KillOriginVector3Id"); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Victim") + .WithMany() + .HasForeignKey("VictimId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "ViewAngles") + .WithMany() + .HasForeignKey("ViewAnglesVector3Id"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientMessage", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientRatingHistory", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFHitLocationCount", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics") + .WithMany("HitLocations") + .HasForeignKey("ClientId", "ServerId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFRating", b => + { + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFClientRatingHistory", "RatingHistory") + .WithMany("Ratings") + .HasForeignKey("RatingHistoryId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServerStatistics", b => + { + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAlias", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFAliasLink", "Link") + .WithMany("Children") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Restrict); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFClient", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFAliasLink", "AliasLink") + .WithMany() + .HasForeignKey("AliasLinkId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("SharedLibraryCore.Database.Models.EFAlias", "CurrentAlias") + .WithMany() + .HasForeignKey("CurrentAliasId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFMeta", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client") + .WithMany("Meta") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFPenalty", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFAliasLink", "Link") + .WithMany("ReceivedPenalties") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Offender") + .WithMany("ReceivedPenalties") + .HasForeignKey("OffenderId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Punisher") + .WithMany("AdministeredPenalties") + .HasForeignKey("PunisherId") + .OnDelete(DeleteBehavior.Restrict); + }); + + modelBuilder.Entity("SharedLibraryCore.Helpers.Vector3", b => + { + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFACSnapshot") + .WithMany("PredictedViewAngles") + .HasForeignKey("EFACSnapshotSnapshotId"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SharedLibraryCore/Migrations/20180605191706_AddEFACSnapshots.cs b/SharedLibraryCore/Migrations/20180605191706_AddEFACSnapshots.cs new file mode 100644 index 000000000..399ee1f95 --- /dev/null +++ b/SharedLibraryCore/Migrations/20180605191706_AddEFACSnapshots.cs @@ -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( + name: "EFACSnapshotSnapshotId", + table: "Vector3", + nullable: true); + + migrationBuilder.CreateTable( + name: "EFACSnapshot", + columns: table => new + { + SnapshotId = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Active = table.Column(nullable: false), + ClientId = table.Column(nullable: false), + CurrentSessionLength = table.Column(nullable: false), + CurrentStrain = table.Column(nullable: false), + CurrentViewAngleVector3Id = table.Column(nullable: true), + Deaths = table.Column(nullable: false), + Distance = table.Column(nullable: false), + EloRating = table.Column(nullable: false), + HitDestinationVector3Id = table.Column(nullable: true), + HitLocation = table.Column(nullable: false), + HitOriginVector3Id = table.Column(nullable: true), + HitType = table.Column(nullable: false), + Hits = table.Column(nullable: false), + Kills = table.Column(nullable: false), + LastStrainAngleVector3Id = table.Column(nullable: true), + SessionAngleOffset = table.Column(nullable: false), + SessionSPM = table.Column(nullable: false), + SessionScore = table.Column(nullable: false), + StrainAngleBetween = table.Column(nullable: false), + TimeSinceLastEvent = table.Column(nullable: false), + WeaponId = table.Column(nullable: false), + When = table.Column(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"); + } + } +} diff --git a/SharedLibraryCore/Migrations/DatabaseContextModelSnapshot.cs b/SharedLibraryCore/Migrations/DatabaseContextModelSnapshot.cs index 8b4822e25..ea3f44236 100644 --- a/SharedLibraryCore/Migrations/DatabaseContextModelSnapshot.cs +++ b/SharedLibraryCore/Migrations/DatabaseContextModelSnapshot.cs @@ -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("SnapshotId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("ClientId"); + + b.Property("CurrentSessionLength"); + + b.Property("CurrentStrain"); + + b.Property("CurrentViewAngleVector3Id"); + + b.Property("Deaths"); + + b.Property("Distance"); + + b.Property("EloRating"); + + b.Property("HitDestinationVector3Id"); + + b.Property("HitLocation"); + + b.Property("HitOriginVector3Id"); + + b.Property("HitType"); + + b.Property("Hits"); + + b.Property("Kills"); + + b.Property("LastStrainAngleVector3Id"); + + b.Property("SessionAngleOffset"); + + b.Property("SessionSPM"); + + b.Property("SessionScore"); + + b.Property("StrainAngleBetween"); + + b.Property("TimeSinceLastEvent"); + + b.Property("WeaponId"); + + b.Property("When"); + + b.HasKey("SnapshotId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CurrentViewAngleVector3Id"); + + b.HasIndex("HitDestinationVector3Id"); + + b.HasIndex("HitOriginVector3Id"); + + b.HasIndex("LastStrainAngleVector3Id"); + + b.ToTable("EFACSnapshot"); + }); + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientKill", b => { b.Property("KillId") @@ -374,6 +438,8 @@ namespace SharedLibraryCore.Migrations b.Property("Vector3Id") .ValueGeneratedOnAdd(); + b.Property("EFACSnapshotSnapshotId"); + b.Property("X"); b.Property("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 } } diff --git a/SharedLibraryCore/Services/PenaltyService.cs b/SharedLibraryCore/Services/PenaltyService.cs index 966eadce8..55caf8fbd 100644 --- a/SharedLibraryCore/Services/PenaltyService.cs +++ b/SharedLibraryCore/Services/PenaltyService.cs @@ -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, diff --git a/WebfrontCore/Controllers/PenaltyController.cs b/WebfrontCore/Controllers/PenaltyController.cs index 23eb5d155..b996cdb24 100644 --- a/WebfrontCore/Controllers/PenaltyController.cs +++ b/WebfrontCore/Controllers/PenaltyController.cs @@ -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, diff --git a/WebfrontCore/ViewComponents/PenaltyListViewComponent.cs b/WebfrontCore/ViewComponents/PenaltyListViewComponent.cs index 1e8ca0c64..ebb729f39 100644 --- a/WebfrontCore/ViewComponents/PenaltyListViewComponent.cs +++ b/WebfrontCore/ViewComponents/PenaltyListViewComponent.cs @@ -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, diff --git a/WebfrontCore/Views/Client/_MessageContext.cshtml b/WebfrontCore/Views/Client/_MessageContext.cshtml new file mode 100644 index 000000000..c40a9fba2 --- /dev/null +++ b/WebfrontCore/Views/Client/_MessageContext.cshtml @@ -0,0 +1,12 @@ +@model IEnumerable +@{ + Layout = null; +} + +
+
@Model.First().Time.ToString()
+ @foreach (var message in Model) + { + @message.Name — @message.Message
+ } +
\ No newline at end of file diff --git a/WebfrontCore/WebfrontCore.csproj b/WebfrontCore/WebfrontCore.csproj index 6968c90a3..8d174a828 100644 --- a/WebfrontCore/WebfrontCore.csproj +++ b/WebfrontCore/WebfrontCore.csproj @@ -53,6 +53,7 @@ +
diff --git a/WebfrontCore/wwwroot/css/bootstrap-custom.scss b/WebfrontCore/wwwroot/css/bootstrap-custom.scss index a05fbeb0c..5a0bb38fd 100644 --- a/WebfrontCore/wwwroot/css/bootstrap-custom.scss +++ b/WebfrontCore/wwwroot/css/bootstrap-custom.scss @@ -201,3 +201,7 @@ select { .client-rating-change-amount { font-size: 1rem; } + +.client-message { + cursor:pointer; +} diff --git a/WebfrontCore/wwwroot/js/profile.js b/WebfrontCore/wwwroot/js/profile.js index 310912f73..9f0b4b14c 100644 --- a/WebfrontCore/wwwroot/js/profile.js +++ b/WebfrontCore/wwwroot/js/profile.js @@ -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 */ @@ -97,7 +135,7 @@ $(document).ready(function () { }) .fail(function (jqxhr, textStatus, error) { $('#mainModal .modal-title').text("Error"); - $('#mainModal .modal-body').html('—'+ error + ''); + $('#mainModal .modal-body').html('—' + error + ''); $('#mainModal').modal(); }); }); @@ -171,10 +209,10 @@ function loadMeta(meta) { const timeRemaining = meta.value.type === 'TempBan' && meta.value.timeRemaining.length > 0 ? `(${meta.value.timeRemaining} remaining)` : ''; - eventString = `
${penaltyToName(meta.value.type)} by ${meta.value.punisherName} for ${meta.value.offense} ${timeRemaining}
`; + eventString = `
${penaltyToName(meta.value.type)} by ${meta.value.punisherName} for ${meta.value.offense} ${timeRemaining}
`; } else { - eventString = `
${penaltyToName(meta.value.type)} ${meta.value.offenderName} for ${meta.value.offense}
`; + eventString = `
${penaltyToName(meta.value.type)} ${meta.value.offenderName} for ${meta.value.offense}
`; } } else if (meta.key.includes("Alias")) { @@ -182,7 +220,7 @@ function loadMeta(meta) { } // it's a message else if (meta.key.includes("Event")) { - eventString = `
> ${meta.value}
`; + eventString = `
> ${meta.value}
`; } $('#profile_events').append(eventString); } diff --git a/_commands.gsc b/_commands.gsc index 551474f8f..e698333f5 100644 --- a/_commands.gsc +++ b/_commands.gsc @@ -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":