diff --git a/Application/EventParsers/IW4EventParser.cs b/Application/EventParsers/IW4EventParser.cs index e16011860..19f1a1f5c 100644 --- a/Application/EventParsers/IW4EventParser.cs +++ b/Application/EventParsers/IW4EventParser.cs @@ -21,7 +21,7 @@ namespace IW4MAdmin.Application.EventParsers { return new GameEvent() { - Type = GameEvent.EventType.Script, + Type = GameEvent.EventType.Kill, Data = logLine, Origin = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 6)), Target = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 2)), @@ -32,13 +32,27 @@ namespace IW4MAdmin.Application.EventParsers if (cleanedEventLine == "say" || cleanedEventLine == "sayteam") { + string message = lineSplit[4].Replace("\x15", ""); + + if (message[0] == '!' || message[1] == '@') + { + return new GameEvent() + { + Type = GameEvent.EventType.Command, + Data = message, + Origin = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 2)), + Owner = server, + Message = message + }; + } + return new GameEvent() { Type = GameEvent.EventType.Say, - Data = lineSplit[4].Replace("\x15", ""), + Data = message, Origin = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 2)), Owner = server, - Message = lineSplit[4].Replace("\x15", "") + Message = message }; } @@ -46,7 +60,7 @@ namespace IW4MAdmin.Application.EventParsers { return new GameEvent() { - Type = GameEvent.EventType.Script, + Type = GameEvent.EventType.ScriptKill, Data = logLine, Origin = server.GetPlayersAsList().First(c => c.NetworkId == lineSplit[1].ConvertLong()), Target = server.GetPlayersAsList().First(c => c.NetworkId == lineSplit[2].ConvertLong()), diff --git a/Application/EventParsers/T6MEventParser.cs b/Application/EventParsers/T6MEventParser.cs index 773034e22..1ebf7c815 100644 --- a/Application/EventParsers/T6MEventParser.cs +++ b/Application/EventParsers/T6MEventParser.cs @@ -20,7 +20,7 @@ namespace IW4MAdmin.Application.EventParsers { return new GameEvent() { - Type = GameEvent.EventType.Script, + Type = GameEvent.EventType.Kill, Data = cleanedEventLine, Origin = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 6)), Target = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 2)), diff --git a/Application/GameEventHandler.cs b/Application/GameEventHandler.cs index 4717bf47d..ca964181a 100644 --- a/Application/GameEventHandler.cs +++ b/Application/GameEventHandler.cs @@ -3,20 +3,23 @@ using SharedLibraryCore.Interfaces; using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; using System.Text; +using System.Threading; namespace IW4MAdmin.Application { class GameEventHandler : IEventHandler { private ConcurrentQueue EventQueue; - private ConcurrentQueue StatusSensitiveQueue; + private Queue StatusSensitiveQueue; private IManager Manager; public GameEventHandler(IManager mgr) { EventQueue = new ConcurrentQueue(); - StatusSensitiveQueue = new ConcurrentQueue(); + StatusSensitiveQueue = new Queue(); + Manager = mgr; } @@ -26,25 +29,30 @@ namespace IW4MAdmin.Application Manager.GetLogger().WriteDebug($"Got new event of type {gameEvent.Type} for {gameEvent.Owner}"); #endif // we need this to keep accurate track of the score - if (gameEvent.Type == GameEvent.EventType.Script || - gameEvent.Type == GameEvent.EventType.Kill || + if (gameEvent.Type == GameEvent.EventType.Kill || + gameEvent.Type == GameEvent.EventType.Damage || + gameEvent.Type == GameEvent.EventType.ScriptDamage || + gameEvent.Type == GameEvent.EventType.ScriptKill || gameEvent.Type == GameEvent.EventType.MapChange) { #if DEBUG Manager.GetLogger().WriteDebug($"Added sensitive event to queue"); #endif - StatusSensitiveQueue.Enqueue(gameEvent); + lock (StatusSensitiveQueue) + { + StatusSensitiveQueue.Enqueue(gameEvent); + } return; } else { EventQueue.Enqueue(gameEvent); + Manager.SetHasEvent(); } #if DEBUG Manager.GetLogger().WriteDebug($"There are now {EventQueue.Count} events in queue"); #endif - Manager.SetHasEvent(); } public string[] GetEventOutput() @@ -56,14 +64,17 @@ namespace IW4MAdmin.Application { if (StatusSensitiveQueue.Count > 0) { - if (!StatusSensitiveQueue.TryDequeue(out GameEvent newEvent)) + lock (StatusSensitiveQueue) { - Manager.GetLogger().WriteWarning("Could not dequeue time sensitive event for processing"); - } + if (!StatusSensitiveQueue.TryDequeue(out GameEvent newEvent)) + { + Manager.GetLogger().WriteWarning("Could not dequeue time sensitive event for processing"); + } - else - { - return newEvent; + else + { + return newEvent; + } } } diff --git a/Application/IO/GameLogEvent.cs b/Application/IO/GameLogEvent.cs index f7e1430c8..ddad05904 100644 --- a/Application/IO/GameLogEvent.cs +++ b/Application/IO/GameLogEvent.cs @@ -1,10 +1,7 @@ using SharedLibraryCore; using SharedLibraryCore.Interfaces; using System; -using System.Collections.Generic; using System.IO; -using System.Text; -using System.Threading; using System.Threading.Tasks; namespace IW4MAdmin.Application.IO @@ -14,7 +11,6 @@ namespace IW4MAdmin.Application.IO Server Server; long PreviousFileSize; GameLogReader Reader; - Timer RefreshInfoTimer; string GameLogFile; class EventState @@ -28,17 +24,25 @@ namespace IW4MAdmin.Application.IO GameLogFile = gameLogPath; Reader = new GameLogReader(gameLogPath, server.EventParser); Server = server; - RefreshInfoTimer = new Timer(OnEvent, new EventState() - { - Log = server.Manager.GetLogger(), - ServerId = server.ToString() - }, 0, 100); + + Task.Run(async () => + { + while (!server.Manager.ShutdownRequested()) + { + OnEvent(new EventState() + { + Log = server.Manager.GetLogger(), + ServerId = server.ToString() + }); + await Task.Delay(100); + } + }); } private void OnEvent(object state) { long newLength = new FileInfo(GameLogFile).Length; - + try { UpdateLogEvents(newLength); diff --git a/Application/IO/GameLogReader.cs b/Application/IO/GameLogReader.cs index 458a00acc..a2e62fc96 100644 --- a/Application/IO/GameLogReader.cs +++ b/Application/IO/GameLogReader.cs @@ -32,7 +32,7 @@ namespace IW4MAdmin.Application.IO string newLine; while (!String.IsNullOrEmpty(newLine = rd.ReadLine())) { - logLines.Add(newLine.Replace("\r\n", "")); + logLines.Add(newLine); } } @@ -49,7 +49,7 @@ namespace IW4MAdmin.Application.IO events.Add(Parser.GetEvent(server, eventLine)); } - catch (Exception e) + catch (Exception) { } } diff --git a/Application/Main.cs b/Application/Main.cs index c8fd40850..38eab43c8 100644 --- a/Application/Main.cs +++ b/Application/Main.cs @@ -18,6 +18,7 @@ namespace IW4MAdmin.Application static public double Version { get; private set; } static public ApplicationManager ServerManager = ApplicationManager.GetInstance(); public static string OperatingDirectory = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location) + Path.DirectorySeparatorChar; + private static ManualResetEventSlim OnShutdownComplete = new ManualResetEventSlim(); public static void Main(string[] args) { @@ -43,6 +44,7 @@ namespace IW4MAdmin.Application CheckDirectories(); ServerManager = ApplicationManager.GetInstance(); + Console.CancelKeyPress += new ConsoleCancelEventHandler(OnCancelKey); Localization.Configure.Initialize(ServerManager.GetApplicationSettings().Configuration()?.CustomLocale); loc = Utilities.CurrentLocalization.LocalizationIndex; @@ -126,30 +128,21 @@ namespace IW4MAdmin.Application if (userInput?.Length > 0) { Origin.CurrentServer = ServerManager.Servers[0]; - GameEvent E = new GameEvent(GameEvent.EventType.Say, userInput, Origin, null, ServerManager.Servers[0]); + GameEvent E = new GameEvent() + { + Type = GameEvent.EventType.Command, + Data = userInput, + Origin = Origin, + Owner = ServerManager.Servers[0] + }; + ServerManager.GetEventHandler().AddEvent(E); + E.OnProcessed.Wait(); } Console.Write('>'); } while (ServerManager.Running); }); - - if (ServerManager.GetApplicationSettings().Configuration().EnableWebFront) - { - Task.Run(() => - { - try - { - WebfrontCore.Program.Init(ServerManager); - } - - catch (Exception e) - { - ServerManager.Logger.WriteWarning("Webfront had unhandled exception"); - ServerManager.Logger.WriteDebug(e.Message); - } - }); - } } catch (Exception e) @@ -164,9 +157,21 @@ namespace IW4MAdmin.Application Console.ReadKey(); } + if (ServerManager.GetApplicationSettings().Configuration().EnableWebFront) + { + Task.Run(() => WebfrontCore.Program.Init(ServerManager)); + } + OnShutdownComplete.Reset(); ServerManager.Start().Wait(); ServerManager.Logger.WriteVerbose(loc["MANAGER_SHUTDOWN_SUCCESS"]); + OnShutdownComplete.Set(); + } + + private static void OnCancelKey(object sender, ConsoleCancelEventArgs e) + { + ServerManager.Stop(); + OnShutdownComplete.Wait(); } static void CheckDirectories() diff --git a/Application/Manager.cs b/Application/Manager.cs index 4c9788bb7..59428fff1 100644 --- a/Application/Manager.cs +++ b/Application/Manager.cs @@ -60,17 +60,10 @@ namespace IW4MAdmin.Application Api = new EventApi(); ServerEventOccurred += Api.OnServerEvent; ConfigHandler = new BaseConfigurationHandler("IW4MAdminSettings"); - Console.CancelKeyPress += new ConsoleCancelEventHandler(OnCancelKey); StartTime = DateTime.UtcNow; OnEvent = new ManualResetEventSlim(); } - private void OnCancelKey(object sender, ConsoleCancelEventArgs args) - { - Stop(); - } - - public IList GetServers() { return Servers; @@ -141,9 +134,9 @@ namespace IW4MAdmin.Application Logger.WriteError($"{Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_EXCEPTION"]} {sensitiveEvent.Owner}"); Logger.WriteDebug("Error Message: " + E.Message); Logger.WriteDebug("Error Trace: " + E.StackTrace); - sensitiveEvent.OnProcessed.Set(); - continue; } + + sensitiveEvent.OnProcessed.Set(); } await Task.Delay(5000); @@ -152,8 +145,8 @@ namespace IW4MAdmin.Application public async Task Init() { - // setup the event handler after the class is initialized - Handler = new GameEventHandler(this); + Running = true; + #region DATABASE var ipList = (await ClientSvc.Find(c => c.Level > Player.Permission.Trusted)) .Select(c => new @@ -304,6 +297,8 @@ namespace IW4MAdmin.Application #region INIT async Task Init(ServerConfiguration Conf) { + // setup the event handler after the class is initialized + Handler = new GameEventHandler(this); try { var ServerInstance = new IW4MServer(this, Conf); @@ -336,8 +331,6 @@ namespace IW4MAdmin.Application await Task.WhenAll(config.Servers.Select(c => Init(c)).ToArray()); #endregion - - Running = true; } private void SendHeartbeat(object state) @@ -419,6 +412,12 @@ namespace IW4MAdmin.Application #endif } + // this happens if a plugin requires login + catch (AuthorizationException e) + { + await newEvent.Origin.Tell($"{Utilities.CurrentLocalization.LocalizationIndex["COMMAND_NOTAUTHORIZED"]} - {e.Message}"); + } + catch (NetworkException e) { Logger.WriteError(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_COMMUNICATION"]); @@ -449,9 +448,7 @@ namespace IW4MAdmin.Application } // this should allow parallel processing of events -#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - Task.WhenAll(eventList); -#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + await Task.WhenAll(eventList); // signal that all events have been processed OnEvent.Reset(); @@ -459,8 +456,8 @@ namespace IW4MAdmin.Application #if !DEBUG HeartbeatTimer.Change(0, Timeout.Infinite); - foreach (var S in Servers) - S.Broadcast("^1" + Utilities.CurrentLocalization.LocalizationIndex["BROADCAST_OFFLINE"]).Wait(); + foreach (var S in _servers) + await S.Broadcast("^1" + Utilities.CurrentLocalization.LocalizationIndex["BROADCAST_OFFLINE"]); #endif _servers.Clear(); } diff --git a/Application/RconParsers/IW4RConParser.cs b/Application/RconParsers/IW4RConParser.cs index 5799d077f..a3b05ba9a 100644 --- a/Application/RconParsers/IW4RConParser.cs +++ b/Application/RconParsers/IW4RConParser.cs @@ -23,11 +23,12 @@ namespace Application.RconParsers TempBan = "tempbanclient {0} \"{1}\"" }; - private static string StatusRegex = @"^( *[0-9]+) +-*([0-9]+) +((?:[A-Z]+|[0-9]+)) +((?:[a-z]|[0-9]){16}|bot[0-9]+) +(.{0,20}) +([0-9]+) +(\d+\.\d+\.\d+.\d+\:-*\d{1,5}|0+.0+:-*\d{1,5}) +(-*[0-9]+) +([0-9]+) *$"; + private static string StatusRegex = @"^( *[0-9]+) +-*([0-9]+) +((?:[A-Z]+|[0-9]+)) +((?:[a-z]|[0-9]){16}|bot[0-9]+|(?:[0-9]+)) +(.{0,20}) +([0-9]+) +(\d+\.\d+\.\d+.\d+\:-*\d{1,5}|0+.0+:-*\d{1,5}) +(-*[0-9]+) +([0-9]+) *$"; public async Task ExecuteCommandAsync(Connection connection, string command) { - return (await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, command)).Skip(1).ToArray(); + var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, command); + return response.Skip(1).ToArray(); } public async Task> GetDvarAsync(Connection connection, string dvarName) diff --git a/Application/Server.cs b/Application/Server.cs index ce6388885..92db6da77 100644 --- a/Application/Server.cs +++ b/Application/Server.cs @@ -25,11 +25,9 @@ namespace IW4MAdmin { public class IW4MServer : Server { - private CancellationToken cts; private static Index loc = Utilities.CurrentLocalization.LocalizationIndex; private GameLogEvent LogEvent; - public IW4MServer(IManager mgr, ServerConfiguration cfg) : base(mgr, cfg) { } public override int GetHashCode() @@ -196,17 +194,22 @@ namespace IW4MAdmin Logger.WriteInfo($"Client {player} connecting..."); - var e = new GameEvent(GameEvent.EventType.Connect, "", player, null, this); - Manager.GetEventHandler().AddEvent(e); - - e.OnProcessed.WaitHandle.WaitOne(5000); if (!Manager.GetApplicationSettings().Configuration().EnableClientVPNs && await VPNCheck.UsingVPN(player.IPAddressString, Manager.GetApplicationSettings().Configuration().IPHubAPIKey)) { await player.Kick(Utilities.CurrentLocalization.LocalizationIndex["SERVER_KICK_VPNS_NOTALLOWED"], new Player() { ClientId = 1 }); + return true; } + var e = new GameEvent() + { + Type = GameEvent.EventType.Connect, + Origin = player, + Owner = this + }; + Manager.GetEventHandler().AddEvent(e); + return true; } @@ -375,14 +378,31 @@ namespace IW4MAdmin await ProcessEvent(E); Manager.GetEventApi().OnServerEvent(this, E); + + Command C = null; + if (E.Type == GameEvent.EventType.Command) + { + try + { + C = await ValidateCommand(E); + } + + catch (CommandException e) + { + Logger.WriteInfo(e.Message); + } + + if (C != null) + { + E.Extra = C; + } + } + // this allows us to catch exceptions but still run it parallel async Task pluginHandlingAsync(Task onEvent, string pluginName) { try { - if (cts.IsCancellationRequested) - return; - await onEvent; } @@ -467,54 +487,16 @@ namespace IW4MAdmin }); } - else if (E.Type == GameEvent.EventType.Script) - { - Manager.GetEventHandler().AddEvent(GameEvent.TransferWaiter(GameEvent.EventType.Kill, E)); - } - if (E.Type == GameEvent.EventType.Say && E.Data?.Length >= 2) { - if (E.Data.Substring(0, 1) == "!" || - E.Data.Substring(0, 1) == "@" || - E.Origin.Level == Player.Permission.Console) + E.Data = E.Data.StripColors(); + + ChatHistory.Add(new ChatInfo() { - Command C = null; - - try - { - C = await ValidateCommand(E); - } - - catch (CommandException e) - { - Logger.WriteInfo(e.Message); - } - - if (C != null) - { - if (C.RequiresTarget && E.Target == null) - { - Logger.WriteWarning("Requested event (command) requiring target does not have a target!"); - } - - E.Extra = C; - - // reprocess event as a command - Manager.GetEventHandler().AddEvent(GameEvent.TransferWaiter(GameEvent.EventType.Command, E)); - } - } - - else // Not a command - { - E.Data = E.Data.StripColors(); - - ChatHistory.Add(new ChatInfo() - { - Name = E.Origin.Name, - Message = E.Data, - Time = DateTime.UtcNow - }); - } + Name = E.Origin.Name, + Message = E.Data, + Time = DateTime.UtcNow + }); } if (E.Type == GameEvent.EventType.MapChange) @@ -533,7 +515,6 @@ namespace IW4MAdmin else { - Gametype = dict["gametype"].StripColors(); Hostname = dict["hostname"]?.StripColors(); @@ -549,7 +530,11 @@ namespace IW4MAdmin Hostname = dict["sv_hostname"].StripColors(); string mapname = dict["mapname"].StripColors(); - CurrentMap = Maps.Find(m => m.Name == mapname) ?? new Map() { Alias = mapname, Name = mapname }; + CurrentMap = Maps.Find(m => m.Name == mapname) ?? new Map() + { + Alias = mapname, + Name = mapname + }; } } @@ -569,7 +554,6 @@ namespace IW4MAdmin await E.Owner.ExecuteCommandAsync(E.Message); } - //todo: move while (ChatHistory.Count > Math.Ceiling((double)ClientNum / 2)) ChatHistory.RemoveAt(0); @@ -634,18 +618,15 @@ namespace IW4MAdmin override public async Task ProcessUpdatesAsync(CancellationToken cts) { - // this isn't really used anymore - this.cts = cts; - try { if (Manager.ShutdownRequested()) { - foreach (var plugin in SharedLibraryCore.Plugins.PluginImporter.ActivePlugins) - await plugin.OnUnloadAsync(); - for (int i = 0; i < Players.Count; i++) await RemovePlayer(i); + + foreach (var plugin in SharedLibraryCore.Plugins.PluginImporter.ActivePlugins) + await plugin.OnUnloadAsync(); } // only check every 2 minutes if the server doesn't seem to be responding @@ -710,7 +691,7 @@ namespace IW4MAdmin { string[] messages = this.ProcessMessageToken(Manager.GetMessageTokens(), BroadcastMessages[NextMessage]).Split(Environment.NewLine); - foreach(string message in messages) + foreach (string message in messages) await Broadcast(message); NextMessage = NextMessage == (BroadcastMessages.Count - 1) ? 0 : NextMessage + 1; @@ -746,7 +727,6 @@ namespace IW4MAdmin if (ServerConfig.UseIW5MParser) RconParser = new IW5MRConParser(); - var version = await this.GetDvarAsync("version"); GameName = Utilities.GetGame(version.Value); @@ -838,7 +818,6 @@ namespace IW4MAdmin $"{basepath.Value.Replace('\\', Path.DirectorySeparatorChar)}{Path.DirectorySeparatorChar}{game.Replace('/', Path.DirectorySeparatorChar)}{Path.DirectorySeparatorChar}{logfile.Value}"; } - // hopefully fix wine drive name mangling if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { diff --git a/Plugins/Stats/Cheat/Detection.cs b/Plugins/Stats/Cheat/Detection.cs index 6f64a7b7d..b48c6bb45 100644 --- a/Plugins/Stats/Cheat/Detection.cs +++ b/Plugins/Stats/Cheat/Detection.cs @@ -13,12 +13,13 @@ namespace IW4MAdmin.Plugins.Stats.Cheat class Detection { int Kills; + int HitCount; int AboveThresholdCount; double AverageKillTime; Dictionary HitLocationCount; double AngleDifferenceAverage; EFClientStatistics ClientStats; - DateTime LastKill; + DateTime LastHit; long LastOffset; ILogger Log; Strain Strain; @@ -74,22 +75,21 @@ namespace IW4MAdmin.Plugins.Stats.Cheat ClientPenalty = Penalty.PenaltyType.Any, }; - if (LastKill == DateTime.MinValue) - LastKill = DateTime.UtcNow; + if (LastHit == DateTime.MinValue) + LastHit = DateTime.UtcNow; HitLocationCount[kill.HitLoc]++; if (!isDamage) { Kills++; - AverageKillTime = (AverageKillTime + (DateTime.UtcNow - LastKill).TotalSeconds) / Kills; } + HitCount++; + #region VIEWANGLES if (kill.AnglesList.Count >= 2) { - double realAgainstPredict = Math.Abs(Vector3.AbsoluteDistance(kill.AnglesList[0], kill.AnglesList[1]) - - (Vector3.AbsoluteDistance(kill.AnglesList[0], kill.ViewAngles) + - Vector3.AbsoluteDistance(kill.AnglesList[1], kill.ViewAngles))); + double realAgainstPredict = Vector3.ViewAngleDistance(kill.AnglesList[0], kill.AnglesList[1], kill.ViewAngles); // LIFETIME var hitLoc = ClientStats.HitLocations @@ -102,26 +102,37 @@ namespace IW4MAdmin.Plugins.Stats.Cheat if (hitLoc.HitOffsetAverage > Thresholds.MaxOffset) { + Log.WriteDebug("*** Reached Max Lifetime Average for Angle Difference ***"); + Log.WriteDebug($"Lifetime Average = {newAverage}"); + Log.WriteDebug($"Bone = {hitLoc.Location}"); + Log.WriteDebug($"HitCount = {hitLoc.HitCount}"); + Log.WriteDebug($"ID = {kill.AttackerId}"); + return new DetectionPenaltyResult() { - ClientPenalty = Penalty.PenaltyType.Ban, + ClientPenalty = Penalty.PenaltyType.Flag, RatioAmount = hitLoc.HitOffsetAverage, - KillCount = ClientStats.SessionKills, + KillCount = hitLoc.HitCount, }; } // SESSION - int sessHitLocCount = HitLocationCount[kill.HitLoc]; - double sessAverage = (AngleDifferenceAverage * (sessHitLocCount - 1)) + realAgainstPredict / sessHitLocCount; + double sessAverage = (AngleDifferenceAverage * (HitCount - 1) + realAgainstPredict) / HitCount; AngleDifferenceAverage = sessAverage; if (sessAverage > Thresholds.MaxOffset) { + Log.WriteDebug("*** Reached Max Session Average for Angle Difference ***"); + Log.WriteDebug($"Session Average = {sessAverage}"); + // Log.WriteDebug($"Bone = {hitLoc.Location}"); + Log.WriteDebug($"HitCount = {HitCount}"); + Log.WriteDebug($"ID = {kill.AttackerId}"); + return new DetectionPenaltyResult() { - ClientPenalty = Penalty.PenaltyType.Ban, - RatioAmount = sessHitLocCount, - KillCount = ClientStats.SessionKills, + ClientPenalty = Penalty.PenaltyType.Flag, + RatioAmount = sessAverage, + KillCount = HitCount, }; } @@ -130,7 +141,9 @@ namespace IW4MAdmin.Plugins.Stats.Cheat #endif } - var currentStrain = Strain.GetStrain(kill.ViewAngles, kill.TimeOffset - LastOffset); + double diff = Math.Max(50, kill.TimeOffset - LastOffset); + var currentStrain = Strain.GetStrain(kill.ViewAngles, diff); + //LastHit = kill.When; LastOffset = kill.TimeOffset; if (currentStrain > ClientStats.MaxStrain) @@ -138,29 +151,38 @@ namespace IW4MAdmin.Plugins.Stats.Cheat ClientStats.MaxStrain = currentStrain; } + if (currentStrain > Thresholds.MaxStrain) + { + Log.WriteDebug("*** Reached Max Strain ***"); + Log.WriteDebug($"Strain = {currentStrain}"); + Log.WriteDebug($"Angles = {kill.ViewAngles} {kill.AnglesList[0]} {kill.AnglesList[1]}"); + Log.WriteDebug($"Time = {diff}"); + Log.WriteDebug($"HitCount = {HitCount}"); + Log.WriteDebug($"ID = {kill.AttackerId}"); + } + if (Strain.TimesReachedMaxStrain >= 3) { return new DetectionPenaltyResult() { - ClientPenalty = Penalty.PenaltyType.Ban, + ClientPenalty = Penalty.PenaltyType.Flag, RatioAmount = ClientStats.MaxStrain, - KillCount = ClientStats.SessionKills, + KillCount = HitCount, }; } #if DEBUG Log.WriteDebug($"Current Strain: {currentStrain}"); #endif - LastKill = kill.When; #endregion #region SESSION_RATIOS if (Kills >= Thresholds.LowSampleMinKills) { - double marginOfError = Thresholds.GetMarginOfError(Kills); + double marginOfError = Thresholds.GetMarginOfError(HitCount); // determine what the max headshot percentage can be for current number of kills - double lerpAmount = Math.Min(1.0, (Kills - Thresholds.LowSampleMinKills) / (double)(/*Thresholds.HighSampleMinKills*/ 60 - Thresholds.LowSampleMinKills)); + double lerpAmount = Math.Min(1.0, (HitCount - Thresholds.LowSampleMinKills) / (double)(/*Thresholds.HighSampleMinKills*/ 60 - Thresholds.LowSampleMinKills)); double maxHeadshotLerpValueForFlag = Thresholds.Lerp(Thresholds.HeadshotRatioThresholdLowSample(2.0), Thresholds.HeadshotRatioThresholdHighSample(2.0), lerpAmount) + marginOfError; double maxHeadshotLerpValueForBan = Thresholds.Lerp(Thresholds.HeadshotRatioThresholdLowSample(3.0), Thresholds.HeadshotRatioThresholdHighSample(3.0), lerpAmount) + marginOfError; // determine what the max bone percentage can be for current number of kills @@ -168,10 +190,10 @@ namespace IW4MAdmin.Plugins.Stats.Cheat double maxBoneRatioLerpValueForBan = Thresholds.Lerp(Thresholds.BoneRatioThresholdLowSample(3.25), Thresholds.BoneRatioThresholdHighSample(3.25), lerpAmount) + marginOfError; // calculate headshot ratio - double currentHeadshotRatio = ((HitLocationCount[IW4Info.HitLocation.head] + HitLocationCount[IW4Info.HitLocation.helmet] + HitLocationCount[IW4Info.HitLocation.neck]) / (double)Kills); + double currentHeadshotRatio = ((HitLocationCount[IW4Info.HitLocation.head] + HitLocationCount[IW4Info.HitLocation.helmet] + HitLocationCount[IW4Info.HitLocation.neck]) / (double)HitCount); // calculate maximum bone - double currentMaxBoneRatio = (HitLocationCount.Values.Select(v => v / (double)Kills).Max()); + double currentMaxBoneRatio = (HitLocationCount.Values.Select(v => v / (double)HitCount).Max()); var bone = HitLocationCount.FirstOrDefault(b => b.Value == HitLocationCount.Values.Max()).Key; #region HEADSHOT_RATIO diff --git a/Plugins/Stats/Helpers/StatManager.cs b/Plugins/Stats/Helpers/StatManager.cs index 42923048e..79ac2c1aa 100644 --- a/Plugins/Stats/Helpers/StatManager.cs +++ b/Plugins/Stats/Helpers/StatManager.cs @@ -132,7 +132,6 @@ namespace IW4MAdmin.Plugins.Stats.Helpers else { - // todo: look at this more statsSvc.ClientStatSvc.Update(clientStats); } @@ -201,10 +200,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers playerStats.TryRemove(pl.ClientId, out EFClientStatistics removedValue3); detectionStats.TryRemove(pl.ClientId, out Cheat.Detection removedValue4); - /* // sync their stats before they leave - clientStats = UpdateStats(clientStats);*/ + // sync their stats before they leave + //clientStats = UpdateStats(clientStats); - // todo: should this be saved every disconnect? statsSvc.ClientStatSvc.Update(clientStats); await statsSvc.ClientStatSvc.SaveChangesAsync(); // increment the total play time @@ -225,7 +223,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers /// Process stats for kill event /// /// - public async Task AddScriptKill(bool isDamage, DateTime time, Player attacker, Player victim, int serverId, string map, string hitLoc, string type, + public async Task AddScriptHit(bool isDamage, DateTime time, Player attacker, Player victim, int serverId, string map, string hitLoc, string type, string damage, string weapon, string killOrigin, string deathOrigin, string viewAngles, string offset, string isKillstreakKill, string Ads, string snapAngles) { var statsSvc = ContextThreads[serverId]; @@ -312,7 +310,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers { clientStats.HitLocations.Single(hl => hl.Location == kill.HitLoc).HitCount += 1; - statsSvc.ClientStatSvc.Update(clientStats); + //statsSvc.ClientStatSvc.Update(clientStats); // await statsSvc.ClientStatSvc.SaveChangesAsync(); } @@ -500,7 +498,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers { scoreDifference = clientStats.RoundScore + clientStats.LastScore; } - else + else if (clientStats.RoundScore > 0 && clientStats.LastScore < clientStats.RoundScore) { scoreDifference = clientStats.RoundScore - clientStats.LastScore; } diff --git a/Plugins/Stats/Plugin.cs b/Plugins/Stats/Plugin.cs index b5f50e033..f86f1f372 100644 --- a/Plugins/Stats/Plugin.cs +++ b/Plugins/Stats/Plugin.cs @@ -44,7 +44,8 @@ namespace IW4MAdmin.Plugins.Stats await Manager.RemovePlayer(E.Origin); break; case GameEvent.EventType.Say: - if (E.Data != string.Empty && E.Data.Trim().Length > 0 && E.Message.Trim()[0] != '!' && E.Origin.ClientId > 1) + if (!string.IsNullOrEmpty(E.Data) && + E.Origin.ClientId > 1) await Manager.AddMessageAsync(E.Origin.ClientId, E.Owner.GetHashCode(), E.Data); break; case GameEvent.EventType.MapChange: @@ -69,23 +70,26 @@ namespace IW4MAdmin.Plugins.Stats break; case GameEvent.EventType.Flag: break; - case GameEvent.EventType.Script: + case GameEvent.EventType.ScriptKill: + string[] killInfo = (E.Data != null) ? E.Data.Split(';') : new string[0]; + if (killInfo.Length >= 13) + await Manager.AddScriptHit(false, E.Time, E.Origin, E.Target, S.GetHashCode(), S.CurrentMap.Name, killInfo[7], killInfo[8], + killInfo[5], killInfo[6], killInfo[3], killInfo[4], killInfo[9], killInfo[10], killInfo[11], killInfo[12], killInfo[13]); break; case GameEvent.EventType.Kill: - string[] killInfo = (E.Data != null) ? E.Data.Split(';') : new string[0]; - if (killInfo.Length >= 9 && killInfo[0].Contains("ScriptKill") && E.Owner.CustomCallback) - await Manager.AddScriptKill(false, E.Time, E.Origin, E.Target, S.GetHashCode(), S.CurrentMap.Name, killInfo[7], killInfo[8], - killInfo[5], killInfo[6], killInfo[3], killInfo[4], killInfo[9], killInfo[10], killInfo[11], killInfo[12], killInfo[13]); - else if (!E.Owner.CustomCallback) + if (!E.Owner.CustomCallback) await Manager.AddStandardKill(E.Origin, E.Target); break; case GameEvent.EventType.Death: break; - //case GameEvent.EventType.Damage: + case GameEvent.EventType.Damage: + if (!E.Owner.CustomCallback) + Manager.AddDamageEvent(E.Data, E.Origin.ClientId, E.Owner.GetHashCode()); + break; case GameEvent.EventType.ScriptDamage: killInfo = (E.Data != null) ? E.Data.Split(';') : new string[0]; - if (killInfo.Length >= 9 && E.Owner.CustomCallback) - await Manager.AddScriptKill(true, E.Time, E.Origin, E.Target, S.GetHashCode(), S.CurrentMap.Name, killInfo[7], killInfo[8], + if (killInfo.Length >= 13) + await Manager.AddScriptHit(true, E.Time, E.Origin, E.Target, S.GetHashCode(), S.CurrentMap.Name, killInfo[7], killInfo[8], killInfo[5], killInfo[6], killInfo[3], killInfo[4], killInfo[9], killInfo[10], killInfo[11], killInfo[12], killInfo[13]); break; } @@ -153,7 +157,7 @@ namespace IW4MAdmin.Plugins.Stats double abdomenRatio = 0; double chestAbdomenRatio = 0; double hitOffsetAverage = 0; - double maxStrain = clientStats.Count(c=> c.MaxStrain > 0) == 0 ? 0 : clientStats.Max(cs => cs.MaxStrain); + double maxStrain = clientStats.Count(c => c.MaxStrain > 0) == 0 ? 0 : clientStats.Max(cs => cs.MaxStrain); //double maxAngle = clientStats.Max(cs => cs.HitLocations.Max(hl => hl.MaxAngleDistance)); if (clientStats.Where(cs => cs.HitLocations.Count > 0).FirstOrDefault() != null) diff --git a/SharedLibraryCore/Commands/NativeCommands.cs b/SharedLibraryCore/Commands/NativeCommands.cs index 66ebd9e92..3735046f7 100644 --- a/SharedLibraryCore/Commands/NativeCommands.cs +++ b/SharedLibraryCore/Commands/NativeCommands.cs @@ -689,7 +689,6 @@ namespace SharedLibraryCore.Commands public override async Task ExecuteAsync(GameEvent E) { - // todo: move unflag to seperate command if (E.Target.Level >= E.Origin.Level) { await E.Origin.Tell($"{Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_FLAG_FAIL"]} ^5{E.Target.Name}"); diff --git a/SharedLibraryCore/Event.cs b/SharedLibraryCore/Event.cs index 884beea78..205b335c8 100644 --- a/SharedLibraryCore/Event.cs +++ b/SharedLibraryCore/Event.cs @@ -33,8 +33,8 @@ namespace SharedLibraryCore Command, // FROM GAME - Script, ScriptDamage, + ScriptKill, Kill, Damage, Death, @@ -49,35 +49,19 @@ namespace SharedLibraryCore Owner = S; OnProcessed = new ManualResetEventSlim(); Time = DateTime.UtcNow; + CurrentEventId++; + Id = CurrentEventId; } public GameEvent() { OnProcessed = new ManualResetEventSlim(); Time = DateTime.UtcNow; + CurrentEventId++; + Id = CurrentEventId; } - public static GameEvent TransferWaiter(EventType newType, GameEvent e) - { - var newEvent = new GameEvent() - { - Data = e.Data, - Extra = e.Extra, - Message = e.Message, - OnProcessed = e.OnProcessed, - Origin = e.Origin, - Owner = e.Owner, - Remote = e.Remote, - Target = e.Target, - Type = newType, - }; - - // hack: prevent the previous event from completing until this one is done - e.OnProcessed = new ManualResetEventSlim(); - newEvent.Time = e.Time; - - return newEvent; - } + private static long CurrentEventId; public EventType Type; public string Data; // Data is usually the message sent by player @@ -89,5 +73,6 @@ namespace SharedLibraryCore public object Extra { get; set; } public ManualResetEventSlim OnProcessed { get; set; } public DateTime Time { get; private set; } + public long Id { get; private set; } } } diff --git a/SharedLibraryCore/Helpers/Vector3.cs b/SharedLibraryCore/Helpers/Vector3.cs index bfbdc0dc4..1bb147936 100644 --- a/SharedLibraryCore/Helpers/Vector3.cs +++ b/SharedLibraryCore/Helpers/Vector3.cs @@ -48,16 +48,41 @@ namespace SharedLibraryCore.Helpers { double deltaX = Math.Abs(b.X -a.X); double deltaY = Math.Abs(b.Y - a.Y); - // double deltaZ = Math.Abs(b.Z - a.Z); + double deltaZ = Math.Abs(b.Z - a.Z); // this 'fixes' the roll-over angles double dx = deltaX < 360.0 / 2 ? deltaX : 360.0 - deltaX; double dy = deltaY < 360.0 / 2 ? deltaY : 360.0 - deltaY; - // double dz = deltaZ < 360.0 / 2 ? deltaZ : 360.0 - deltaZ; + double dz = deltaZ < 360.0 / 2 ? deltaZ : 360.0 - deltaZ; return Math.Sqrt((dx * dx) + (dy * dy) /*+ (dz * dz)*/); } + public static double ViewAngleDistance(Vector3 a, Vector3 b, Vector3 c) + { + double dabX = Math.Abs(a.X - b.X); + dabX = dabX < 360.0 / 2 ? dabX : 360.0 - dabX; + double dabY = Math.Abs(a.Y - b.Y); + dabY = dabY < 360.0 / 2 ? dabY : 360.0 - dabY; + + double dacX = Math.Abs(a.X - c.X); + dacX = dacX < 360.0 / 2 ? dacX : 360.0 - dacX; + double dacY = Math.Abs(a.Y - c.Y); + dacY = dacY < 360.0 / 2 ? dacY : 360.0 - dacY; + + double dbcX = Math.Abs(b.X - c.X); + dbcX = dbcX < 360.0 / 2 ? dbcX : 360.0 - dbcX; + double dbcY = Math.Abs(b.Y - c.Y); + dbcY = dbcY < 360.0 / 2 ? dbcY : 360.0 - dbcY; + + double deltaX = (dabX - dacX - dbcX) / 2.0; + deltaX = deltaX < 360.0 / 2 ? deltaX : 360.0 - deltaX; + double deltaY = (dabY - dacY - dbcY) / 2.0; + deltaY = deltaY < 360.0 / 2 ? deltaY : 360.0 - deltaY; + + return Math.Round(Math.Sqrt((deltaX * deltaX) + (deltaY * deltaY)), 4); + } + public static Vector3 Subtract(Vector3 a, Vector3 b) => new Vector3(b.X - a.X, b.Y - a.Y, b.Z - a.Z); public double DotProduct(Vector3 a) => (a.X * this.X) + (a.Y * this.Y) + (a.Z * this.Z); diff --git a/SharedLibraryCore/Objects/Player.cs b/SharedLibraryCore/Objects/Player.cs index 06b8e2ce3..188a1dba1 100644 --- a/SharedLibraryCore/Objects/Player.cs +++ b/SharedLibraryCore/Objects/Player.cs @@ -40,20 +40,25 @@ namespace SharedLibraryCore.Objects public async Task Tell(String Message) { - - // await CurrentServer.Tell(Message, this); - var e = new GameEvent() + // this is console or remote so send immediately + if (ClientNumber < 0) { - Message = Message, - Target = this, - Owner = CurrentServer, - Type = GameEvent.EventType.Tell, - Data = Message - }; + await CurrentServer.Tell(Message, this); + } - CurrentServer.Manager.GetEventHandler().AddEvent(e); - // this ensures the output it sent before returning - await Task.Run(() => e.OnProcessed.Wait()); + else + { + var e = new GameEvent() + { + Message = Message, + Target = this, + Owner = CurrentServer, + Type = GameEvent.EventType.Tell, + Data = Message + }; + + CurrentServer.Manager.GetEventHandler().AddEvent(e); + } } public async Task Kick(String Message, Player Sender) diff --git a/SharedLibraryCore/RCon/Connection.cs b/SharedLibraryCore/RCon/Connection.cs index 53eb55642..2f75ce75b 100644 --- a/SharedLibraryCore/RCon/Connection.cs +++ b/SharedLibraryCore/RCon/Connection.cs @@ -56,8 +56,6 @@ namespace SharedLibraryCore.RCon { public IPEndPoint Endpoint { get; private set; } public string RConPassword { get; private set; } - public ConcurrentQueue ResponseQueue; - //Socket ServerConnection; ILogger Log; int FailedSends; int FailedReceives; @@ -79,13 +77,6 @@ namespace SharedLibraryCore.RCon OnReceived = new ManualResetEvent(false); } - ~Connection() - { - /*ServerConnection.Shutdown(SocketShutdown.Both); - ServerConnection.Close(); - ServerConnection.Dispose();*/ - } - private void OnConnectedCallback(IAsyncResult ar) { var serverSocket = (Socket)ar.AsyncState; @@ -144,12 +135,12 @@ namespace SharedLibraryCore.RCon if (!connectionState.Buffer.Take(4).ToArray().SequenceEqual(new byte[] { 0xFF, 0xFF, 0xFF, 0xFF })) throw new NetworkException("Unexpected packet received"); - if (FailedReceives == 0 && serverConnection.Available > 0) - { - serverConnection.BeginReceive(connectionState.Buffer, 0, connectionState.Buffer.Length, 0, - new AsyncCallback(OnReceivedCallback), connectionState); - } - else + /* if (FailedReceives == 0 && serverConnection.Available > 0) + { + serverConnection.BeginReceive(connectionState.Buffer, 0, connectionState.Buffer.Length, 0, + new AsyncCallback(OnReceivedCallback), connectionState); + } + else*/ { response = connectionState.ResponseString.ToString(); OnReceived.Set(); @@ -169,7 +160,7 @@ namespace SharedLibraryCore.RCon catch (ObjectDisposedException) { - Log.WriteWarning($"Tried to check for more available bytes for disposed socket on {Endpoint}"); + // Log.WriteWarning($"Tried to check for more available bytes for disposed socket on {Endpoint}"); } } @@ -211,7 +202,9 @@ namespace SharedLibraryCore.RCon retrySend: try { - +#if DEBUG + Console.WriteLine($"Sending Command {parameters}"); +#endif if (!OnConnected.WaitOne(StaticHelpers.SocketTimeout)) throw new SocketException((int)SocketError.TimedOut); diff --git a/SharedLibraryCore/Server.cs b/SharedLibraryCore/Server.cs index 15b76a532..936394f28 100644 --- a/SharedLibraryCore/Server.cs +++ b/SharedLibraryCore/Server.cs @@ -41,7 +41,6 @@ namespace SharedLibraryCore PlayerHistory = new Queue(); ChatHistory = new List(); NextMessage = 0; - OnEvent = new ManualResetEventSlim(); CustomSayEnabled = Manager.GetApplicationSettings().Configuration().EnableCustomSayName; CustomSayName = Manager.GetApplicationSettings().Configuration().CustomSayName; InitializeTokens(); @@ -128,7 +127,7 @@ namespace SharedLibraryCore #if !DEBUG string formattedMessage = String.Format(RconParser.GetCommandPrefixes().Say, Message); - var e = new GameEvent() + var e = new GameEvent() { Message = formattedMessage, Data = formattedMessage, @@ -139,8 +138,8 @@ namespace SharedLibraryCore Manager.GetEventHandler().AddEvent(e); #else Logger.WriteVerbose(Message.StripColors()); - await Task.CompletedTask; #endif + await Task.CompletedTask; } /// @@ -162,18 +161,21 @@ namespace SharedLibraryCore if (Target.Level == Player.Permission.Console) { Console.ForegroundColor = ConsoleColor.Cyan; - Console.WriteLine(Utilities.StripColors(Message)); + Console.WriteLine(Message.StripColors()); Console.ForegroundColor = ConsoleColor.Gray; } if (CommandResult.Count > 15) CommandResult.RemoveAt(0); - CommandResult.Add(new CommandResponseInfo() + if (Target.ClientNumber < 0) { - Response = Utilities.StripColors(Message), - ClientId = Target.ClientId - }); + CommandResult.Add(new CommandResponseInfo() + { + Response = Message.StripColors(), + ClientId = Target.ClientId + }); + } } /// @@ -261,7 +263,7 @@ namespace SharedLibraryCore { BroadcastMessages = new List(); - if(ServerConfig.AutoMessages != null) + if (ServerConfig.AutoMessages != null) BroadcastMessages.AddRange(ServerConfig.AutoMessages); BroadcastMessages.AddRange(Manager.GetApplicationSettings().Configuration().AutoMessages); } @@ -315,7 +317,6 @@ namespace SharedLibraryCore public RCon.Connection RemoteConnection { get; protected set; } public IRConParser RconParser { get; protected set; } public IEventParser EventParser { get; set; } - public ManualResetEventSlim OnEvent { get; private set; } // Internal protected string IP; @@ -327,6 +328,7 @@ namespace SharedLibraryCore protected TimeSpan LastMessage; protected IFile LogFile; protected DateTime LastPoll; + protected ManualResetEventSlim OnRemoteCommandResponse; // only here for performance private bool CustomSayEnabled; diff --git a/SharedLibraryCore/Utilities.cs b/SharedLibraryCore/Utilities.cs index 7a69b4cb0..d8b5b8e5b 100644 --- a/SharedLibraryCore/Utilities.cs +++ b/SharedLibraryCore/Utilities.cs @@ -407,15 +407,15 @@ namespace SharedLibraryCore public static Task SetDvarAsync(this Server server, string dvarName, object dvarValue) => server.RconParser.SetDvarAsync(server.RemoteConnection, dvarName, dvarValue); - public static Task ExecuteCommandAsync(this Server server, string commandName) => server.RconParser.ExecuteCommandAsync(server.RemoteConnection, commandName); - + public static async Task ExecuteCommandAsync(this Server server, string commandName) => await server.RconParser.ExecuteCommandAsync(server.RemoteConnection, commandName); + public static Task> GetStatusAsync(this Server server) => server.RconParser.GetStatusAsync(server.RemoteConnection); public static async Task> GetInfoAsync(this Server server) { var response = await server.RemoteConnection.SendQueryAsync(RCon.StaticHelpers.QueryType.GET_INFO); return response.FirstOrDefault(r => r[0] == '\\')?.DictionaryFromKeyValue(); - } + } } } diff --git a/WebfrontCore/Controllers/ConsoleController.cs b/WebfrontCore/Controllers/ConsoleController.cs index b82453426..fb1187cec 100644 --- a/WebfrontCore/Controllers/ConsoleController.cs +++ b/WebfrontCore/Controllers/ConsoleController.cs @@ -39,7 +39,7 @@ namespace WebfrontCore.Controllers var remoteEvent = new GameEvent() { - Type = GameEvent.EventType.Say, + Type = GameEvent.EventType.Command, Data = command, Origin = client, Owner = server, @@ -48,8 +48,7 @@ namespace WebfrontCore.Controllers Manager.GetEventHandler().AddEvent(remoteEvent); // wait for the event to process - - await Task.Run(() => remoteEvent.OnProcessed.Wait()); + await Task.Run(() => remoteEvent.OnProcessed.Wait(5000)); var response = server.CommandResult.Where(c => c.ClientId == client.ClientId).ToList(); // remove the added command response diff --git a/WebfrontCore/Views/Client/Profile/Index.cshtml b/WebfrontCore/Views/Client/Profile/Index.cshtml index 71a8d3eea..9bee6c784 100644 --- a/WebfrontCore/Views/Client/Profile/Index.cshtml +++ b/WebfrontCore/Views/Client/Profile/Index.cshtml @@ -31,13 +31,13 @@
@if (Model.LevelInt < (int)ViewBag.User.Level && - (SharedLibraryCore.Objects.Player.Permission)Model.LevelInt != SharedLibraryCore.Objects.Player.Permission.Banned) + (SharedLibraryCore.Objects.Player.Permission)Model.LevelInt != SharedLibraryCore.Objects.Player.Permission.Banned) { } @if (Model.LevelInt < (int)ViewBag.User.Level && - (SharedLibraryCore.Objects.Player.Permission)Model.LevelInt == SharedLibraryCore.Objects.Player.Permission.Banned) + (SharedLibraryCore.Objects.Player.Permission)Model.LevelInt == SharedLibraryCore.Objects.Player.Permission.Banned) { } @@ -45,7 +45,7 @@
@{ - @Model.NetworkId.ToString("X")
+ @Model.NetworkId.ToString("X")
foreach (string alias in Model.Aliases) { @alias
@@ -67,10 +67,10 @@
@Model.Level
- @loc["WEBFRONT_PROFILE_PLAYER"] @Model.TimePlayed @loc["GLOBAL_HOURS"] + @loc["WEBFRONT_PROFILE_PLAYER"] @Model.TimePlayed @loc["GLOBAL_HOURS"]
- @loc["WEBFRONT_PROFILE_FSEEN"] @Model.FirstSeen @loc["WEBFRONT_PENALTY_TEMPLATE_AGO"] + @loc["WEBFRONT_PROFILE_FSEEN"] @Model.FirstSeen @loc["WEBFRONT_PENALTY_TEMPLATE_AGO"]
@loc["WEBFRONT_PROFILE_LSEEN"] @Model.LastSeen @loc["WEBFRONT_PENALTY_TEMPLATE_AGO"]