diff --git a/Application/API/EventAPI.cs b/Application/API/EventAPI.cs index ec664c0a7..20aa1c510 100644 --- a/Application/API/EventAPI.cs +++ b/Application/API/EventAPI.cs @@ -46,7 +46,7 @@ namespace IW4MAdmin.Application.API FlaggedMessageCount = 0; - E.Owner.Broadcast(Utilities.CurrentLocalization.LocalizationIndex["GLOBAL_REPORT"]).Wait(); + E.Owner.Broadcast(Utilities.CurrentLocalization.LocalizationIndex["GLOBAL_REPORT"]).Wait(5000); Events.Enqueue(new EventInfo( EventInfo.EventType.ALERT, EventInfo.EventVersion.IW4MAdmin, diff --git a/Application/BuildScripts/PostPublish.bat b/Application/BuildScripts/PostPublish.bat index 8c0803d45..eee1cb7ed 100644 --- a/Application/BuildScripts/PostPublish.bat +++ b/Application/BuildScripts/PostPublish.bat @@ -53,3 +53,6 @@ del "%SolutionDir%Publish\Windows\*pdb" if exist "%SolutionDir%Publish\WindowsPrerelease\web.config" del "%SolutionDir%Publish\WindowsPrerelease\web.config" del "%SolutionDir%Publish\WindowsPrerelease\*pdb" + +echo making start script +@echo dotnet IW4MAdmin.dll > "%SolutionDir%Publish\WindowsPrerelease\StartIW4MAdmin.cmd" diff --git a/Application/Manager.cs b/Application/Manager.cs index 9f65137a7..26ded3dac 100644 --- a/Application/Manager.cs +++ b/Application/Manager.cs @@ -44,7 +44,6 @@ namespace IW4MAdmin.Application EventApi Api; GameEventHandler Handler; ManualResetEventSlim OnEvent; - Timer HeartbeatTimer; private ApplicationManager() { @@ -337,66 +336,75 @@ namespace IW4MAdmin.Application { var heartbeatState = (HeartbeatState)state; - if (!heartbeatState.Connected) + while (Running) { - try + if (!heartbeatState.Connected) { - Heartbeat.Send(this, true).Wait(5000); - heartbeatState.Connected = true; - } - - catch (Exception e) - { - heartbeatState.Connected = false; - Logger.WriteWarning($"Could not connect to heartbeat server - {e.Message}"); - } - } - - else - { - try - { - Heartbeat.Send(this).Wait(5000); - } - catch (System.Net.Http.HttpRequestException e) - { - Logger.WriteWarning($"Could not send heartbeat - {e.Message}"); - } - - catch (AggregateException e) - { - Logger.WriteWarning($"Could not send heartbeat - {e.Message}"); - var exceptions = e.InnerExceptions.Where(ex => ex.GetType() == typeof(RestEase.ApiException)); - - foreach (var ex in exceptions) + try { - if (((RestEase.ApiException)ex).StatusCode == System.Net.HttpStatusCode.Unauthorized) + Heartbeat.Send(this, true).Wait(5000); + heartbeatState.Connected = true; + } + + catch (Exception e) + { + heartbeatState.Connected = false; + Logger.WriteWarning($"Could not connect to heartbeat server - {e.Message}"); + } + } + + else + { + try + { + Heartbeat.Send(this).Wait(5000); + } + + catch (System.Net.Http.HttpRequestException e) + { + Logger.WriteWarning($"Could not send heartbeat - {e.Message}"); + } + + catch (AggregateException e) + { + Logger.WriteWarning($"Could not send heartbeat - {e.Message}"); + var exceptions = e.InnerExceptions.Where(ex => ex.GetType() == typeof(RestEase.ApiException)); + + foreach (var ex in exceptions) + { + if (((RestEase.ApiException)ex).StatusCode == System.Net.HttpStatusCode.Unauthorized) + { + heartbeatState.Connected = false; + } + } + } + + catch (RestEase.ApiException e) + { + Logger.WriteWarning($"Could not send heartbeat - {e.Message}"); + if (e.StatusCode == System.Net.HttpStatusCode.Unauthorized) { heartbeatState.Connected = false; } } - } - - catch (RestEase.ApiException e) - { - Logger.WriteWarning($"Could not send heartbeat - {e.Message}"); - if (e.StatusCode == System.Net.HttpStatusCode.Unauthorized) + catch (Exception e) { - heartbeatState.Connected = false; + Logger.WriteWarning($"Could not send heartbeat - {e.Message}"); } - } + } + Task.Delay(30000).Wait(); } } public async Task Start() { -#if !DEBUG - // start heartbeat - HeartbeatTimer = new Timer(SendHeartbeat, new HeartbeatState(), 0, 30000); -#endif // this needs to be run seperately from the main thread #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed +#if !DEBUG + // start heartbeat + Task.Run(() => SendHeartbeat(new HeartbeatState())); +#endif Task.Run(() => UpdateStatus(null)); #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed @@ -439,7 +447,7 @@ namespace IW4MAdmin.Application while (Running) { // wait for new event to be added - OnEvent.Wait(); + OnEvent.Wait(5000); // todo: sequencially or parallelize? while ((queuedEvent = Handler.GetNextEvent()) != null) @@ -454,8 +462,6 @@ namespace IW4MAdmin.Application OnEvent.Reset(); } #if !DEBUG - HeartbeatTimer.Change(0, Timeout.Infinite); - foreach (var S in _servers) await S.Broadcast("^1" + Utilities.CurrentLocalization.LocalizationIndex["BROADCAST_OFFLINE"]); #endif diff --git a/Application/Server.cs b/Application/Server.cs index 31c84fd2e..34a263a8c 100644 --- a/Application/Server.cs +++ b/Application/Server.cs @@ -230,7 +230,9 @@ namespace IW4MAdmin var e = new GameEvent(GameEvent.EventType.Disconnect, "", Leaving, null, this); Manager.GetEventHandler().AddEvent(e); - e.OnProcessed.WaitHandle.WaitOne(5000); + + // wait until the disconnect event is complete + e.OnProcessed.Wait(); Leaving.TotalConnectionTime += (int)(DateTime.UtcNow - Leaving.ConnectionTime).TotalSeconds; Leaving.LastConnection = DateTime.UtcNow; @@ -487,7 +489,7 @@ namespace IW4MAdmin { ChatHistory.Add(new ChatInfo() { - Name = E.Origin?.Name ?? "ERROR!", + Name = E.Origin.Name, Message = "DISCONNECTED", Time = DateTime.UtcNow }); @@ -505,8 +507,8 @@ namespace IW4MAdmin { ChatHistory.Add(new ChatInfo() { - Name = E.Origin?.Name ?? "ERROR!", - Message = E.Data, + Name = E.Origin.Name, + Message = E.Data ?? "NULL", Time = DateTime.UtcNow }); } diff --git a/Plugins/Stats/Cheat/Detection.cs b/Plugins/Stats/Cheat/Detection.cs index 03ec4763d..96f7e5a20 100644 --- a/Plugins/Stats/Cheat/Detection.cs +++ b/Plugins/Stats/Cheat/Detection.cs @@ -82,7 +82,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat hitLoc.HitOffsetAverage = (float)newAverage; if (hitLoc.HitOffsetAverage > Thresholds.MaxOffset && - hitLoc.HitCount > 15) + hitLoc.HitCount > 100) { Log.WriteDebug("*** Reached Max Lifetime Average for Angle Difference ***"); Log.WriteDebug($"Lifetime Average = {newAverage}"); @@ -104,7 +104,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat AngleDifferenceAverage = sessAverage; if (sessAverage > Thresholds.MaxOffset && - HitCount > 15) + HitCount > 30) { Log.WriteDebug("*** Reached Max Session Average for Angle Difference ***"); Log.WriteDebug($"Session Average = {sessAverage}"); @@ -125,7 +125,8 @@ namespace IW4MAdmin.Plugins.Stats.Cheat Log.WriteDebug($"PredictVsReal={realAgainstPredict}"); #endif } - var currentStrain = Strain.GetStrain(kill.ViewAngles, Math.Max(50, kill.TimeOffset - LastOffset)); + double currentStrain = Strain.GetStrain(isDamage, kill.Damage, kill.ViewAngles, Math.Max(50, kill.TimeOffset - LastOffset)); + currentStrain *= ClientStats.SPM / 179.0; LastOffset = kill.TimeOffset; if (currentStrain > ClientStats.MaxStrain) @@ -149,12 +150,26 @@ namespace IW4MAdmin.Plugins.Stats.Cheat Tracker.ClearChanges(); } - if (Strain.TimesReachedMaxStrain >= 3) + // flag + if (currentStrain > Thresholds.MaxStrainFlag) { return new DetectionPenaltyResult() { ClientPenalty = Penalty.PenaltyType.Flag, - Value = ClientStats.MaxStrain, + Value = currentStrain, + HitCount = HitCount, + Type = DetectionType.Strain + }; + } + + // ban + if (currentStrain > Thresholds.MaxStrainBan + && Kills > Thresholds.LowSampleMinKills) + { + return new DetectionPenaltyResult() + { + ClientPenalty = Penalty.PenaltyType.Flag, + Value = currentStrain, HitCount = HitCount, Type = DetectionType.Strain }; diff --git a/Plugins/Stats/Cheat/Strain.cs b/Plugins/Stats/Cheat/Strain.cs index f9c5b7662..6929fcb62 100644 --- a/Plugins/Stats/Cheat/Strain.cs +++ b/Plugins/Stats/Cheat/Strain.cs @@ -8,7 +8,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat { class Strain : ITrackable { - private static double StrainDecayBase = 0.15; + private const double StrainDecayBase = 0.9; private double CurrentStrain; private Vector3 LastAngle; private double LastDeltaTime; @@ -16,7 +16,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat public int TimesReachedMaxStrain { get; private set; } - public double GetStrain(Vector3 newAngle, double deltaTime) + public double GetStrain(bool isDamage, int damage, Vector3 newAngle, double deltaTime) { if (LastAngle == null) LastAngle = newAngle; @@ -26,21 +26,36 @@ namespace IW4MAdmin.Plugins.Stats.Cheat double decayFactor = GetDecay(deltaTime); CurrentStrain *= decayFactor; +#if DEBUG + Console.WriteLine($"Decay Factor = {decayFactor} "); +#endif + double[] distance = Helpers.Extensions.AngleStuff(newAngle, LastAngle); LastDistance = distance[0] + distance[1]; // this happens on first kill if ((distance[0] == 0 && distance[1] == 0) || - deltaTime == 0 || + deltaTime == 0 || double.IsNaN(CurrentStrain)) { return CurrentStrain; } double newStrain = Math.Pow(distance[0] + distance[1], 0.99) / deltaTime; + + if (damage < 100 && isDamage) + { + newStrain *= Math.Pow(damage, 2) / 10000.0; + } + + else if (damage > 100) + { + newStrain *= damage / 100.0; + } + CurrentStrain += newStrain; - if (CurrentStrain > Thresholds.MaxStrainFlag) + if (CurrentStrain > Thresholds.MaxStrainBan) TimesReachedMaxStrain++; LastAngle = newAngle; @@ -52,6 +67,6 @@ namespace IW4MAdmin.Plugins.Stats.Cheat return $"Strain - {CurrentStrain}, Angle - {LastAngle}, Delta Time - {LastDeltaTime}, Distance - {LastDistance}"; } - private double GetDecay(double deltaTime) => Math.Pow(StrainDecayBase, deltaTime / 1000.0); + 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/Cheat/Thresholds.cs b/Plugins/Stats/Cheat/Thresholds.cs index 5b032b68f..b3ca34caa 100644 --- a/Plugins/Stats/Cheat/Thresholds.cs +++ b/Plugins/Stats/Cheat/Thresholds.cs @@ -27,9 +27,9 @@ namespace IW4MAdmin.Plugins.Stats.Cheat public const int HighSampleMinKills = 100; public const double KillTimeThreshold = 0.2; - public const double MaxStrainBan = 0.4399; + public const double MaxStrainBan = 2.5; public const double MaxOffset = 1.2; - public const double MaxStrainFlag = 1; + public const double MaxStrainFlag = 2.0; public static double GetMarginOfError(int numKills) => 1.6455 / Math.Sqrt(numKills); diff --git a/Plugins/Stats/Commands/MostPlayed.cs b/Plugins/Stats/Commands/MostPlayed.cs new file mode 100644 index 000000000..54686dab2 --- /dev/null +++ b/Plugins/Stats/Commands/MostPlayed.cs @@ -0,0 +1,75 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Linq; +using System.Threading.Tasks; + +using SharedLibraryCore; +using SharedLibraryCore.Objects; +using IW4MAdmin.Plugins.Stats.Models; +using SharedLibraryCore.Database; +using System.Collections.Generic; + +namespace IW4MAdmin.Plugins.Stats.Commands +{ + class MostPlayed : Command + { + public static async Task> GetMostPlayed(Server s) + { + int serverId = s.GetHashCode(); + List mostPlayed = new List() + { + $"^5--{Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_MOSTPLAYED_TEXT"]}--" + }; + + using (var db = new DatabaseContext()) + { + db.ChangeTracker.AutoDetectChangesEnabled = false; + db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; + + var thirtyDaysAgo = DateTime.UtcNow.AddMonths(-1); + + var iqStats = (from stats in db.Set() + join client in db.Clients + on stats.ClientId equals client.ClientId + join alias in db.Aliases + on client.CurrentAliasId equals alias.AliasId + where stats.ServerId == serverId + where client.Level != Player.Permission.Banned + where client.LastConnection >= thirtyDaysAgo + orderby stats.Kills descending + select new + { + alias.Name, + client.TotalConnectionTime, + stats.Kills + }) + .Take(5); + + var iqList = await iqStats.ToListAsync(); + + mostPlayed.AddRange(iqList.Select(stats => + $"^3{stats.Name}^7 - ^5{stats.Kills} ^7{Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_KILLS"]} | ^5{Utilities.GetTimePassed(DateTime.UtcNow.AddSeconds(-stats.TotalConnectionTime), false)} ^7{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_PLAYER"].ToLower()}")); + } + + + return mostPlayed; + } + + public MostPlayed() : base("mostplayed", Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_MOSTPLAYED_DESC"], "mp", Player.Permission.User, false) { } + + public override async Task ExecuteAsync(GameEvent E) + { + var topStats = await GetMostPlayed(E.Owner); + if (!E.Message.IsBroadcastCommand()) + { + foreach (var stat in topStats) + await E.Origin.Tell(stat); + } + else + { + foreach (var stat in topStats) + await E.Owner.Broadcast(stat); + } + } + } +} diff --git a/Plugins/Stats/Commands/TopStats.cs b/Plugins/Stats/Commands/TopStats.cs index 52da27449..719866a62 100644 --- a/Plugins/Stats/Commands/TopStats.cs +++ b/Plugins/Stats/Commands/TopStats.cs @@ -40,10 +40,19 @@ namespace IW4MAdmin.Plugins.Stats.Commands where client.Level != Player.Permission.Banned where client.LastConnection >= thirtyDaysAgo orderby stats.Skill descending - select $"^3{alias.Name}^7 - ^5{stats.KDR} ^7KDR | ^5{stats.Skill} ^7{Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_SKILL"]}") + select new + { + stats.KDR, + stats.Skill, + stats.EloRating, + alias.Name + }) .Take(5); - topStatsText.AddRange(await iqStats.ToListAsync()); + var statsList = (await iqStats.ToListAsync()) + .Select(stats => $"^3{stats.Name}^7 - ^5{stats.KDR} ^7{Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_KDR"]} | ^5{stats.Skill} ^7{Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_SKILL"]} | ^5{stats.EloRating} ^7{Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_TOPSTATS_RATING"]}"); + + topStatsText.AddRange(statsList); } // no one qualified diff --git a/Plugins/Stats/Commands/ViewStats.cs b/Plugins/Stats/Commands/ViewStats.cs index cda1193ed..7a659ff25 100644 --- a/Plugins/Stats/Commands/ViewStats.cs +++ b/Plugins/Stats/Commands/ViewStats.cs @@ -53,13 +53,13 @@ namespace IW4MAdmin.Plugins.Stats.Commands if (E.Target != null) { pStats = clientStats.Find(c => c.ServerId == serverId && c.ClientId == E.Target.ClientId).First(); - statLine = $"^5{pStats.Kills} ^7{loc["PLUGINS_STATS_TEXT_KILLS"]} | ^5{pStats.Deaths} ^7{loc["PLUGINS_STATS_TEXT_DEATHS"]} | ^5{pStats.KDR} ^7KDR | ^5{pStats.Skill} ^7{loc["PLUGINS_STATS_TEXT_SKILL"]}"; + statLine = $"^5{pStats.Kills} ^7{loc["PLUGINS_STATS_TEXT_KILLS"]} | ^5{pStats.Deaths} ^7{loc["PLUGINS_STATS_TEXT_DEATHS"]} | ^5{pStats.KDR} ^7KDR | ^5{pStats.Skill} ^7{loc["PLUGINS_STATS_TEXT_SKILL"]} | ^5{pStats.EloRating} ^7{loc["PLUGINS_STATS_COMMANDS_TOPSTATS_RATING"].ToUpper()}"; } else { pStats = pStats = clientStats.Find(c => c.ServerId == serverId && c.ClientId == E.Origin.ClientId).First(); - statLine = $"^5{pStats.Kills} ^7{loc["PLUGINS_STATS_TEXT_KILLS"]} | ^5{pStats.Deaths} ^7{loc["PLUGINS_STATS_TEXT_DEATHS"]} | ^5{pStats.KDR} ^7KDR | ^5{pStats.Skill} ^7{loc["PLUGINS_STATS_TEXT_SKILL"]}"; + statLine = $"^5{pStats.Kills} ^7{loc["PLUGINS_STATS_TEXT_KILLS"]} | ^5{pStats.Deaths} ^7{loc["PLUGINS_STATS_TEXT_DEATHS"]} | ^5{pStats.KDR} ^7KDR | ^5{pStats.Skill} ^7{loc["PLUGINS_STATS_TEXT_SKILL"]} | ^5{pStats.EloRating} ^7{loc["PLUGINS_STATS_COMMANDS_TOPSTATS_RATING"].ToUpper()}"; } if (E.Message.IsBroadcastCommand()) diff --git a/Plugins/Stats/Helpers/ServerStats.cs b/Plugins/Stats/Helpers/ServerStats.cs index 2fcfd324e..5763b566c 100644 --- a/Plugins/Stats/Helpers/ServerStats.cs +++ b/Plugins/Stats/Helpers/ServerStats.cs @@ -1,13 +1,6 @@ -using SharedLibraryCore; -using IW4MAdmin.Plugins.Stats.Cheat; +using IW4MAdmin.Plugins.Stats.Cheat; using IW4MAdmin.Plugins.Stats.Models; -using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using SharedLibraryCore.Services; namespace IW4MAdmin.Plugins.Stats.Helpers { diff --git a/Plugins/Stats/Helpers/StatManager.cs b/Plugins/Stats/Helpers/StatManager.cs index 0916d44d8..11d3826a8 100644 --- a/Plugins/Stats/Helpers/StatManager.cs +++ b/Plugins/Stats/Helpers/StatManager.cs @@ -118,6 +118,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers ServerId = serverId, Skill = 0.0, SPM = 0.0, + EloRating = 200.0, HitLocations = Enum.GetValues(typeof(IW4Info.HitLocation)).OfType().Select(hl => new EFHitLocationCount() { Active = true, @@ -145,6 +146,11 @@ namespace IW4MAdmin.Plugins.Stats.Helpers //await statsSvc.ClientStatSvc.SaveChangesAsync(); } + if (clientStats.EloRating == 0.0) + { + clientStats.EloRating = clientStats.Skill; + } + // set these on connecting clientStats.LastActive = DateTime.UtcNow; clientStats.LastStatCalculation = DateTime.UtcNow; @@ -479,6 +485,30 @@ namespace IW4MAdmin.Plugins.Stats.Helpers // process the attacker's stats after the kills attackerStats = UpdateStats(attackerStats); + // calulate elo + if (Servers[attackerStats.ServerId].PlayerStats.Count > 1) + { + double attackerLobbyRating = Servers[attackerStats.ServerId].PlayerStats + .Where(cs => cs.Value.ClientId != attackerStats.ClientId) + .Average(cs => cs.Value.EloRating); + + double victimLobbyRating = Servers[attackerStats.ServerId].PlayerStats + .Where(cs => cs.Value.ClientId != victimStats.ClientId) + .Average(cs => cs.Value.EloRating); + + double attackerEloDifference = Math.Log(attackerLobbyRating) - Math.Log(attackerStats.EloRating); + double winPercentage = 1.0 / (1 + Math.Pow(10, attackerEloDifference / 3.0)); + + double victimEloDifference = Math.Log(victimLobbyRating) - Math.Log(victimStats.EloRating); + double lossPercentage = 1.0 / (1 + Math.Pow(10, victimEloDifference / 3.0)); + + attackerStats.EloRating += 24.0 * (1 - winPercentage); + victimStats.EloRating -= 24.0 * winPercentage; + + attackerStats.EloRating = Math.Max(0, Math.Round(attackerStats.EloRating, 2)); + victimStats.EloRating = Math.Max(0, Math.Round(victimStats.EloRating, 2)); + } + // update after calculation attackerStats.TimePlayed += (int)(DateTime.UtcNow - attackerStats.LastActive).TotalSeconds; victimStats.TimePlayed += (int)(DateTime.UtcNow - victimStats.LastActive).TotalSeconds; diff --git a/Plugins/Stats/Models/EFClientStatistics.cs b/Plugins/Stats/Models/EFClientStatistics.cs index a462b872c..72d32cc8b 100644 --- a/Plugins/Stats/Models/EFClientStatistics.cs +++ b/Plugins/Stats/Models/EFClientStatistics.cs @@ -22,9 +22,8 @@ namespace IW4MAdmin.Plugins.Stats.Models public int Kills { get; set; } [Required] public int Deaths { get; set; } - + public double EloRating { get; set; } public virtual ICollection HitLocations { get; set; } - [NotMapped] public double KDR { diff --git a/Plugins/Stats/Plugin.cs b/Plugins/Stats/Plugin.cs index f86f1f372..44dc47abe 100644 --- a/Plugins/Stats/Plugin.cs +++ b/Plugins/Stats/Plugin.cs @@ -114,7 +114,9 @@ namespace IW4MAdmin.Plugins.Stats int kills = clientStats.Sum(c => c.Kills); int deaths = clientStats.Sum(c => c.Deaths); double kdr = Math.Round(kills / (double)deaths, 2); - double skill = Math.Round(clientStats.Sum(c => c.Skill) / clientStats.Where(c => c.Skill > 0).Count(), 2); + var validSkillValues = clientStats.Where(c => c.Skill > 0); + int skillPlayTime = validSkillValues.Sum(s => s.TimePlayed); + double skill = Math.Round(validSkillValues.Sum(c => c.Skill * c.TimePlayed / skillPlayTime), 2); double spm = Math.Round(clientStats.Sum(c => c.SPM) / clientStats.Where(c => c.SPM > 0).Count(), 1); return new List() @@ -241,7 +243,7 @@ namespace IW4MAdmin.Plugins.Stats }).ToList(); messageMeta.Add(new ProfileMeta() { - Key = "Messages", + Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_MESSAGES"], Value = messages.Count }); diff --git a/SharedLibraryCore/Migrations/20180516023249_AddEloField.Designer.cs b/SharedLibraryCore/Migrations/20180516023249_AddEloField.Designer.cs new file mode 100644 index 000000000..e01536cd7 --- /dev/null +++ b/SharedLibraryCore/Migrations/20180516023249_AddEloField.Designer.cs @@ -0,0 +1,436 @@ +// +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("20180516023249_AddEloField")] + partial class AddEloField + { + 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.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.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("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.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.EFPenalty", b => + { + b.Property("PenaltyId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + 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("X"); + + b.Property("Y"); + + b.Property("Z"); + + b.HasKey("Vector3Id"); + + b.ToTable("Vector3"); + }); + + 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.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.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.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); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SharedLibraryCore/Migrations/20180516023249_AddEloField.cs b/SharedLibraryCore/Migrations/20180516023249_AddEloField.cs new file mode 100644 index 000000000..8f3c3c03d --- /dev/null +++ b/SharedLibraryCore/Migrations/20180516023249_AddEloField.cs @@ -0,0 +1,25 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; +using System.Collections.Generic; + +namespace SharedLibraryCore.Migrations +{ + public partial class AddEloField : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "EloRating", + table: "EFClientStatistics", + nullable: false, + defaultValue: 0.0); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "EloRating", + table: "EFClientStatistics"); + } + } +} diff --git a/SharedLibraryCore/Migrations/DatabaseContextModelSnapshot.cs b/SharedLibraryCore/Migrations/DatabaseContextModelSnapshot.cs index e3c6c51e9..4007141cd 100644 --- a/SharedLibraryCore/Migrations/DatabaseContextModelSnapshot.cs +++ b/SharedLibraryCore/Migrations/DatabaseContextModelSnapshot.cs @@ -102,6 +102,8 @@ namespace SharedLibraryCore.Migrations b.Property("Deaths"); + b.Property("EloRating"); + b.Property("Kills"); b.Property("MaxStrain"); diff --git a/SharedLibraryCore/RCon/Connection.cs b/SharedLibraryCore/RCon/Connection.cs index 0d44dba83..41d7e49ad 100644 --- a/SharedLibraryCore/RCon/Connection.cs +++ b/SharedLibraryCore/RCon/Connection.cs @@ -250,7 +250,7 @@ namespace SharedLibraryCore.RCon if (FailedReceives >= 4) { - throw new NetworkException($"{Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_COMMUNICATION"]} {socketConnection.RemoteEndPoint}"); + throw new NetworkException($"{Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_COMMUNICATION"]} {socketConnection.RemoteEndPoint.ToString()}"); } } @@ -258,7 +258,7 @@ namespace SharedLibraryCore.RCon { if (FailedReceives >= 4) { - Log.WriteVerbose($"Resumed receive RCon connection from {socketConnection.RemoteEndPoint}"); + Log.WriteVerbose($"Resumed receive RCon connection from {socketConnection.RemoteEndPoint.ToString()}"); FailedReceives = 0; } } diff --git a/WebfrontCore/Views/Client/Profile/Index.cshtml b/WebfrontCore/Views/Client/Profile/Index.cshtml index 0c8841544..796d06aac 100644 --- a/WebfrontCore/Views/Client/Profile/Index.cshtml +++ b/WebfrontCore/Views/Client/Profile/Index.cshtml @@ -80,7 +80,10 @@
- +
+ @Model.ConnectionCount + @loc["WEBFRONT_CLIENT_META_CONNECTIONS"] +
diff --git a/WebfrontCore/Views/Server/_ClientActivity.cshtml b/WebfrontCore/Views/Server/_ClientActivity.cshtml index 5b2f33e31..51bc6b391 100644 --- a/WebfrontCore/Views/Server/_ClientActivity.cshtml +++ b/WebfrontCore/Views/Server/_ClientActivity.cshtml @@ -8,19 +8,24 @@ @{ for (int i = 0; i < Model.ChatHistory.Count; i++) { - string message = @Model.ChatHistory[i].Message; + if (Model.ChatHistory[i] == null || + Model.ChatHistory[i].Message == null || + Model.ChatHistory[i].Name == null) + { + continue; + } + if (Model.ChatHistory[i].Message == "CONNECTED") { @Model.ChatHistory[i].Name
} if (Model.ChatHistory[i].Message == "DISCONNECTED") { - @Model.ChatHistory[i].Name
} if (Model.ChatHistory[i].Message != "CONNECTED" && Model.ChatHistory[i].Message != "DISCONNECTED") { - @Model.ChatHistory[i].Name — @message.Substring(0, Math.Min(65, message.Length))
+ @Model.ChatHistory[i].Name — @Model.ChatHistory[i].Message.Substring(0, Math.Min(65, Model.ChatHistory[i].Message.Length))
} } } @@ -55,7 +60,13 @@ @{ for (int i = 0; i < Model.ChatHistory.Count; i++) { - string message = @Model.ChatHistory[i].Message; + if (Model.ChatHistory[i] == null || + Model.ChatHistory[i].Message == null || + Model.ChatHistory[i].Name == null) + { + continue; + } + if (Model.ChatHistory[i].Message == "CONNECTED") { @Model.ChatHistory[i].Name
@@ -67,7 +78,7 @@ } if (Model.ChatHistory[i].Message != "CONNECTED" && Model.ChatHistory[i].Message != "DISCONNECTED") { - @Model.ChatHistory[i].Name — @message.Substring(0, Math.Min(65, message.Length))
+ @Model.ChatHistory[i].Name — @Model.ChatHistory[i].Message.Substring(0, Math.Min(65, Model.ChatHistory[i].Message.Length))
} } }